diff --git a/.gitmodules b/.gitmodules index a177c249..4f902007 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,3 @@ -[submodule "Engine/cpp/ThirdParty/bx"] - path = Engine/cpp/ThirdParty/bx - url = https://github.com/bkaradzic/bx -[submodule "Engine/cpp/ThirdParty/bimg"] - path = Engine/cpp/ThirdParty/bimg - url = https://github.com/bkaradzic/bimg -[submodule "Engine/cpp/ThirdParty/bgfx"] - path = Engine/cpp/ThirdParty/bgfx - url = https://github.com/bkaradzic/bgfx -[submodule "Engine/cpp/ThirdParty/sdl"] +[submodule "Engine/cpp/thirdparty/sdl"] path = Engine/cpp/ThirdParty/sdl url = https://github.com/libsdl-org/SDL diff --git a/CMakeLists.txt b/CMakeLists.txt index dee67b71..8f95e25c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(DraconicEngine LANGUAGES C CXX) find_package(Git QUIET) find_package(Threads REQUIRED) +find_package(Vulkan REQUIRED) if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") message(STATUS "Updating submodules...") @@ -20,36 +21,12 @@ endif() list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") -option(BUILD_BENCHMARKS "Build micro-benchmarks (nanobench)" OFF) - include(CTest) include(Compiler) include(Modules) -include(Shaders) # Engine runtime (C++) add_subdirectory(Engine/cpp) # Samples (C++) add_subdirectory(Samples/cpp) - -# Aggregate benchmark targets: `benchmarks` builds them all, `run-benchmarks` -# builds and runs each in turn. -if(BUILD_BENCHMARKS) - get_property(DRACO_BENCH_TARGETS GLOBAL PROPERTY DRACO_BENCH_TARGETS) - if(DRACO_BENCH_TARGETS) - add_custom_target(benchmarks) - add_dependencies(benchmarks ${DRACO_BENCH_TARGETS}) - - set(BENCH_RUN_CMDS "") - foreach(BENCH_TARGET IN LISTS DRACO_BENCH_TARGETS) - list(APPEND BENCH_RUN_CMDS COMMAND $) - endforeach() - add_custom_target(run-benchmarks - ${BENCH_RUN_CMDS} - DEPENDS ${DRACO_BENCH_TARGETS} - USES_TERMINAL - VERBATIM - ) - endif() -endif() diff --git a/Engine/cpp/CMakeLists.txt b/Engine/cpp/CMakeLists.txt index 946eed00..1f0caec5 100644 --- a/Engine/cpp/CMakeLists.txt +++ b/Engine/cpp/CMakeLists.txt @@ -1,9 +1,6 @@ add_subdirectory(ThirdParty SYSTEM) add_modules_library(Runtime SHARED) -# Runtime exposes the whole engine through the `draconic` module. Shell (the single -# library holding both backends) is linked PUBLIC, so a consumer only links Runtime and -# does `import draconic;`; createShell() resolves from Shell. -target_link_libraries(Runtime PUBLIC Core Shell Scene Rendering) +target_link_libraries(Runtime PUBLIC Core) # TODO: binding library \ No newline at end of file diff --git a/Engine/cpp/Runtime/Animation/AnimationModule.cppm b/Engine/cpp/Runtime/Animation/AnimationModule.cppm new file mode 100644 index 00000000..9f6bf9b3 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/AnimationModule.cppm @@ -0,0 +1,16 @@ +/// Primary module for animation - the engine's skeletal-animation foundation. Re-exports all partitions. +/// +/// The animation foundation (foundation only - no rendering dependency; the renderer +/// consumes evaluated skinning matrices). Depends solely on Core (math + containers). Aggregates the +/// partitions: easing (:easing), the skeleton hierarchy (:skeleton), and the pose view (:pose). +/// The clip/sampler/player and the animation graph land in later partitions. + +export module animation; + +export import :easing; +export import :skeleton; +export import :pose; +export import :clip; +export import :sampler; +export import :player; +export import :graph; diff --git a/Engine/cpp/Runtime/Animation/Clip.cppm b/Engine/cpp/Runtime/Animation/Clip.cppm new file mode 100644 index 00000000..80ec273b --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Clip.cppm @@ -0,0 +1,213 @@ +/// Animation clip, tracks, and keyframes (`:clip` partition). +/// +/// Animation data: keyframed tracks (position/rotation/scale per bone) + timed events, grouped into +/// an AnimationClip. / AnimationTrack / +/// Keyframe / AnimationEvent. Tracks are heap-owned (UniquePtr) so GetOrCreate*Track pointers stay +/// valid as more tracks are added (matching the Beef List> of references). + +module; +#include +#include +#include +#include +#include +#include + +export module animation:clip; + +import core; + +using namespace draco; + +export namespace draco::animation { + +// Event callback: (event name, event time in seconds). The engine's move-only delegate. Defined here +// (with AnimationEvent) so both the player and the graph's IAnimationStateNode can use it. +using AnimationEventHandler = std::function; + +// Keyframe interpolation mode. +enum class InterpolationMode { Step, Linear, CubicSpline }; + +// A keyframed value at a time (tangents used only for cubic spline). +template +struct Keyframe { + f32 time = 0.0f; + T value = {}; + T inTangent = {}; + T outTangent= {}; + + Keyframe() = default; + Keyframe(f32 t, const T& v) : time(t), value(v) {} + Keyframe(f32 t, const T& v, const T& in, const T& out) : time(t), value(v), inTangent(in), outTangent(out) {} +}; + +// The keyframe interval surrounding a sample time + the interpolation factor. +struct KeyframeLookup { i32 prev = -1; i32 next = -1; f32 t = 0.0f; }; + +// Keyframes for one property of one bone. +template +class AnimationTrack { +public: + i32 boneIndex = 0; + InterpolationMode interpolation = InterpolationMode::Linear; + + [[nodiscard]] std::vector>& keyframes() noexcept { return m_keyframes; } + [[nodiscard]] const std::vector>& keyframes() const noexcept { return m_keyframes; } + + void addKeyframe(f32 time, const T& value) { m_keyframes.push_back(Keyframe{ time, value }); } + void addKeyframe(f32 time, const T& value, const T& inTan, const T& outTan) { + m_keyframes.push_back(Keyframe{ time, value, inTan, outTan }); + } + + // Stable insertion sort by time (keyframes are typically already time-ordered from import). + void sortKeyframes() { + for (usize i = 1; i < m_keyframes.size(); ++i) { + Keyframe key = m_keyframes[i]; + usize j = i; + while (j > 0 && m_keyframes[j - 1].time > key.time) { m_keyframes[j] = m_keyframes[j - 1]; --j; } + m_keyframes[j] = key; + } + } + + // Find the keyframe interval (prev,next) + factor t for `time`. Clamps to ends. + [[nodiscard]] KeyframeLookup findKeyframes(f32 time) const { + const i32 count = static_cast(m_keyframes.size()); + if (count == 0) { return KeyframeLookup{ -1, -1, 0.0f }; } + if (count == 1) { return KeyframeLookup{ 0, 0, 0.0f }; } + if (time <= m_keyframes[0].time) { return KeyframeLookup{ 0, 0, 0.0f }; } + if (time >= m_keyframes[static_cast(count - 1)].time) { return KeyframeLookup{ count - 1, count - 1, 0.0f }; } + + i32 low = 0, high = count - 1; + while (low < high - 1) { + const i32 mid = (low + high) / 2; + if (m_keyframes[static_cast(mid)].time <= time) { low = mid; } else { high = mid; } + } + const f32 duration = m_keyframes[static_cast(high)].time - m_keyframes[static_cast(low)].time; + const f32 t = (duration > 0.0f) ? (time - m_keyframes[static_cast(low)].time) / duration : 0.0f; + return KeyframeLookup{ low, high, t }; + } + +private: + std::vector> m_keyframes; +}; + +// An event placed at a time in a clip; fires when playback crosses it. +struct AnimationEvent { + f32 time = 0.0f; + std::u8string name; + AnimationEvent() = default; + AnimationEvent(f32 t, std::u8string_view n) : time(t), name(n) {} +}; + +// All tracks + events for one animation. A resource product (Object) so a cooked AnimationClipSource +// can build into it via the resource system. +class AnimationClip { +public: + AnimationClip() = default; + explicit AnimationClip(std::u8string_view name, f32 duration = 0.0f, bool isLooping = false) + : duration(duration), isLooping(isLooping), m_name(name) {} + + AnimationClip(const AnimationClip&) = delete; + AnimationClip& operator=(const AnimationClip&) = delete; + + f32 duration = 0.0f; + bool isLooping = false; + + [[nodiscard]] std::u8string& name() noexcept { return m_name; } + [[nodiscard]] const std::u8string& name() const noexcept { return m_name; } + + using Vec3Track = AnimationTrack; + using QuatTrack = AnimationTrack; + + [[nodiscard]] std::vector>& positionTracks() noexcept { return m_positionTracks; } + [[nodiscard]] std::vector>& rotationTracks() noexcept { return m_rotationTracks; } + [[nodiscard]] std::vector>& scaleTracks() noexcept { return m_scaleTracks; } + [[nodiscard]] std::vector& events() noexcept { return m_events; } + [[nodiscard]] const std::vector>& positionTracks() const noexcept { return m_positionTracks; } + [[nodiscard]] const std::vector>& rotationTracks() const noexcept { return m_rotationTracks; } + [[nodiscard]] const std::vector>& scaleTracks() const noexcept { return m_scaleTracks; } + [[nodiscard]] const std::vector& events() const noexcept { return m_events; } + + [[nodiscard]] Vec3Track* getOrCreatePositionTrack(i32 boneIndex) { return getOrCreate(m_positionTracks, boneIndex); } + [[nodiscard]] QuatTrack* getOrCreateRotationTrack(i32 boneIndex) { return getOrCreate(m_rotationTracks, boneIndex); } + [[nodiscard]] Vec3Track* getOrCreateScaleTrack(i32 boneIndex) { return getOrCreate(m_scaleTracks, boneIndex); } + + void sortAllKeyframes() { + for (auto& t : m_positionTracks) { t->sortKeyframes(); } + for (auto& t : m_rotationTracks) { t->sortKeyframes(); } + for (auto& t : m_scaleTracks) { t->sortKeyframes(); } + } + + void addEvent(f32 time, std::u8string_view name) { m_events.push_back(AnimationEvent{ time, name }); } + + void sortEvents() { + for (usize i = 1; i < m_events.size(); ++i) { + AnimationEvent key = static_cast(m_events[i]); + usize j = i; + while (j > 0 && m_events[j - 1].time > key.time) { m_events[j] = static_cast(m_events[j - 1]); --j; } + m_events[j] = static_cast(key); + } + } + + // Fire events crossed in (prevTime, currentTime]. currentTime may exceed Duration when looping. + // `handler` is any callable (std::u8string_view name, f32 time). + template + void fireEvents(f32 prevTime, f32 currentTime, Handler&& handler) const { + if (m_events.empty() || duration <= 0.0f) { return; } + + if (currentTime > prevTime && currentTime <= duration) { + for (const AnimationEvent& e : m_events) { + if (e.time > prevTime && e.time <= currentTime) { handler(std::u8string_view{ e.name }, e.time); } + } + } else if (currentTime > duration) { + if (isLooping) { + for (const AnimationEvent& e : m_events) { + if (e.time > prevTime && e.time <= duration) { handler(std::u8string_view{ e.name }, e.time); } + } + f32 wrapped = currentTime; + while (wrapped >= duration) { wrapped -= duration; } + for (const AnimationEvent& e : m_events) { + if (e.time <= wrapped) { handler(std::u8string_view{ e.name }, e.time); } + } + } else { + for (const AnimationEvent& e : m_events) { + if (e.time > prevTime && e.time <= duration) { handler(std::u8string_view{ e.name }, e.time); } + } + } + } + } + + // Duration = latest keyframe time across all tracks. + void computeDuration() { + duration = 0.0f; + for (auto& t : m_positionTracks) { if (!t->keyframes().empty()) { duration = std::max(duration, t->keyframes()[t->keyframes().size() - 1].time); } } + for (auto& t : m_rotationTracks) { if (!t->keyframes().empty()) { duration = std::max(duration, t->keyframes()[t->keyframes().size() - 1].time); } } + for (auto& t : m_scaleTracks) { if (!t->keyframes().empty()) { duration = std::max(duration, t->keyframes()[t->keyframes().size() - 1].time); } } + } + + // Reset in place (resource hot-reload keeps outside references valid). + void clearForReload() { + m_positionTracks.clear(); m_rotationTracks.clear(); m_scaleTracks.clear(); m_events.clear(); + duration = 0.0f; isLooping = false; m_name.clear(); + } + +private: + template + [[nodiscard]] Track* getOrCreate(std::vector>& tracks, i32 boneIndex) { + for (auto& t : tracks) { if (t->boneIndex == boneIndex) { return t.get(); } } + std::unique_ptr track = std::make_unique(); + track->boneIndex = boneIndex; + Track* raw = track.get(); + tracks.push_back(static_cast&&>(track)); + return raw; + } + + std::u8string m_name; + std::vector> m_positionTracks; + std::vector> m_rotationTracks; + std::vector> m_scaleTracks; + std::vector m_events; +}; + + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/ClipTests.test.cpp b/Engine/cpp/Runtime/Animation/ClipTests.test.cpp new file mode 100644 index 00000000..9c94d84a --- /dev/null +++ b/Engine/cpp/Runtime/Animation/ClipTests.test.cpp @@ -0,0 +1,112 @@ +// AnimationEvent + AnimationClip event storage + FireEvents. Ported from the applicable parts of +// the reference event suite (Player/ClipStateNode/BlendTree parts land later). +#include +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +TEST_CASE("event: constructor sets time and name") +{ + const AnimationEvent e{ 0.5f, u8"Footstep" }; + CHECK(e.time == 0.5f); + CHECK(e.name == std::u8string_view{ u8"Footstep" }); +} + +TEST_CASE("clip: AddEvent increases count") +{ + AnimationClip clip{ u8"Test", 2.0f }; + CHECK(clip.events().size() == 0); + clip.addEvent(0.5f, u8"Hit"); + CHECK(clip.events().size() == 1); + clip.addEvent(1.0f, u8"Sound"); + CHECK(clip.events().size() == 2); +} + +TEST_CASE("clip: SortEvents sorts by time") +{ + AnimationClip clip{ u8"Test", 2.0f }; + clip.addEvent(1.5f, u8"C"); + clip.addEvent(0.2f, u8"A"); + clip.addEvent(0.8f, u8"B"); + clip.sortEvents(); + CHECK(clip.events()[0].time == 0.2f); + CHECK(clip.events()[1].time == 0.8f); + CHECK(clip.events()[2].time == 1.5f); + CHECK(clip.events()[0].name == std::u8string_view{ u8"A" }); + CHECK(clip.events()[2].name == std::u8string_view{ u8"C" }); +} + +TEST_CASE("clip: FireEvents crossing behavior") +{ + { // crosses threshold -> fires + AnimationClip clip{ u8"Test", 2.0f }; + clip.addEvent(0.5f, u8"Hit"); + int count = 0; + clip.fireEvents(0.0f, 1.0f, [&](std::u8string_view, f32) { ++count; }); + CHECK(count == 1); + } + { // before threshold -> no fire + AnimationClip clip{ u8"Test", 2.0f }; + clip.addEvent(1.5f, u8"Hit"); + int count = 0; + clip.fireEvents(0.0f, 1.0f, [&](std::u8string_view, f32) { ++count; }); + CHECK(count == 0); + } + { // exact end time fires (inclusive right) + AnimationClip clip{ u8"Test", 1.0f }; + clip.addEvent(0.5f, u8"Exact"); + int count = 0; + clip.fireEvents(0.3f, 0.5f, [&](std::u8string_view, f32) { ++count; }); + CHECK(count == 1); + } +} + +TEST_CASE("clip: FireEvents in order + loop wrap + non-looping past duration") +{ + { // multiple fire in order + AnimationClip clip{ u8"Test", 2.0f }; + clip.addEvent(0.3f, u8"A"); clip.addEvent(0.7f, u8"B"); clip.addEvent(1.2f, u8"C"); + clip.sortEvents(); + std::vector fired; + clip.fireEvents(0.0f, 1.5f, [&](std::u8string_view n, f32) { fired.push_back(n); }); + CHECK(fired.size() == 3); + CHECK(fired[0] == std::u8string_view{ u8"A" }); + CHECK(fired[1] == std::u8string_view{ u8"B" }); + CHECK(fired[2] == std::u8string_view{ u8"C" }); + } + { // looping wrap: prev 0.9 -> cur 1.3 on a 1.0s loop fires only the early (post-wrap) event + AnimationClip clip{ u8"Test", 1.0f, true }; + clip.addEvent(0.2f, u8"Early"); clip.addEvent(0.8f, u8"Late"); + clip.sortEvents(); + std::vector fired; + clip.fireEvents(0.9f, 1.3f, [&](std::u8string_view n, f32) { fired.push_back(n); }); + CHECK(fired.size() == 1); + CHECK(fired[0] == std::u8string_view{ u8"Early" }); + } + { // non-looping past duration fires remaining up to duration + AnimationClip clip{ u8"Test", 1.0f, false }; + clip.addEvent(0.8f, u8"NearEnd"); clip.addEvent(1.0f, u8"AtEnd"); + clip.sortEvents(); + int count = 0; + clip.fireEvents(0.5f, 1.5f, [&](std::u8string_view, f32) { ++count; }); + CHECK(count == 2); + } +} + +TEST_CASE("clip: ComputeDuration from latest keyframe") +{ + AnimationClip clip{ u8"Test" }; + AnimationClip::Vec3Track* pos = clip.getOrCreatePositionTrack(0); + pos->addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + pos->addKeyframe(1.25f, math::Vector3{ 1, 0, 0 }); + clip.computeDuration(); + CHECK(math::nearlyEqual(clip.duration, 1.25f)); + // GetOrCreate returns the SAME track for the same bone. + CHECK(clip.getOrCreatePositionTrack(0) == pos); + CHECK(clip.positionTracks().size() == 1); +} diff --git a/Engine/cpp/Runtime/Animation/Easing.cppm b/Engine/cpp/Runtime/Animation/Easing.cppm new file mode 100644 index 00000000..6baebf62 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Easing.cppm @@ -0,0 +1,78 @@ +/// Easing types (`:easing` partition). +/// +/// EasingType: a serializable enum mapping 1:1 to the core easing functions (core.math.easings). +/// The functions themselves live in core math; +/// this is the animation-facing enum + lookup. + +module; + +export module animation:easing; + +import core; + +using namespace draco; + +export namespace draco::animation { + +// Serializable easing type (1:1 with the core Easings family). Order matters - it's the serialized +// value and several tools index by it. +enum class EasingType : i32 { + Linear = 0, + EaseInQuadratic, EaseOutQuadratic, EaseInOutQuadratic, + EaseInCubic, EaseOutCubic, EaseInOutCubic, + EaseInQuartic, EaseOutQuartic, EaseInOutQuartic, + EaseInQuintic, EaseOutQuintic, EaseInOutQuintic, + EaseInSin, EaseOutSin, EaseInOutSin, + EaseInExponential, EaseOutExponential, EaseInOutExponential, + EaseInCircular, EaseOutCircular, EaseInOutCircular, + EaseInBack, EaseOutBack, EaseInOutBack, + EaseInElastic, EaseOutElastic, EaseInOutElastic, + EaseInBounce, EaseOutBounce, EaseInOutBounce, + Count +}; + +// Maps EasingType -> the corresponding core easing function (never null). +[[nodiscard]] inline math::EasingFunction toFunction(EasingType type) noexcept { + switch (type) { + case EasingType::Linear: return math::easeInLinear; + case EasingType::EaseInQuadratic: return math::easeInQuadratic; + case EasingType::EaseOutQuadratic: return math::easeOutQuadratic; + case EasingType::EaseInOutQuadratic: return math::easeInOutQuadratic; + case EasingType::EaseInCubic: return math::easeInCubic; + case EasingType::EaseOutCubic: return math::easeOutCubic; + case EasingType::EaseInOutCubic: return math::easeInOutCubic; + case EasingType::EaseInQuartic: return math::easeInQuartic; + case EasingType::EaseOutQuartic: return math::easeOutQuartic; + case EasingType::EaseInOutQuartic: return math::easeInOutQuartic; + case EasingType::EaseInQuintic: return math::easeInQuintic; + case EasingType::EaseOutQuintic: return math::easeOutQuintic; + case EasingType::EaseInOutQuintic: return math::easeInOutQuintic; + case EasingType::EaseInSin: return math::easeInSin; + case EasingType::EaseOutSin: return math::easeOutSin; + case EasingType::EaseInOutSin: return math::easeInOutSin; + case EasingType::EaseInExponential: return math::easeInExponential; + case EasingType::EaseOutExponential: return math::easeOutExponential; + case EasingType::EaseInOutExponential:return math::easeInOutExponential; + case EasingType::EaseInCircular: return math::easeInCircular; + case EasingType::EaseOutCircular: return math::easeOutCircular; + case EasingType::EaseInOutCircular: return math::easeInOutCircular; + case EasingType::EaseInBack: return math::easeInBack; + case EasingType::EaseOutBack: return math::easeOutBack; + case EasingType::EaseInOutBack: return math::easeInOutBack; + case EasingType::EaseInElastic: return math::easeInElastic; + case EasingType::EaseOutElastic: return math::easeOutElastic; + case EasingType::EaseInOutElastic: return math::easeInOutElastic; + case EasingType::EaseInBounce: return math::easeInBounce; + case EasingType::EaseOutBounce: return math::easeOutBounce; + case EasingType::EaseInOutBounce: return math::easeInOutBounce; + default: return math::easeInLinear; + } +} + +// Applies an easing function to an interpolation factor t in [0,1]. +[[nodiscard]] inline f32 applyEasing(EasingType type, f32 t) noexcept { + if (type == EasingType::Linear) { return t; } + return toFunction(type)(t); +} + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/EasingTypeTests.test.cpp b/Engine/cpp/Runtime/Animation/EasingTypeTests.test.cpp new file mode 100644 index 00000000..15bd99e4 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/EasingTypeTests.test.cpp @@ -0,0 +1,33 @@ +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +TEST_CASE("easing: Linear returns input unchanged") +{ + CHECK(applyEasing(EasingType::Linear, 0.0f) == 0.0f); + CHECK(applyEasing(EasingType::Linear, 0.5f) == 0.5f); + CHECK(applyEasing(EasingType::Linear, 1.0f) == 1.0f); +} + +TEST_CASE("easing: ToFunction non-null + Apply boundary values for all types") +{ + for (i32 i = 0; i < static_cast(EasingType::Count); ++i) + { + const EasingType type = static_cast(i); + CHECK(toFunction(type) != nullptr); + CHECK(std::abs(applyEasing(type, 0.0f)) < 0.01f); + CHECK(std::abs(applyEasing(type, 1.0f) - 1.0f) < 0.01f); + } +} + +TEST_CASE("easing: in/out shape at midpoint") +{ + CHECK(applyEasing(EasingType::EaseInQuadratic, 0.5f) < 0.5f); // slow start + CHECK(applyEasing(EasingType::EaseOutQuadratic, 0.5f) > 0.5f); // fast start + CHECK(std::abs(applyEasing(EasingType::EaseInOutCubic, 0.5f) - 0.5f) < 0.01f); // symmetric +} diff --git a/Engine/cpp/Runtime/Animation/Graph.cppm b/Engine/cpp/Runtime/Animation/Graph.cppm new file mode 100644 index 00000000..49a0e310 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Graph.cppm @@ -0,0 +1,672 @@ +/// Animation state graph and blend trees (`:graph` partition). +/// +/// The animation graph stack: state nodes (clip + 1D/2D +/// blend trees), per-bone masks, parameters + conditions + transitions, states, layers, the graph +/// definition, and the per-instance AnimationGraphPlayer (state machine + layer blending). Resource +/// refs (editor/serialization) are dropped - this is the runtime foundation. Polymorphic node +/// dispatch uses a NodeType tag + static_cast (the engine builds with -fno-rtti, so no dynamic_cast). + +module; +#include +#include +#include +#include +#include +#include +#include + +export module animation:graph; + +import core; +import :skeleton; // Skeleton, Bone, BoneTransform +import :clip; // AnimationClip, AnimationEventHandler +import :sampler; // SampleClip, BlendPoses + +using namespace draco; + +export namespace draco::animation { + +// ---- state nodes --------------------------------------------------------------------------- + +enum class NodeType { Clip, BlendTree1D, BlendTree2D }; + +// A node that produces an animation pose. Implemented by ClipStateNode + the blend trees. +class IAnimationStateNode { +public: + virtual ~IAnimationStateNode() = default; + [[nodiscard]] virtual NodeType type() const noexcept = 0; + // Evaluate at normalized time [0..1] into outPoses. + virtual void evaluate(const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const = 0; + [[nodiscard]] virtual f32 duration() const noexcept = 0; + virtual void fireEvents(f32 prevNormalizedTime, f32 currentNormalizedTime, bool looping, const AnimationEventHandler& handler) const = 0; +}; + +// Wraps a single AnimationClip (borrowed). +class ClipStateNode final : public IAnimationStateNode { +public: + explicit ClipStateNode(AnimationClip* clip) : m_clip(clip) {} + [[nodiscard]] NodeType type() const noexcept override { return NodeType::Clip; } + [[nodiscard]] AnimationClip* clip() const noexcept { return m_clip; } + + void evaluate(const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const override { + if (m_clip == nullptr) { return; } + sampleClip(*m_clip, skeleton, normalizedTime * m_clip->duration, outPoses); + } + [[nodiscard]] f32 duration() const noexcept override { return m_clip != nullptr ? m_clip->duration : 0.0f; } + + void fireEvents(f32 prevNorm, f32 currentNorm, bool looping, const AnimationEventHandler& handler) const override { + if (m_clip == nullptr || !handler || m_clip->events().empty() || m_clip->duration <= 0.0f) { return; } + const f32 prevAbs = prevNorm * m_clip->duration; + const f32 curAbs = currentNorm * m_clip->duration; + if (looping && currentNorm < prevNorm) { + // wrapped: (prevAbs, Duration] then [0, curAbs] + for (const AnimationEvent& e : m_clip->events()) { if (e.time > prevAbs && e.time <= m_clip->duration) { handler(std::u8string_view{ e.name }, e.time); } } + for (const AnimationEvent& e : m_clip->events()) { if (e.time <= curAbs) { handler(std::u8string_view{ e.name }, e.time); } } + } else { + m_clip->fireEvents(prevAbs, curAbs, handler); + } + } +private: + AnimationClip* m_clip = nullptr; +}; + +// ---- bone mask ----------------------------------------------------------------------------- + +class BoneMask { +public: + explicit BoneMask(i32 boneCount, f32 defaultWeight = 1.0f) { + m_weights.resize(boneCount < 0 ? 0 : static_cast(boneCount)); + for (usize i = 0; i < m_weights.size(); ++i) { m_weights[i] = defaultWeight; } + } + [[nodiscard]] i32 boneCount() const noexcept { return static_cast(m_weights.size()); } + [[nodiscard]] f32 getWeight(i32 boneIndex) const { + return (boneIndex >= 0 && static_cast(boneIndex) < m_weights.size()) ? m_weights[static_cast(boneIndex)] : 0.0f; + } + void setWeight(i32 boneIndex, f32 weight) { + if (boneIndex >= 0 && static_cast(boneIndex) < m_weights.size()) { m_weights[static_cast(boneIndex)] = std::clamp(weight, 0.0f, 1.0f); } + } + void setAll(f32 weight) { const f32 c = std::clamp(weight, 0.0f, 1.0f); for (usize i = 0; i < m_weights.size(); ++i) { m_weights[i] = c; } } + void setBoneChainWeight(const Skeleton& skeleton, i32 boneIndex, f32 weight) { + if (boneIndex < 0 || boneIndex >= skeleton.boneCount()) { return; } + const f32 c = std::clamp(weight, 0.0f, 1.0f); + setWeight(boneIndex, c); + if (const Bone* bone = skeleton.getBone(boneIndex)) { + for (i32 child : bone->children) { setBoneChainWeight(skeleton, child, c); } + } + } + [[nodiscard]] std::span weights() const noexcept { return { m_weights.data(), m_weights.size() }; } +private: + std::vector m_weights; +}; + +// ---- blend trees --------------------------------------------------------------------------- + +struct BlendTree1DEntry { f32 threshold = 0.0f; AnimationClip* clip = nullptr; }; + +// Blends clips along one float axis (Parameter). Entries kept sorted by threshold. +class BlendTree1D final : public IAnimationStateNode { +public: + i32 parameterIndex = -1; // graph parameter that drives this tree + f32 parameter = 0.0f; + + [[nodiscard]] NodeType type() const noexcept override { return NodeType::BlendTree1D; } + [[nodiscard]] std::vector& entries() noexcept { return m_entries; } + + void addEntry(f32 threshold, AnimationClip* clip) { + // Append then bubble left into sorted-by-threshold position (Array has no Insert). + m_entries.push_back(BlendTree1DEntry{ threshold, clip }); + for (usize i = m_entries.size() - 1; i > 0; --i) { + if (m_entries[i - 1].threshold <= m_entries[i].threshold) { break; } + const BlendTree1DEntry tmp = m_entries[i - 1]; m_entries[i - 1] = m_entries[i]; m_entries[i] = tmp; + } + } + + void evaluate(const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const override { + if (m_entries.empty()) { return; } + if (m_entries.size() == 1) { sampleEntry(0, skeleton, normalizedTime, outPoses); return; } + if (parameter <= m_entries[0].threshold) { sampleEntry(0, skeleton, normalizedTime, outPoses); return; } + if (parameter >= m_entries[m_entries.size() - 1].threshold) { sampleEntry(m_entries.size() - 1, skeleton, normalizedTime, outPoses); return; } + + usize lowIdx = 0, highIdx = 1; + for (usize i = 0; i + 1 < m_entries.size(); ++i) { + if (parameter >= m_entries[i].threshold && parameter <= m_entries[i + 1].threshold) { lowIdx = i; highIdx = i + 1; break; } + } + AnimationClip* clipA = m_entries[lowIdx].clip; + AnimationClip* clipB = m_entries[highIdx].clip; + if (clipA == nullptr && clipB == nullptr) { return; } + if (clipA == nullptr) { sampleEntry(highIdx, skeleton, normalizedTime, outPoses); return; } + if (clipB == nullptr) { sampleEntry(lowIdx, skeleton, normalizedTime, outPoses); return; } + + const f32 range = m_entries[highIdx].threshold - m_entries[lowIdx].threshold; + const f32 blend = (range > 0.0f) ? (parameter - m_entries[lowIdx].threshold) / range : 0.0f; + const usize n = static_cast(skeleton.boneCount()); + std::vector a; a.resize(n); + std::vector b; b.resize(n); + sampleClip(*clipA, skeleton, normalizedTime * clipA->duration, std::span{ a.data(), n }); + sampleClip(*clipB, skeleton, normalizedTime * clipB->duration, std::span{ b.data(), n }); + blendPoses(std::span{ a.data(), n }, std::span{ b.data(), n }, blend, outPoses); + } + + [[nodiscard]] f32 duration() const noexcept override { + f32 bestDist = 3.4e38f, bestDuration = 0.0f; + for (const BlendTree1DEntry& e : m_entries) { + const f32 dist = std::abs(e.threshold - parameter); + if (dist < bestDist && e.clip != nullptr) { bestDist = dist; bestDuration = e.clip->duration; } + } + return bestDuration; + } + void fireEvents(f32, f32, bool, const AnimationEventHandler&) const override {} // blend trees don't fire clip events + +private: + void sampleEntry(usize idx, const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const { + AnimationClip* clip = m_entries[idx].clip; + if (clip != nullptr) { sampleClip(*clip, skeleton, normalizedTime * clip->duration, outPoses); } + } + std::vector m_entries; +}; + +struct BlendTree2DEntry { math::Vector2 position = math::Vector2{ 0, 0 }; AnimationClip* clip = nullptr; }; + +// Blends clips in a 2D parameter space via inverse-distance weighting. +class BlendTree2D final : public IAnimationStateNode { +public: + i32 parameterIndexX = -1, parameterIndexY = -1; + f32 parameterX = 0.0f, parameterY = 0.0f; + + [[nodiscard]] NodeType type() const noexcept override { return NodeType::BlendTree2D; } + [[nodiscard]] std::vector& entries() noexcept { return m_entries; } + void addEntry(math::Vector2 position, AnimationClip* clip) { m_entries.push_back(BlendTree2DEntry{ position, clip }); } + void addEntry(f32 x, f32 y, AnimationClip* clip) { m_entries.push_back(BlendTree2DEntry{ math::Vector2{ x, y }, clip }); } + + void evaluate(const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const override { + if (m_entries.empty()) { return; } + if (m_entries.size() == 1) { sampleEntry(0, skeleton, normalizedTime, outPoses); return; } + + const math::Vector2 paramPos{ parameterX, parameterY }; + std::vector weights; weights.resize(m_entries.size()); + f32 totalWeight = 0.0f; + for (usize i = 0; i < m_entries.size(); ++i) { + const f32 dist = length(paramPos - m_entries[i].position); + if (dist < 0.0001f) { sampleEntry(i, skeleton, normalizedTime, outPoses); return; } // on top of an entry + weights[i] = 1.0f / dist; + totalWeight += weights[i]; + } + if (totalWeight > 0.0f) { for (usize i = 0; i < weights.size(); ++i) { weights[i] /= totalWeight; } } + + const usize n = static_cast(skeleton.boneCount()); + std::vector temp; temp.resize(n); + bool firstSample = true; + for (usize i = 0; i < m_entries.size(); ++i) { + if (weights[i] < 0.001f || m_entries[i].clip == nullptr) { continue; } + AnimationClip* clip = m_entries[i].clip; + sampleClip(*clip, skeleton, normalizedTime * clip->duration, std::span{ temp.data(), n }); + if (firstSample) { + for (usize b = 0; b < n && b < outPoses.size(); ++b) { outPoses[b] = temp[b]; } + firstSample = false; + if (weights[i] > 0.999f) { return; } + continue; + } + f32 accumulated = 0.0f; + for (usize j = 0; j < i; ++j) { if (weights[j] >= 0.001f && m_entries[j].clip != nullptr) { accumulated += weights[j]; } } + const f32 relative = weights[i] / (accumulated + weights[i]); + blendPoses(std::span{ outPoses.data(), outPoses.size() }, std::span{ temp.data(), n }, relative, outPoses); + } + } + + [[nodiscard]] f32 duration() const noexcept override { + const math::Vector2 paramPos{ parameterX, parameterY }; + f32 totalWeight = 0.0f, totalDuration = 0.0f; + for (const BlendTree2DEntry& e : m_entries) { + if (e.clip == nullptr) { continue; } + const f32 dist = length(paramPos - e.position); + if (dist < 0.0001f) { return e.clip->duration; } + const f32 w = 1.0f / dist; + totalWeight += w; totalDuration += w * e.clip->duration; + } + return totalWeight > 0.0f ? totalDuration / totalWeight : 0.0f; + } + void fireEvents(f32, f32, bool, const AnimationEventHandler&) const override {} + +private: + void sampleEntry(usize idx, const Skeleton& skeleton, f32 normalizedTime, std::span outPoses) const { + AnimationClip* clip = m_entries[idx].clip; + if (clip != nullptr) { sampleClip(*clip, skeleton, normalizedTime * clip->duration, outPoses); } + } + std::vector m_entries; +}; + +// ---- parameters + conditions + transitions ------------------------------------------------- + +enum class AnimationParameterType { Float, Int, Bool, Trigger }; + +// A named value driving transitions/blend trees. Value type (copied into the player's runtime set). +class AnimationGraphParameter { +public: + AnimationGraphParameter() = default; + AnimationGraphParameter(std::u8string_view name, AnimationParameterType t) : type(t), m_name(name) {} + + AnimationParameterType type = AnimationParameterType::Float; + f32 floatValue = 0.0f; + i32 intValue = 0; + bool boolValue = false; + + [[nodiscard]] const std::u8string& name() const noexcept { return m_name; } + void consumeTrigger() { if (type == AnimationParameterType::Trigger) { boolValue = false; } } +private: + std::u8string m_name; +}; + +enum class ComparisonOp { Equal, NotEqual, Greater, Less, GreaterEqual, LessEqual }; + +// One transition condition: compare a parameter against a threshold. +struct AnimationGraphCondition { + i32 parameterIndex = 0; + ComparisonOp op = ComparisonOp::Equal; + f32 threshold = 0.0f; + + AnimationGraphCondition() = default; + AnimationGraphCondition(i32 paramIndex, ComparisonOp o, f32 thr = 0.0f) : parameterIndex(paramIndex), op(o), threshold(thr) {} + + [[nodiscard]] bool evaluate(const AnimationGraphParameter* param) const { + if (param == nullptr) { return false; } + switch (param->type) { + case AnimationParameterType::Float: return compareFloat(param->floatValue); + case AnimationParameterType::Int: return compareFloat(static_cast(param->intValue)); + case AnimationParameterType::Bool: + case AnimationParameterType::Trigger: + switch (op) { + case ComparisonOp::Equal: return param->boolValue == (threshold > 0.5f); + case ComparisonOp::NotEqual: return param->boolValue != (threshold > 0.5f); + default: return param->boolValue; + } + } + return false; + } +private: + [[nodiscard]] bool compareFloat(f32 value) const { + switch (op) { + case ComparisonOp::Equal: return std::abs(value - threshold) < 0.0001f; + case ComparisonOp::NotEqual: return std::abs(value - threshold) >= 0.0001f; + case ComparisonOp::Greater: return value > threshold; + case ComparisonOp::Less: return value < threshold; + case ComparisonOp::GreaterEqual: return value >= threshold; + case ComparisonOp::LessEqual: return value <= threshold; + } + return false; + } +}; + +// A transition between states; fires when all conditions hold (+ optional exit-time gate). +class AnimationGraphTransition { +public: + i32 sourceStateIndex = -1; // -1 = "Any State" + i32 destStateIndex = 0; + f32 duration = 0.25f; // cross-fade seconds + bool hasExitTime = false; + f32 exitTime = 1.0f; + i32 priority = 0; // lower = higher priority + + [[nodiscard]] std::vector& conditions() noexcept { return m_conditions; } + [[nodiscard]] const std::vector& conditions() const noexcept { return m_conditions; } + + void addBoolCondition(i32 parameterIndex, bool expected = true) { m_conditions.push_back(AnimationGraphCondition{ parameterIndex, ComparisonOp::Equal, expected ? 1.0f : 0.0f }); } + void addFloatCondition(i32 parameterIndex, ComparisonOp op, f32 threshold) { m_conditions.push_back(AnimationGraphCondition{ parameterIndex, op, threshold }); } + void addIntCondition(i32 parameterIndex, ComparisonOp op, i32 threshold) { m_conditions.push_back(AnimationGraphCondition{ parameterIndex, op, static_cast(threshold) }); } + + // All conditions must hold against the parameter set. Empty -> unconditional (true). + [[nodiscard]] bool evaluateConditions(std::span parameters) const { + for (const AnimationGraphCondition& c : m_conditions) { + if (c.parameterIndex < 0 || static_cast(c.parameterIndex) >= parameters.size()) { return false; } + if (!c.evaluate(¶meters[static_cast(c.parameterIndex)])) { return false; } + } + return true; + } +private: + std::vector m_conditions; +}; + +// ---- states + layers + graph --------------------------------------------------------------- + +// A state wraps a node (clip or blend tree) + playback settings. Owns the node when constructed with +// a UniquePtr; borrows it (no delete) when constructed with a raw pointer. +class AnimationGraphState { +public: + AnimationGraphState(std::u8string_view name, IAnimationStateNode* node) : m_name(name), m_node(node) {} + AnimationGraphState(std::u8string_view name, std::unique_ptr node) + : m_name(name), m_node(node.get()), m_owned(static_cast&&>(node)), m_ownsNode(true) {} + + f32 speed = 1.0f; + bool loop = true; + + [[nodiscard]] const std::u8string& name() const noexcept { return m_name; } + [[nodiscard]] IAnimationStateNode* node() const noexcept { return m_node; } + [[nodiscard]] bool ownsNode() const noexcept { return m_ownsNode; } + [[nodiscard]] f32 duration() const noexcept { return m_node != nullptr ? m_node->duration() : 0.0f; } +private: + std::u8string m_name; + IAnimationStateNode* m_node = nullptr; // active node (borrowed or == m_owned) + std::unique_ptr m_owned; // set when owning + bool m_ownsNode = false; +}; + +enum class LayerBlendMode { Override, Additive }; + +// A layer: states + transitions + an optional bone mask. Layer 0 is the base; others blend on top. +class AnimationLayer { +public: + explicit AnimationLayer(std::u8string_view name) : m_name(name) {} + + i32 defaultStateIndex = 0; + LayerBlendMode blendMode = LayerBlendMode::Override; + f32 weight = 1.0f; + + [[nodiscard]] const std::u8string& name() const noexcept { return m_name; } + [[nodiscard]] std::vector>& states() noexcept { return m_states; } + [[nodiscard]] std::vector>& transitions() noexcept { return m_transitions; } + [[nodiscard]] BoneMask* mask() const noexcept { return m_mask.get(); } + void setMask(std::unique_ptr mask) { m_mask = static_cast&&>(mask); } + + i32 addState(std::unique_ptr state) { const i32 idx = static_cast(m_states.size()); m_states.push_back(static_cast&&>(state)); return idx; } + void addTransition(std::unique_ptr t) { m_transitions.push_back(static_cast&&>(t)); } + [[nodiscard]] AnimationGraphState* getState(i32 index) const { + return (index >= 0 && static_cast(index) < m_states.size()) ? m_states[static_cast(index)].get() : nullptr; + } + [[nodiscard]] i32 findStateIndex(std::u8string_view name) const { + for (usize i = 0; i < m_states.size(); ++i) { if (m_states[i]->name() == name) { return static_cast(i); } } + return -1; + } +private: + std::u8string m_name; + std::vector> m_states; + std::vector> m_transitions; + std::unique_ptr m_mask; +}; + +// Shared graph definition: parameters + layers. Multiple players can reference one graph. +// A resource product (Object) so a cooked AnimationGraphSource can build into it. +class AnimationGraph { +public: + [[nodiscard]] std::vector& parameters() noexcept { return m_parameters; } + [[nodiscard]] const std::vector& parameters() const noexcept { return m_parameters; } + [[nodiscard]] std::vector>& layers() noexcept { return m_layers; } + + i32 addParameter(std::u8string_view name, AnimationParameterType type) { + const i32 idx = static_cast(m_parameters.size()); + m_parameters.push_back(AnimationGraphParameter{ name, type }); + return idx; + } + [[nodiscard]] i32 findParameter(std::u8string_view name) const { + for (usize i = 0; i < m_parameters.size(); ++i) { if (m_parameters[i].name() == name) { return static_cast(i); } } + return -1; + } + [[nodiscard]] AnimationGraphParameter* getParameter(i32 index) { + return (index >= 0 && static_cast(index) < m_parameters.size()) ? &m_parameters[static_cast(index)] : nullptr; + } + i32 addLayer(std::unique_ptr layer) { const i32 idx = static_cast(m_layers.size()); m_layers.push_back(static_cast&&>(layer)); return idx; } +private: + std::vector m_parameters; + std::vector> m_layers; +}; + +// ---- graph player -------------------------------------------------------------------------- + +// Per-layer runtime state (current/previous state + cross-fade + scratch poses). +struct AnimationGraphLayerRuntime { + i32 currentStateIndex = -1; + f32 currentTime = 0.0f; + i32 previousStateIndex = -1; + f32 previousTime = 0.0f; + f32 transitionElapsed = 0.0f; + f32 transitionDuration = 0.0f; + bool isTransitioning = false; + std::vector poses; + std::vector prevPoses; + + void init(usize boneCount) { poses.resize(boneCount); prevPoses.resize(boneCount); } + void reset(i32 defaultStateIndex) { + currentStateIndex = defaultStateIndex; currentTime = 0.0f; + previousStateIndex = -1; previousTime = 0.0f; + transitionElapsed = 0.0f; transitionDuration = 0.0f; isTransitioning = false; + } +}; + +// Evaluates an AnimationGraph for one skeleton instance (state machine + layer blending). +class AnimationGraphPlayer { +public: + AnimationGraphPlayer(AnimationGraph& graph, Skeleton& skeleton) : m_graph(&graph), m_skeleton(&skeleton) { + const usize boneCount = static_cast(skeleton.boneCount()); + m_finalPoses.resize(boneCount); + m_skinningMatrices.resize(boneCount); + m_prevSkinningMatrices.resize(boneCount); + for (usize i = 0; i < boneCount; ++i) { m_skinningMatrices[i] = math::Matrix4::identity(); m_prevSkinningMatrices[i] = math::Matrix4::identity(); } + + // Runtime parameter copy (each player has independent values). + for (const AnimationGraphParameter& p : graph.parameters()) { m_parameters.push_back(p); } + + for (auto& layer : graph.layers()) { + AnimationGraphLayerRuntime rt; rt.init(boneCount); rt.reset(layer->defaultStateIndex); + m_layerRuntimes.push_back(static_cast(rt)); + } + + // Auto-link blend trees from their stored parameter indices (NodeType tag - no RTTI). + for (auto& layer : graph.layers()) { + for (auto& state : layer->states()) { + IAnimationStateNode* node = state->node(); + if (node == nullptr) { continue; } + if (node->type() == NodeType::BlendTree1D) { + auto* t = static_cast(node); + if (t->parameterIndex >= 0) { m_links1D.push_back(Link1D{ t, t->parameterIndex }); } + } else if (node->type() == NodeType::BlendTree2D) { + auto* t = static_cast(node); + if (t->parameterIndexX >= 0 || t->parameterIndexY >= 0) { m_links2D.push_back(Link2D{ t, t->parameterIndexX, t->parameterIndexY }); } + } + } + } + resetToBind(); + } + + ~AnimationGraphPlayer() = default; + AnimationGraphPlayer(const AnimationGraphPlayer&) = delete; + AnimationGraphPlayer& operator=(const AnimationGraphPlayer&) = delete; + + [[nodiscard]] Skeleton& getSkeleton() noexcept { return *m_skeleton; } + [[nodiscard]] AnimationGraph& getGraph() noexcept { return *m_graph; } + + // --- parameter access --- + void setFloat(i32 i, f32 v) { if (valid(i)) { m_parameters[static_cast(i)].floatValue = v; } } + void setFloat(std::u8string_view n, f32 v) { setFloat(m_graph->findParameter(n), v); } + [[nodiscard]] f32 getFloat(i32 i) const { return valid(i) ? m_parameters[static_cast(i)].floatValue : 0.0f; } + void setInt(i32 i, i32 v) { if (valid(i)) { m_parameters[static_cast(i)].intValue = v; } } + void setInt(std::u8string_view n, i32 v) { setInt(m_graph->findParameter(n), v); } + [[nodiscard]] i32 getInt(i32 i) const { return valid(i) ? m_parameters[static_cast(i)].intValue : 0; } + void setBool(i32 i, bool v) { if (valid(i)) { m_parameters[static_cast(i)].boolValue = v; } } + void setBool(std::u8string_view n, bool v) { setBool(m_graph->findParameter(n), v); } + [[nodiscard]] bool getBool(i32 i) const { return valid(i) ? m_parameters[static_cast(i)].boolValue : false; } + void setTrigger(i32 i) { if (valid(i)) { m_parameters[static_cast(i)].boolValue = true; } } + void setTrigger(std::u8string_view n){ setTrigger(m_graph->findParameter(n)); } + + void setEventHandler(AnimationEventHandler handler) { m_eventHandler = static_cast(handler); } + + void update(f32 deltaTime) { + for (usize i = 0; i < m_skinningMatrices.size(); ++i) { m_prevSkinningMatrices[i] = m_skinningMatrices[i]; } + syncBlendTreeParameters(); + for (usize i = 0; i < m_graph->layers().size() && i < m_layerRuntimes.size(); ++i) { + updateLayer(*m_graph->layers()[i], m_layerRuntimes[i], deltaTime); + } + for (AnimationGraphParameter& p : m_parameters) { p.consumeTrigger(); } + combineLayers(); + m_matricesDirty = true; + } + + [[nodiscard]] std::span getSkinningMatrices() { + if (m_matricesDirty) { + m_skeleton->computeSkinningMatrices(std::span{ m_finalPoses.data(), m_finalPoses.size() }, + std::span{ m_skinningMatrices.data(), m_skinningMatrices.size() }); + m_matricesDirty = false; + } + return std::span{ m_skinningMatrices.data(), m_skinningMatrices.size() }; + } + [[nodiscard]] std::span getPrevSkinningMatrices() const { return { m_prevSkinningMatrices.data(), m_prevSkinningMatrices.size() }; } + [[nodiscard]] std::span getLocalPoses() noexcept { return { m_finalPoses.data(), m_finalPoses.size() }; } + + // --- state query --- + [[nodiscard]] i32 getCurrentStateIndex(i32 layerIndex = 0) const { return (layerIndex >= 0 && static_cast(layerIndex) < m_layerRuntimes.size()) ? m_layerRuntimes[static_cast(layerIndex)].currentStateIndex : -1; } + [[nodiscard]] bool isTransitioning(i32 layerIndex = 0) const { return (layerIndex >= 0 && static_cast(layerIndex) < m_layerRuntimes.size()) ? m_layerRuntimes[static_cast(layerIndex)].isTransitioning : false; } + [[nodiscard]] f32 getCurrentNormalizedTime(i32 layerIndex = 0) const { return (layerIndex >= 0 && static_cast(layerIndex) < m_layerRuntimes.size()) ? m_layerRuntimes[static_cast(layerIndex)].currentTime : 0.0f; } + + void resetToBind() { + for (i32 i = 0; i < m_skeleton->boneCount() && static_cast(i) < m_finalPoses.size(); ++i) { + const Bone* bone = m_skeleton->getBone(i); + m_finalPoses[static_cast(i)] = (bone != nullptr) ? bone->localBindPose : BoneTransform{}; + } + m_matricesDirty = true; + } + void forceState(i32 stateIndex, i32 layerIndex = 0) { + if (layerIndex >= 0 && static_cast(layerIndex) < m_layerRuntimes.size()) { + AnimationGraphLayerRuntime& rt = m_layerRuntimes[static_cast(layerIndex)]; + rt.currentStateIndex = stateIndex; rt.currentTime = 0.0f; rt.isTransitioning = false; rt.previousStateIndex = -1; + } + } + +private: + [[nodiscard]] bool valid(i32 i) const noexcept { return i >= 0 && static_cast(i) < m_parameters.size(); } + [[nodiscard]] std::span params() const noexcept { return { m_parameters.data(), m_parameters.size() }; } + + void updateLayer(AnimationLayer& layer, AnimationGraphLayerRuntime& rt, f32 deltaTime) { + if (rt.currentStateIndex < 0 || static_cast(rt.currentStateIndex) >= layer.states().size()) { return; } + AnimationGraphState* currentState = layer.getState(rt.currentStateIndex); + + if (!rt.isTransitioning) { evaluateTransitions(layer, rt); } + + if (rt.isTransitioning) { + const f32 prevNorm = rt.currentTime; + advanceStateTime(*currentState, rt.currentTime, deltaTime); + if (AnimationGraphState* prevState = layer.getState(rt.previousStateIndex)) { advanceStateTime(*prevState, rt.previousTime, deltaTime); } + currentState = layer.getState(rt.currentStateIndex); // (re-fetch; index unchanged) + if (m_eventHandler && currentState != nullptr && currentState->node() != nullptr) { + currentState->node()->fireEvents(prevNorm, rt.currentTime, currentState->loop, m_eventHandler); + } + rt.transitionElapsed += deltaTime; + if (rt.transitionElapsed >= rt.transitionDuration) { rt.isTransitioning = false; rt.previousStateIndex = -1; } + } else { + const f32 prevNorm = rt.currentTime; + advanceStateTime(*currentState, rt.currentTime, deltaTime); + if (m_eventHandler && currentState->node() != nullptr) { + currentState->node()->fireEvents(prevNorm, rt.currentTime, currentState->loop, m_eventHandler); + } + } + sampleLayerPoses(layer, rt); + } + + void advanceStateTime(AnimationGraphState& state, f32& normalizedTime, f32 deltaTime) { + if (state.duration() <= 0.0f) { return; } + normalizedTime += (deltaTime * state.speed) / state.duration(); + if (state.loop) { + while (normalizedTime >= 1.0f) { normalizedTime -= 1.0f; } + while (normalizedTime < 0.0f) { normalizedTime += 1.0f; } + } else { + normalizedTime = std::clamp(normalizedTime, 0.0f, 1.0f); + } + } + + void evaluateTransitions(AnimationLayer& layer, AnimationGraphLayerRuntime& rt) { + AnimationGraphTransition* best = nullptr; + i32 bestPriority = 2147483647; + for (auto& tr : layer.transitions()) { + if (tr->sourceStateIndex != -1 && tr->sourceStateIndex != rt.currentStateIndex) { continue; } + if (tr->destStateIndex == rt.currentStateIndex) { continue; } + if (tr->hasExitTime && rt.currentTime < tr->exitTime) { continue; } + if (!tr->evaluateConditions(params())) { continue; } + if (tr->priority < bestPriority) { bestPriority = tr->priority; best = tr.get(); } + } + if (best != nullptr) { + rt.previousStateIndex = rt.currentStateIndex; + rt.previousTime = rt.currentTime; + rt.currentStateIndex = best->destStateIndex; + rt.currentTime = 0.0f; + rt.transitionElapsed = 0.0f; + rt.transitionDuration = std::max(best->duration, 0.001f); + rt.isTransitioning = true; + } + } + + void sampleLayerPoses(AnimationLayer& layer, AnimationGraphLayerRuntime& rt) { + if (rt.currentStateIndex < 0) { return; } + AnimationGraphState* currentState = layer.getState(rt.currentStateIndex); + if (currentState == nullptr || currentState->node() == nullptr) { return; } + + if (rt.isTransitioning && rt.previousStateIndex >= 0) { + AnimationGraphState* prevState = layer.getState(rt.previousStateIndex); + if (prevState != nullptr && prevState->node() != nullptr) { + prevState->node()->evaluate(*m_skeleton, rt.previousTime, std::span{ rt.prevPoses.data(), rt.prevPoses.size() }); + currentState->node()->evaluate(*m_skeleton, rt.currentTime, std::span{ rt.poses.data(), rt.poses.size() }); + const f32 blend = std::clamp(rt.transitionElapsed / rt.transitionDuration, 0.0f, 1.0f); + blendPoses(std::span{ rt.prevPoses.data(), rt.prevPoses.size() }, + std::span{ rt.poses.data(), rt.poses.size() }, blend, + std::span{ rt.poses.data(), rt.poses.size() }); + return; + } + } + currentState->node()->evaluate(*m_skeleton, rt.currentTime, std::span{ rt.poses.data(), rt.poses.size() }); + } + + void syncBlendTreeParameters() { + for (const Link1D& l : m_links1D) { if (valid(l.paramIndex)) { l.tree->parameter = m_parameters[static_cast(l.paramIndex)].floatValue; } } + for (const Link2D& l : m_links2D) { + if (valid(l.paramIndexX)) { l.tree->parameterX = m_parameters[static_cast(l.paramIndexX)].floatValue; } + if (valid(l.paramIndexY)) { l.tree->parameterY = m_parameters[static_cast(l.paramIndexY)].floatValue; } + } + } + + void combineLayers() { + if (m_layerRuntimes.empty()) { return; } + // Base layer writes directly. + const AnimationGraphLayerRuntime& base = m_layerRuntimes[0]; + for (usize i = 0; i < m_finalPoses.size() && i < base.poses.size(); ++i) { m_finalPoses[i] = base.poses[i]; } + + for (usize layerIdx = 1; layerIdx < m_layerRuntimes.size() && layerIdx < m_graph->layers().size(); ++layerIdx) { + AnimationLayer& layer = *m_graph->layers()[layerIdx]; + const AnimationGraphLayerRuntime& rt = m_layerRuntimes[layerIdx]; + if (layer.weight <= 0.0f) { continue; } + const BoneMask* mask = layer.mask(); + + if (layer.blendMode == LayerBlendMode::Override) { + for (usize b = 0; b < m_finalPoses.size() && b < rt.poses.size(); ++b) { + const f32 w = layer.weight * (mask != nullptr ? mask->getWeight(static_cast(b)) : 1.0f); + if (w > 0.0f) { m_finalPoses[b] = BoneTransform::lerp(m_finalPoses[b], rt.poses[b], w); } + } + } else { // Additive: delta from bind pose + for (usize b = 0; b < m_finalPoses.size() && b < rt.poses.size(); ++b) { + const f32 w = layer.weight * (mask != nullptr ? mask->getWeight(static_cast(b)) : 1.0f); + if (w <= 0.0f) { continue; } + const Bone* bone = m_skeleton->getBone(static_cast(b)); + const BoneTransform bind = (bone != nullptr) ? bone->localBindPose : BoneTransform{}; + const math::Vector3 deltaPos = rt.poses[b].position - bind.position; + const math::Quaternion deltaRot = rt.poses[b].rotation * inverse(bind.rotation); + const math::Vector3 deltaScale = rt.poses[b].scale / bind.scale; // component-wise + m_finalPoses[b].position = m_finalPoses[b].position + deltaPos * w; + m_finalPoses[b].rotation = slerp(math::Quaternion::identity, deltaRot, w) * m_finalPoses[b].rotation; + m_finalPoses[b].scale = m_finalPoses[b].scale * lerp(math::Vector3::one, deltaScale, w); + } + } + } + } + + struct Link1D { BlendTree1D* tree; i32 paramIndex; }; + struct Link2D { BlendTree2D* tree; i32 paramIndexX; i32 paramIndexY; }; + + AnimationGraph* m_graph = nullptr; + Skeleton* m_skeleton = nullptr; + std::vector m_layerRuntimes; + std::vector m_parameters; // runtime copy + std::vector m_finalPoses; + std::vector m_skinningMatrices; + std::vector m_prevSkinningMatrices; + bool m_matricesDirty = true; + AnimationEventHandler m_eventHandler; + std::vector m_links1D; + std::vector m_links2D; +}; + + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/GraphTests.test.cpp b/Engine/cpp/Runtime/Animation/GraphTests.test.cpp new file mode 100644 index 00000000..42acea8e --- /dev/null +++ b/Engine/cpp/Runtime/Animation/GraphTests.test.cpp @@ -0,0 +1,257 @@ +// Animation graph stack: parameters, conditions, transitions, states, layers, blend trees, graph, +// and a graph-player smoke test, plus a state-machine integration check. +#include +#include +#include +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +static IAnimationStateNode* kNullNode = static_cast(nullptr); + +// ---- BoneMask ---- +TEST_CASE("graph: BoneMask weights, clamping, out-of-range") +{ + BoneMask mask{ 4, 1.0f }; + CHECK(mask.boneCount() == 4); + CHECK(mask.getWeight(0) == 1.0f); + mask.setWeight(2, 0.75f); + CHECK(mask.getWeight(2) == 0.75f); + mask.setWeight(0, 2.0f); CHECK(mask.getWeight(0) == 1.0f); // clamp high + mask.setWeight(1, -1.0f); CHECK(mask.getWeight(1) == 0.0f); // clamp low + CHECK(mask.getWeight(-1) == 0.0f); // out of range + CHECK(mask.getWeight(99) == 0.0f); + BoneMask m2{ 2, 0.0f }; + m2.setAll(5.0f); CHECK(m2.getWeight(0) == 1.0f); // SetAll clamps +} + +TEST_CASE("graph: BoneMask SetBoneChainWeight follows hierarchy") +{ + Skeleton s{ 3 }; + s.bones()[0].index = 0; s.bones()[0].parentIndex = -1; + s.bones()[1].index = 1; s.bones()[1].parentIndex = 0; + s.bones()[2].index = 2; s.bones()[2].parentIndex = 1; + s.findRootBones(); s.buildChildIndices(); + BoneMask mask{ 3, 0.0f }; + mask.setBoneChainWeight(s, 0, 1.0f); // root + all descendants + CHECK(mask.getWeight(0) == 1.0f); + CHECK(mask.getWeight(1) == 1.0f); + CHECK(mask.getWeight(2) == 1.0f); +} + +// ---- AnimationGraphParameter ---- +TEST_CASE("graph: parameter set/get + trigger consume") +{ + AnimationGraphParameter sp{ u8"Speed", AnimationParameterType::Float }; + CHECK(sp.floatValue == 0.0f); + CHECK(sp.type == AnimationParameterType::Float); + CHECK(sp.name() == std::u8string_view{ u8"Speed" }); + sp.floatValue = 1.5f; CHECK(sp.floatValue == 1.5f); + + AnimationGraphParameter trig{ u8"Fire", AnimationParameterType::Trigger }; + trig.boolValue = true; + trig.consumeTrigger(); CHECK(trig.boolValue == false); // trigger resets + + AnimationGraphParameter b{ u8"Flag", AnimationParameterType::Bool }; + b.boolValue = true; b.consumeTrigger(); CHECK(b.boolValue == true); // bool not affected +} + +// ---- AnimationGraphCondition ---- +TEST_CASE("graph: condition float/int/bool comparisons") +{ + AnimationGraphParameter sp{ u8"Speed", AnimationParameterType::Float }; + sp.floatValue = 0.5f; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Greater, 0.1f }.evaluate(&sp) == true); + sp.floatValue = 0.05f; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Greater, 0.1f }.evaluate(&sp) == false); + sp.floatValue = 0.1f; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::LessEqual, 0.1f }.evaluate(&sp) == true); + sp.floatValue = 1.0f; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Equal, 1.0f }.evaluate(&sp) == true); + + AnimationGraphParameter lvl{ u8"Level", AnimationParameterType::Int }; + lvl.intValue = 5; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Greater, 3.0f }.evaluate(&lvl) == true); + + AnimationGraphParameter g{ u8"Grounded", AnimationParameterType::Bool }; + g.boolValue = true; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Equal, 1.0f }.evaluate(&g) == true); // "is true" + g.boolValue = false; + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Equal, 0.0f }.evaluate(&g) == true); // "is false" + + CHECK(AnimationGraphCondition{ 0, ComparisonOp::Greater, 0.0f }.evaluate(nullptr) == false); +} + +// ---- AnimationGraphTransition ---- +TEST_CASE("graph: transition condition evaluation") +{ + std::vector params; + params.push_back(AnimationGraphParameter{ u8"Speed", AnimationParameterType::Float }); + params.push_back(AnimationGraphParameter{ u8"Grounded", AnimationParameterType::Bool }); + params[0].floatValue = 1.0f; + params[1].boolValue = true; + const std::span pv{ params.data(), params.size() }; + + AnimationGraphTransition t; + CHECK(t.sourceStateIndex == -1); // defaults + CHECK(t.duration == 0.25f); + CHECK(t.evaluateConditions(pv) == true); // no conditions -> unconditional + + t.addFloatCondition(0, ComparisonOp::Greater, 0.1f); + t.addBoolCondition(1, true); + CHECK(t.evaluateConditions(pv) == true); // both met + params[1].boolValue = false; + CHECK(t.evaluateConditions(pv) == false); // one fails + + AnimationGraphTransition bad; + bad.addFloatCondition(5, ComparisonOp::Greater, 0.0f); // index out of range + CHECK(bad.evaluateConditions(pv) == false); +} + +// ---- AnimationGraphState ---- +TEST_CASE("graph: state defaults + node ownership") +{ + AnimationGraphState idle{ u8"Idle", kNullNode }; + CHECK(idle.name() == std::u8string_view{ u8"Idle" }); + CHECK(idle.node() == nullptr); + CHECK(idle.speed == 1.0f); + CHECK(idle.loop == true); + CHECK(idle.ownsNode() == false); + CHECK(idle.duration() == 0.0f); + + // Owned node freed on destruction (UniquePtr) - no leak/crash. + { + AnimationGraphState owned{ u8"Owned", std::make_unique(static_cast(nullptr)) }; + CHECK(owned.ownsNode() == true); + } + // Borrowed node survives the state. + ClipStateNode node{ nullptr }; + { AnimationGraphState borrow{ u8"Borrow", &node }; CHECK(borrow.ownsNode() == false); } + CHECK(node.duration() == 0.0f); +} + +// ---- AnimationLayer ---- +TEST_CASE("graph: layer states/transitions/mask") +{ + AnimationLayer layer{ u8"Base" }; + CHECK(layer.name() == std::u8string_view{ u8"Base" }); + CHECK(layer.defaultStateIndex == 0); + CHECK(layer.blendMode == LayerBlendMode::Override); + CHECK(layer.weight == 1.0f); + CHECK(layer.mask() == nullptr); + + CHECK(layer.addState(std::make_unique(std::u8string_view{ u8"Idle" }, kNullNode)) == 0); + CHECK(layer.addState(std::make_unique(std::u8string_view{ u8"Walk" }, kNullNode)) == 1); + CHECK(layer.states().size() == 2); + CHECK(layer.getState(1)->name() == std::u8string_view{ u8"Walk" }); + CHECK(layer.getState(-1) == nullptr); + CHECK(layer.getState(99) == nullptr); + CHECK(layer.findStateIndex(u8"Walk") == 1); + + layer.addTransition(std::make_unique()); + CHECK(layer.transitions().size() == 1); + + layer.setMask(std::make_unique(4, 0.0f)); + layer.mask()->setWeight(0, 1.0f); + CHECK(layer.mask()->getWeight(0) == 1.0f); + CHECK(layer.mask()->getWeight(1) == 0.0f); +} + +// ---- BlendTree1D / 2D ---- +TEST_CASE("graph: BlendTree1D sorts by threshold, duration, empty-safe") +{ + BlendTree1D tree; + tree.addEntry(1.0f, nullptr); + tree.addEntry(0.0f, nullptr); + tree.addEntry(0.5f, nullptr); + CHECK(tree.entries().size() == 3); + CHECK(tree.entries()[0].threshold == 0.0f); + CHECK(tree.entries()[1].threshold == 0.5f); + CHECK(tree.entries()[2].threshold == 1.0f); + CHECK(tree.parameter == 0.0f); + CHECK(tree.duration() == 0.0f); // null clips + tree.addEntry(0.5f, nullptr); CHECK(tree.entries().size() == 4); // duplicates allowed + + Skeleton empty{ 0 }; + BoneTransform poses[4] = {}; + BlendTree1D blank; + blank.evaluate(empty, 0.0f, std::span{ poses, 4 }); // empty -> no crash +} + +TEST_CASE("graph: BlendTree2D entries + positions + duration") +{ + BlendTree2D tree; + tree.addEntry(0.0f, 0.0f, nullptr); + tree.addEntry(math::Vector2{ 1, 0 }, nullptr); + tree.addEntry(-1.0f, 2.5f, nullptr); + CHECK(tree.entries().size() == 3); + CHECK(tree.entries()[1].position.x == 1.0f); + CHECK(tree.entries()[2].position.x == -1.0f); + CHECK(tree.entries()[2].position.y == 2.5f); + CHECK(tree.parameterX == 0.0f); + CHECK(tree.duration() == 0.0f); +} + +// ---- AnimationGraph ---- +TEST_CASE("graph: parameters + layers + find (case sensitive)") +{ + AnimationGraph graph; + CHECK(graph.addParameter(u8"Speed", AnimationParameterType::Float) == 0); + CHECK(graph.addParameter(u8"Grounded", AnimationParameterType::Bool) == 1); + CHECK(graph.parameters().size() == 2); + CHECK(graph.findParameter(u8"Speed") == 0); + CHECK(graph.findParameter(u8"Missing") == -1); + CHECK(graph.findParameter(u8"speed") == -1); // case sensitive + CHECK(graph.getParameter(0)->type == AnimationParameterType::Float); + CHECK(graph.getParameter(-1) == nullptr); + + CHECK(graph.addLayer(std::make_unique(std::u8string_view{ u8"Base" })) == 0); + CHECK(graph.addLayer(std::make_unique(std::u8string_view{ u8"Upper" })) == 1); + CHECK(graph.layers().size() == 2); +} + +// ---- AnimationGraphPlayer (state-machine integration) ---- +TEST_CASE("graph: player transitions states on a bool parameter") +{ + Skeleton skel{ 1 }; + skel.bones()[0].index = 0; skel.bones()[0].parentIndex = -1; + skel.findRootBones(); skel.buildChildIndices(); skel.computeInverseBindPoses(); + + // Two clips so states have non-zero duration (the player advances normalized time by dt/duration). + AnimationClip idleClip{ u8"idle", 1.0f, true }; + idleClip.getOrCreatePositionTrack(0)->addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + idleClip.getOrCreatePositionTrack(0)->addKeyframe(1.0f, math::Vector3{ 0, 0, 0 }); + AnimationClip walkClip{ u8"walk", 1.0f, true }; + walkClip.getOrCreatePositionTrack(0)->addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + walkClip.getOrCreatePositionTrack(0)->addKeyframe(1.0f, math::Vector3{ 0, 0, 0 }); + + AnimationGraph graph; + const i32 pMoving = graph.addParameter(u8"Moving", AnimationParameterType::Bool); + auto layer = std::make_unique(std::u8string_view{ u8"Base" }); + layer->addState(std::make_unique(std::u8string_view{ u8"Idle" }, + std::make_unique(&idleClip))); // state 0 + layer->addState(std::make_unique(std::u8string_view{ u8"Walk" }, + std::make_unique(&walkClip))); // state 1 + auto toWalk = std::make_unique(); + toWalk->sourceStateIndex = 0; toWalk->destStateIndex = 1; toWalk->duration = 0.001f; + toWalk->addBoolCondition(pMoving, true); + layer->addTransition(static_cast&&>(toWalk)); + graph.addLayer(static_cast&&>(layer)); + + AnimationGraphPlayer player{ graph, skel }; + CHECK(player.getCurrentStateIndex() == 0); // starts at default (Idle) + + player.update(0.016f); + CHECK(player.getCurrentStateIndex() == 0); // condition false -> stays + + player.setBool(pMoving, true); + player.update(0.016f); + CHECK(player.getCurrentStateIndex() == 1); // transitioned to Walk + std::span skin = player.getSkinningMatrices(); + CHECK(skin.size() == 1); +} diff --git a/Engine/cpp/Runtime/Animation/Player.cppm b/Engine/cpp/Runtime/Animation/Player.cppm new file mode 100644 index 00000000..964e14cf --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Player.cppm @@ -0,0 +1,177 @@ +/// Single-clip animation player (`:player` partition). +/// +/// AnimationPlayer: single-clip playback for one skeleton instance - time advance, looping/clamping, +/// event firing, and evaluation into skinning matrices (+ previous frame for motion vectors). Ported +/// faithfully from The skeleton + clip are borrowed (not owned). + +module; +#include +#include +#include +#include + +export module animation:player; + +import core; +import :skeleton; +import :clip; +import :sampler; +import :pose; + +using namespace draco; + +export namespace draco::animation { + +enum class PlaybackState { Stopped, Playing, Paused }; +// AnimationEventHandler is defined in :clip (shared with the graph). + +class AnimationPlayer { +public: + explicit AnimationPlayer(Skeleton& skeleton) : m_skeleton(&skeleton) { + const usize boneCount = static_cast(skeleton.boneCount()); + m_localPoses.resize(boneCount); + m_skinningMatrices.resize(boneCount); + m_prevSkinningMatrices.resize(boneCount); + resetToBind(); + } + + ~AnimationPlayer() = default; + AnimationPlayer(const AnimationPlayer&) = delete; + AnimationPlayer& operator=(const AnimationPlayer&) = delete; + + [[nodiscard]] Skeleton& getSkeleton() noexcept { return *m_skeleton; } + [[nodiscard]] AnimationClip* currentClip() const noexcept { return m_currentClip; } + [[nodiscard]] PlaybackState state() const noexcept { return m_state; } + [[nodiscard]] f32 currentTime() const noexcept { return m_currentTime; } + + f32 speed = 1.0f; // playback speed multiplier + + // Setting the time marks the skinning matrices dirty (re-evaluated on next access). + void setCurrentTime(f32 t) noexcept { if (m_currentTime != t) { m_currentTime = t; m_matricesDirty = true; } } + + // Plays a clip (borrowed). restart resets the clock to 0. + void play(AnimationClip* clip, bool restart = true) { + m_currentClip = clip; + if (restart) { m_currentTime = 0.0f; m_prevTime = 0.0f; } + m_state = PlaybackState::Playing; + m_matricesDirty = true; + } + + void stop() { + m_state = PlaybackState::Stopped; + m_currentTime = 0.0f; m_prevTime = 0.0f; + m_currentClip = nullptr; + resetToBind(); + } + + void pause() { if (m_state == PlaybackState::Playing) { m_state = PlaybackState::Paused; } } + void resume() { if (m_state == PlaybackState::Paused) { m_state = PlaybackState::Playing; } } + + // Sets the event handler (takes ownership; replaces any previous handler). + void setEventHandler(AnimationEventHandler handler) { m_eventHandler = static_cast(handler); } + + // Resets all local poses to the skeleton's bind pose. + void resetToBind() { + for (i32 i = 0; i < m_skeleton->boneCount() && static_cast(i) < m_localPoses.size(); ++i) { + const Bone* bone = m_skeleton->getBone(i); + m_localPoses[static_cast(i)] = (bone != nullptr) ? bone->localBindPose : BoneTransform{}; + } + m_matricesDirty = true; + } + + // Advances playback by deltaTime: fires crossed events, then loops/clamps. + void update(f32 deltaTime) { + if (m_state != PlaybackState::Playing || m_currentClip == nullptr) { return; } + + // Snapshot current skinning matrices as the previous frame (motion vectors). + const usize n = m_skinningMatrices.size(); + for (usize i = 0; i < n; ++i) { m_prevSkinningMatrices[i] = m_skinningMatrices[i]; } + + const f32 prevTime = m_prevTime; + m_currentTime += deltaTime * speed; + + // Fire events before wrapping (so loop crossings are detected). + if (m_eventHandler && !m_currentClip->events().empty()) { + m_currentClip->fireEvents(prevTime, m_currentTime, + [this](std::u8string_view name, f32 time) { m_eventHandler(name, time); }); + } + + if (m_currentClip->isLooping) { + if (m_currentClip->duration > 0.0f) { + while (m_currentTime >= m_currentClip->duration) { m_currentTime -= m_currentClip->duration; } + while (m_currentTime < 0.0f) { m_currentTime += m_currentClip->duration; } + } + } else { + if (m_currentTime >= m_currentClip->duration) { m_currentTime = m_currentClip->duration; m_state = PlaybackState::Stopped; } + else if (m_currentTime < 0.0f) { m_currentTime = 0.0f; m_state = PlaybackState::Stopped; } + } + + m_prevTime = m_currentTime; + m_matricesDirty = true; + } + + // Samples the current clip + computes skinning matrices (only if dirty). Call before rendering. + void evaluate() { + if (!m_matricesDirty) { return; } + if (m_currentClip != nullptr) { + sampleClip(*m_currentClip, *m_skeleton, m_currentTime, std::span{ m_localPoses.data(), m_localPoses.size() }); + } + m_skeleton->computeSkinningMatrices(std::span{ m_localPoses.data(), m_localPoses.size() }, + std::span{ m_skinningMatrices.data(), m_skinningMatrices.size() }); + m_matricesDirty = false; + } + + // Current skinning matrices for GPU upload (evaluates if needed). + [[nodiscard]] std::span getSkinningMatrices() { + evaluate(); + return std::span{ m_skinningMatrices.data(), m_skinningMatrices.size() }; + } + [[nodiscard]] std::span getPrevSkinningMatrices() const { + return std::span{ m_prevSkinningMatrices.data(), m_prevSkinningMatrices.size() }; + } + + // Push externally-computed matrices (used by the graph player to drive this player's output). + void overrideSkinningMatrices(std::span current, std::span prev) { + const usize c = std::min(current.size(), m_skinningMatrices.size()); + for (usize i = 0; i < c; ++i) { m_skinningMatrices[i] = current[i]; } + const usize p = std::min(prev.size(), m_prevSkinningMatrices.size()); + for (usize i = 0; i < p; ++i) { m_prevSkinningMatrices[i] = prev[i]; } + m_matricesDirty = false; + } + + [[nodiscard]] std::span getLocalPoses() noexcept { return { m_localPoses.data(), m_localPoses.size() }; } + [[nodiscard]] AnimationPose getPose() noexcept { return AnimationPose{ getLocalPoses() }; } + + // Directly set a bone's local transform (procedural animation). + void setBonePose(i32 boneIndex, const BoneTransform& pose) { + if (boneIndex >= 0 && static_cast(boneIndex) < m_localPoses.size()) { + m_localPoses[static_cast(boneIndex)] = pose; + m_matricesDirty = true; + } + } + + // Blend another clip on top of the current local poses (weight 0..1). + void blendAnimation(AnimationClip* clip, f32 time, f32 weight) { + if (clip == nullptr || weight <= 0.0f) { return; } + std::vector blend; blend.resize(static_cast(m_skeleton->boneCount())); + sampleClip(*clip, *m_skeleton, time, std::span{ blend.data(), blend.size() }); + const usize n = std::min(m_localPoses.size(), blend.size()); + for (usize i = 0; i < n; ++i) { m_localPoses[i] = BoneTransform::lerp(m_localPoses[i], blend[i], weight); } + m_matricesDirty = true; + } + +private: + Skeleton* m_skeleton = nullptr; // borrowed + AnimationClip* m_currentClip = nullptr; // borrowed + f32 m_currentTime = 0.0f; + f32 m_prevTime = 0.0f; + PlaybackState m_state = PlaybackState::Stopped; + bool m_matricesDirty = true; + + std::vector m_localPoses; + std::vector m_skinningMatrices; + std::vector m_prevSkinningMatrices; + AnimationEventHandler m_eventHandler; +}; + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/PlayerTests.test.cpp b/Engine/cpp/Runtime/Animation/PlayerTests.test.cpp new file mode 100644 index 00000000..dbe43a69 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/PlayerTests.test.cpp @@ -0,0 +1,118 @@ +// AnimationPlayer: playback, event firing, looping, evaluation. Ports the player section of +// the reference event suite + adds playback/eval coverage. +#include +#include +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +// A 1-bone skeleton with a position clip moving +Y over 1s. +static void setupBone(Skeleton& s) +{ + s.bones()[0].index = 0; s.bones()[0].parentIndex = -1; + s.findRootBones(); s.buildChildIndices(); s.computeInverseBindPoses(); +} + +TEST_CASE("player: event fires when time crosses (single update)") +{ + Skeleton skel{ 2 }; + setupBone(skel); + AnimationPlayer player{ skel }; + int fireCount = 0; + player.setEventHandler(AnimationEventHandler{ [&](std::u8string_view, f32) { ++fireCount; } }); + + AnimationClip clip{ u8"Test", 1.0f }; + clip.addEvent(0.5f, u8"Hit"); + player.play(&clip); + player.update(0.6f); + CHECK(fireCount == 1); +} + +TEST_CASE("player: events fire in order across updates") +{ + Skeleton skel{ 2 }; + setupBone(skel); + AnimationPlayer player{ skel }; + std::vector fired; + player.setEventHandler(AnimationEventHandler{ [&](std::u8string_view n, f32) { fired.push_back(n); } }); + + AnimationClip clip{ u8"Test", 2.0f }; + clip.addEvent(0.3f, u8"A"); clip.addEvent(0.8f, u8"B"); clip.addEvent(1.5f, u8"C"); + clip.sortEvents(); + player.play(&clip); + player.update(0.5f); CHECK(fired.size() == 1); CHECK(fired[0] == std::u8string_view{ u8"A" }); + player.update(0.5f); CHECK(fired.size() == 2); CHECK(fired[1] == std::u8string_view{ u8"B" }); + player.update(0.5f); CHECK(fired.size() == 3); CHECK(fired[2] == std::u8string_view{ u8"C" }); +} + +TEST_CASE("player: looping clip fires event each loop + wraps time") +{ + Skeleton skel{ 2 }; + setupBone(skel); + AnimationPlayer player{ skel }; + int fireCount = 0; + player.setEventHandler(AnimationEventHandler{ [&](std::u8string_view, f32) { ++fireCount; } }); + + AnimationClip clip{ u8"Test", 1.0f, /*looping*/ true }; + clip.addEvent(0.5f, u8"Hit"); + player.play(&clip); + player.update(0.8f); CHECK(fireCount == 1); + player.update(0.8f); CHECK(fireCount == 2); + CHECK(player.currentTime() < 1.0f); // wrapped + CHECK(player.state() == PlaybackState::Playing); // looping never stops +} + +TEST_CASE("player: non-looping clip clamps + stops at the end") +{ + Skeleton skel{ 2 }; + setupBone(skel); + AnimationPlayer player{ skel }; + AnimationClip clip{ u8"Test", 1.0f, /*looping*/ false }; + player.play(&clip); + player.update(2.0f); + CHECK(math::nearlyEqual(player.currentTime(), 1.0f)); + CHECK(player.state() == PlaybackState::Stopped); +} + +TEST_CASE("player: SetEventHandler replaces the old handler") +{ + Skeleton skel{ 2 }; + setupBone(skel); + AnimationPlayer player{ skel }; + int count1 = 0, count2 = 0; + AnimationClip clip{ u8"Test", 1.0f }; + clip.addEvent(0.5f, u8"Hit"); + + player.setEventHandler(AnimationEventHandler{ [&](std::u8string_view, f32) { ++count1; } }); + player.play(&clip); player.update(0.6f); + CHECK(count1 == 1); CHECK(count2 == 0); + + player.setEventHandler(AnimationEventHandler{ [&](std::u8string_view, f32) { ++count2; } }); + player.play(&clip); player.update(0.6f); + CHECK(count1 == 1); CHECK(count2 == 1); +} + +TEST_CASE("player: Evaluate drives skinning matrices from the clip") +{ + Skeleton skel{ 1 }; + skel.bones()[0].index = 0; skel.bones()[0].parentIndex = -1; + skel.findRootBones(); skel.buildChildIndices(); skel.computeInverseBindPoses(); + + AnimationClip clip{ u8"move", 1.0f }; + AnimationClip::Vec3Track* pos = clip.getOrCreatePositionTrack(0); + pos->addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + pos->addKeyframe(1.0f, math::Vector3{ 0, 10, 0 }); + + AnimationPlayer player{ skel }; + player.play(&clip); + player.setCurrentTime(1.0f); + std::span skin = player.getSkinningMatrices(); + // At t=1 the bone translated +10 in Y from its (identity) bind pose -> skin matrix has Ty=10. + CHECK(skin.size() == 1); + CHECK(math::nearlyEqual(skin[0].m[3][1], 10.0f)); +} diff --git a/Engine/cpp/Runtime/Animation/Pose.cppm b/Engine/cpp/Runtime/Animation/Pose.cppm new file mode 100644 index 00000000..476e4d95 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Pose.cppm @@ -0,0 +1,31 @@ +/// Animation pose (`:pose` partition). +/// +/// AnimationPose: a non-owning view over per-bone local transforms (+ optional morph weights) +/// representing one evaluated pose. The backing arrays must outlive the view. Ported faithfully from +/// + +module; +#include + +export module animation:pose; + +import core; +import :skeleton; // BoneTransform + +using namespace draco; + +export namespace draco::animation { + +struct AnimationPose { + std::span boneTransforms; // per-bone local transforms + std::span morphWeights; // per-morph-target weights (empty until morph support) + + AnimationPose() = default; + explicit AnimationPose(std::span bones, std::span morphs = {}) noexcept + : boneTransforms(bones), morphWeights(morphs) {} + + [[nodiscard]] usize boneCount() const noexcept { return boneTransforms.size(); } + [[nodiscard]] bool hasMorphWeights() const noexcept { return morphWeights.size() > 0; } +}; + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/PoseTests.test.cpp b/Engine/cpp/Runtime/Animation/PoseTests.test.cpp new file mode 100644 index 00000000..d0a54395 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/PoseTests.test.cpp @@ -0,0 +1,43 @@ +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +TEST_CASE("pose: constructor with bone transforms sets bone count") +{ + BoneTransform transforms[4] = {}; + const AnimationPose pose{ std::span{ transforms, 4 } }; + CHECK(pose.boneCount() == 4); + CHECK(pose.hasMorphWeights() == false); +} + +TEST_CASE("pose: constructor with morph weights") +{ + BoneTransform transforms[2] = {}; + f32 morphs[3] = { 0.5f, 0.0f, 1.0f }; + const AnimationPose pose{ std::span{ transforms, 2 }, std::span{ morphs, 3 } }; + CHECK(pose.boneCount() == 2); + CHECK(pose.hasMorphWeights() == true); + CHECK(pose.morphWeights.size() == 3); +} + +TEST_CASE("pose: empty pose has zero bones") +{ + const AnimationPose pose{ std::span{} }; + CHECK(pose.boneCount() == 0); + CHECK(pose.hasMorphWeights() == false); +} + +TEST_CASE("pose: bone transforms are accessible") +{ + BoneTransform transforms[2] = {}; + transforms[0].position = math::Vector3{ 1, 2, 3 }; + transforms[1].position = math::Vector3{ 4, 5, 6 }; + const AnimationPose pose{ std::span{ transforms, 2 } }; + CHECK(pose.boneTransforms[0].position.x == 1.0f); + CHECK(pose.boneTransforms[1].position.x == 4.0f); +} diff --git a/Engine/cpp/Runtime/Animation/Sampler.cppm b/Engine/cpp/Runtime/Animation/Sampler.cppm new file mode 100644 index 00000000..b70ababb --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Sampler.cppm @@ -0,0 +1,125 @@ +/// Clip sampling and pose blending (`:sampler` partition). +/// +/// Stateless sampling of clips/tracks into bone poses + pose blending (lerp + additive). Ported +/// faithfully from + +module; +#include +#include +#include + +export module animation:sampler; + +import core; +import :skeleton; // BoneTransform, Skeleton +import :clip; // AnimationTrack, AnimationClip, InterpolationMode + +using namespace draco; + +export namespace draco::animation { + +// --- cubic spline (Hermite) helpers --- +[[nodiscard]] inline math::Vector3 cubicSplineVec3(const Keyframe& prev, const Keyframe& next, f32 t, f32 duration) { + const f32 t2 = t * t, t3 = t2 * t; + const math::Vector3 p0 = prev.value; + const math::Vector3 m0 = prev.outTangent * duration; + const math::Vector3 p1 = next.value; + const math::Vector3 m1 = next.inTangent * duration; + const f32 h00 = 2.0f * t3 - 3.0f * t2 + 1.0f; + const f32 h10 = t3 - 2.0f * t2 + t; + const f32 h01 = -2.0f * t3 + 3.0f * t2; + const f32 h11 = t3 - t2; + return p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11; +} + +[[nodiscard]] inline math::Quaternion cubicSplineQuat(const Keyframe& prev, const Keyframe& next, f32 t, f32 /*duration*/) { + // Simplified Hermite + normalize (squad would be more accurate). + const f32 t2 = t * t, t3 = t2 * t; + const f32 h00 = 2.0f * t3 - 3.0f * t2 + 1.0f; + const f32 h01 = -2.0f * t3 + 3.0f * t2; + return normalize(slerp(prev.value, next.value, h01 / (h00 + h01))); +} + +// Sample a math::Vector3 track at `time` (returns `defaultValue` if empty). +[[nodiscard]] inline math::Vector3 sampleVec3(const AnimationTrack* track, f32 time, math::Vector3 defaultValue = math::Vector3{ 0, 0, 0 }) { + if (track == nullptr || track->keyframes().empty()) { return defaultValue; } + const KeyframeLookup k = track->findKeyframes(time); + if (k.prev < 0) { return defaultValue; } + const Keyframe& prev = track->keyframes()[static_cast(k.prev)]; + const Keyframe& next = track->keyframes()[static_cast(k.next)]; + switch (track->interpolation) { + case InterpolationMode::Step: return prev.value; + case InterpolationMode::Linear: return lerp(prev.value, next.value, k.t); + case InterpolationMode::CubicSpline: return cubicSplineVec3(prev, next, k.t, next.time - prev.time); + } + return prev.value; +} + +// Sample a math::Quaternion track at `time` (returns `defaultValue` if empty). +[[nodiscard]] inline math::Quaternion sampleQuat(const AnimationTrack* track, f32 time, math::Quaternion defaultValue = math::Quaternion::identity) { + if (track == nullptr || track->keyframes().empty()) { return defaultValue; } + const KeyframeLookup k = track->findKeyframes(time); + if (k.prev < 0) { return defaultValue; } + const Keyframe& prev = track->keyframes()[static_cast(k.prev)]; + const Keyframe& next = track->keyframes()[static_cast(k.next)]; + switch (track->interpolation) { + case InterpolationMode::Step: return prev.value; + case InterpolationMode::Linear: return slerp(prev.value, next.value, k.t); + case InterpolationMode::CubicSpline: return cubicSplineQuat(prev, next, k.t, next.time - prev.time); + } + return prev.value; +} + +// Sample a clip at `time` into `outPoses` (bind pose for un-animated bones). Handles looping/clamp. +inline void sampleClip(const AnimationClip& clip, const Skeleton& skeleton, f32 time, std::span outPoses) { + const i32 boneCount = skeleton.boneCount(); + for (i32 i = 0; i < boneCount && static_cast(i) < outPoses.size(); ++i) { + const Bone* bone = skeleton.getBone(i); + outPoses[static_cast(i)] = (bone != nullptr) ? bone->localBindPose : BoneTransform{}; + } + + f32 sampleTime = time; + if (clip.isLooping && clip.duration > 0.0f) { + sampleTime = time - clip.duration * std::floor(time / clip.duration); // positive modulo + if (sampleTime < 0.0f) { sampleTime += clip.duration; } + } else { + sampleTime = std::clamp(time, 0.0f, clip.duration); + } + + for (const auto& track : clip.positionTracks()) { + const i32 b = track->boneIndex; + if (b >= 0 && static_cast(b) < outPoses.size()) { + outPoses[static_cast(b)].position = sampleVec3(track.get(), sampleTime, outPoses[static_cast(b)].position); + } + } + for (const auto& track : clip.rotationTracks()) { + const i32 b = track->boneIndex; + if (b >= 0 && static_cast(b) < outPoses.size()) { + outPoses[static_cast(b)].rotation = sampleQuat(track.get(), sampleTime, outPoses[static_cast(b)].rotation); + } + } + for (const auto& track : clip.scaleTracks()) { + const i32 b = track->boneIndex; + if (b >= 0 && static_cast(b) < outPoses.size()) { + outPoses[static_cast(b)].scale = sampleVec3(track.get(), sampleTime, outPoses[static_cast(b)].scale); + } + } +} + +// Linear blend of two poses (0 = a, 1 = b). +inline void blendPoses(std::span a, std::span b, f32 factor, std::span out) { + const usize count = std::min(std::min(a.size(), b.size()), out.size()); + for (usize i = 0; i < count; ++i) { out[i] = BoneTransform::lerp(a[i], b[i], factor); } +} + +// Additive blend: base + (additive * weight). +inline void additivePoses(std::span base, std::span additive, f32 weight, std::span out) { + const usize count = std::min(std::min(base.size(), additive.size()), out.size()); + for (usize i = 0; i < count; ++i) { + out[i].position = base[i].position + additive[i].position * weight; + out[i].rotation = slerp(math::Quaternion::identity, additive[i].rotation, weight) * base[i].rotation; + out[i].scale = base[i].scale * lerp(math::Vector3::one, additive[i].scale, weight); + } +} + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/SamplerTests.test.cpp b/Engine/cpp/Runtime/Animation/SamplerTests.test.cpp new file mode 100644 index 00000000..816b8bb1 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/SamplerTests.test.cpp @@ -0,0 +1,64 @@ +// AnimationSampler: track/clip sampling + pose blending. Covers the sampling math directly. +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +TEST_CASE("sampler: math::Vector3 track linear interpolation") +{ + AnimationTrack track; + track.addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + track.addKeyframe(1.0f, math::Vector3{ 10, 0, 0 }); + CHECK(math::nearlyEqual(sampleVec3(&track, 0.0f), math::Vector3{ 0, 0, 0 })); + CHECK(math::nearlyEqual(sampleVec3(&track, 0.5f), math::Vector3{ 5, 0, 0 })); + CHECK(math::nearlyEqual(sampleVec3(&track, 1.0f), math::Vector3{ 10, 0, 0 })); + CHECK(math::nearlyEqual(sampleVec3(&track, 2.0f), math::Vector3{ 10, 0, 0 })); // clamps past last + // empty / null returns the default + CHECK(math::nearlyEqual(sampleVec3(nullptr, 0.5f, math::Vector3{ 9, 9, 9 }), math::Vector3{ 9, 9, 9 })); +} + +TEST_CASE("sampler: Step interpolation holds previous value") +{ + AnimationTrack track; + track.interpolation = InterpolationMode::Step; + track.addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + track.addKeyframe(1.0f, math::Vector3{ 10, 0, 0 }); + CHECK(math::nearlyEqual(sampleVec3(&track, 0.5f), math::Vector3{ 0, 0, 0 })); // holds prev until next key +} + +TEST_CASE("sampler: SampleClip animates targeted bones, bind pose otherwise") +{ + Skeleton skel{ 2 }; + skel.bones()[0].localBindPose.position = math::Vector3{ 0, 0, 0 }; + skel.bones()[1].localBindPose.position = math::Vector3{ 7, 7, 7 }; + + AnimationClip clip{ u8"move", 1.0f }; + AnimationClip::Vec3Track* pos = clip.getOrCreatePositionTrack(0); + pos->addKeyframe(0.0f, math::Vector3{ 0, 0, 0 }); + pos->addKeyframe(1.0f, math::Vector3{ 0, 10, 0 }); + + BoneTransform poses[2] = {}; + sampleClip(clip, skel, 0.5f, std::span{ poses, 2 }); + CHECK(math::nearlyEqual(poses[0].position, math::Vector3{ 0, 5, 0 })); // animated + CHECK(math::nearlyEqual(poses[1].position, math::Vector3{ 7, 7, 7 })); // untouched -> bind pose +} + +TEST_CASE("sampler: BlendPoses + AdditivePoses") +{ + BoneTransform a[1] = {}; a[0].position = math::Vector3{ 0, 0, 0 }; a[0].scale = math::Vector3{ 1, 1, 1 }; + BoneTransform b[1] = {}; b[0].position = math::Vector3{ 10, 0, 0 }; b[0].scale = math::Vector3{ 3, 3, 3 }; + BoneTransform out[1] = {}; + + blendPoses(std::span{ a, 1 }, std::span{ b, 1 }, 0.5f, std::span{ out, 1 }); + CHECK(math::nearlyEqual(out[0].position, math::Vector3{ 5, 0, 0 })); + CHECK(math::nearlyEqual(out[0].scale, math::Vector3{ 2, 2, 2 })); + + BoneTransform base[1] = {}; base[0].position = math::Vector3{ 1, 0, 0 }; base[0].scale = math::Vector3{ 1, 1, 1 }; + BoneTransform add[1] = {}; add[0].position = math::Vector3{ 0, 5, 0 }; add[0].scale = math::Vector3{ 1, 1, 1 }; + additivePoses(std::span{ base, 1 }, std::span{ add, 1 }, 1.0f, std::span{ out, 1 }); + CHECK(math::nearlyEqual(out[0].position, math::Vector3{ 1, 5, 0 })); // base + additive*weight +} diff --git a/Engine/cpp/Runtime/Animation/Skeleton.cppm b/Engine/cpp/Runtime/Animation/Skeleton.cppm new file mode 100644 index 00000000..b5c9b563 --- /dev/null +++ b/Engine/cpp/Runtime/Animation/Skeleton.cppm @@ -0,0 +1,196 @@ +/// Skeleton and bones (`:skeleton` partition). +/// +/// The skeletal hierarchy A `Bone` is a +/// node with a local bind pose + inverse bind matrix; the `Skeleton` owns the bones and computes +/// world-space + skinning matrices from a set of local poses, evaluated parents-before-children. +/// +/// `BoneTransform` reuses `math::Transform` (position/rotation/scale, S*R*T) - byte-for-byte the +/// compact BoneTransform, with Lerp/ToMatrix already in core math. + +module; +#include +#include +#include +#include +#include + +export module animation:skeleton; + +import core; + +using namespace draco; + +export namespace draco::animation { + +// Look up `k` in an associative container, returning a pointer to its value or nullptr. +template +[[nodiscard]] auto mapFind(Map& m, const Key& k) -> decltype(&m.find(k)->second) { + auto it = m.find(k); + return it != m.end() ? &it->second : nullptr; +} + +// A compact per-bone transform for animation (pos/rot/scale). Reuses the core math type. +using BoneTransform = math::Transform; + +// One bone in a skeleton hierarchy. Value type owned by the Skeleton's bone array (no per-bone heap +// allocation, unlike the Beef original's Bone[] of references). +struct Bone { + std::u8string name; + i32 index = 0; + i32 parentIndex = -1; // -1 = root + BoneTransform localBindPose = {}; // local transform relative to parent (bind pose) + math::Matrix4 inverseBindPose = math::Matrix4::identity();// model space -> bone space + math::Matrix4 rootCorrection = math::Matrix4::identity();// missing-ancestor transform for roots (e.g. FBX axis conv) + std::vector children; +}; + +// A resource product (Object), so a cooked SkeletonSource can build into it via the resource system. +class Skeleton { +public: + Skeleton() = default; + // Creates `boneCount` default bones with sequential indices (the loader fills the rest). + explicit Skeleton(i32 boneCount) { + m_bones.resize(boneCount < 0 ? 0 : static_cast(boneCount)); + for (usize i = 0; i < m_bones.size(); ++i) { m_bones[i].index = static_cast(i); } + } + + Skeleton(const Skeleton&) = delete; + Skeleton& operator=(const Skeleton&) = delete; + + [[nodiscard]] i32 boneCount() const noexcept { return static_cast(m_bones.size()); } + [[nodiscard]] std::vector& bones() noexcept { return m_bones; } + [[nodiscard]] const std::vector& bones() const noexcept { return m_bones; } + [[nodiscard]] std::span rootBones() const noexcept { return { m_rootBones.data(), m_rootBones.size() }; } + [[nodiscard]] std::u8string& name() noexcept { return m_name; } + [[nodiscard]] const std::u8string& name() const noexcept { return m_name; } + + // Re-populate this same instance (resource hot-reload keeps outside references valid). + void clearForReload(i32 boneCount) { + m_bones.clear(); m_rootBones.clear(); m_hierarchicalOrder.clear(); m_nameMap.clear(); m_name.clear(); + m_bones.resize(boneCount < 0 ? 0 : static_cast(boneCount)); + for (usize i = 0; i < m_bones.size(); ++i) { m_bones[i].index = static_cast(i); } + } + + // Bone index by name, or -1 if not found (requires buildNameMap()). + [[nodiscard]] i32 findBone(std::u8string_view name) const { + if (const i32* idx = mapFind(m_nameMap, std::u8string{ name })) { return *idx; } + return -1; + } + + [[nodiscard]] Bone* getBone(i32 index) { return inBounds(index) ? &m_bones[static_cast(index)] : nullptr; } + [[nodiscard]] const Bone* getBone(i32 index) const { return inBounds(index) ? &m_bones[static_cast(index)] : nullptr; } + + // Build the name->index lookup. Call after all bones + names are set. + void buildNameMap() { + m_nameMap.clear(); + for (const Bone& b : m_bones) { if (!b.name.empty()) { m_nameMap.insert_or_assign(b.name, b.index); } } + } + + // Cache root-bone indices (parentIndex < 0). Call after parents are set. + void findRootBones() { + m_rootBones.clear(); + for (const Bone& b : m_bones) { if (b.parentIndex < 0) { m_rootBones.push_back(b.index); } } + } + + // Build each bone's child-index list + the parents-before-children evaluation order. Call after + // parents are set (and after FindRootBones for a correct hierarchical order). + void buildChildIndices() { + const usize n = m_bones.size(); + std::vector childCounts; childCounts.resize(n); + for (usize i = 0; i < n; ++i) { childCounts[i] = 0; } + for (const Bone& b : m_bones) { if (b.parentIndex >= 0 && inBounds(b.parentIndex)) { ++childCounts[static_cast(b.parentIndex)]; } } + + for (usize i = 0; i < n; ++i) { + m_bones[i].children.clear(); + m_bones[i].children.reserve(static_cast(childCounts[i])); + } + for (const Bone& b : m_bones) { + if (b.parentIndex >= 0 && inBounds(b.parentIndex)) { m_bones[static_cast(b.parentIndex)].children.push_back(b.index); } + } + buildHierarchicalOrder(); + } + + // World bind pose -> inverse bind matrix for each bone. Call after local bind poses + parents set. + void computeInverseBindPoses() { + const usize n = m_bones.size(); + m_worldScratch.resize(n); + computeWorldPoses(std::span{}, std::span{ m_worldScratch.data(), n }); // bind pose + for (usize i = 0; i < n; ++i) { m_bones[i].inverseBindPose = inverse(m_worldScratch[i]); } + } + + // World-space matrices from local transforms (empty `localPoses` => bind pose), parents first. + void computeWorldPoses(std::span localPoses, std::span outWorldPoses) { + if (!m_hierarchicalOrder.empty()) { + for (i32 boneIndex : m_hierarchicalOrder) { computeBoneWorldPose(boneIndex, localPoses, outWorldPoses); } + } else { + for (i32 i = 0; i < boneCount(); ++i) { computeBoneWorldPose(i, localPoses, outWorldPoses); } + } + } + + // Final skinning matrices = inverseBindPose * worldPose (row-vector: vertex * skin = v * IBM * world). + void computeSkinningMatrices(std::span localPoses, std::span outSkinningMatrices) { + const usize n = m_bones.size(); + m_worldScratch.resize(n); + computeWorldPoses(localPoses, std::span{ m_worldScratch.data(), n }); + for (usize i = 0; i < n; ++i) { + if (i < outSkinningMatrices.size()) { + outSkinningMatrices[i] = m_bones[i].inverseBindPose * m_worldScratch[i]; + } + } + } + +private: + [[nodiscard]] bool inBounds(i32 i) const noexcept { return i >= 0 && static_cast(i) < m_bones.size(); } + + void computeBoneWorldPose(i32 boneIndex, std::span localPoses, std::span outWorldPoses) { + const usize bi = static_cast(boneIndex); + if (!inBounds(boneIndex) || bi >= outWorldPoses.size()) { return; } + const Bone& bone = m_bones[bi]; + + const BoneTransform local = (bi < localPoses.size()) ? localPoses[bi] : bone.localBindPose; + const math::Matrix4 localMatrix = local.toMatrix(); + + if (bone.parentIndex >= 0 && inBounds(bone.parentIndex) && static_cast(bone.parentIndex) < outWorldPoses.size()) { + outWorldPoses[bi] = localMatrix * outWorldPoses[static_cast(bone.parentIndex)]; // child = local * parent + } else { + outWorldPoses[bi] = localMatrix * bone.rootCorrection; + } + } + + // BFS over the hierarchy so parents precede children; orphans appended at the end. + void buildHierarchicalOrder() { + const usize n = m_bones.size(); + m_hierarchicalOrder.clear(); + m_hierarchicalOrder.reserve(n); + + std::vector queue; + for (i32 root : m_rootBones) { queue.push_back(root); } + usize head = 0; + while (head < queue.size()) { + const i32 boneIndex = queue[head++]; + m_hierarchicalOrder.push_back(boneIndex); + if (inBounds(boneIndex)) { + for (i32 child : m_bones[static_cast(boneIndex)].children) { queue.push_back(child); } + } + } + + // Append any orphans not reached from a root. + if (m_hierarchicalOrder.size() < n) { + for (i32 i = 0; i < static_cast(n); ++i) { + bool found = false; + for (i32 ordered : m_hierarchicalOrder) { if (ordered == i) { found = true; break; } } + if (!found) { m_hierarchicalOrder.push_back(i); } + } + } + } + + std::u8string m_name; + std::vector m_bones; + std::vector m_rootBones; + std::vector m_hierarchicalOrder; + std::unordered_map m_nameMap; + std::vector m_worldScratch; // reused world-pose scratch (skinning hot path) +}; + + +} // namespace draco::animation diff --git a/Engine/cpp/Runtime/Animation/SkeletonTests.test.cpp b/Engine/cpp/Runtime/Animation/SkeletonTests.test.cpp new file mode 100644 index 00000000..5149479c --- /dev/null +++ b/Engine/cpp/Runtime/Animation/SkeletonTests.test.cpp @@ -0,0 +1,94 @@ +// Skeleton hierarchy: name lookup, root/child/order building, world-pose accumulation, and +// skinning-matrix correctness (identity at bind pose). No prior test existed; this covers the +// ported math directly. +#include +#include +#include +#include + +import core; +import animation; + +using namespace draco; +using namespace draco::animation; + +// A 3-bone chain root(0) -> child(1) -> grandchild(2), each translated +Y from its parent. +static void buildChain(Skeleton& s) +{ + std::vector& bones = s.bones(); + bones[0].index = 0; bones[0].parentIndex = -1; bones[0].name = std::u8string{ u8"root" }; + bones[0].localBindPose.position = math::Vector3{ 0, 10, 0 }; + bones[1].index = 1; bones[1].parentIndex = 0; bones[1].name = std::u8string{ u8"child" }; + bones[1].localBindPose.position = math::Vector3{ 0, 5, 0 }; + bones[2].index = 2; bones[2].parentIndex = 1; bones[2].name = std::u8string{ u8"grandchild" }; + bones[2].localBindPose.position = math::Vector3{ 0, 2, 0 }; + s.buildNameMap(); + s.findRootBones(); + s.buildChildIndices(); +} + +TEST_CASE("skeleton: name map + root finding") +{ + Skeleton s{ 3 }; + buildChain(s); + CHECK(s.boneCount() == 3); + CHECK(s.findBone(u8"child") == 1); + CHECK(s.findBone(u8"grandchild") == 2); + CHECK(s.findBone(u8"missing") == -1); + CHECK(s.rootBones().size() == 1); + CHECK(s.rootBones()[0] == 0); +} + +TEST_CASE("skeleton: world poses accumulate down the hierarchy (bind pose)") +{ + Skeleton s{ 3 }; + buildChain(s); + + math::Matrix4 world[3]; + s.computeWorldPoses(std::span{}, std::span{ world, 3 }); + + // Pure translations compose additively along the chain (+Y). + CHECK(math::nearlyEqual(world[0].m[3][1], 10.0f)); + CHECK(math::nearlyEqual(world[1].m[3][1], 15.0f)); + CHECK(math::nearlyEqual(world[2].m[3][1], 17.0f)); +} + +TEST_CASE("skeleton: skinning matrices are identity at the bind pose") +{ + Skeleton s{ 3 }; + buildChain(s); + s.computeInverseBindPoses(); + + math::Matrix4 skin[3]; + s.computeSkinningMatrices(std::span{}, std::span{ skin, 3 }); + + // skin = inverseBind * world; evaluated at the bind pose this is identity for every bone. + for (int i = 0; i < 3; ++i) + { + CHECK(math::nearlyEqual(skin[i].m[0][0], 1.0f)); + CHECK(math::nearlyEqual(skin[i].m[1][1], 1.0f)); + CHECK(math::nearlyEqual(skin[i].m[2][2], 1.0f)); + CHECK(math::nearlyEqual(skin[i].m[3][0], 0.0f)); + CHECK(math::nearlyEqual(skin[i].m[3][1], 0.0f)); + CHECK(math::nearlyEqual(skin[i].m[3][2], 0.0f)); + } +} + +TEST_CASE("skeleton: hierarchical order lists parents before children") +{ + // A child declared BEFORE its parent in the array must still evaluate after it. + Skeleton s{ 2 }; + std::vector& bones = s.bones(); + bones[0].index = 0; bones[0].parentIndex = 1; // bone 0's parent is bone 1 + bones[1].index = 1; bones[1].parentIndex = -1; // bone 1 is the root + bones[0].localBindPose.position = math::Vector3{ 0, 1, 0 }; + bones[1].localBindPose.position = math::Vector3{ 0, 100, 0 }; + s.findRootBones(); + s.buildChildIndices(); + + math::Matrix4 world[2]; + s.computeWorldPoses(std::span{}, std::span{ world, 2 }); + // If order were wrong, bone 0 would miss bone 1's transform. Expect 100 + 1 = 101. + CHECK(math::nearlyEqual(world[0].m[3][1], 101.0f)); + CHECK(math::nearlyEqual(world[1].m[3][1], 100.0f)); +} diff --git a/Engine/cpp/Runtime/CMakeLists.txt b/Engine/cpp/Runtime/CMakeLists.txt index 9411c89c..2923bbd6 100644 --- a/Engine/cpp/Runtime/CMakeLists.txt +++ b/Engine/cpp/Runtime/CMakeLists.txt @@ -1,10 +1,43 @@ add_modules_library(Core PIC) +add_modules_library(Profiler) +add_modules_library(Animation) add_modules_library(Shell) -add_modules_library(Rendering PIC) -add_modules_library(Scene) +add_modules_library(Rendering/Shaders) +add_modules_library(Rendering/RHI PIC) +add_modules_library(Rendering/RHI/Null) +add_modules_library(Rendering/RHI/Validation) +add_modules_library(Rendering/RHI/Vulkan) +if(WIN32) + add_modules_library(Rendering/RHI/DX12) +endif() +add_modules_library(Rendering/Shaders/System) +add_modules_library(Rendering/Graphics) +add_modules_library(Rendering/RenderGraph) +add_modules_library(Rendering/Image) +add_modules_library(Rendering/Image/IO) +add_modules_library(Rendering/Texture) +add_modules_library(Rendering/Geometry) +add_modules_library(Rendering/Materials) +add_modules_library(Rendering/Materials/PSO) +# Model is one module whose partitions keep their folder separation: the loaders (FBX, +# GLTF) and the IO registry live in subdirs, so append them (and the ufbx/cgltf impl +# TUs) to the target the glob created from the top-level interface partitions. +add_modules_library(Rendering/Model) +target_sources(Rendering_Model PUBLIC FILE_SET CXX_MODULES FILES + Rendering/Model/IO/ModelIOModule.cppm + Rendering/Model/FBX/FbxLoader.cppm + Rendering/Model/GLTF/GltfLoader.cppm) +target_sources(Rendering_Model PRIVATE + Rendering/Model/FBX/ufbx_impl.cpp + Rendering/Model/GLTF/cgltf_impl.cpp) target_link_libraries(Core PUBLIC Definitions Math IO Memory Logging) - +# Profiler: lightweight hierarchical CPU scope profiler (Core-only). Instrumentation is +# via the DRACO_PROFILE_SCOPE macro in Profiler/Profiler.h; off by default (DRACO_PROFILING). +target_link_libraries(Profiler PUBLIC Core) +# Animation: skeletal-animation foundation (skeleton/pose/clip/sampler/player/graph), +# Core-only and RHI-free; the renderer consumes its skinning matrices. +target_link_libraries(Animation PUBLIC Core) # Shell is a single library: the interface plus both backends, separated only by folder # (Desktop/ = SDL3, Null/ = headless). SDL3 is a PRIVATE implementation detail; nothing # outside Shell sees it. createShell() dispatches to the backend chosen at compile time @@ -33,5 +66,58 @@ if(CMAKE_TESTING_ENABLED) add_test(NAME ${SHELL_TEST_TARGET} COMMAND ${SHELL_TEST_TARGET} --reporter junit --out "Testing/${SHELL_TEST_TARGET}.xml") endforeach() endif() -target_link_libraries(Rendering PUBLIC RHI RenderGraph Mesh Material QuadRenderer Renderer) -target_link_libraries(Scene PUBLIC Renderable TransformComponent Camera) +# Rendering: RHI is the backend-agnostic interface module; Null is the headless +# no-op backend (for tests/tools). GPU backends (Vulkan, DX12) come later. +# Shaders: HLSL compilation via DXC. DXC's headers come from Draco::DXCHeaders +# (which also carries DRACO_DXC_PATH); the runtime library is dlopen'd, so only +# the platform's dl library is linked. +target_link_libraries(Rendering_Shaders PUBLIC Core Draco::DXCHeaders PRIVATE ${CMAKE_DL_LIBS}) +# ShaderSystem: variant compile-on-demand + cache. Needs the RHI (to create GPU +# modules) on top of the RHI-free shaders module. Its test uses the Null backend. +target_link_libraries(Rendering_Shaders_System PUBLIC Rendering_Shaders Rendering_RHI Rendering_RHI_Null) +target_link_libraries(Rendering_RHI PUBLIC Core) +target_link_libraries(Rendering_RHI_Null PUBLIC Rendering_RHI) +# Validation wraps any backend; its test wraps the Null backend, so Null is +# PUBLIC here to give the test the rhi.null module interface. +target_link_libraries(Rendering_RHI_Validation PUBLIC Rendering_RHI Rendering_RHI_Null) +# Vulkan GPU backend. The Vulkan loader is PRIVATE: VkIncludes.h/vulkan.h live in +# the module's global fragment, so importers of rhi.vk don't need the headers, but +# the static lib's loader symbols still propagate to the final link. +target_link_libraries(Rendering_RHI_Vulkan PUBLIC Rendering_RHI PRIVATE Vulkan::Vulkan) +# DX12 GPU backend (Windows only). Links the D3D12/DXGI system libraries privately; +# IID_PPV_ARGS uses __uuidof, which Clang flags as a language extension. +if(WIN32) + target_link_libraries(Rendering_RHI_DX12 PUBLIC Rendering_RHI PRIVATE d3d12 dxgi dxguid d3dcompiler) + target_compile_options(Rendering_RHI_DX12 PRIVATE $<$:-Wno-language-extension-token>) +endif() +# Graphics render host: one target holding the graphics / graphics.null / graphics.gpu +# modules. The core `graphics` interface imports only base RHI + Shell; graphics.gpu +# pulls the backends. Shell is PUBLIC for the headless test. DX12 is added +# (with DRACO_HAS_DX12) only on Windows. +# RenderGraph: pass dependency resolution, transient aliasing, automatic barriers +# over the RHI. Backend-agnostic (base RHI only); Null is PUBLIC for its headless tests. +target_link_libraries(Rendering_RenderGraph PUBLIC Core Rendering_RHI Rendering_RHI_Null) +# Image: CPU-side image data + manipulation (no RHI). IO adds stb load/save; stb is +# PRIVATE (its headers live in the module's global fragment / the stb_impl TU). +target_link_libraries(Rendering_Image PUBLIC Core) +target_link_libraries(Rendering_Image_IO PUBLIC Rendering_Image Core PRIVATE stb_image) +# Texture: image->RHI format descriptors (bridges CPU image data and the GPU RHI). +target_link_libraries(Rendering_Texture PUBLIC Core Rendering_Image Rendering_RHI) +# Geometry: CPU mesh data (static/skinned vertex streams, index buffers, primitives). +target_link_libraries(Rendering_Geometry PUBLIC Core) +# Materials: data-driven material model (PipelineConfig, Material/Builder/Instance, and +# the bind-group-inferring MaterialSystem). Needs the RHI + Shaders (ShaderFlags); Null +# is PUBLIC for the headless test. +target_link_libraries(Rendering_Materials PUBLIC Core Rendering_RHI Rendering_Shaders Rendering_RHI_Null) +# PSO cache: bridges the ShaderSystem (variants) and Materials (PipelineConfig). Its test +# compiles real shaders via the ShaderSystem against the Null backend. +target_link_libraries(Rendering_Materials_PSO PUBLIC Rendering_Materials Rendering_Shaders_System Rendering_RHI Rendering_RHI_Null) +# Model: importer representation + FBX(ufbx)/GLTF(cgltf) loaders. Loads embedded/external +# textures via Image; the ufbx/cgltf single-header libs are compiled in the impl TUs. +target_link_libraries(Rendering_Model PUBLIC Core Rendering_Image Rendering_Image_IO PRIVATE ufbx cgltf) +target_link_libraries(Rendering_Graphics PUBLIC + Rendering_RHI Rendering_RHI_Null Rendering_RHI_Vulkan Rendering_RHI_Validation Shell) +if(WIN32) + target_link_libraries(Rendering_Graphics PUBLIC Rendering_RHI_DX12) + target_compile_definitions(Rendering_Graphics PUBLIC DRACO_HAS_DX12) +endif() diff --git a/Engine/cpp/Runtime/Core/CMakeLists.txt b/Engine/cpp/Runtime/Core/CMakeLists.txt index cf2a4391..0aeb5f18 100644 --- a/Engine/cpp/Runtime/Core/CMakeLists.txt +++ b/Engine/cpp/Runtime/Core/CMakeLists.txt @@ -1,12 +1,13 @@ -add_modules_library(Definitions) -add_modules_library(Math) -add_modules_library(IO) -add_modules_library(Memory) -add_modules_library(Logging) -add_modules_library(Containers) +# PIC: these are linked into the shared Runtime library (via Core), so their objects +# must be position-independent - a non-PIC static (e.g. Quaternion::identity) otherwise +# fails to relocate when making the shared object. +add_modules_library(Definitions PIC) +add_modules_library(Math PIC) +add_modules_library(IO PIC) +add_modules_library(Memory PIC) +add_modules_library(Logging PIC) -target_link_libraries(Math PUBLIC Definitions bx) -target_link_libraries(IO PUBLIC Definitions stb_image) +target_link_libraries(Math PUBLIC Definitions) +target_link_libraries(IO PUBLIC Definitions) target_link_libraries(Memory PUBLIC Definitions Math) target_link_libraries(Logging PUBLIC Definitions) -target_link_libraries(Containers PUBLIC Definitions Memory Math) diff --git a/Engine/cpp/Runtime/Core/Core.cppm b/Engine/cpp/Runtime/Core/Core.cppm index f7c8ebda..25feb11a 100644 --- a/Engine/cpp/Runtime/Core/Core.cppm +++ b/Engine/cpp/Runtime/Core/Core.cppm @@ -2,6 +2,7 @@ export module core; export import core.defs; export import core.version; export import core.math; +export import core.color; export import core.memory; export import core.io; export import core.logging; diff --git a/Engine/cpp/Runtime/Core/Definitions/Definitions.cppm b/Engine/cpp/Runtime/Core/Definitions/Definitions.cppm index a1f9ebf4..2c1d6d8b 100644 --- a/Engine/cpp/Runtime/Core/Definitions/Definitions.cppm +++ b/Engine/cpp/Runtime/Core/Definitions/Definitions.cppm @@ -4,6 +4,7 @@ module; export module core.defs; export import core.stdtypes; +export import core.status; static_assert(__cplusplus >= 202207L, "Minimum of C++23 required."); diff --git a/Engine/cpp/Runtime/Core/Definitions/Status.cppm b/Engine/cpp/Runtime/Core/Definitions/Status.cppm new file mode 100644 index 00000000..165808c1 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Definitions/Status.cppm @@ -0,0 +1,38 @@ +// Status / ErrorCode - success or an error code, no payload. For operations +// that return a value-or-error, use std::expected instead. + +export module core.status; + +import core.stdtypes; + +export namespace draco +{ + enum class ErrorCode : u32 + { + Ok = 0, + Unknown, + InvalidArgument, + OutOfRange, + OutOfMemory, + NotFound, + NotSupported, + AlreadyExists, + Internal, + }; + + class Status + { + public: + constexpr Status() = default; + constexpr Status(ErrorCode code) : m_code(code) {} + + [[nodiscard]] constexpr ErrorCode code() const { return m_code; } + [[nodiscard]] constexpr bool isOk() const { return m_code == ErrorCode::Ok; } + [[nodiscard]] constexpr explicit operator bool() const { return isOk(); } + + friend constexpr bool operator==(Status a, Status b) { return a.m_code == b.m_code; } + + private: + ErrorCode m_code = ErrorCode::Ok; + }; +} diff --git a/Engine/cpp/Runtime/Core/IO/IO.cppm b/Engine/cpp/Runtime/Core/IO/IO.cppm index ae8d71ae..998c0ca1 100644 --- a/Engine/cpp/Runtime/Core/IO/IO.cppm +++ b/Engine/cpp/Runtime/Core/IO/IO.cppm @@ -1,4 +1,3 @@ export module core.io; export import core.io.filesystem; -export import core.io.loader.image; diff --git a/Engine/cpp/Runtime/Core/IO/ImageLoader.cpp b/Engine/cpp/Runtime/Core/IO/ImageLoader.cpp deleted file mode 100644 index 1e634881..00000000 --- a/Engine/cpp/Runtime/Core/IO/ImageLoader.cpp +++ /dev/null @@ -1,63 +0,0 @@ -module; - -#include -#include -#include -#include - -#define STB_IMAGE_IMPLEMENTATION -#include - -module core.io.loader.image; - -import core.stdtypes; - -// TODO: I'm too lazy to write code so we need somethin' better (AR-DEV-1) - -namespace draco::core::io::loader::image -{ - ImageData loadImage(const std::filesystem::path& path) - { - ImageData result; - - std::error_code ec; - if (!std::filesystem::exists(path, ec) || ec) { - std::println("Error: Image path does not exist: {}", path.string()); - return result; - } - - int width, height, channels; - // STBI_rgb_alpha forces the output to be 4 bytes per pixel (RGBA) - unsigned char* data = stbi_load(path.string().c_str(), &width, &height, &channels, STBI_rgb_alpha); - - if (!data) { - std::println("Error: Failed to decode image: {}", path.string()); - return result; - } - - if (width <= 0 || height <= 0) { - stbi_image_free(data); - return result; - } - - const usize w = static_cast(width); - const usize h = static_cast(height); - if (w > (std::numeric_limits::max() / 4) / h) { - stbi_image_free(data); - return result; - } - - usize size = w * h * 4; - - result.pixels.assign(data, data + size); - result.width = static_cast(width); - result.height = static_cast(height); - result.channels = 4; - result.isValid = true; - - // Free the memory allocated by stb - stbi_image_free(data); - - return result; - } -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/IO/ImageLoader.cppm b/Engine/cpp/Runtime/Core/IO/ImageLoader.cppm deleted file mode 100644 index 0c94d4bf..00000000 --- a/Engine/cpp/Runtime/Core/IO/ImageLoader.cppm +++ /dev/null @@ -1,23 +0,0 @@ -module; - -#include -#include - -export module core.io.loader.image; - -import core.stdtypes; - -export namespace draco::core::io::loader::image -{ - struct ImageData - { - std::vector pixels{}; - u32 width = 0; - u32 height = 0; - u8 channels = 0; - bool isValid = false; - }; - - // Load an image file (PNG, JPG, etc.) & decode it to raw RGBA8 - ImageData loadImage(const std::filesystem::path& path); -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/Math/AABB.cppm b/Engine/cpp/Runtime/Core/Math/AABB.cppm new file mode 100644 index 00000000..73894dac --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/AABB.cppm @@ -0,0 +1,33 @@ +module; + +#include +#include + +export module core.math.aabb; + +import core.defs; +import core.stdtypes; +import core.math.types; + +export namespace draco::math { + +// An axis-aligned bounding box defined by its min/max corners. +struct AABB { + Vector3 min{ 0, 0, 0 }; + Vector3 max{ 0, 0, 0 }; + + // An inverted box (min = +inf, max = -inf) so the first expand() sets both corners. + [[nodiscard]] static AABB empty() noexcept { + constexpr f32 inf = std::numeric_limits::infinity(); + return AABB{ Vector3{ inf, inf, inf }, Vector3{ -inf, -inf, -inf } }; + } + // Grows the box to contain point `p`. + void expand(Vector3 p) noexcept { + min = Vector3{ std::min(min.x, p.x), std::min(min.y, p.y), std::min(min.z, p.z) }; + max = Vector3{ std::max(max.x, p.x), std::max(max.y, p.y), std::max(max.z, p.z) }; + } + [[nodiscard]] Vector3 center() const noexcept { return (min + max) * 0.5f; } + [[nodiscard]] Vector3 size() const noexcept { return max - min; } +}; + +} // namespace draco::math diff --git a/Engine/cpp/Runtime/Core/Math/AABB.test.cpp b/Engine/cpp/Runtime/Core/Math/AABB.test.cpp new file mode 100644 index 00000000..e3d4d8e4 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/AABB.test.cpp @@ -0,0 +1,49 @@ +#include + +import core.stdtypes; +import core.math; + +using namespace draco; +using namespace draco::math; + +TEST_CASE("aabb: empty() is inverted so the first expand() seats both corners") +{ + AABB box = AABB::empty(); + CHECK(box.min.x > box.max.x); // inverted before any point + + box.expand(Vector3{ 1.0f, 2.0f, 3.0f }); + CHECK(box.min.x == 1.0f); + CHECK(box.max.x == 1.0f); + CHECK(box.min.z == 3.0f); + CHECK(box.max.z == 3.0f); +} + +TEST_CASE("aabb: expand grows to contain all points") +{ + AABB box = AABB::empty(); + box.expand(Vector3{ -1.0f, -2.0f, -3.0f }); + box.expand(Vector3{ 4.0f, 5.0f, 6.0f }); + box.expand(Vector3{ 0.0f, 0.0f, 0.0f }); + + CHECK(box.min.x == -1.0f); + CHECK(box.min.y == -2.0f); + CHECK(box.min.z == -3.0f); + CHECK(box.max.x == 4.0f); + CHECK(box.max.y == 5.0f); + CHECK(box.max.z == 6.0f); +} + +TEST_CASE("aabb: center and size") +{ + const AABB box{ Vector3{ -2.0f, 0.0f, 1.0f }, Vector3{ 4.0f, 6.0f, 5.0f } }; + + const Vector3 c = box.center(); + CHECK(c.x == doctest::Approx(1.0f)); + CHECK(c.y == doctest::Approx(3.0f)); + CHECK(c.z == doctest::Approx(3.0f)); + + const Vector3 s = box.size(); + CHECK(s.x == doctest::Approx(6.0f)); + CHECK(s.y == doctest::Approx(6.0f)); + CHECK(s.z == doctest::Approx(4.0f)); +} diff --git a/Engine/cpp/Runtime/Core/Math/Color.cppm b/Engine/cpp/Runtime/Core/Math/Color.cppm new file mode 100644 index 00000000..35e7eb8b --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Color.cppm @@ -0,0 +1,166 @@ +module; + +#include +#include + +export module core.color; + +import core.stdtypes; + +export namespace draco +{ + // ======================================================================= + // Color - linear RGBA, float components (typically 0..1). + // ======================================================================= + struct Color + { + f32 r = 0.0f; + f32 g = 0.0f; + f32 b = 0.0f; + f32 a = 1.0f; + + constexpr Color() noexcept = default; + constexpr Color(f32 inR, f32 inG, f32 inB, f32 inA = 1.0f) noexcept : r(inR), g(inG), b(inB), a(inA) {} + + // Packs to 0xRRGGBBAA (components clamped to 0..1). + [[nodiscard]] u32 toRGBA8() const noexcept + { + const auto byteOf = [](f32 c) -> u32 + { + return static_cast(std::clamp(c, 0.0f, 1.0f) * 255.0f + 0.5f); + }; + return (byteOf(r) << 24) | (byteOf(g) << 16) | (byteOf(b) << 8) | byteOf(a); + } + + [[nodiscard]] static Color fromRGBA8(u32 packed) noexcept + { + return Color{ static_cast((packed >> 24) & 0xFFu) / 255.0f, + static_cast((packed >> 16) & 0xFFu) / 255.0f, + static_cast((packed >> 8) & 0xFFu) / 255.0f, + static_cast(packed & 0xFFu) / 255.0f }; + } + + static const Color White; + static const Color Black; + static const Color Red; + static const Color Green; + static const Color Blue; + static const Color Transparent; + }; + + inline constexpr Color Color::White{ 1.0f, 1.0f, 1.0f, 1.0f }; + inline constexpr Color Color::Black{ 0.0f, 0.0f, 0.0f, 1.0f }; + inline constexpr Color Color::Red{ 1.0f, 0.0f, 0.0f, 1.0f }; + inline constexpr Color Color::Green{ 0.0f, 1.0f, 0.0f, 1.0f }; + inline constexpr Color Color::Blue{ 0.0f, 0.0f, 1.0f, 1.0f }; + inline constexpr Color Color::Transparent{ 0.0f, 0.0f, 0.0f, 0.0f }; + + [[nodiscard]] constexpr Color operator*(Color c, f32 s) noexcept { return { c.r * s, c.g * s, c.b * s, c.a * s }; } + [[nodiscard]] constexpr Color operator+(Color a, Color b) noexcept { return { a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a }; } + [[nodiscard]] constexpr bool operator==(Color a, Color b) noexcept + { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; + } + + [[nodiscard]] constexpr Color lerp(Color a, Color b, f32 t) noexcept + { + return { std::lerp(a.r, b.r, t), std::lerp(a.g, b.g, t), std::lerp(a.b, b.b, t), std::lerp(a.a, b.a, t) }; + } + + [[nodiscard]] inline bool nearlyEqual(Color a, Color b, f32 epsilon = 1e-5f) noexcept + { + return std::abs(a.r - b.r) <= epsilon && std::abs(a.g - b.g) <= epsilon + && std::abs(a.b - b.b) <= epsilon && std::abs(a.a - b.a) <= epsilon; + } + + // ======================================================================= + // Color32 - packed 8-bit RGBA, for compact storage: image pixels, vertex + // colors, asset interchange. The byte-color counterpart of the float Color + // (which stays the linear/HDR runtime currency). Conversions are plain + // 0..255 <-> 0..1 with NO gamma; sRGB<->linear is handled explicitly at the + // texture/format edge (ImageColorSpace, *UnormSrgb formats). + // ======================================================================= + struct Color32 + { + u8 r = 0; + u8 g = 0; + u8 b = 0; + u8 a = 255; + + constexpr Color32() noexcept = default; + constexpr Color32(u8 inR, u8 inG, u8 inB, u8 inA = 255) noexcept : r(inR), g(inG), b(inB), a(inA) {} + + // Packs to 0xRRGGBBAA. + [[nodiscard]] constexpr u32 toRGBA8() const noexcept + { + return (static_cast(r) << 24) | (static_cast(g) << 16) + | (static_cast(b) << 8) | static_cast(a); + } + + [[nodiscard]] static constexpr Color32 fromRGBA8(u32 packed) noexcept + { + return Color32{ static_cast((packed >> 24) & 0xFFu), + static_cast((packed >> 16) & 0xFFu), + static_cast((packed >> 8) & 0xFFu), + static_cast(packed & 0xFFu) }; + } + + static const Color32 White; + static const Color32 Black; + static const Color32 Red; + static const Color32 Green; + static const Color32 Blue; + static const Color32 Transparent; + }; + + inline constexpr Color32 Color32::White{ 255, 255, 255, 255 }; + inline constexpr Color32 Color32::Black{ 0, 0, 0, 255 }; + inline constexpr Color32 Color32::Red{ 255, 0, 0, 255 }; + inline constexpr Color32 Color32::Green{ 0, 255, 0, 255 }; + inline constexpr Color32 Color32::Blue{ 0, 0, 255, 255 }; + inline constexpr Color32 Color32::Transparent{ 0, 0, 0, 0 }; + + [[nodiscard]] constexpr bool operator==(Color32 a, Color32 b) noexcept + { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; + } + + // Float Color -> packed bytes (components clamped to 0..1, rounded). + [[nodiscard]] constexpr Color32 toColor32(Color c) noexcept + { + const auto byteOf = [](f32 v) -> u8 + { + return static_cast(std::clamp(v, 0.0f, 1.0f) * 255.0f + 0.5f); + }; + return Color32{ byteOf(c.r), byteOf(c.g), byteOf(c.b), byteOf(c.a) }; + } + + // Packed bytes -> float Color (exact 0..255 -> 0..1; round-trips toColor32). + [[nodiscard]] constexpr Color toColor(Color32 c) noexcept + { + return Color{ static_cast(c.r) / 255.0f, static_cast(c.g) / 255.0f, + static_cast(c.b) / 255.0f, static_cast(c.a) / 255.0f }; + } + + // sRGB <-> linear transfer functions (the standard IEC 61966-2-1 EOTF). For + // decoding sRGB-authored colors to linear before linear-space blending/shading. + [[nodiscard]] inline f32 srgbToLinear(f32 c) noexcept + { + return (c <= 0.04045f) ? (c / 12.92f) : std::pow((c + 0.055f) / 1.055f, 2.4f); + } + + [[nodiscard]] inline f32 linearToSrgb(f32 c) noexcept + { + return (c <= 0.0031308f) ? (c * 12.92f) : (1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f); + } + + // Color32 (sRGB-authored) -> linear float Color (RGB through the sRGB EOTF; + // alpha is linear). For uploading UI/SVG colors to a linear/sRGB pipeline. + [[nodiscard]] inline Color toLinear(Color32 c) noexcept + { + return Color{ srgbToLinear(static_cast(c.r) / 255.0f), + srgbToLinear(static_cast(c.g) / 255.0f), + srgbToLinear(static_cast(c.b) / 255.0f), + static_cast(c.a) / 255.0f }; + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Color.test.cpp b/Engine/cpp/Runtime/Core/Math/Color.test.cpp new file mode 100644 index 00000000..ea855d9d --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Color.test.cpp @@ -0,0 +1,39 @@ +#include +#include + +import core.stdtypes; +import core.color; + +using namespace draco; + +TEST_CASE("color: Color32 packs and unpacks 0xRRGGBBAA") +{ + const Color32 c{ 12, 34, 56, 78 }; + CHECK(c.toRGBA8() == 0x0C22384Eu); + CHECK(Color32::fromRGBA8(0x0C22384Eu) == c); + CHECK(Color32::fromRGBA8(c.toRGBA8()) == c); // round-trip +} + +TEST_CASE("color: named constants") +{ + CHECK(Color32::White == Color32{ 255, 255, 255, 255 }); + CHECK(Color32::Black == Color32{ 0, 0, 0, 255 }); + CHECK(Color32::Transparent.a == 0); + CHECK(Color::Red == Color{ 1.0f, 0.0f, 0.0f, 1.0f }); + CHECK(Color::White.toRGBA8() == 0xFFFFFFFFu); +} + +TEST_CASE("color: Color <-> Color32 round-trips exactly") +{ + const Color32 c{ 10, 20, 30, 255 }; + CHECK(toColor32(toColor(c)) == c); + CHECK(toColor32(Color{ 1.0f, 0.0f, 0.5f, 1.0f }) == Color32{ 255, 0, 128, 255 }); +} + +TEST_CASE("color: sRGB transfer round-trips") +{ + for (f32 v : { 0.0f, 0.25f, 0.5f, 1.0f }) + { + CHECK(std::abs(linearToSrgb(srgbToLinear(v)) - v) < 1e-4f); + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Easings.cppm b/Engine/cpp/Runtime/Core/Math/Easings.cppm new file mode 100644 index 00000000..de611333 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Easings.cppm @@ -0,0 +1,174 @@ +module; + +#include + +export module core.math.easings; + +import core.defs; +import core.math.constants; + +// Standard easing functions: each maps an interpolation factor t in [0,1] to an eased +// value (also ~[0,1]). Pure functions over the scalar math; consumed by animation +// (EasingType) and UI transitions. +export namespace draco::math { + +// A function mapping t in [0,1] to an eased interpolation factor. +using EasingFunction = f32 (*)(f32); + +[[nodiscard]] inline f32 easeInLinear(f32 t) noexcept { return t; } +[[nodiscard]] inline f32 easeOutLinear(f32 t) noexcept { return t; } + +// --- quadratic --- +[[nodiscard]] inline f32 easeInQuadratic(f32 t) noexcept { return t * t; } +[[nodiscard]] inline f32 easeOutQuadratic(f32 t) noexcept { return -1.0f * t * (t - 2.0f); } +[[nodiscard]] inline f32 easeInOutQuadratic(f32 t) noexcept +{ + t *= 2.0f; + if (t < 1.0f) { return 0.5f * t * t; } + t -= 1.0f; + return -0.5f * (t * (t - 2.0f) - 1.0f); +} + +// --- cubic --- +[[nodiscard]] inline f32 easeInCubic(f32 t) noexcept { return t * t * t; } +[[nodiscard]] inline f32 easeOutCubic(f32 t) noexcept { t -= 1.0f; return t * t * t + 1.0f; } +[[nodiscard]] inline f32 easeInOutCubic(f32 t) noexcept +{ + t *= 2.0f; + if (t < 1.0f) { return 0.5f * t * t * t; } + t -= 2.0f; + return 0.5f * (t * t * t + 2.0f); +} + +// --- quartic --- +[[nodiscard]] inline f32 easeInQuartic(f32 t) noexcept { return t * t * t * t; } +[[nodiscard]] inline f32 easeOutQuartic(f32 t) noexcept { t -= 1.0f; return -1.0f * (t * t * t * t - 1.0f); } +[[nodiscard]] inline f32 easeInOutQuartic(f32 t) noexcept +{ + t *= 2.0f; + if (t < 1.0f) { return 0.5f * t * t * t * t; } + t -= 2.0f; + return -0.5f * (t * t * t * t - 2.0f); +} + +// --- quintic --- +[[nodiscard]] inline f32 easeInQuintic(f32 t) noexcept { return t * t * t * t * t; } +[[nodiscard]] inline f32 easeOutQuintic(f32 t) noexcept { t -= 1.0f; return t * t * t * t * t + 1.0f; } +[[nodiscard]] inline f32 easeInOutQuintic(f32 t) noexcept +{ + t *= 2.0f; + if (t < 1.0f) { return 0.5f * t * t * t * t * t; } + t -= 2.0f; + return 0.5f * (t * t * t * t * t + 2.0f); +} + +// --- sinusoidal --- +[[nodiscard]] inline f32 easeInSin(f32 t) noexcept { return -1.0f * std::cos(t * PI2) + 1.0f; } +[[nodiscard]] inline f32 easeOutSin(f32 t) noexcept { return std::sin(t * PI2); } +[[nodiscard]] inline f32 easeInOutSin(f32 t) noexcept { return -0.5f * (std::cos(PI * t) - 1.0f); } + +// --- exponential --- +[[nodiscard]] inline f32 easeInExponential(f32 t) noexcept +{ + if (t == 0.0f) { return 0.0f; } + return std::pow(2.0f, 10.0f * (t - 1.0f)); +} +[[nodiscard]] inline f32 easeOutExponential(f32 t) noexcept +{ + if (t == 1.0f) { return 1.0f; } + return -std::pow(2.0f, -10.0f * t) + 1.0f; +} +[[nodiscard]] inline f32 easeInOutExponential(f32 t) noexcept +{ + if (t == 0.0f) { return 0.0f; } + if (t == 1.0f) { return 1.0f; } + t *= 2.0f; + if (t < 1.0f) { return 0.5f * std::pow(2.0f, 10.0f * (t - 1.0f)); } + t -= 1.0f; + return 0.5f * (-std::pow(2.0f, -10.0f * t) + 2.0f); +} + +// --- circular --- +[[nodiscard]] inline f32 easeInCircular(f32 t) noexcept { return -1.0f * (std::sqrt(1.0f - t * t) - 1.0f); } +[[nodiscard]] inline f32 easeOutCircular(f32 t) noexcept { t -= 1.0f; return std::sqrt(1.0f - t * t); } +[[nodiscard]] inline f32 easeInOutCircular(f32 t) noexcept +{ + t *= 2.0f; + if (t < 1.0f) { return -0.5f * (std::sqrt(1.0f - t * t) - 1.0f); } + t -= 2.0f; + return 0.5f * (std::sqrt(1.0f - t * t) + 1.0f); +} + +// --- back (overshoot) --- +[[nodiscard]] inline f32 easeInBack(f32 t) noexcept +{ + constexpr f32 s = 1.70158f; + return t * t * ((s + 1.0f) * t - s); +} +[[nodiscard]] inline f32 easeOutBack(f32 t) noexcept +{ + constexpr f32 s = 1.70158f; + t -= 1.0f; + return t * t * ((s + 1.0f) * t + s) + 1.0f; +} +[[nodiscard]] inline f32 easeInOutBack(f32 t) noexcept +{ + constexpr f32 s = 1.70158f; + constexpr f32 s2 = s * 1.525f; + t *= 2.0f; + if (t < 1.0f) { return 0.5f * (t * t * ((s2 + 1.0f) * t - s2)); } + t -= 2.0f; + return 0.5f * (t * t * ((s2 + 1.0f) * t + s2) + 2.0f); +} + +// --- elastic --- +[[nodiscard]] inline f32 easeInElastic(f32 t) noexcept +{ + if (t == 0.0f) { return 0.0f; } + if (t == 1.0f) { return 1.0f; } + constexpr f32 p = 0.3f; + constexpr f32 s = p / 4.0f; + t -= 1.0f; + return -(std::pow(2.0f, 10.0f * t) * std::sin((t - s) * (2.0f * PI) / p)); +} +[[nodiscard]] inline f32 easeOutElastic(f32 t) noexcept +{ + if (t == 0.0f) { return 0.0f; } + if (t == 1.0f) { return 1.0f; } + constexpr f32 p = 0.3f; + constexpr f32 s = p / 4.0f; + return std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * PI) / p) + 1.0f; +} +[[nodiscard]] inline f32 easeInOutElastic(f32 t) noexcept +{ + if (t == 0.0f) { return 0.0f; } + if (t == 1.0f) { return 1.0f; } + t *= 2.0f; + constexpr f32 p = 0.3f * 1.5f; + constexpr f32 s = p / 4.0f; + if (t < 1.0f) + { + t -= 1.0f; + return -0.5f * (std::pow(2.0f, 10.0f * t) * std::sin((t - s) * (2.0f * PI) / p)); + } + t -= 1.0f; + return std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * PI) / p) * 0.5f + 1.0f; +} + +// --- bounce (out defined first; in/inout reference it) --- +[[nodiscard]] inline f32 easeOutBounce(f32 t) noexcept +{ + if (t < 1.0f / 2.75f) { return 7.5625f * t * t; } + if (t < 2.0f / 2.75f) { t -= 1.5f / 2.75f; return 7.5625f * t * t + 0.75f; } + if (t < 2.5f / 2.75f) { t -= 2.25f / 2.75f; return 7.5625f * t * t + 0.9375f; } + t -= 2.625f / 2.75f; + return 7.5625f * t * t + 0.984375f; +} +[[nodiscard]] inline f32 easeInBounce(f32 t) noexcept { return 1.0f - easeOutBounce(1.0f - t); } +[[nodiscard]] inline f32 easeInOutBounce(f32 t) noexcept +{ + if (t < 0.5f) { return easeInBounce(t * 2.0f) * 0.5f; } + return easeOutBounce(t * 2.0f - 1.0f) * 0.5f + 0.5f; +} + +} // namespace draco::math diff --git a/Engine/cpp/Runtime/Core/Math/Easings.test.cpp b/Engine/cpp/Runtime/Core/Math/Easings.test.cpp new file mode 100644 index 00000000..e88b7311 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Easings.test.cpp @@ -0,0 +1,43 @@ +#include + +import core.stdtypes; +import core.math; + +using namespace draco; +using namespace draco::math; + +TEST_CASE("easings: endpoints map 0->0 and 1->1") +{ + const EasingFunction fns[] = { + easeInQuadratic, easeOutQuadratic, easeInOutQuadratic, + easeInCubic, easeOutCubic, easeInOutCubic, + easeInQuartic, easeOutQuartic, easeInOutQuartic, + easeInQuintic, easeOutQuintic, easeInOutQuintic, + easeInSin, easeOutSin, easeInOutSin, + easeInExponential, easeOutExponential, easeInOutExponential, + easeInCircular, easeOutCircular, easeInOutCircular, + easeInBack, easeOutBack, easeInOutBack, + easeInElastic, easeOutElastic, easeInOutElastic, + easeInBounce, easeOutBounce, easeInOutBounce, + }; + for (EasingFunction f : fns) { + CHECK(f(0.0f) == doctest::Approx(0.0f).epsilon(0.001)); + CHECK(f(1.0f) == doctest::Approx(1.0f).epsilon(0.001)); + } +} + +TEST_CASE("easings: linear is the identity; quadratic is t*t") +{ + CHECK(easeInLinear(0.25f) == doctest::Approx(0.25f)); + CHECK(easeInLinear(0.9f) == doctest::Approx(0.9f)); + CHECK(easeInQuadratic(0.5f) == doctest::Approx(0.25f)); + CHECK(easeInCubic(0.5f) == doctest::Approx(0.125f)); +} + +TEST_CASE("easings: in/out symmetry at the midpoint of an ease-in-out") +{ + // ease-in-out families cross 0.5 at t=0.5. + CHECK(easeInOutQuadratic(0.5f) == doctest::Approx(0.5f)); + CHECK(easeInOutCubic(0.5f) == doctest::Approx(0.5f)); + CHECK(easeInOutSin(0.5f) == doctest::Approx(0.5f)); +} diff --git a/Engine/cpp/Runtime/Core/Math/Functions.cppm b/Engine/cpp/Runtime/Core/Math/Functions.cppm index 6a411f06..52969c71 100644 --- a/Engine/cpp/Runtime/Core/Math/Functions.cppm +++ b/Engine/cpp/Runtime/Core/Math/Functions.cppm @@ -1,5 +1,6 @@ module; +#include #include #include #include @@ -114,6 +115,21 @@ export namespace draco::math { return static_cast(std::pow(x, y)); } + // Trigonometry (radians) and square root. + [[nodiscard]] inline f32 sin(f32 x) noexcept { return std::sin(x); } + [[nodiscard]] inline f32 cos(f32 x) noexcept { return std::cos(x); } + [[nodiscard]] inline f32 tan(f32 x) noexcept { return std::tan(x); } + [[nodiscard]] inline f32 acos(f32 x) noexcept { return std::acos(x); } + [[nodiscard]] inline f32 sqrt(f32 x) noexcept { return std::sqrt(x); } + + // Approximate comparisons against an epsilon (defaults to CMP_EPSILON). + [[nodiscard]] constexpr bool nearlyEqual(f32 a, f32 b, f32 epsilon = CMP_EPSILON) noexcept { + return abs(a - b) <= epsilon; + } + [[nodiscard]] constexpr bool nearlyZero(f32 v, f32 epsilon = CMP_EPSILON) noexcept { + return abs(v) <= epsilon; + } + template constexpr T lerp(T from, T to, T weight) noexcept { return std::lerp(from, to, weight); @@ -202,4 +218,4 @@ export namespace draco::math { T d = (control_1 - start) * T{3.} * omt2 + (control_2 - control_1) * T{6.} * omt * t + (end - control_2) * T{3.} * t2; return d; } -} +} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/Math/Math.cppm b/Engine/cpp/Runtime/Core/Math/Math.cppm index bf983a28..4cf45737 100644 --- a/Engine/cpp/Runtime/Core/Math/Math.cppm +++ b/Engine/cpp/Runtime/Core/Math/Math.cppm @@ -3,5 +3,10 @@ export module core.math; export import core.defs; export import core.math.constants; export import core.math.functions; +export import core.math.easings; export import core.math.types; +export import core.math.packed; +export import core.math.aabb; +export import core.math.matrix4; +export import core.math.quaternion; export import core.math.transform; diff --git a/Engine/cpp/Runtime/Core/Math/Math.test.cpp b/Engine/cpp/Runtime/Core/Math/Math.test.cpp index 4b300766..55686ebe 100644 --- a/Engine/cpp/Runtime/Core/Math/Math.test.cpp +++ b/Engine/cpp/Runtime/Core/Math/Math.test.cpp @@ -1,4 +1,5 @@ #include +#include import core.math; @@ -449,28 +450,16 @@ TEST_SUITE("vector2") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector2; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector2 v{1.0f, 2.0f}; - static constexpr Vector2 offset = Vector2::xAxis(CMP_EPSILON); - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector2{0.5f * CMP_EPSILON, 0.0f})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector2{2.0f * CMP_EPSILON, 0.0f})); // beyond epsilon } } @@ -904,28 +893,16 @@ TEST_SUITE("vector3") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector3; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector3 v{1.0f, 2.0f, 3.0f}; - static constexpr Vector3 offset = Vector3::xAxis(CMP_EPSILON); - - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector3{0.0f, 0.0f, 0.5f * CMP_EPSILON})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector3{0.0f, 0.0f, 2.0f * CMP_EPSILON})); // beyond epsilon (z lane) } TEST_CASE("cross") { @@ -1395,43 +1372,241 @@ TEST_SUITE("vector4") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector4; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector4 v{1.0f, 2.0f, 3.0f, 4.0f}; - static constexpr Vector4 offset = Vector4::xAxis(CMP_EPSILON); - - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector4{0.0f, 0.0f, 0.0f, 0.5f * CMP_EPSILON})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector4{0.0f, 0.0f, 0.0f, 2.0f * CMP_EPSILON})); // beyond epsilon (w lane) } TEST_CASE("Transform") { using math::Transform; static constexpr Transform transform; - STATIC_REQUIRE(transform.position[0] == 0.f); - STATIC_REQUIRE(transform.position[1] == 0.f); - STATIC_REQUIRE(transform.position[2] == 0.f); - STATIC_REQUIRE(transform.rotation[0] == 0.f); - STATIC_REQUIRE(transform.rotation[1] == 0.f); - STATIC_REQUIRE(transform.rotation[2] == 0.f); - STATIC_REQUIRE(transform.scale[0] == 1.f); - STATIC_REQUIRE(transform.scale[1] == 1.f); - STATIC_REQUIRE(transform.scale[2] == 1.f); + // Default: position 0, rotation identity, scale 1. + STATIC_REQUIRE(transform.position.x == 0.f); + STATIC_REQUIRE(transform.position.y == 0.f); + STATIC_REQUIRE(transform.position.z == 0.f); + STATIC_REQUIRE(transform.rotation.x == 0.f); + STATIC_REQUIRE(transform.rotation.y == 0.f); + STATIC_REQUIRE(transform.rotation.z == 0.f); + STATIC_REQUIRE(transform.rotation.w == 1.f); + STATIC_REQUIRE(transform.scale.x == 1.f); + STATIC_REQUIRE(transform.scale.y == 1.f); + STATIC_REQUIRE(transform.scale.z == 1.f); + } +} + +TEST_SUITE("matrix4") { + TEST_CASE("identity and multiply") { + using namespace draco::math; + const Matrix4 id = Matrix4::identity(); + const Matrix4 t = Matrix4::translation(Vector3{ 1.0f, 2.0f, 3.0f }); + + CHECK(nearlyEqual(id * t, t)); + CHECK(nearlyEqual(t * id, t)); + + static_assert(Matrix4::identity()[0, 0] == 1.0f); + static_assert(Matrix4::identity()[0, 1] == 0.0f); + } + + TEST_CASE("translation lives in the last row (row vectors)") { + using namespace draco::math; + const Matrix4 t = Matrix4::translation(Vector3{ 10.0f, 20.0f, 30.0f }); + CHECK(t.m[3][0] == 10.0f); + CHECK(t.m[3][1] == 20.0f); + CHECK(t.m[3][2] == 30.0f); + + const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, t); + CHECK(nearlyEqual(p, Vector3{ 11.0f, 21.0f, 31.0f })); + + // Directions ignore translation. + CHECK(nearlyEqual(transformDirection(Vector3{ 1.0f, 0.0f, 0.0f }, t), Vector3{ 1.0f, 0.0f, 0.0f })); + } + + TEST_CASE("rotationZ(90) maps +X to +Y (row vectors)") { + using namespace draco::math; + const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f)); + CHECK(nearlyEqual(transformDirection(Vector3::xAxis(), rz), Vector3::yAxis())); + + // rotationY(90) maps +Z to +X. + const Matrix4 ry = Matrix4::rotationY(degToRad(90.0f)); + CHECK(nearlyEqual(transformDirection(Vector3::zAxis(), ry), Vector3::xAxis())); + } + + TEST_CASE("2D affine helpers (transformPoint2D, operator==)") { + using namespace draco::math; + CHECK(Matrix4::identity() == Matrix4::identity()); + CHECK_FALSE(Matrix4::translation(Vector3{ 1, 0, 0 }) == Matrix4::identity()); + + const Matrix4 t = Matrix4::translation(Vector3{ 5.0f, 7.0f, 0.0f }); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 2.0f }, t), Vector2{ 6.0f, 9.0f })); + + // Row vectors, left-to-right: v * (S * T). + const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 3.0f, 1.0f }) * Matrix4::translation(Vector3{ 1.0f, 1.0f, 0.0f }); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 1.0f }, st), Vector2{ 3.0f, 4.0f })); + + const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f)); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 0.0f }, rz), Vector2{ 0.0f, 1.0f })); + } + + TEST_CASE("composition reads left-to-right (scale then translate)") { + using namespace draco::math; + const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 2.0f, 2.0f }) * Matrix4::translation(Vector3{ 1.0f, 0.0f, 0.0f }); + const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, st); + CHECK(nearlyEqual(p, Vector3{ 3.0f, 2.0f, 2.0f })); + } + + TEST_CASE("perspective has the expected projective structure") { + using namespace draco::math; + const Matrix4 proj = Matrix4::perspectiveFovRH(degToRad(90.0f), 1.0f, 1.0f, 100.0f); + CHECK(proj.m[2][3] == -1.0f); // w' = -z (RH) + CHECK(nearlyEqual(proj.m[0][0], 1.0f)); // xScale = 1/tan(45) at aspect 1 + + // Depth mapping: the near plane (z = -zNear) maps to NDC depth 0, the far plane + // (z = -zFar) to NDC depth 1 - exercising the near/far-derived coefficients. + const auto ndcDepth = [&](f32 z) { + const Vector4 clip = Vector4{ 0.0f, 0.0f, z, 1.0f } * proj; + return clip.z / clip.w; + }; + CHECK(nearlyEqual(ndcDepth(-1.0f), 0.0f, 1.0e-4f)); + CHECK(nearlyEqual(ndcDepth(-100.0f), 1.0f, 1.0e-4f)); + } + + TEST_CASE("determinant") { + using namespace draco::math; + CHECK(nearlyEqual(determinant(Matrix4::identity()), 1.0f)); + CHECK(nearlyEqual(determinant(Matrix4::scale(Vector3{ 2.0f, 3.0f, 4.0f })), 24.0f)); + } + + TEST_CASE("inverse undoes the transform") { + using namespace draco::math; + Transform xform; + xform.scale = Vector3{ 2.0f, 0.5f, 3.0f }; + xform.rotation = Quaternion::fromAxisAngle(normalize(Vector3{ 1.0f, 2.0f, 3.0f }), degToRad(50.0f)); + xform.position = Vector3{ 5.0f, -2.0f, 1.0f }; + + const Matrix4 m = xform.toMatrix(); + const Matrix4 inv = inverse(m); + + CHECK(nearlyEqual(m * inv, Matrix4::identity(), 1.0e-3f)); + CHECK(nearlyEqual(inv * m, Matrix4::identity(), 1.0e-3f)); + + // Point transformed then inverse-transformed returns to itself. + const Vector3 p{ 3.0f, 4.0f, 5.0f }; + const Vector3 roundTrip = transformPoint(transformPoint(p, m), inv); + CHECK(nearlyEqual(roundTrip, p, 1.0e-3f)); + + // Singular matrix: tryInverse reports the failure explicitly (no silent identity). + CHECK_FALSE(tryInverse(Matrix4::scale(Vector3::zero)).has_value()); + + // A small-but-invertible scale must NOT be flagged singular by the scale-aware + // threshold (an absolute-epsilon determinant check would wrongly reject it). + const Vector3 tinyScale{ 1.0e-3f, 1.0e-3f, 1.0e-3f }; + const auto tiny = tryInverse(Matrix4::scale(tinyScale)); + REQUIRE(tiny.has_value()); + CHECK(nearlyEqual(Matrix4::scale(tinyScale) * *tiny, Matrix4::identity(), 1.0e-2f)); + } +} + +TEST_SUITE("quaternion") { + TEST_CASE("rotates vectors and agrees with its matrix") { + using namespace draco::math; + const Quaternion q = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + // 90 deg about Z maps +X to +Y. + CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), Vector3::yAxis())); + + // Quaternion rotation and its matrix agree. + const Matrix4 r = rotationMatrix(q); + CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), transformDirection(Vector3::xAxis(), r))); + CHECK(nearlyEqual(rotateVector(q, Vector3{ 0.3f, -0.5f, 0.8f }), + transformDirection(Vector3{ 0.3f, -0.5f, 0.8f }, r))); + + // Identity does nothing. + CHECK(nearlyEqual(rotateVector(Quaternion::identity, Vector3{ 1.0f, 2.0f, 3.0f }), Vector3{ 1.0f, 2.0f, 3.0f })); + + // Composition: two 45-deg rotations == one 90-deg. + const Quaternion half = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(45.0f)); + CHECK(nearlyEqual(rotateVector(half * half, Vector3::xAxis()), Vector3::yAxis())); + + // Composition is non-commutative: distinct-axis rotations depend on order, and + // q1 * q2 applies q2 first, then q1 (so it equals the sequential rotation). + const Quaternion qx = Quaternion::fromAxisAngle(Vector3::xAxis(), degToRad(90.0f)); + const Quaternion qy = Quaternion::fromAxisAngle(Vector3::yAxis(), degToRad(90.0f)); + const Vector3 zv{ 0.0f, 0.0f, 1.0f }; + CHECK(nearlyEqual(rotateVector(qx * qy, zv), rotateVector(qx, rotateVector(qy, zv)))); + CHECK_FALSE(nearlyEqual(rotateVector(qx * qy, zv), rotateVector(qy * qx, zv))); + + // A zero-length axis has no direction to rotate about: fall back to identity + // rather than divide by zero and produce a NaN quaternion. + CHECK(nearlyEqual(Quaternion::fromAxisAngle(Vector3::zero, degToRad(90.0f)), Quaternion::identity)); + + // q and -q are the same rotation, so nearlyEqual treats them as equal. + CHECK(nearlyEqual(q, Quaternion{ -q.x, -q.y, -q.z, -q.w })); + } + + TEST_CASE("slerp endpoints and midpoint") { + using namespace draco::math; + const Quaternion a = Quaternion::identity; + const Quaternion b = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + CHECK(nearlyEqual(slerp(a, b, 0.0f), a)); + CHECK(nearlyEqual(slerp(a, b, 1.0f), b)); + + // Halfway 0..90 deg about Z is 45 deg: +X -> (cos45, sin45, 0). + const Quaternion mid = slerp(a, b, 0.5f); + const Vector3 rotated = rotateVector(mid, Vector3::xAxis()); + const f32 c = std::cos(degToRad(45.0f)); + CHECK(nearlyEqual(rotated, Vector3{ c, c, 0.0f }, 1.0e-4f)); + + // Antipodal endpoint: -b is the same rotation as b, so slerp's shortest-path sign + // correction gives a finite midpoint representing that same halfway rotation. + const Quaternion negB = Quaternion{ -b.x, -b.y, -b.z, -b.w }; + const Quaternion midAnti = slerp(a, negB, 0.5f); + CHECK(isFinite(midAnti.x)); + CHECK(nearlyEqual(rotateVector(midAnti, Vector3::xAxis()), Vector3{ c, c, 0.0f }, 1.0e-4f)); + } +} +TEST_SUITE("transform") { + TEST_CASE("composes scale, rotation, translation") { + using namespace draco::math; + Transform xform; + xform.scale = Vector3{ 2.0f, 2.0f, 2.0f }; + xform.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + xform.position = Vector3{ 5.0f, 0.0f, 0.0f }; + + const Matrix4 m = xform.toMatrix(); + + // (1,0,0) -> scale*2 -> (2,0,0) -> rot90Z -> (0,2,0) -> +translate -> (5,2,0) + const Vector3 p = transformPoint(Vector3::xAxis(), m); + CHECK(nearlyEqual(p, Vector3{ 5.0f, 2.0f, 0.0f })); + + // Identity transform is a no-op. + Transform id; + CHECK(nearlyEqual(transformPoint(Vector3{ 7.0f, 8.0f, 9.0f }, id.toMatrix()), + Vector3{ 7.0f, 8.0f, 9.0f })); + } + + TEST_CASE("lerp interpolates position, scale, and rotation") { + using namespace draco::math; + Transform a; + Transform b; + b.position = Vector3{ 10.0f, 0.0f, 0.0f }; + b.scale = Vector3{ 3.0f, 3.0f, 3.0f }; + b.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + const Transform mid = Transform::lerp(a, b, 0.5f); + CHECK(nearlyEqual(mid.position, Vector3{ 5.0f, 0.0f, 0.0f })); + CHECK(nearlyEqual(mid.scale, Vector3{ 2.0f, 2.0f, 2.0f })); + // Rotation slerped to 45 deg about Z: +X -> (cos45, sin45, 0). + const f32 c = std::cos(degToRad(45.0f)); + CHECK(nearlyEqual(rotateVector(mid.rotation, Vector3::xAxis()), Vector3{ c, c, 0.0f }, 1.0e-4f)); } } \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/Math/Matrix4.cpp b/Engine/cpp/Runtime/Core/Math/Matrix4.cpp new file mode 100644 index 00000000..724ebb5b --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Matrix4.cpp @@ -0,0 +1,72 @@ +// Matrix4 implementation: the 4x4 inverse routines. Kept out of the module interface +// so Matrix4.cppm carries no // - only these bodies need them. +module; + +#include +#include +#include +#include + +module core.math.matrix4; + +import core.stdtypes; +import core.math.constants; + +namespace draco::math +{ + std::optional tryInverse(const Matrix4& mat) noexcept + { + const f32* m = &mat.m[0][0]; + f32 inv[16]; + + inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10]; + inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10]; + inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9]; + inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9]; + inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]; + inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]; + inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9]; + inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9]; + inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]; + inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]; + inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]; + inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]; + inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]; + inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]; + inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]; + inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]; + + const f32 det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]; + + // Scale-aware singularity: by Hadamard's inequality |det| <= product of the + // row norms, so reject relative to that product, not an absolute epsilon. + const auto rowNorm = [&](usize r) { + const f32* row = m + r * 4; + return std::sqrt(row[0]*row[0] + row[1]*row[1] + row[2]*row[2] + row[3]*row[3]); + }; + const f32 normProduct = rowNorm(0) * rowNorm(1) * rowNorm(2) * rowNorm(3); + if (std::abs(det) <= CMP_EPSILON * normProduct) + { + return std::nullopt; + } + + const f32 invDet = 1.0f / det; + Matrix4 result{}; + f32* out = &result.m[0][0]; + for (usize i = 0; i < 16; ++i) + { + out[i] = inv[i] * invDet; + } + return result; + } + + Matrix4 inverse(const Matrix4& mat) noexcept + { + if (const std::optional inv = tryInverse(mat)) { return *inv; } + assert(false && "Matrix4 inverse(): singular matrix; use tryInverse() to handle it"); + Matrix4 result{}; + f32* out = &result.m[0][0]; + for (usize i = 0; i < 16; ++i) { out[i] = std::numeric_limits::quiet_NaN(); } + return result; + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Matrix4.cppm b/Engine/cpp/Runtime/Core/Math/Matrix4.cppm new file mode 100644 index 00000000..5aa0e630 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Matrix4.cppm @@ -0,0 +1,242 @@ +module; + +#include // tryInverse() return type; the rest of the impl lives in Matrix4.cpp + +export module core.math.matrix4; + +import core.stdtypes; +import core.math.types; +import core.math.functions; +import core.math.constants; + +// Matrix4: 4x4 row-major matrix - transforms, projections (perspective/ortho/ +// lookAt RH), multiply, transpose/determinant/inverse, point/direction xform. +// +// Conventions: row-major storage m[row][col]; row vectors (v' = v * M); +// composition left-to-right; right-handed projections, NDC depth [0,1]; +// translation in the last row. + +export namespace draco::math +{ + // ======================================================================= + // Matrix4 - 4x4, row-major, row-vector convention. + // ======================================================================= + struct Matrix4 + { + f32 m[4][4]; + + // Raw row-major float pointer (16 contiguous floats), e.g. for GPU upload. + [[nodiscard]] const f32* data() const noexcept { return &m[0][0]; } + [[nodiscard]] f32* data() noexcept { return &m[0][0]; } + + [[nodiscard]] constexpr f32 operator[](usize row, usize col) const noexcept + { + return m[row][col]; + } + [[nodiscard]] constexpr f32& operator[](usize row, usize col) noexcept + { + return m[row][col]; + } + + [[nodiscard]] static constexpr Matrix4 identity() noexcept + { + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static constexpr Matrix4 translation(Vector3 t) noexcept + { + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { t.x, t.y, t.z, 1.0f } } }; + } + + [[nodiscard]] static constexpr Matrix4 scale(Vector3 s) noexcept + { + return Matrix4{ { { s.x, 0.0f, 0.0f, 0.0f }, + { 0.0f, s.y, 0.0f, 0.0f }, + { 0.0f, 0.0f, s.z, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationX(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, c, s, 0.0f }, + { 0.0f, -s, c, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationY(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { c, 0.0f, -s, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { s, 0.0f, c, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationZ(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { c, s, 0.0f, 0.0f }, + { -s, c, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + // Right-handed perspective, NDC z in [0, 1]. + [[nodiscard]] static Matrix4 perspectiveFovRH(f32 fovYRadians, f32 aspect, f32 zNear, f32 zFar) noexcept + { + const f32 yScale = 1.0f / tan(fovYRadians * 0.5f); + const f32 xScale = yScale / aspect; + const f32 zRange = zFar / (zNear - zFar); + return Matrix4{ { { xScale, 0.0f, 0.0f, 0.0f }, + { 0.0f, yScale, 0.0f, 0.0f }, + { 0.0f, 0.0f, zRange, -1.0f }, + { 0.0f, 0.0f, zNear * zRange, 0.0f } } }; + } + + [[nodiscard]] static Matrix4 orthographicRH(f32 width, f32 height, f32 zNear, f32 zFar) noexcept + { + const f32 zRange = 1.0f / (zNear - zFar); + return Matrix4{ { { 2.0f / width, 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f / height, 0.0f, 0.0f }, + { 0.0f, 0.0f, zRange, 0.0f }, + { 0.0f, 0.0f, zNear * zRange, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 lookAtRH(Vector3 eye, Vector3 target, Vector3 up) noexcept + { + const Vector3 zAxis = normalize(eye - target); // camera looks down -z + const Vector3 xAxis = normalize(cross(up, zAxis)); + const Vector3 yAxis = cross(zAxis, xAxis); + return Matrix4{ { { xAxis.x, yAxis.x, zAxis.x, 0.0f }, + { xAxis.y, yAxis.y, zAxis.y, 0.0f }, + { xAxis.z, yAxis.z, zAxis.z, 0.0f }, + { -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1.0f } } }; + } + }; + + [[nodiscard]] constexpr Matrix4 operator*(const Matrix4& a, const Matrix4& b) noexcept + { + Matrix4 result{}; + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + f32 sum = 0.0f; + for (usize k = 0; k < 4; ++k) + { + sum += a.m[row][k] * b.m[k][col]; + } + result.m[row][col] = sum; + } + } + return result; + } + + // Row-vector transform: v' = v * M. + [[nodiscard]] constexpr Vector4 operator*(Vector4 v, const Matrix4& m) noexcept + { + return { v.x * m.m[0][0] + v.y * m.m[1][0] + v.z * m.m[2][0] + v.w * m.m[3][0], + v.x * m.m[0][1] + v.y * m.m[1][1] + v.z * m.m[2][1] + v.w * m.m[3][1], + v.x * m.m[0][2] + v.y * m.m[1][2] + v.z * m.m[2][2] + v.w * m.m[3][2], + v.x * m.m[0][3] + v.y * m.m[1][3] + v.z * m.m[2][3] + v.w * m.m[3][3] }; + } + + [[nodiscard]] constexpr Matrix4 transpose(const Matrix4& a) noexcept + { + Matrix4 result{}; + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + result.m[row][col] = a.m[col][row]; + } + } + return result; + } + + // Transforms a position (implicit w = 1, translation applied). + [[nodiscard]] constexpr Vector3 transformPoint(Vector3 p, const Matrix4& m) noexcept + { + return { p.x * m.m[0][0] + p.y * m.m[1][0] + p.z * m.m[2][0] + m.m[3][0], + p.x * m.m[0][1] + p.y * m.m[1][1] + p.z * m.m[2][1] + m.m[3][1], + p.x * m.m[0][2] + p.y * m.m[1][2] + p.z * m.m[2][2] + m.m[3][2] }; + } + + // Transforms a direction (implicit w = 0, translation ignored). + [[nodiscard]] constexpr Vector3 transformDirection(Vector3 d, const Matrix4& m) noexcept + { + return { d.x * m.m[0][0] + d.y * m.m[1][0] + d.z * m.m[2][0], + d.x * m.m[0][1] + d.y * m.m[1][1] + d.z * m.m[2][1], + d.x * m.m[0][2] + d.y * m.m[1][2] + d.z * m.m[2][2] }; + } + + // Transforms a 2D position (implicit z = 0, w = 1, translation applied; + // result projected back to 2D). For 2D affine transforms stored in a Matrix4. + [[nodiscard]] constexpr Vector2 transformPoint2D(Vector2 p, const Matrix4& m) noexcept + { + return { p.x * m.m[0][0] + p.y * m.m[1][0] + m.m[3][0], + p.x * m.m[0][1] + p.y * m.m[1][1] + m.m[3][1] }; + } + + // Exact element-wise equality (e.g. for an identity fast-path). + [[nodiscard]] constexpr bool operator==(const Matrix4& a, const Matrix4& b) noexcept + { + for (usize row = 0; row < 4; ++row) + for (usize col = 0; col < 4; ++col) + if (a.m[row][col] != b.m[row][col]) { return false; } + return true; + } + + [[nodiscard]] constexpr bool nearlyEqual(const Matrix4& a, const Matrix4& b, f32 epsilon = CMP_EPSILON) noexcept + { + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + if (!nearlyEqual(a.m[row][col], b.m[row][col], epsilon)) { return false; } + } + } + return true; + } + + [[nodiscard]] inline f32 determinant(const Matrix4& mat) noexcept + { + const f32* m = &mat.m[0][0]; + const f32 s0 = m[0] * m[5] - m[1] * m[4]; + const f32 s1 = m[0] * m[6] - m[2] * m[4]; + const f32 s2 = m[0] * m[7] - m[3] * m[4]; + const f32 s3 = m[1] * m[6] - m[2] * m[5]; + const f32 s4 = m[1] * m[7] - m[3] * m[5]; + const f32 s5 = m[2] * m[7] - m[3] * m[6]; + const f32 c5 = m[10] * m[15] - m[11] * m[14]; + const f32 c4 = m[9] * m[15] - m[11] * m[13]; + const f32 c3 = m[9] * m[14] - m[10] * m[13]; + const f32 c2 = m[8] * m[15] - m[11] * m[12]; + const f32 c1 = m[8] * m[14] - m[10] * m[12]; + const f32 c0 = m[8] * m[13] - m[9] * m[12]; + return s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0; + } + + // Full 4x4 inverse (adjugate / determinant). Returns nullopt for a singular + // matrix, judged with a scale-aware (Hadamard row-norm) threshold so a small- + // but-invertible matrix (e.g. a 1e-3 uniform scale) is not wrongly rejected. + // (Implementation in Matrix4.cpp.) + [[nodiscard]] std::optional tryInverse(const Matrix4& mat) noexcept; + + // Convenience inverse for matrices known to be invertible. On a singular matrix it + // does NOT silently return identity (which would hide the failure) - it asserts in + // debug and returns a not-a-number matrix so the error propagates visibly. Prefer + // tryInverse() whenever a matrix may be singular. (Implementation in Matrix4.cpp.) + [[nodiscard]] Matrix4 inverse(const Matrix4& mat) noexcept; +} diff --git a/Engine/cpp/Runtime/Core/Math/Packed.cppm b/Engine/cpp/Runtime/Core/Math/Packed.cppm new file mode 100644 index 00000000..ff7e887d --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Packed.cppm @@ -0,0 +1,58 @@ +module; + +#include + +export module core.math.packed; + +import core.defs; +import core.stdtypes; +import core.math.types; + +export namespace draco::math { + +// Packed vector components with no SIMD alignment padding. The VectorN types are +// alignas(16) (a Vector3 occupies 16 bytes), which pads tightly-laid-out data such as +// GPU vertex streams past its canonical stride. Float2/3/4 stay byte-tight and convert +// implicitly to/from the aligned VectorN so math still reads naturally. +struct Float2 { + f32 x = 0, y = 0; + constexpr Float2() noexcept = default; + constexpr Float2(f32 x, f32 y) noexcept : x(x), y(y) {} + constexpr Float2(Vector2 v) noexcept : x(v.x), y(v.y) {} + constexpr operator Vector2() const noexcept { return { x, y }; } + constexpr Float2 operator-(Float2 o) const noexcept { return { x - o.x, y - o.y }; } + constexpr Float2 operator+(Float2 o) const noexcept { return { x + o.x, y + o.y }; } +}; + +struct Float3 { + f32 x = 0, y = 0, z = 0; + constexpr Float3() noexcept = default; + constexpr Float3(f32 x, f32 y, f32 z) noexcept : x(x), y(y), z(z) {} + constexpr Float3(Vector3 v) noexcept : x(v.x), y(v.y), z(v.z) {} + constexpr operator Vector3() const noexcept { return { x, y, z }; } + constexpr Float3 operator-(Float3 o) const noexcept { return { x - o.x, y - o.y, z - o.z }; } + constexpr Float3 operator+(Float3 o) const noexcept { return { x + o.x, y + o.y, z + o.z }; } + constexpr Float3 operator*(f32 s) const noexcept { return { x * s, y * s, z * s }; } + constexpr Float3& operator+=(Float3 o) noexcept { x += o.x; y += o.y; z += o.z; return *this; } +}; + +struct Float4 { + f32 x = 0, y = 0, z = 0, w = 0; + constexpr Float4() noexcept = default; + constexpr Float4(f32 x, f32 y, f32 z, f32 w) noexcept : x(x), y(y), z(z), w(w) {} + constexpr Float4(Vector4 v) noexcept : x(v.x), y(v.y), z(v.z), w(v.w) {} + constexpr operator Vector4() const noexcept { return { x, y, z, w }; } +}; + +[[nodiscard]] constexpr f32 dot(Float3 a, Float3 b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z; } +[[nodiscard]] constexpr Float3 cross(Float3 a, Float3 b) noexcept { + return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x }; +} +[[nodiscard]] constexpr f32 lengthSquared(Float3 v) noexcept { return dot(v, v); } +[[nodiscard]] inline f32 length(Float3 v) noexcept { return std::sqrt(dot(v, v)); } +[[nodiscard]] inline Float3 normalize(Float3 v) noexcept { + const f32 len = length(v); + return len > 0.0f ? Float3{ v.x / len, v.y / len, v.z / len } : v; +} + +} // namespace draco::math diff --git a/Engine/cpp/Runtime/Core/Math/Packed.test.cpp b/Engine/cpp/Runtime/Core/Math/Packed.test.cpp new file mode 100644 index 00000000..5239955e --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Packed.test.cpp @@ -0,0 +1,90 @@ +#include +#include + +import core.stdtypes; +import core.math; + +using namespace draco; +using namespace draco::math; + +TEST_CASE("packed: sizes are byte-tight (no SIMD padding)") +{ + CHECK(sizeof(Float2) == 8); + CHECK(sizeof(Float3) == 12); + CHECK(sizeof(Float4) == 16); + CHECK(alignof(Float3) == 4); // unlike alignas(16) Vector3 +} + +TEST_CASE("packed: converts to and from the aligned VectorN") +{ + const Float3 f{ 1.0f, 2.0f, 3.0f }; + const Vector3 v = f; // Float3 -> Vector3 + CHECK(v.x == 1.0f); + CHECK(v.y == 2.0f); + CHECK(v.z == 3.0f); + + const Float3 back = Vector3{ 4.0f, 5.0f, 6.0f }; // Vector3 -> Float3 + CHECK(back.x == 4.0f); + CHECK(back.y == 5.0f); + CHECK(back.z == 6.0f); + + const Float2 f2 = Vector2{ 7.0f, 8.0f }; + CHECK(f2.x == 7.0f); + CHECK(f2.y == 8.0f); + const Float4 f4 = Vector4{ 1.0f, 2.0f, 3.0f, 4.0f }; + CHECK(f4.w == 4.0f); +} + +TEST_CASE("packed: arithmetic operators") +{ + const Float3 a{ 1.0f, 2.0f, 3.0f }; + const Float3 b{ 4.0f, 5.0f, 6.0f }; + + const Float3 sum = a + b; + CHECK(sum.x == 5.0f); + CHECK(sum.z == 9.0f); + + const Float3 diff = b - a; + CHECK(diff.x == 3.0f); + CHECK(diff.y == 3.0f); + + const Float3 scaled = a * 2.0f; + CHECK(scaled.x == 2.0f); + CHECK(scaled.z == 6.0f); + + Float3 acc{ 0, 0, 0 }; + acc += a; + acc += b; + CHECK(acc.x == 5.0f); + CHECK(acc.z == 9.0f); + + const Float2 p{ 1.0f, 2.0f }; + const Float2 q{ 3.0f, 5.0f }; + CHECK((q - p).x == 2.0f); + CHECK((q + p).y == 7.0f); +} + +TEST_CASE("packed: vector helpers") +{ + const Float3 x{ 1.0f, 0.0f, 0.0f }; + const Float3 y{ 0.0f, 1.0f, 0.0f }; + + CHECK(dot(x, y) == 0.0f); + CHECK(dot(x, x) == 1.0f); + + const Float3 z = cross(x, y); // right-handed: x cross y = z + CHECK(z.x == 0.0f); + CHECK(z.y == 0.0f); + CHECK(z.z == 1.0f); + + const Float3 v{ 3.0f, 4.0f, 0.0f }; + CHECK(lengthSquared(v) == 25.0f); + CHECK(length(v) == doctest::Approx(5.0f)); + + const Float3 n = normalize(v); + CHECK(length(n) == doctest::Approx(1.0f)); + CHECK(n.x == doctest::Approx(0.6f)); + CHECK(n.y == doctest::Approx(0.8f)); + + CHECK(normalize(Float3{ 0, 0, 0 }).x == 0.0f); // zero stays zero +} diff --git a/Engine/cpp/Runtime/Core/Math/Quaternion.cppm b/Engine/cpp/Runtime/Core/Math/Quaternion.cppm new file mode 100644 index 00000000..07e86bb4 --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Quaternion.cppm @@ -0,0 +1,135 @@ +export module core.math.quaternion; + +import core.stdtypes; +import core.math.types; +import core.math.matrix4; +import core.math.functions; +import core.math.constants; + +// Quaternion: unit quaternion rotation - fromAxisAngle, Hamilton product, +// conjugate/dot/normalize/slerp, rotateVector, and rotationMatrix (-> Matrix4). +// Row-vector convention (v' = v * M), matching Matrix4. + +export namespace draco::math +{ + // ======================================================================= + // Quaternion - unit quaternion rotation (x, y, z, w). + // ======================================================================= + struct Quaternion + { + f32 x = 0.0f; + f32 y = 0.0f; + f32 z = 0.0f; + f32 w = 1.0f; + + constexpr Quaternion() noexcept = default; + constexpr Quaternion(f32 inX, f32 inY, f32 inZ, f32 inW) noexcept : x(inX), y(inY), z(inZ), w(inW) {} + + [[nodiscard]] static Quaternion fromAxisAngle(Vector3 axis, f32 radians) noexcept + { + // A zero-length axis is not a rotation; normalize() would divide by zero, + // so fall back to identity rather than produce a NaN/degenerate quaternion. + if (nearlyZero(dot(axis, axis))) + { + return Quaternion{ 0.0f, 0.0f, 0.0f, 1.0f }; + } + const f32 half = radians * 0.5f; + const f32 s = sin(half); + const Vector3 a = normalize(axis); + return Quaternion{ a.x * s, a.y * s, a.z * s, cos(half) }; + } + + static const Quaternion identity; + }; + + inline constexpr Quaternion Quaternion::identity{ 0.0f, 0.0f, 0.0f, 1.0f }; + + // Hamilton product: applies `b` then `a` to a vector. + [[nodiscard]] constexpr Quaternion operator*(Quaternion a, Quaternion b) noexcept + { + return { a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, + a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z }; + } + + [[nodiscard]] constexpr Quaternion conjugate(Quaternion q) noexcept { return { -q.x, -q.y, -q.z, q.w }; } + [[nodiscard]] constexpr f32 dot(Quaternion a, Quaternion b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; } + + // General inverse (= conjugate / |q|^2). For unit quaternions this equals the conjugate. + [[nodiscard]] inline Quaternion inverse(Quaternion q) noexcept + { + const f32 lengthSq = dot(q, q); + if (lengthSq <= CMP_EPSILON * CMP_EPSILON) { return Quaternion::identity; } + const f32 inv = 1.0f / lengthSq; + return { -q.x * inv, -q.y * inv, -q.z * inv, q.w * inv }; + } + + [[nodiscard]] inline Quaternion normalize(Quaternion q) noexcept + { + const f32 lengthSq = dot(q, q); + if (lengthSq <= CMP_EPSILON * CMP_EPSILON) { return Quaternion::identity; } + const f32 inv = 1.0f / sqrt(lengthSq); + return { q.x * inv, q.y * inv, q.z * inv, q.w * inv }; + } + + [[nodiscard]] constexpr Vector3 rotateVector(Quaternion q, Vector3 v) noexcept + { + const Vector3 u{ q.x, q.y, q.z }; + const f32 s = q.w; + return u * (2.0f * dot(u, v)) + v * (s * s - dot(u, u)) + cross(u, v) * (2.0f * s); + } + + [[nodiscard]] inline bool nearlyEqual(Quaternion a, Quaternion b, f32 epsilon = CMP_EPSILON) noexcept + { + const bool same = nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) + && nearlyEqual(a.z, b.z, epsilon) && nearlyEqual(a.w, b.w, epsilon); + // q and -q represent the same rotation (and slerp/normalize may flip the sign), + // so accept the antipodal quaternion as equal too. + const bool antipodal = nearlyEqual(a.x, -b.x, epsilon) && nearlyEqual(a.y, -b.y, epsilon) + && nearlyEqual(a.z, -b.z, epsilon) && nearlyEqual(a.w, -b.w, epsilon); + return same || antipodal; + } + + // Spherical linear interpolation along the shortest arc; result is unit. + [[nodiscard]] inline Quaternion slerp(Quaternion a, Quaternion b, f32 t) noexcept + { + f32 cosTheta = dot(a, b); + if (cosTheta < 0.0f) // shortest path + { + b = Quaternion{ -b.x, -b.y, -b.z, -b.w }; + cosTheta = -cosTheta; + } + + if (cosTheta > 0.9995f) // nearly parallel - lerp + normalize + { + return normalize(Quaternion{ a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t, + a.z + (b.z - a.z) * t, + a.w + (b.w - a.w) * t }); + } + + const f32 theta0 = acos(cosTheta); + const f32 theta = theta0 * t; + const f32 sinTheta = sin(theta); + const f32 sinTheta0 = sin(theta0); + const f32 s1 = sinTheta / sinTheta0; + const f32 s0 = cos(theta) - cosTheta * s1; + return Quaternion{ a.x * s0 + b.x * s1, + a.y * s0 + b.y * s1, + a.z * s0 + b.z * s1, + a.w * s0 + b.w * s1 }; + } + + // Rotation matrix for a unit quaternion (row-vector convention). + [[nodiscard]] constexpr Matrix4 rotationMatrix(Quaternion q) noexcept + { + const f32 xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z; + const f32 xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z; + const f32 wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z; + return Matrix4{ { { 1.0f - 2.0f * (yy + zz), 2.0f * (xy + wz), 2.0f * (xz - wy), 0.0f }, + { 2.0f * (xy - wz), 1.0f - 2.0f * (xx + zz), 2.0f * (yz + wx), 0.0f }, + { 2.0f * (xz + wy), 2.0f * (yz - wx), 1.0f - 2.0f * (xx + yy), 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Transform.cpp b/Engine/cpp/Runtime/Core/Math/Transform.cpp deleted file mode 100644 index 5e1029b4..00000000 --- a/Engine/cpp/Runtime/Core/Math/Transform.cpp +++ /dev/null @@ -1,38 +0,0 @@ -module; - -#include - -#include - -module core.math.transform; - -import core.stdtypes; - -namespace draco::math -{ - void Transform::toMatrix(f32 out[16]) const - { - f32 translation[16]; - f32 rx[16]; - f32 ry[16]; - f32 rz[16]; - f32 scale[16]; - f32 temp[16]; - - bx::mtxIdentity(out); - - bx::mtxScale(scale, scale[0], scale[1], scale[2]); - - bx::mtxRotateX(rx, rotation[0]); - bx::mtxRotateY(ry, rotation[1]); - bx::mtxRotateZ(rz, rotation[2]); - - bx::mtxTranslate(translation, position[0], position[1], position[2]); - - // scale * rotation * translation - bx::mtxMul(temp, scale, rx); - bx::mtxMul(temp, temp, ry); - bx::mtxMul(temp, temp, rz); - bx::mtxMul(out, temp, translation); - } -} diff --git a/Engine/cpp/Runtime/Core/Math/Transform.cppm b/Engine/cpp/Runtime/Core/Math/Transform.cppm index 176cbe7c..75843110 100644 --- a/Engine/cpp/Runtime/Core/Math/Transform.cppm +++ b/Engine/cpp/Runtime/Core/Math/Transform.cppm @@ -1,70 +1,44 @@ -module; - export module core.math.transform; -import core.math.types; + import core.stdtypes; +import core.math.types; +import core.math.matrix4; +import core.math.quaternion; export namespace draco::math { + // ======================================================================= + // Transform - position / rotation / scale, composed as S * R * T. + // ======================================================================= struct Transform { - f32 position[3] = { 0.0f, 0.0f, 0.0f }; - f32 rotation[3] = { 0.0f, 0.0f, 0.0f }; // Euler (radians) - f32 scale[3] = { 1.0f, 1.0f, 1.0f }; - - constexpr void setPosition(f32 x, f32 y, f32 z) noexcept { - position[0] = x; - position[1] = y; - position[2] = z; - } - - constexpr void setPosition(const Vector3& v) noexcept { - position[0] = v.x; - position[1] = v.y; - position[2] = v.z; - } - - [[nodiscard]] constexpr Vector3 getPosition() const noexcept { - return Vector3{position[0], position[1], position[2]}; - } - - constexpr void setRotation(const f32 x, const f32 y, const f32 z) noexcept - { - rotation[0] = x; - rotation[1] = y; - rotation[2] = z; - } + // Direct constexpr construction (not Vector3::zero / Quaternion::identity, which + // are static-const constants and so cannot seed a constexpr default member init). + Vector3 position{ 0.0f, 0.0f, 0.0f }; + Quaternion rotation{ 0.0f, 0.0f, 0.0f, 1.0f }; + Vector3 scale{ 1.0f, 1.0f, 1.0f }; - constexpr void setRotation(const Vector3& v) noexcept + [[nodiscard]] Matrix4 toMatrix() const noexcept { - rotation[0] = v.x; - rotation[1] = v.y; - rotation[2] = v.z; - } - - [[nodiscard]] constexpr Vector3 getRotation() const noexcept { - return Vector3{rotation[0], rotation[1], rotation[2]}; + Matrix4 result = Matrix4::scale(scale) * rotationMatrix(rotation); + result.m[3][0] = position.x; + result.m[3][1] = position.y; + result.m[3][2] = position.z; + return result; } - constexpr void setScale(const f32 x, const f32 y, const f32 z) noexcept + // Component-wise interpolation: position/scale lerp, rotation slerp. + [[nodiscard]] static Transform lerp(const Transform& a, const Transform& b, f32 t) noexcept { - scale[0] = x; - scale[1] = y; - scale[2] = z; + return Transform{ + draco::math::lerp(a.position, b.position, t), + slerp(a.rotation, b.rotation, t), + draco::math::lerp(a.scale, b.scale, t), + }; } - - constexpr void setScale(const Vector3& v) noexcept - { - scale[0] = v.x; - scale[1] = v.y; - scale[2] = v.z; - } - - [[nodiscard]] constexpr Vector3 getScale() const noexcept { - return Vector3{scale[0], scale[1], scale[2]}; - } - - // Compute column-major matrix from transform - void toMatrix(f32 out[16]) const; }; + + // Identity transform (position 0, rotation identity, scale 1) - the + // default-constructed value. + inline constexpr Transform identityTransform{}; } diff --git a/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm b/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm index c9df0996..773190a6 100644 --- a/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm +++ b/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm @@ -63,6 +63,10 @@ export namespace draco::math { [[nodiscard]] static Vector3 spherical(f32 azimuth, f32 inclination, f32 radius = 1.0f) noexcept; [[nodiscard]] static Vector3 cylindrical(f32 angle, f32 radius = 1.0f, f32 height = 0.0f) noexcept; + // constant vectors (defined in Vector3.cppm) + static const Vector3 zero; + static const Vector3 one; + // element access [[nodiscard]] constexpr f32& operator[](i32 i) noexcept; [[nodiscard]] constexpr const f32& operator[](i32 i) const noexcept; diff --git a/Engine/cpp/Runtime/Core/Math/Vector2.cppm b/Engine/cpp/Runtime/Core/Math/Vector2.cppm index cc1ccaa3..795625a9 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector2.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector2.cppm @@ -413,9 +413,9 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector2& a, const Vector2& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector2& a, const Vector2& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon); } } diff --git a/Engine/cpp/Runtime/Core/Math/Vector3.cppm b/Engine/cpp/Runtime/Core/Math/Vector3.cppm index b7bc2cef..9b5a25e2 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector3.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector3.cppm @@ -441,15 +441,19 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector3& a, const Vector3& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector3& a, const Vector3& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) && nearlyEqual(a.z, b.z, epsilon); } // Returns cross product [[nodiscard]] constexpr Vector3 cross(const Vector3& a, const Vector3& b) noexcept { return { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x }; } + + // constant vectors + inline constexpr Vector3 Vector3::zero{ 0.0f, 0.0f, 0.0f }; + inline constexpr Vector3 Vector3::one{ 1.0f, 1.0f, 1.0f }; } export namespace std { diff --git a/Engine/cpp/Runtime/Core/Math/Vector4.cppm b/Engine/cpp/Runtime/Core/Math/Vector4.cppm index c4b1fe41..9c55fea7 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector4.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector4.cppm @@ -493,9 +493,10 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector4& a, const Vector4& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector4& a, const Vector4& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) + && nearlyEqual(a.z, b.z, epsilon) && nearlyEqual(a.w, b.w, epsilon); } } // namespace draco::math diff --git a/Engine/cpp/Runtime/Draconic.cppm b/Engine/cpp/Runtime/Draconic.cppm index 26f06e66..e246c36e 100644 --- a/Engine/cpp/Runtime/Draconic.cppm +++ b/Engine/cpp/Runtime/Draconic.cppm @@ -1,6 +1,3 @@ export module draconic; export import core; -export import shell; -export import scene; -export import rendering; diff --git a/Engine/cpp/Runtime/Profiler/Profiler.cppm b/Engine/cpp/Runtime/Profiler/Profiler.cppm new file mode 100644 index 00000000..5720db34 --- /dev/null +++ b/Engine/cpp/Runtime/Profiler/Profiler.cppm @@ -0,0 +1,224 @@ +/// Lightweight hierarchical CPU scope profiler - the `profiler` module. +/// +/// Scopes nest into a per-thread tree; at frame end every thread's samples merge into +/// the completed-frame snapshot. Timing uses a monotonic high-res counter. Instrument +/// with the DRACO_PROFILE_SCOPE macro (Profiler.h) - it compiles to nothing when +/// DRACO_PROFILING is off. Thread-safe: scopes touch only thread-local state; the +/// registry + frame swap are mutex-guarded, and the merge runs at frame end when +/// workers are idle. + +module; +#include +#include +#include +#include +#include +#include +#include + +export module profiler; + +import core; + +using namespace draco; + +namespace draco::profiler { + +// NOTE: local stand-ins. Replace with core timing + text-formatting facilities once +// draco.core provides them. +namespace detail { + inline void appendArg(std::u8string& out, std::u8string_view v) { out.append(v); } + template requires std::is_arithmetic_v + void appendArg(std::u8string& out, T v) { + const std::string s = std::to_string(v); + out.append(reinterpret_cast(s.data()), s.size()); + } +} + +inline void appendFormat(std::u8string& out, std::u8string_view fmt) { out.append(fmt); } + +template +void appendFormat(std::u8string& out, std::u8string_view fmt, A&& a, Rest&&... rest) { + const auto pos = fmt.find(u8"{}"); + if (pos == std::u8string_view::npos) { out.append(fmt); return; } + out.append(fmt.substr(0, pos)); + detail::appendArg(out, std::forward(a)); + appendFormat(out, fmt.substr(pos + 2), std::forward(rest)...); +} + +// Monotonic CPU tick counter (nanoseconds since the steady-clock epoch). +[[nodiscard]] inline u64 getTicks() noexcept { + return static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); +} +// getTicks() deltas are nanoseconds; convert to milliseconds for reports. +[[nodiscard]] inline f64 ticksToMilliseconds(u64 ticks) noexcept { + return static_cast(ticks) / 1000000.0; +} + +} // namespace draco::profiler + +export namespace draco::profiler { + +// One completed scope. `name` is a borrowed string literal (the macro passes literals). +struct ProfileSample { + const char* name = ""; + u64 startTick = 0; // getTicks() at scope entry + u64 durationTicks = 0; + u32 depth = 0; // nesting depth within its thread + u32 threadIndex = 0; +}; + +// A finished frame: its samples (across all threads) + the frame's wall-clock span. +struct ProfileFrame { + u64 frameNumber = 0; + u64 frameStartTick = 0; + u64 frameDurationTicks = 0; + std::vector samples; + + [[nodiscard]] f64 frameMs() const noexcept { return ticksToMilliseconds(frameDurationTicks); } +}; + +// The profiler singleton. beginFrame/endFrame bracket a frame; beginScope/endScope (via the RAII +// helper) record nested scopes. completedFrame() returns the last finished frame for reporting. +class Profiler { +public: + [[nodiscard]] static Profiler& get() noexcept { static Profiler instance; return instance; } + + void setEnabled(bool e) noexcept { m_enabled = e; } + [[nodiscard]] bool enabled() const noexcept { return m_enabled; } + + void beginFrame() noexcept { + if (!m_enabled) { return; } + m_frameStartTick = getTicks(); + } + + void endFrame() noexcept { + if (!m_enabled) { return; } + const u64 endTick = getTicks(); + std::scoped_lock lock(m_mutex); + m_completed.samples.clear(); + m_completed.frameNumber = m_frameNumber; + m_completed.frameStartTick = m_frameStartTick; + m_completed.frameDurationTicks = (endTick >= m_frameStartTick) ? (endTick - m_frameStartTick) : 0; + // Workers are idle at frame end, so draining their buffers here needs no per-thread lock. + for (ThreadData* td : m_threads) { + for (const ProfileSample& s : td->samples) { m_completed.samples.push_back(s); } + td->samples.clear(); + td->stack.clear(); // defensive: any unbalanced scopes don't leak into the next frame + } + pushHistory(m_completed.frameMs()); + ++m_frameNumber; + } + + void beginScope(const char* name) noexcept { + if (!m_enabled) { return; } + ThreadData& td = local(); + td.stack.push_back(ActiveScope{ name, getTicks(), static_cast(td.stack.size()) }); + } + + void endScope() noexcept { + if (!m_enabled) { return; } + ThreadData& td = local(); + if (td.stack.empty()) { return; } + const ActiveScope a = td.stack[td.stack.size() - 1]; + td.stack.pop_back(); + const u64 now = getTicks(); + td.samples.push_back(ProfileSample{ a.name, a.startTick, + (now >= a.startTick) ? (now - a.startTick) : 0, a.depth, td.index }); + } + + [[nodiscard]] const ProfileFrame& completedFrame() const noexcept { return m_completed; } + + // Rolling average of recent frame times (ms), for a stable headline number. + [[nodiscard]] f64 averageFrameMs() const noexcept { + if (m_historyCount == 0) { return 0.0; } + f64 sum = 0.0; + for (u32 i = 0; i < m_historyCount; ++i) { sum += m_history[i]; } + return sum / static_cast(m_historyCount); + } + + // Human-readable dump: frame headline (this frame + rolling avg) then the scope tree, indented + // by depth and ordered by start time so parents precede children. (The P-key prints this.) + [[nodiscard]] std::u8string buildReport() const { + std::u8string out; + const ProfileFrame& f = m_completed; + appendFormat(out, u8"=== Profile: frame {} {} ms (avg {} ms) {} samples ===\n", + f.frameNumber, f.frameMs(), averageFrameMs(), f.samples.size()); + + // Order by start time (so parents precede children) without mutating m_completed. + std::vector order; + for (u32 i = 0; i < static_cast(f.samples.size()); ++i) { order.push_back(i); } + for (u32 i = 1; i < static_cast(order.size()); ++i) { + const u32 v = order[i]; + u32 j = i; + while (j > 0 && f.samples[order[j - 1]].startTick > f.samples[v].startTick) { order[j] = order[j - 1]; --j; } + order[j] = v; + } + for (u32 idx : order) { + const ProfileSample& s = f.samples[idx]; + out.append(u8" "); + for (u32 d = 0; d < s.depth; ++d) { out.append(u8" "); } + out.append(reinterpret_cast(s.name)); // scope names are ASCII (valid UTF-8) + appendFormat(out, u8": {} ms [t{}]\n", ticksToMilliseconds(s.durationTicks), s.threadIndex); + } + return out; + } + + Profiler(const Profiler&) = delete; + Profiler& operator=(const Profiler&) = delete; + +private: + Profiler() = default; + ~Profiler() { for (ThreadData* td : m_threads) { delete td; } } + + struct ActiveScope { const char* name; u64 startTick; u32 depth; }; + struct ThreadData { + u32 index = 0; + std::vector samples; // completed scopes this frame + std::vector stack; // currently-open scopes + }; + + // Per-thread state, created + registered on first use (registry guarded by the mutex). + [[nodiscard]] ThreadData& local() { + if (s_local == nullptr) { + ThreadData* td = new ThreadData(); + std::scoped_lock lock(m_mutex); + td->index = m_nextThreadIndex++; + m_threads.push_back(td); + s_local = td; + } + return *s_local; + } + + void pushHistory(f64 ms) noexcept { + m_history[m_historyHead] = ms; + m_historyHead = (m_historyHead + 1) % kHistory; + if (m_historyCount < kHistory) { ++m_historyCount; } + } + + static constexpr u32 kHistory = 64; + + bool m_enabled = true; + u64 m_frameNumber = 0; + u64 m_frameStartTick = 0; + ProfileFrame m_completed; + std::mutex m_mutex; // guards the thread registry + the frame swap + std::vector m_threads; + u32 m_nextThreadIndex = 0; + f64 m_history[kHistory] = {}; + u32 m_historyHead = 0; + u32 m_historyCount = 0; + + inline static thread_local ThreadData* s_local = nullptr; +}; + +// RAII scope: brackets a profiled region. Use via DRACO_PROFILE_SCOPE (Profiler.h). +class ScopedProfile { +public: + explicit ScopedProfile(const char* name) noexcept { Profiler::get().beginScope(name); } + ~ScopedProfile() noexcept { Profiler::get().endScope(); } + ScopedProfile(const ScopedProfile&) = delete; + ScopedProfile& operator=(const ScopedProfile&) = delete; +}; + +} // namespace draco::profiler diff --git a/Engine/cpp/Runtime/Profiler/Profiler.h b/Engine/cpp/Runtime/Profiler/Profiler.h new file mode 100644 index 00000000..b0765cdc --- /dev/null +++ b/Engine/cpp/Runtime/Profiler/Profiler.h @@ -0,0 +1,28 @@ +// Profiler instrumentation macros. +// +// Include this header (it's just macros) AND `import profiler` in a TU that instruments. +// DRACO_PROFILE_SCOPE("Name") profiles the enclosing block; the frame macros bracket a frame. +// When DRACO_PROFILING is off (shipping builds), every macro compiles to nothing. +#pragma once + +#ifndef DRACO_PROFILING +#define DRACO_PROFILING 0 +#endif + +#define DRACO_PROFILE_CONCAT_(a, b) a##b +#define DRACO_PROFILE_CONCAT(a, b) DRACO_PROFILE_CONCAT_(a, b) + +#if DRACO_PROFILING + +#define DRACO_PROFILE_SCOPE(name) \ + ::draco::profiler::ScopedProfile DRACO_PROFILE_CONCAT(dracoProfScope_, __LINE__){ (name) } +#define DRACO_PROFILE_FRAME_BEGIN() ::draco::profiler::Profiler::get().beginFrame() +#define DRACO_PROFILE_FRAME_END() ::draco::profiler::Profiler::get().endFrame() + +#else + +#define DRACO_PROFILE_SCOPE(name) ((void)0) +#define DRACO_PROFILE_FRAME_BEGIN() ((void)0) +#define DRACO_PROFILE_FRAME_END() ((void)0) + +#endif diff --git a/Engine/cpp/Runtime/Profiler/ProfilerTests.test.cpp b/Engine/cpp/Runtime/Profiler/ProfilerTests.test.cpp new file mode 100644 index 00000000..7a792a8e --- /dev/null +++ b/Engine/cpp/Runtime/Profiler/ProfilerTests.test.cpp @@ -0,0 +1,81 @@ +#include +#include +#include + +import core; +import profiler; + +using namespace draco; +using namespace draco::profiler; + +TEST_CASE("profiler: a single scope is recorded") +{ + Profiler& p = Profiler::get(); + p.setEnabled(true); + p.beginFrame(); + { ScopedProfile s("Alpha"); } + p.endFrame(); + + const ProfileFrame& f = p.completedFrame(); + REQUIRE(f.samples.size() == 1u); + CHECK(std::u8string_view(reinterpret_cast(f.samples[0].name)) == std::u8string_view(u8"Alpha")); + CHECK(f.samples[0].depth == 0u); +} + +TEST_CASE("profiler: nested scopes get increasing depth") +{ + Profiler& p = Profiler::get(); + p.setEnabled(true); + p.beginFrame(); + { + ScopedProfile outer("Outer"); + { ScopedProfile inner("Inner"); } + { ScopedProfile inner2("Inner2"); } + } + p.endFrame(); + + const ProfileFrame& f = p.completedFrame(); + REQUIRE(f.samples.size() == 3u); + // endScope records on close, so children appear before the parent; find by name. + u32 outerDepth = 99, innerDepth = 99; + for (usize i = 0; i < f.samples.size(); ++i) { + std::u8string_view n(reinterpret_cast(f.samples[i].name)); + if (n == std::u8string_view(u8"Outer")) { outerDepth = f.samples[i].depth; } + if (n == std::u8string_view(u8"Inner")) { innerDepth = f.samples[i].depth; } + } + CHECK(outerDepth == 0u); + CHECK(innerDepth == 1u); +} + +TEST_CASE("profiler: disabled records nothing") +{ + Profiler& p = Profiler::get(); + p.setEnabled(true); + p.beginFrame(); p.endFrame(); // clean baseline: an empty completed frame + REQUIRE(p.completedFrame().samples.size() == 0u); + + p.setEnabled(false); + p.beginFrame(); + { ScopedProfile s("Ignored"); } + p.endFrame(); + p.setEnabled(true); + CHECK(p.completedFrame().samples.size() == 0u); // unchanged - disabled did nothing +} + +TEST_CASE("profiler: frame number advances and a report builds") +{ + Profiler& p = Profiler::get(); + p.setEnabled(true); + p.beginFrame(); + { ScopedProfile s("Work"); } + p.endFrame(); + const u64 n0 = p.completedFrame().frameNumber; + + p.beginFrame(); + { ScopedProfile s("Work"); } + p.endFrame(); + CHECK(p.completedFrame().frameNumber == n0 + 1u); + + std::u8string report = p.buildReport(); + CHECK(report.size() > 0u); +} diff --git a/Engine/cpp/Runtime/Rendering/CMakeLists.txt b/Engine/cpp/Runtime/Rendering/CMakeLists.txt deleted file mode 100644 index c26aa4a5..00000000 --- a/Engine/cpp/Runtime/Rendering/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -add_modules_library(RHI PIC) -add_modules_library(RenderGraph) -add_modules_library(Renderer PIC) -add_modules_library(Mesh) -add_modules_library(Material) -add_modules_library(QuadRenderer) - -target_link_libraries(RHI PUBLIC Core bgfx bx) -target_link_libraries(RenderGraph PUBLIC RHI bx) -target_link_libraries(Mesh PUBLIC RHI Core) -target_link_libraries(Material PUBLIC RHI) -target_link_libraries(QuadRenderer PUBLIC RHI RenderGraph bgfx bx) -target_link_libraries(Renderer PUBLIC Core RHI RenderGraph Mesh QuadRenderer Material bgfx bx) \ No newline at end of file diff --git a/Engine/cpp/Runtime/Rendering/Geometry/GeometryModule.cppm b/Engine/cpp/Runtime/Rendering/Geometry/GeometryModule.cppm new file mode 100644 index 00000000..6f6c7ab6 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/GeometryModule.cppm @@ -0,0 +1,15 @@ +/// Primary module for geometry - the engine's runtime mesh format. Re-exports all partitions. +/// +/// Distinct from draco.model (the importer's representation of a loaded file): this +/// is the canonical, GPU-upload-ready mesh the renderer and scene components consume. +/// A converter (tooling, not ported) turns an imported ModelMesh into a StaticMesh / +/// SkinnedMesh. Aggregates the partitions: value types (:types), the index buffer +/// (:index_buffer), the StaticMesh/SkinnedMesh formats (:mesh), and procedural +/// primitives (:primitives). + +export module geometry; + +export import :types; +export import :index_buffer; +export import :mesh; +export import :primitives; diff --git a/Engine/cpp/Runtime/Rendering/Geometry/GeometryTests.test.cpp b/Engine/cpp/Runtime/Rendering/Geometry/GeometryTests.test.cpp new file mode 100644 index 00000000..2086e051 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/GeometryTests.test.cpp @@ -0,0 +1,132 @@ +// The engine runtime mesh format: vertex/stream sizes, the index buffer, StaticMesh +// geometry ops, and the key design point -- SkinnedMesh IS-A StaticMesh, so its static +// stream is usable anywhere a StaticMesh is, with the skinning stream discoverable via +// the virtual hooks. Plus the procedural primitives. +#include +#include + +import core; +import geometry; + +using namespace draco; +using namespace draco::geometry; + +TEST_CASE("stream layouts are the GPU-canonical sizes") +{ + CHECK(sizeof(StaticMeshVertex) == 48); + CHECK(sizeof(VertexSkinning) == 24); + CHECK(StaticMesh::vertexStride() == 48); + CHECK(SkinnedMesh::skinningStride() == 24); +} + +TEST_CASE("index buffer: format, set/get, raw size") +{ + IndexBuffer ib(IndexBuffer::Format::U16); + CHECK(ib.indexSize() == 2); + ib.resize(3); + ib.addTriangle(0, 1, 2); + CHECK(ib.count() == 3); + CHECK(ib.get(0) == 0); + CHECK(ib.get(2) == 2); + CHECK(ib.dataSize() == 6); + CHECK(ib.rawData() != nullptr); + + IndexBuffer ib32(IndexBuffer::Format::U32); + CHECK(ib32.indexSize() == 4); + ib32.resize(2); + ib32.set(0, 70000); // exceeds u16 range -> needs 32-bit + CHECK(ib32.get(0) == 70000); +} + +TEST_CASE("static mesh: generated normals + tangents + bounds on a quad") +{ + std::unique_ptr mesh = Primitives::quad(2.0f, 2.0f); + REQUIRE(mesh); + CHECK(mesh->vertexCount() == 4); + CHECK(mesh->indexCount() == 6); + CHECK(mesh->subMeshes.size() == 1); + + mesh->generateNormals(); + for (const StaticMeshVertex& v : mesh->vertices) { + CHECK(v.normal.z == doctest::Approx(1.0f)); // quad faces +Z + CHECK(lengthSquared(v.tangent) == doctest::Approx(1.0f)); // unit tangents + } + + mesh->calculateBounds(); + CHECK(mesh->bounds.min.x == doctest::Approx(-1.0f)); + CHECK(mesh->bounds.max.y == doctest::Approx(1.0f)); + CHECK(mesh->vertexDataSize() == 4 * 48); + CHECK(mesh->vertexData() != nullptr); +} + +TEST_CASE("skinned mesh IS-A static mesh: static stream is substitutable") +{ + std::unique_ptr skinned = std::make_unique(); + skinned->skeletonIndex = 3; + // static stream (inherited) + skinned->vertices.push_back(StaticMeshVertex{ math::Vector3{ 0, 0, 0 }, math::Vector3{ 0, 1, 0 }, math::Vector2{ 0, 0 }, 0xFFFFFFFFu, math::Vector3{ 1, 0, 0 } }); + skinned->vertices.push_back(StaticMeshVertex{ math::Vector3{ 1, 0, 0 }, math::Vector3{ 0, 1, 0 }, math::Vector2{ 1, 0 }, 0xFFFFFFFFu, math::Vector3{ 1, 0, 0 } }); + skinned->vertices.push_back(StaticMeshVertex{ math::Vector3{ 0, 1, 0 }, math::Vector3{ 0, 1, 0 }, math::Vector2{ 0, 1 }, 0xFFFFFFFFu, math::Vector3{ 1, 0, 0 } }); + // parallel skinning stream + VertexSkinning s{}; s.joints[0] = 2; s.weights = math::Vector4{ 1, 0, 0, 0 }; + for (u32 i = 0; i < 3; ++i) { skinned->skinning.push_back(s); } + + // pass it where a StaticMesh& is expected -- the static ops just work + StaticMesh& asStatic = *skinned; + asStatic.calculateBounds(); + CHECK(asStatic.vertexCount() == 3); + CHECK(asStatic.bounds.max.x == doctest::Approx(1.0f)); + + // a consumer holding the base ref can discover + reach the skinning stream + CHECK(asStatic.isSkinned()); + CHECK(asStatic.skinningStream().size() == 3); + CHECK(asStatic.skinningStream()[0].joints[0] == 2); + + // and downcast safely (via the virtual isSkinned() tag, no RTTI) + SkinnedMesh* down = toSkinned(&asStatic); + REQUIRE(down != nullptr); + CHECK(down->skeletonIndex == 3); + CHECK(down->skinningDataSize() == 3 * 24); + + // a plain static mesh reports not-skinned + an empty stream + StaticMesh plain; + CHECK_FALSE(plain.isSkinned()); + CHECK(plain.skinningStream().size() == 0); + CHECK(toSkinned(&plain) == nullptr); +} + +TEST_CASE("primitives: cube + sphere + plane are well-formed") +{ + std::unique_ptr cube = Primitives::cube(2.0f); + REQUIRE(cube); + CHECK(cube->vertexCount() == 24); // 4 verts x 6 faces (hard normals) + CHECK(cube->indexCount() == 36); + CHECK(cube->bounds.min.x == doctest::Approx(-1.0f)); + CHECK(cube->bounds.max.z == doctest::Approx(1.0f)); + + std::unique_ptr sphere = Primitives::sphere(1.0f, 16, 8); + REQUIRE(sphere); + CHECK(sphere->indexCount() == 16 * 8 * 6); + // every surface point is ~radius from the origin + for (const StaticMeshVertex& v : sphere->vertices) { + CHECK(length(v.position) == doctest::Approx(1.0f).epsilon(0.01)); + } + + std::unique_ptr plane = Primitives::plane(4.0f, 4.0f, 2, 2); + REQUIRE(plane); + CHECK(plane->vertexCount() == 9); // (2+1) x (2+1) + CHECK(plane->indexCount() == 2 * 2 * 6); +} + +TEST_CASE("clear-for-reload empties in place (skinned clears both streams)") +{ + std::unique_ptr mesh = std::make_unique(); + mesh->vertices.push_back(StaticMeshVertex{}); + mesh->skinning.push_back(VertexSkinning{}); + mesh->skeletonIndex = 5; + + mesh->clearForReload(); + CHECK(mesh->vertexCount() == 0); + CHECK(mesh->skinningStream().size() == 0); + CHECK(mesh->skeletonIndex == -1); +} diff --git a/Engine/cpp/Runtime/Rendering/Geometry/IndexBuffer.cppm b/Engine/cpp/Runtime/Rendering/Geometry/IndexBuffer.cppm new file mode 100644 index 00000000..36f53853 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/IndexBuffer.cppm @@ -0,0 +1,81 @@ +/// Format-tagged 16/32-bit index buffer (`:index_buffer` partition). +/// +/// A format-tagged index buffer (16- or 32-bit), stored as raw bytes so it uploads +/// straight to a GPU index buffer. An internal write cursor supports streaming +/// append (Add/AddTriangle) for the primitive generators + importers. + +module; + +#include +#include + +export module geometry:index_buffer; + +import core; + +using namespace draco; + +export namespace draco::geometry { + +class IndexBuffer { +public: + enum class Format : u8 { U16, U32 }; + + explicit IndexBuffer(Format format = Format::U32) : m_format(format) {} + + [[nodiscard]] Format getFormat() const noexcept { return m_format; } + [[nodiscard]] u32 indexSize() const noexcept { return m_format == Format::U16 ? 2u : 4u; } + [[nodiscard]] u32 count() const noexcept { return m_count; } + [[nodiscard]] u32 dataSize() const noexcept { return m_count * indexSize(); } + [[nodiscard]] const u8* rawData() const noexcept { return m_count > 0 ? m_data.data() : nullptr; } + + void reserve(u32 count) { + const u32 needed = count * indexSize(); + if (m_data.size() < needed) { m_data.resize(needed); } + } + + // Sets the logical count (growing storage) and rewinds the append cursor. + void resize(u32 count) { + m_count = count; + reserve(count); + m_writePos = 0; + } + + void clear() { m_count = 0; m_writePos = 0; } + + void set(u32 index, u32 value) { + if (index >= m_count) { return; } + const u32 offset = index * indexSize(); + if (m_format == Format::U16) { + const u16 v = static_cast(value); + std::memcpy(m_data.data() + offset, &v, 2); + } else { + std::memcpy(m_data.data() + offset, &value, 4); + } + } + + [[nodiscard]] u32 get(u32 index) const { + if (index >= m_count) { return 0; } + const u32 offset = index * indexSize(); + if (m_format == Format::U16) { + u16 v = 0; std::memcpy(&v, m_data.data() + offset, 2); return v; + } + u32 v = 0; std::memcpy(&v, m_data.data() + offset, 4); return v; + } + + // Append using the internal cursor (call Resize/Reserve first). + void add(u32 value) { + if (m_writePos >= m_count) { return; } + set(m_writePos, value); + ++m_writePos; + } + void addTriangle(u32 a, u32 b, u32 c) { add(a); add(b); add(c); } + +private: + std::vector m_data; + u32 m_count = 0; + u32 m_writePos = 0; + Format m_format = Format::U32; +}; + +} // namespace draco::geometry diff --git a/Engine/cpp/Runtime/Rendering/Geometry/Mesh.cppm b/Engine/cpp/Runtime/Rendering/Geometry/Mesh.cppm new file mode 100644 index 00000000..384fd2c2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/Mesh.cppm @@ -0,0 +1,188 @@ +/// StaticMesh / SkinnedMesh runtime mesh formats (`:mesh` partition). +/// +/// StaticMesh is the engine's runtime mesh: a static vertex stream (48B), an index +/// buffer, submeshes, bounds, and the geometry ops (normals/tangents/bounds) that +/// operate purely on the static stream. +/// +/// SkinnedMesh IS-A StaticMesh: it inherits the entire static stream + all the ops +/// and adds only a *parallel* skinning stream (joints/weights, 24B) + a skeleton +/// reference. Because the static data is byte-identical and in the same place, a +/// SkinnedMesh can be passed anywhere a StaticMesh& is expected - the static draw +/// path just works; the skinned path additionally binds the skinning stream. The +/// virtual isSkinned()/skinningStream() hooks let a consumer holding a StaticMesh& +/// discover + bind the skinning stream without RTTI. + +module; + +#include +#include +#include +#include +#include + +export module geometry:mesh; + +import core; +import :types; +import :index_buffer; + +using namespace draco; + +export namespace draco::geometry { + +class StaticMesh { +public: + virtual ~StaticMesh() = default; + + std::u8string name; + std::vector vertices; + IndexBuffer indices{ IndexBuffer::Format::U32 }; + std::vector subMeshes; + math::AABB bounds = math::AABB::empty(); + + [[nodiscard]] u32 vertexCount() const noexcept { return static_cast(vertices.size()); } + [[nodiscard]] u32 indexCount() const noexcept { return indices.count(); } + [[nodiscard]] static constexpr u32 vertexStride() noexcept { return sizeof(StaticMeshVertex); } + + // Raw static-stream bytes for GPU upload (null when empty). + [[nodiscard]] const u8* vertexData() const noexcept { + return vertices.empty() ? nullptr : reinterpret_cast(vertices.data()); + } + [[nodiscard]] u32 vertexDataSize() const noexcept { return vertexCount() * vertexStride(); } + + // Substitutability hooks: a consumer with a StaticMesh& can ask whether it is + // really skinned and bind the parallel stream, no RTTI needed. + [[nodiscard]] virtual bool isSkinned() const noexcept { return false; } + [[nodiscard]] virtual std::span skinningStream() const noexcept { return {}; } + + // Resets the mesh to empty in place, so a hot-reload can re-populate the same + // instance without invalidating outside references (GPU caches, renderers). + virtual void clearForReload() { + name.clear(); + vertices.clear(); + indices.clear(); + subMeshes.clear(); + bounds = math::AABB::empty(); + } + + // Recomputes `bounds` from the static stream. + math::AABB& calculateBounds() { + if (vertices.empty()) { bounds = math::AABB{ math::Vector3::zero, math::Vector3::zero }; return bounds; } + bounds = math::AABB::empty(); + for (const StaticMeshVertex& v : vertices) { bounds.expand(v.position); } + return bounds; + } + + // Smooth normals: accumulate per-triangle face normals into shared vertices. + void generateNormals() { + const u32 triangles = triangleCount(); + if (triangles == 0) { return; } + for (StaticMeshVertex& v : vertices) { v.normal = math::Float3{ 0, 0, 0 }; } + for (u32 t = 0; t < triangles; ++t) { + const u32 i0 = corner(t, 0), i1 = corner(t, 1), i2 = corner(t, 2); + const math::Float3 e1 = vertices[i1].position - vertices[i0].position; + const math::Float3 e2 = vertices[i2].position - vertices[i0].position; + const math::Float3 faceNormal = cross(e1, e2); + vertices[i0].normal += faceNormal; + vertices[i1].normal += faceNormal; + vertices[i2].normal += faceNormal; + } + for (StaticMeshVertex& v : vertices) { + v.normal = (lengthSquared(v.normal) > 0.0001f) ? normalize(v.normal) : math::Float3{ 0, 1, 0 }; + } + } + + // Tangents for normal mapping: per-triangle (deltaUV-weighted) accumulation, + // then Gram-Schmidt orthogonalization against the normal. + void generateTangents() { + const u32 triangles = triangleCount(); + if (triangles == 0) { return; } + for (StaticMeshVertex& v : vertices) { v.tangent = math::Float3{ 0, 0, 0 }; } + for (u32 t = 0; t < triangles; ++t) { + const u32 i0 = corner(t, 0), i1 = corner(t, 1), i2 = corner(t, 2); + const math::Float3 dp1 = vertices[i1].position - vertices[i0].position; + const math::Float3 dp2 = vertices[i2].position - vertices[i0].position; + const math::Float2 du1 = vertices[i1].texCoord - vertices[i0].texCoord; + const math::Float2 du2 = vertices[i2].texCoord - vertices[i0].texCoord; + const f32 denom = du1.x * du2.y - du2.x * du1.y; + math::Float3 tangent = math::Float3{ 0, 0, 0 }; + if (std::abs(denom) > 0.0001f) { + const f32 r = 1.0f / denom; + tangent = (dp1 * du2.y - dp2 * du1.y) * r; + } + vertices[i0].tangent += tangent; + vertices[i1].tangent += tangent; + vertices[i2].tangent += tangent; + } + for (StaticMeshVertex& v : vertices) { + if (lengthSquared(v.tangent) > 0.0001f) { + v.tangent = v.tangent - v.normal * dot(v.normal, v.tangent); // orthogonalize + v.tangent = (lengthSquared(v.tangent) > 0.0001f) ? normalize(v.tangent) : defaultTangent(v.normal); + } else { + v.tangent = defaultTangent(v.normal); + } + } + } + + // Pack a 0..1 float color to RGBA8 with R in the low byte (Unorm8x4 order). + [[nodiscard]] static u32 packColor(math::Vector4 c) { + const u32 r = static_cast(std::clamp(c.x, 0.0f, 1.0f) * 255.0f); + const u32 g = static_cast(std::clamp(c.y, 0.0f, 1.0f) * 255.0f); + const u32 b = static_cast(std::clamp(c.z, 0.0f, 1.0f) * 255.0f); + const u32 a = static_cast(std::clamp(c.w, 0.0f, 1.0f) * 255.0f); + return r | (g << 8) | (b << 16) | (a << 24); + } + [[nodiscard]] static u32 packColor(Color32 c) { + return static_cast(c.r) | (static_cast(c.g) << 8) + | (static_cast(c.b) << 16) | (static_cast(c.a) << 24); + } + +protected: + [[nodiscard]] u32 triangleCount() const noexcept { + const bool hasIndices = indices.count() > 0; + return hasIndices ? indices.count() / 3u : vertexCount() / 3u; + } + // Vertex index of corner `c` (0..2) of triangle `t` (indexed or sequential). + [[nodiscard]] u32 corner(u32 t, u32 c) const noexcept { + return (indices.count() > 0) ? indices.get(t * 3 + c) : (t * 3 + c); + } + [[nodiscard]] static math::Float3 defaultTangent(math::Float3 normal) { + const math::Float3 t = (std::abs(normal.y) < 0.9f) ? cross(normal, math::Float3{ 0, 1, 0 }) : cross(normal, math::Float3{ 1, 0, 0 }); + return (lengthSquared(t) > 0.0001f) ? normalize(t) : math::Float3{ 1, 0, 0 }; + } +}; + +// A skinned mesh: the static stream + a parallel skinning stream + a skeleton ref. +class SkinnedMesh final : public StaticMesh { +public: + std::vector skinning; // parallel to StaticMesh::vertices + i32 skeletonIndex = -1; // index into the import skeleton list (-1 = none) + + [[nodiscard]] bool isSkinned() const noexcept override { return true; } + [[nodiscard]] std::span skinningStream() const noexcept override { + return { skinning.data(), skinning.size() }; + } + + [[nodiscard]] static constexpr u32 skinningStride() noexcept { return sizeof(VertexSkinning); } + [[nodiscard]] const u8* skinningData() const noexcept { + return skinning.empty() ? nullptr : reinterpret_cast(skinning.data()); + } + [[nodiscard]] u32 skinningDataSize() const noexcept { return static_cast(skinning.size()) * skinningStride(); } + + void clearForReload() override { + StaticMesh::clearForReload(); + skinning.clear(); + skeletonIndex = -1; + } +}; + +// Safe downcast to SkinnedMesh via the virtual isSkinned() tag - no RTTI. SkinnedMesh +// is the sole StaticMesh subclass, so an isSkinned() StaticMesh is always a SkinnedMesh. +[[nodiscard]] inline SkinnedMesh* toSkinned(StaticMesh* m) noexcept { + return (m && m->isSkinned()) ? static_cast(m) : nullptr; +} +[[nodiscard]] inline const SkinnedMesh* toSkinned(const StaticMesh* m) noexcept { + return (m && m->isSkinned()) ? static_cast(m) : nullptr; +} + +} // namespace draco::geometry diff --git a/Engine/cpp/Runtime/Rendering/Geometry/Primitives.cppm b/Engine/cpp/Runtime/Rendering/Geometry/Primitives.cppm new file mode 100644 index 00000000..b98442c4 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/Primitives.cppm @@ -0,0 +1,137 @@ +/// Procedural primitive meshes (`:primitives` partition). +/// +/// Procedural primitive meshes (debug shapes / placeholders / tests). Each returns a +/// fully-formed StaticMesh - static vertex stream + 32-bit indices + one submesh + +/// generated tangents + bounds. These build the typed StaticMesh directly (no generic +/// untyped vertex-buffer indirection). + +module; + +#include + +export module geometry:primitives; + +import core; +import :types; +import :mesh; + +using namespace draco; + +export namespace draco::geometry { + +class Primitives { +public: + // A unit quad in the XY plane (two triangles), facing +Z. + [[nodiscard]] static std::unique_ptr quad(f32 width = 1.0f, f32 height = 1.0f) { + std::unique_ptr mesh = std::make_unique(); + const f32 hw = width * 0.5f, hh = height * 0.5f; + const u32 white = 0xFFFFFFFFu; + mesh->vertices.push_back(StaticMeshVertex{ math::Vector3{ -hw, -hh, 0 }, math::Vector3{ 0, 0, 1 }, math::Vector2{ 0, 1 }, white, math::Vector3{ 1, 0, 0 } }); + mesh->vertices.push_back(StaticMeshVertex{ math::Vector3{ hw, -hh, 0 }, math::Vector3{ 0, 0, 1 }, math::Vector2{ 1, 1 }, white, math::Vector3{ 1, 0, 0 } }); + mesh->vertices.push_back(StaticMeshVertex{ math::Vector3{ hw, hh, 0 }, math::Vector3{ 0, 0, 1 }, math::Vector2{ 1, 0 }, white, math::Vector3{ 1, 0, 0 } }); + mesh->vertices.push_back(StaticMeshVertex{ math::Vector3{ -hw, hh, 0 }, math::Vector3{ 0, 0, 1 }, math::Vector2{ 0, 0 }, white, math::Vector3{ 1, 0, 0 } }); + const u32 quad[6] = { 0, 1, 2, 0, 2, 3 }; + mesh->indices.resize(6); + for (u32 i : quad) { mesh->indices.add(i); } + finish(*mesh); + return mesh; + } + + // An axis-aligned cube of edge `size`, 24 verts (hard per-face normals). + [[nodiscard]] static std::unique_ptr cube(f32 size = 1.0f) { + std::unique_ptr mesh = std::make_unique(); + const f32 h = size * 0.5f; + mesh->indices.resize(36); // 6 faces x 2 triangles x 3 indices + // 6 faces: (origin corner, edge-u, edge-v, normal) + addFace(*mesh, math::Vector3{ -h, -h, h }, math::Vector3{ 1, 0, 0 }, math::Vector3{ 0, 1, 0 }, math::Vector3{ 0, 0, 1 }, size); // +Z + addFace(*mesh, math::Vector3{ h, -h, -h }, math::Vector3{ -1, 0, 0 }, math::Vector3{ 0, 1, 0 }, math::Vector3{ 0, 0, -1 }, size); // -Z + addFace(*mesh, math::Vector3{ h, -h, h }, math::Vector3{ 0, 0, -1 }, math::Vector3{ 0, 1, 0 }, math::Vector3{ 1, 0, 0 }, size); // +X + addFace(*mesh, math::Vector3{ -h, -h, -h }, math::Vector3{ 0, 0, 1 }, math::Vector3{ 0, 1, 0 }, math::Vector3{ -1, 0, 0 }, size); // -X + addFace(*mesh, math::Vector3{ -h, h, h }, math::Vector3{ 1, 0, 0 }, math::Vector3{ 0, 0, -1 }, math::Vector3{ 0, 1, 0 }, size); // +Y + addFace(*mesh, math::Vector3{ -h, -h, -h }, math::Vector3{ 1, 0, 0 }, math::Vector3{ 0, 0, 1 }, math::Vector3{ 0, -1, 0 }, size); // -Y + finish(*mesh); + return mesh; + } + + // A flat grid in the XZ plane, `width` x `depth`, subdivided. + [[nodiscard]] static std::unique_ptr plane(f32 width = 1.0f, f32 depth = 1.0f, u32 xSegments = 1, u32 zSegments = 1) { + std::unique_ptr mesh = std::make_unique(); + const u32 xs = xSegments < 1 ? 1 : xSegments, zs = zSegments < 1 ? 1 : zSegments; + const u32 white = 0xFFFFFFFFu; + for (u32 z = 0; z <= zs; ++z) { + for (u32 x = 0; x <= xs; ++x) { + const f32 u = static_cast(x) / static_cast(xs); + const f32 v = static_cast(z) / static_cast(zs); + mesh->vertices.push_back(StaticMeshVertex{ + math::Vector3{ (u - 0.5f) * width, 0.0f, (v - 0.5f) * depth }, math::Vector3{ 0, 1, 0 }, math::Vector2{ u, v }, white, math::Vector3{ 1, 0, 0 } }); + } + } + mesh->indices.resize(xs * zs * 6); + const u32 rowStride = xs + 1; + for (u32 z = 0; z < zs; ++z) { + for (u32 x = 0; x < xs; ++x) { + const u32 i0 = z * rowStride + x, i1 = i0 + 1, i2 = i0 + rowStride, i3 = i2 + 1; + mesh->indices.addTriangle(i0, i2, i1); + mesh->indices.addTriangle(i1, i2, i3); + } + } + finish(*mesh); + return mesh; + } + + // A UV sphere of `radius` with `segments` longitudes and `rings` latitudes. + [[nodiscard]] static std::unique_ptr sphere(f32 radius = 0.5f, u32 segments = 32, u32 rings = 16) { + std::unique_ptr mesh = std::make_unique(); + const u32 seg = segments < 3 ? 3 : segments, rng = rings < 2 ? 2 : rings; + const u32 white = 0xFFFFFFFFu; + for (u32 r = 0; r <= rng; ++r) { + const f32 v = static_cast(r) / static_cast(rng); + const f32 phi = v * math::PI; // 0..pi (pole to pole) + const f32 sinPhi = math::sin(phi), cosPhi = math::cos(phi); + for (u32 s = 0; s <= seg; ++s) { + const f32 u = static_cast(s) / static_cast(seg); + const f32 theta = u * 2.0f * math::PI; + const math::Vector3 n{ math::cos(theta) * sinPhi, cosPhi, math::sin(theta) * sinPhi }; + mesh->vertices.push_back(StaticMeshVertex{ n * radius, n, math::Vector2{ u, v }, white, math::Vector3{ 1, 0, 0 } }); + } + } + mesh->indices.resize(seg * rng * 6); + const u32 rowStride = seg + 1; + for (u32 r = 0; r < rng; ++r) { + for (u32 s = 0; s < seg; ++s) { + const u32 i0 = r * rowStride + s, i1 = i0 + 1, i2 = i0 + rowStride, i3 = i2 + 1; + // CCW-from-outside winding: (a,b,c),(b,d,c) with a=i0,b=i1,c=i2,d=i3. + // Using (i0,i2,i1)/(i1,i2,i3) - last two swapped - reverses the front face + // to inward, so back-face culling would hide the outer shell. + mesh->indices.addTriangle(i0, i1, i2); + mesh->indices.addTriangle(i1, i3, i2); + } + } + finish(*mesh); + return mesh; + } + +private: + // Adds a quad face (4 verts, 2 tris) anchored at `origin`, spanning `size` along + // unit edges `eu`/`ev`, with face normal `n`. The caller pre-sizes the index + // buffer; indices append through its cursor. + static void addFace(StaticMesh& mesh, math::Vector3 origin, math::Vector3 eu, math::Vector3 ev, math::Vector3 n, f32 size) { + const u32 base = mesh.vertexCount(); + const u32 white = 0xFFFFFFFFu; + const math::Vector3 u = eu * size, v = ev * size; + mesh.vertices.push_back(StaticMeshVertex{ origin, n, math::Vector2{ 0, 1 }, white, math::Vector3{ 1, 0, 0 } }); + mesh.vertices.push_back(StaticMeshVertex{ origin + u, n, math::Vector2{ 1, 1 }, white, math::Vector3{ 1, 0, 0 } }); + mesh.vertices.push_back(StaticMeshVertex{ origin + u + v, n, math::Vector2{ 1, 0 }, white, math::Vector3{ 1, 0, 0 } }); + mesh.vertices.push_back(StaticMeshVertex{ origin + v, n, math::Vector2{ 0, 0 }, white, math::Vector3{ 1, 0, 0 } }); + mesh.indices.addTriangle(base, base + 1, base + 2); + mesh.indices.addTriangle(base, base + 2, base + 3); + } + + static void finish(StaticMesh& mesh) { + mesh.generateTangents(); + mesh.calculateBounds(); + mesh.subMeshes.push_back(SubMesh{ 0, static_cast(mesh.indexCount()), 0, PrimitiveType::Triangles }); + } +}; + +} // namespace draco::geometry diff --git a/Engine/cpp/Runtime/Rendering/Geometry/Types.cppm b/Engine/cpp/Runtime/Rendering/Geometry/Types.cppm new file mode 100644 index 00000000..8559c962 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Geometry/Types.cppm @@ -0,0 +1,60 @@ +/// Value-type vocabulary for the runtime mesh format (`:types` partition). +/// +/// Value-type vocabulary for the engine's runtime mesh format (distinct from +/// draco.model, which is the importer's representation of a loaded file): the +/// primitive topology, a submesh range, and the two vertex streams. A skinned mesh +/// reuses the static vertex stream and adds a *parallel* skinning stream, so its +/// static data is byte-identical to a static mesh (see :mesh). + +module; + +export module geometry:types; + +import core; + +using namespace draco; + +export namespace draco::geometry { + +// Primitive topology a submesh is drawn with. +enum class PrimitiveType : u8 { + Triangles, TriangleStrip, TriangleFan, Lines, LineStrip, Points, +}; + +// A contiguous index range with its own material + topology. +struct SubMesh { + i32 startIndex = 0; + i32 indexCount = 0; + i32 materialIndex = 0; + PrimitiveType primitiveType = PrimitiveType::Triangles; +}; + +// The static vertex stream - 48 bytes, matching VertexLayoutType::Mesh (locations +// 0..4). Uses core's packed math::FloatN so the stream stays byte-tight for direct GPU +// upload (the aligned VectorN would pad it past 48 bytes). +struct StaticMeshVertex { + math::Float3 position{ 0, 0, 0 }; // 12 + math::Float3 normal{ 0, 1, 0 }; // 12 + math::Float2 texCoord{ 0, 0 }; // 8 + u32 color = 0xFFFFFFFFu; // 4 packed RGBA, R in the low byte (Unorm8x4) + math::Float3 tangent{ 1, 0, 0 }; // 12 + // total: 48 + + constexpr StaticMeshVertex() noexcept = default; + constexpr StaticMeshVertex(math::Vector3 pos, math::Vector3 nrm, math::Vector2 uv, u32 col, math::Vector3 tan) noexcept + : position(pos), normal(nrm), texCoord(uv), color(col), tangent(tan) {} +}; + +// The parallel skinning stream - 24 bytes (locations 6/7). One per static vertex; a +// skinned mesh stores this alongside the inherited static stream rather than +// interleaving, so the static stream stays substitutable for a static mesh. +struct VertexSkinning { + u16 joints[4] = { 0, 0, 0, 0 }; // 8 bone indices (uint16x4, packed uint32x2) + math::Float4 weights{ 1, 0, 0, 0 }; // 16 bone weights (sum to 1) + // total: 24 +}; + +static_assert(sizeof(StaticMeshVertex) == 48, "static vertex must stay 48 bytes (VertexLayoutType::Mesh)"); +static_assert(sizeof(VertexSkinning) == 24, "skinning stream must stay 24 bytes (locations 6/7)"); + +} // namespace draco::geometry diff --git a/Engine/cpp/Runtime/Rendering/Graphics/Graphics.cppm b/Engine/cpp/Runtime/Rendering/Graphics/Graphics.cppm new file mode 100644 index 00000000..4f5c6bad --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Graphics/Graphics.cppm @@ -0,0 +1,366 @@ +// The `graphics` module. +// +// The RHI render host, promoted out of the per-sample bring-up code so samples, +// the UI, and the renderer share one tested path. Two pieces: +// +// GraphicsDevice — the SHARED GPU: backend (validation-wrapped) + adapter + +// logical device + graphics queue, plus the CPU frame-in-flight ring index. +// Created once for the whole app. Hands out RenderWindows. +// +// RenderWindow — a single window's PRESENTATION target: surface + swapchain + +// a per-window ring of command pools/fences. Created/destroyed at runtime +// (the basis for detachable UI windows). The main window is just the first. +// +// A FrameContext is the per-window, per-frame hand-off the host gives a consumer: +// an acquired backbuffer + a host-created encoder. The host owns acquire / fence +// sync / submit / present and the backbuffer state transitions; the consumer +// records content (or calls beginBackbufferPass for the common clear+pass case). +// +// Multi-window is uniform: there is no "main window" special case here — an app +// renders a list of RenderWindows, each independent, sharing one GraphicsDevice. + +module; + +#include +#include +#include +#include +#include + +export module graphics; + +import core.stdtypes; +import core.status; +import rhi; +import shell; +// Backend factories live in sibling modules so this core host imports only the +// base RHI (keeps it GPU-backend-agnostic and avoids importing heavy backend +// modules into this interface — which GCC's module reader chokes on): +// graphics.null — createNullGraphicsDevice (headless) +// graphics.gpu — createGraphicsDevice (Vulkan/DX12) + +using namespace draco; +using namespace draco::shell; // IShell + input/window types +namespace rhi = draco::rhi; + +export namespace draco::graphics +{ + // Null is a real headless option (CI / servers / tests) — no GPU required. + enum class BackendType : u8 { Vulkan, DX12, Null }; + + struct GraphicsDeviceDesc + { + BackendType backend = BackendType::Vulkan; + // Validation (our RHI-layer wrapper AND the backend's own layers, e.g. Vulkan validation) + // defaults ON for dev builds and OFF for optimized/shipping builds (NDEBUG — set for + // Release/RelWithDebInfo/MinSizeRel). A profiling run (RelWithDebInfo) therefore measures + // the real cost, not the validation overhead. Override explicitly to force either way. +#if defined(NDEBUG) + bool enableValidation = false; +#else + bool enableValidation = true; +#endif + u32 framesInFlight = 2; // CPU-ahead ring depth + rhi::DeviceFeatures requiredFeatures = {}; + }; + + struct RenderWindowDesc + { + rhi::TextureFormat format = rhi::TextureFormat::BGRA8UnormSrgb; + rhi::PresentMode presentMode = rhi::PresentMode::Fifo; + u32 bufferCount = 2; // swapchain images + }; + + class GraphicsDevice; // forward (RenderWindow refs it; defined complete below) + class RenderWindow; // forward (FrameContext refs it) + + // Typed per-window payload. The UI layer stashes its {RootView, VGContext, + // VGRenderer} here without the host knowing the type — capability as*() idiom. + class IRenderWindowData + { + public: + virtual ~IRenderWindowData() = default; + }; + + // Per-window, per-frame hand-off. Returned by RenderWindow::beginFrame and + // passed to consumers (Subsystem::render / IApplicationModule::onRenderWindow). + struct FrameContext + { + bool valid = false; // false => skip (minimized/acquire failed) + RenderWindow* window = nullptr; // which window this frame targets + u32 frameIndex = 0; // device ring index (0..framesInFlight-1) + u32 width = 0; + u32 height = 0; + rhi::CommandEncoder* encoder = nullptr; // primary, host-created + rhi::CommandPool* pool = nullptr; // this frame's pool (extra encoders) + rhi::Texture* backbuffer = nullptr; + rhi::TextureView* backbufferView = nullptr; + + // Convenience for the common 2D/UI case: open a render pass that clears + // and targets the backbuffer (the Undefined->RenderTarget transition was + // already done by the host in beginFrame). A RenderGraph-driven renderer + // ignores this and records on `encoder` directly. + rhi::RenderPassEncoder* beginBackbufferPass(rhi::ClearColor clear) + { + rhi::ColorAttachment ca{}; + ca.view = backbufferView; + ca.loadOp = rhi::LoadOp::Clear; + ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = clear; + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + m_pass = (encoder != nullptr) ? encoder->beginRenderPass(rpd) : nullptr; + return m_pass; + } + void endBackbufferPass() { if (m_pass != nullptr) { m_pass->end(); m_pass = nullptr; } } + + // One-call clear of the backbuffer (open a clear pass, close it). For + // minimal apps that just want a visible, cleared window without touching + // RHI types. + void clear(f32 r, f32 g, f32 b, f32 a = 1.0f) + { + rhi::ClearColor c; c.r = r; c.g = g; c.b = b; c.a = a; + beginBackbufferPass(c); + endBackbufferPass(); + } + + private: + rhi::RenderPassEncoder* m_pass = nullptr; + }; + + // A window's presentation target. Owns its surface/swapchain and a per-frame + // ring of command pools + fences (per-window present sync). Destroyed via the + // GraphicsDevice that made it. + class RenderWindow + { + public: + // Built by GraphicsDevice::createRenderWindow; takes ownership of the RHI + // objects. Vectors are sized to framesInFlight. + RenderWindow(GraphicsDevice& device, IWindow& window, + rhi::Surface* surface, rhi::SwapChain* swapChain, + std::vector&& pools, std::vector&& fences) noexcept + : m_device(&device), m_window(&window), m_surface(surface), m_swapChain(swapChain), + m_pools(std::move(pools)), + m_fences(std::move(fences)), + m_width(window.width()), m_height(window.height()) + { + m_fenceValues.resize(m_fences.size(), 0ull); + } + + ~RenderWindow(); + + RenderWindow(const RenderWindow&) = delete; + RenderWindow& operator=(const RenderWindow&) = delete; + + [[nodiscard]] IWindow& window() noexcept { return *m_window; } + [[nodiscard]] rhi::SwapChain* swap() noexcept { return m_swapChain; } + + // Poll the window size; recreate the swapchain if it changed. Returns true + // when a resize happened. + bool syncSize(); + + // Acquire this window's backbuffer and open a host-created encoder from the + // current frame's pool (after the per-window fence guards reuse). The + // returned FrameContext is invalid when the window is minimized/zero-sized + // or acquisition fails — the caller skips rendering it this frame. + FrameContext beginFrame(); + + // Transition the backbuffer to Present, submit (signalling the per-window + // fence), and present. No-op for an invalid frame. + void endFrame(FrameContext& frame); + + [[nodiscard]] IRenderWindowData* data() noexcept { return m_data.get(); } + void setData(std::unique_ptr data) noexcept { m_data = std::move(data); } + + private: + GraphicsDevice* m_device; // borrowed + IWindow* m_window; // borrowed + rhi::Surface* m_surface; // owned + rhi::SwapChain* m_swapChain; // owned + std::vector m_pools; // owned, one per frame-in-flight + std::vector m_fences; // owned, one per frame-in-flight + std::vector m_fenceValues; + u32 m_width; + u32 m_height; + std::unique_ptr m_data; // optional typed payload + }; + + class GraphicsDevice + { + public: + // Build a GraphicsDevice from an already-created backend (takes + // ownership): enumerate adapters, create the logical device + graphics + // queue. Backend-agnostic — GPU backends (Vulkan/DX12) are built by the + // `graphics.gpu` factory, which then calls this. On failure the backend is + // destroyed. + static std::expected, ErrorCode> fromBackend( + rhi::Backend* backend, u32 framesInFlight, const rhi::DeviceFeatures& features = {}) + { + if (backend == nullptr) { return std::unexpected(ErrorCode::Unknown); } + + std::span adapters = backend->enumerateAdapters(); + if (adapters.size() == 0) { backend->destroy(); return std::unexpected(ErrorCode::Unknown); } + + rhi::DeviceDesc dd{}; + dd.graphicsQueueCount = 1; + dd.requiredFeatures = features; + rhi::Device* device = nullptr; + if (!adapters[0]->createDevice(dd, device).isOk()) { backend->destroy(); return std::unexpected(ErrorCode::Unknown); } + + rhi::Queue* queue = device->getQueue(rhi::QueueType::Graphics); + if (queue == nullptr) { device->destroy(); backend->destroy(); return std::unexpected(ErrorCode::Unknown); } + + const u32 frames = framesInFlight == 0 ? 1 : framesInFlight; + return std::make_unique(backend, device, queue, frames); + } + + GraphicsDevice(rhi::Backend* backend, rhi::Device* device, rhi::Queue* queue, u32 framesInFlight) noexcept + : m_backend(backend), m_device(device), m_queue(queue), m_framesInFlight(framesInFlight) {} + + ~GraphicsDevice() + { + if (m_device != nullptr) { m_device->waitIdle(); m_device->destroy(); m_device = nullptr; } + if (m_backend != nullptr) { m_backend->destroy(); m_backend = nullptr; } + } + + GraphicsDevice(const GraphicsDevice&) = delete; + GraphicsDevice& operator=(const GraphicsDevice&) = delete; + + // Create a presentation target (surface + swapchain + per-frame pools/ + // fences) for a window. The window must outlive the RenderWindow. + std::expected, ErrorCode> createRenderWindow(IWindow& window, const RenderWindowDesc& desc) + { + const NativeWindow nw = window.native(); + rhi::Surface* surface = nullptr; + if (!m_backend->createSurface(nw.window, nw.display, surface).isOk()) { return std::unexpected(ErrorCode::Unknown); } + + rhi::SwapChainDesc sd{}; + sd.width = window.width(); + sd.height = window.height(); + sd.format = desc.format; + sd.presentMode = desc.presentMode; + sd.bufferCount = desc.bufferCount; + sd.label = u8"renderwindow"; + rhi::SwapChain* swapChain = nullptr; + if (!m_device->createSwapChain(surface, sd, swapChain).isOk()) + { + m_device->destroySurface(surface); + return std::unexpected(ErrorCode::Unknown); + } + + std::vector pools; + std::vector fences; + for (u32 i = 0; i < m_framesInFlight; ++i) + { + rhi::CommandPool* pool = nullptr; + rhi::Fence* fence = nullptr; + if (!m_device->createCommandPool(rhi::QueueType::Graphics, pool).isOk() || + !m_device->createFence(0, fence).isOk()) + { + for (rhi::CommandPool* p : pools) { m_device->destroyCommandPool(p); } + for (rhi::Fence* f : fences) { m_device->destroyFence(f); } + if (pool != nullptr) { m_device->destroyCommandPool(pool); } + m_device->destroySwapChain(swapChain); + m_device->destroySurface(surface); + return std::unexpected(ErrorCode::Unknown); + } + pools.push_back(pool); + fences.push_back(fence); + } + + return std::make_unique(*this, window, surface, swapChain, + std::move(pools), std::move(fences)); + } + + [[nodiscard]] rhi::Device* raw() noexcept { return m_device; } + [[nodiscard]] rhi::Queue* gfxQueue() noexcept { return m_queue; } + [[nodiscard]] u32 framesInFlight() const noexcept { return m_framesInFlight; } + [[nodiscard]] u32 currentFrame() const noexcept { return m_currentFrame; } + + // Advance the CPU frame ring once per app frame (after all windows are + // rendered). Consumers key per-frame GPU resources on currentFrame(). + void advanceFrame() noexcept { m_currentFrame = (m_currentFrame + 1) % m_framesInFlight; } + + private: + rhi::Backend* m_backend; // owned + rhi::Device* m_device; // owned + rhi::Queue* m_queue; // borrowed from device + u32 m_framesInFlight; + u32 m_currentFrame = 0; + }; + + // ----- RenderWindow out-of-line defs (need GraphicsDevice complete) ----- + + inline RenderWindow::~RenderWindow() + { + rhi::Device* dev = m_device->raw(); + if (dev != nullptr) { dev->waitIdle(); } + for (rhi::CommandPool* p : m_pools) { if (p != nullptr) { dev->destroyCommandPool(p); } } + for (rhi::Fence* f : m_fences) { if (f != nullptr) { dev->destroyFence(f); } } + if (m_swapChain != nullptr) { dev->destroySwapChain(m_swapChain); m_swapChain = nullptr; } + if (m_surface != nullptr) { dev->destroySurface(m_surface); m_surface = nullptr; } + } + + inline bool RenderWindow::syncSize() + { + const u32 nw = m_window->width(); + const u32 nh = m_window->height(); + if (nw == 0 || nh == 0) { return false; } + if (nw == m_width && nh == m_height) { return false; } + m_width = nw; + m_height = nh; + m_device->raw()->waitIdle(); + m_swapChain->resize(nw, nh); + return true; + } + + inline FrameContext RenderWindow::beginFrame() + { + if (m_window->isMinimized() || m_window->width() == 0 || m_window->height() == 0) { return FrameContext{}; } + + const u32 fi = m_device->currentFrame(); + // Guard reuse of this slot's pool/backbuffer: wait the GPU's last + // submission against this window's fence at this ring slot. + if (m_fenceValues[fi] > 0) { m_fences[fi]->wait(m_fenceValues[fi], ~0ull); } + + if (!m_swapChain->acquireNextImage().isOk()) { return FrameContext{}; } + + m_pools[fi]->reset(); + rhi::CommandEncoder* enc = nullptr; + if (!m_pools[fi]->createEncoder(enc).isOk() || enc == nullptr) { return FrameContext{}; } + + // Host-managed backbuffer transition (Undefined -> RenderTarget). + enc->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + FrameContext f; + f.valid = true; + f.window = this; + f.frameIndex = fi; + f.width = m_window->width(); + f.height = m_window->height(); + f.encoder = enc; + f.pool = m_pools[fi]; + f.backbuffer = m_swapChain->currentTexture(); + f.backbufferView = m_swapChain->currentTextureView(); + return f; + } + + inline void RenderWindow::endFrame(FrameContext& frame) + { + if (!frame.valid) { return; } + const u32 fi = frame.frameIndex; + + frame.encoder->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = frame.encoder->finish(); + + ++m_fenceValues[fi]; + rhi::CommandBuffer* cbs[1] = { cb }; + m_device->gfxQueue()->submit(std::span(cbs, 1), m_fences[fi], m_fenceValues[fi]); + + m_swapChain->present(m_device->gfxQueue()); + m_pools[fi]->destroyEncoder(frame.encoder); + frame.encoder = nullptr; + } +} diff --git a/Engine/cpp/Runtime/Rendering/Graphics/GraphicsGpu.cppm b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsGpu.cppm new file mode 100644 index 00000000..f7ea8d3c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsGpu.cppm @@ -0,0 +1,70 @@ +// The `graphics.gpu` module. +// +// The GPU-backend factory for GraphicsDevice: turns a GraphicsDeviceDesc into a +// live device on Vulkan or DX12 (validation-wrapped on request), then delegates +// the backend-agnostic bring-up to GraphicsDevice::fromBackend. This is the only +// Vulkan-coupled part of the render host, kept separate so the host types — and +// everything built on them (Application, the UI, the renderer) — stay GPU-backend +// agnostic and build headlessly. Null is delegated to createNullGraphicsDevice. + +module; + +#include +#include + +export module graphics.gpu; + +import core.stdtypes; +import core.status; +import rhi; +import rhi.vk; +#ifdef DRACO_HAS_DX12 +import rhi.dx12; +#endif +import rhi.validation; +import graphics; +import graphics.null; // Null backend delegation + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::graphics +{ + // Create a GraphicsDevice for the requested backend. Returns an error if the + // backend is unavailable (e.g. DX12 off this platform) or bring-up fails. + std::expected, ErrorCode> createGraphicsDevice(const GraphicsDeviceDesc& desc) + { + if (desc.backend == BackendType::Null) + { + return createNullGraphicsDevice(desc.framesInFlight); + } + + rhi::Backend* raw = nullptr; + switch (desc.backend) + { + case BackendType::Vulkan: + { + rhi::vk::VkBackendDesc bd{}; + bd.enableValidation = desc.enableValidation; + if (!rhi::vk::createBackend(bd, raw).isOk()) { return std::unexpected(ErrorCode::Unknown); } + break; + } + case BackendType::DX12: + { +#ifdef DRACO_HAS_DX12 + rhi::dx12::DxBackendDesc bd{}; + bd.enableValidation = desc.enableValidation; + if (!rhi::dx12::createDxBackend(bd, raw).isOk()) { return std::unexpected(ErrorCode::Unknown); } +#else + return std::unexpected(ErrorCode::Unknown); +#endif + break; + } + case BackendType::Null: + break; // handled above + } + + rhi::Backend* backend = desc.enableValidation ? rhi::validation::createValidatedBackend(raw) : raw; + return GraphicsDevice::fromBackend(backend, desc.framesInFlight, desc.requiredFeatures); + } +} diff --git a/Engine/cpp/Runtime/Rendering/Graphics/GraphicsNull.cppm b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsNull.cppm new file mode 100644 index 00000000..6304ccfe --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsNull.cppm @@ -0,0 +1,34 @@ +// The `graphics.null` module. +// +// Headless GraphicsDevice factory over the Null RHI backend (no GPU). For CI, +// servers, and tests, and the reference for what a real backend provides. Kept in +// its own module so the core host (graphics) imports only the base RHI — importing +// a backend module into the core interface trips GCC's module reader, and keeps +// the host GPU-backend-agnostic. + +module; + +#include +#include + +export module graphics.null; + +import core.stdtypes; +import core.status; +import rhi; +import rhi.null; +import graphics; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::graphics +{ + // Create a headless GraphicsDevice backed by the Null RHI. No Vulkan required. + std::expected, ErrorCode> createNullGraphicsDevice(u32 framesInFlight = 2) + { + rhi::Backend* raw = nullptr; + if (!rhi::null::createNullBackend(raw).isOk()) { return std::unexpected(ErrorCode::Unknown); } + return GraphicsDevice::fromBackend(raw, framesInFlight); + } +} diff --git a/Engine/cpp/Runtime/Rendering/Graphics/GraphicsRhi.test.cpp b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsRhi.test.cpp new file mode 100644 index 00000000..bf19016b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Graphics/GraphicsRhi.test.cpp @@ -0,0 +1,131 @@ +// Headless tests for the RHI render host (GraphicsDevice + RenderWindow + +// FrameContext) over the Null RHI backend + null shell — no GPU required. +// Covers device bring-up, per-window frame begin/end, the frame-in-flight ring, +// multi-window rendering, resize, and the minimized-skip path. +#include + +import core; +import graphics; +import graphics.null; +import shell; +import shell.null; + +using namespace draco; +using namespace draco::graphics; +using namespace draco::shell; + +TEST_CASE("graphics: GraphicsDevice brings up over the null backend") +{ + auto created = createNullGraphicsDevice(); + REQUIRE(created.has_value()); + auto& gd = created.value(); + REQUIRE(gd.get() != nullptr); + CHECK(gd->raw() != nullptr); + CHECK(gd->gfxQueue() != nullptr); + CHECK(gd->framesInFlight() == 2u); + CHECK(gd->currentFrame() == 0u); +} + +TEST_CASE("graphics: a window renders, and the frame ring advances") +{ + NullShell shell; + auto created = createNullGraphicsDevice(2); + REQUIRE(created.has_value()); + auto& gd = created.value(); + + auto rwResult = gd->createRenderWindow(*shell.mainWindow(), RenderWindowDesc{}); + REQUIRE(rwResult.has_value()); + auto& rw = rwResult.value(); + + // Frame 0 + FrameContext f0 = rw->beginFrame(); + CHECK(f0.valid); + CHECK(f0.frameIndex == 0u); + CHECK(f0.window == rw.get()); + CHECK(f0.encoder != nullptr); + CHECK(f0.backbufferView != nullptr); + rw->endFrame(f0); + gd->advanceFrame(); + CHECK(gd->currentFrame() == 1u); + + // Frame 1 uses the next ring slot + FrameContext f1 = rw->beginFrame(); + CHECK(f1.valid); + CHECK(f1.frameIndex == 1u); + rw->endFrame(f1); + gd->advanceFrame(); + CHECK(gd->currentFrame() == 0u); // wraps with framesInFlight == 2 + + // Frame 2 wraps back to slot 0 and must wait the slot-0 fence cleanly. + FrameContext f2 = rw->beginFrame(); + CHECK(f2.valid); + CHECK(f2.frameIndex == 0u); + rw->endFrame(f2); +} + +TEST_CASE("graphics: two windows render independently in one app frame") +{ + NullShell shell; + IWindowManager* wm = shell.windowManager(); + auto second = wm->createWindow(WindowSettings{}); + REQUIRE(second.has_value()); + + auto created = createNullGraphicsDevice(2); + REQUIRE(created.has_value()); + auto& gd = created.value(); + + auto a = gd->createRenderWindow(*shell.mainWindow(), RenderWindowDesc{}); + auto b = gd->createRenderWindow(*second.value(), RenderWindowDesc{}); + REQUIRE(a.has_value()); + REQUIRE(b.has_value()); + + // Both windows render in the same app frame at the same ring index, then the + // device advances once. + FrameContext fa = a.value()->beginFrame(); + FrameContext fb = b.value()->beginFrame(); + CHECK(fa.valid); + CHECK(fb.valid); + CHECK(fa.frameIndex == fb.frameIndex); + CHECK(fa.window != fb.window); + a.value()->endFrame(fa); + b.value()->endFrame(fb); + gd->advanceFrame(); + CHECK(gd->currentFrame() == 1u); +} + +TEST_CASE("graphics: syncSize resizes the swapchain when the window changes") +{ + NullShell shell; + auto created = createNullGraphicsDevice(); + REQUIRE(created.has_value()); + auto& gd = created.value(); + + auto rwResult = gd->createRenderWindow(*shell.mainWindow(), RenderWindowDesc{}); + REQUIRE(rwResult.has_value()); + auto& rw = rwResult.value(); + + CHECK_FALSE(rw->syncSize()); // nothing changed yet + + static_cast(shell.mainWindow())->resize(1600, 900); + CHECK(rw->syncSize()); // picked up the change + CHECK(rw->swap()->width() == 1600u); + CHECK(rw->swap()->height() == 900u); + CHECK_FALSE(rw->syncSize()); // stable again +} + +TEST_CASE("graphics: a minimized window yields an invalid frame") +{ + NullShell shell; + auto created = createNullGraphicsDevice(); + REQUIRE(created.has_value()); + auto& gd = created.value(); + + auto rwResult = gd->createRenderWindow(*shell.mainWindow(), RenderWindowDesc{}); + REQUIRE(rwResult.has_value()); + auto& rw = rwResult.value(); + + static_cast(shell.mainWindow())->setMinimized(true); + FrameContext f = rw->beginFrame(); + CHECK_FALSE(f.valid); + rw->endFrame(f); // must be a harmless no-op +} diff --git a/Engine/cpp/Runtime/Rendering/Image/AtlasBuilder.cppm b/Engine/cpp/Runtime/Rendering/Image/AtlasBuilder.cppm new file mode 100644 index 00000000..1ff1880a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/AtlasBuilder.cppm @@ -0,0 +1,209 @@ +// Draconic::Image - :atlas_builder partition. +// +// RectI (integer rectangle for atlas regions) and ImageAtlasBuilder - a +// general-purpose shelf-packing atlas packer that combines multiple RGBA8 +// images into one atlas texture (UI themes, sprite sheets, ...). Ported from +// Sedulous.Images/ImageAtlasBuilder.bf. + +module; + +#include + +#include +#include +#include +#include +#include + +export module image:atlas_builder; + +import core; +import :pixel_format; +import :image_data; + +using namespace draco; + +export namespace draco::image +{ + /// Integer rectangle for atlas regions. + struct RectI + { + i32 x = 0; + i32 y = 0; + i32 width = 0; + i32 height = 0; + + constexpr RectI() noexcept = default; + constexpr RectI(i32 inX, i32 inY, i32 w, i32 h) noexcept : x(inX), y(inY), width(w), height(h) {} + }; + + /// General-purpose image atlas packer (shelf packing). Combines RGBA8 images + /// into one atlas texture. Images are not owned (caller keeps them alive). + class ImageAtlasBuilder + { + public: + /// minSize/maxSize: atlas dimensions (rounded up to powers of two). + /// padding: pixels between packed images. + explicit ImageAtlasBuilder(u32 minSize = 256, u32 maxSize = 4096, u32 padding = 1) + : m_minSize(nextPowerOf2(minSize)), m_maxSize(nextPowerOf2(maxSize)), m_padding(padding) {} + + /// The built atlas image. Null until build() succeeds. + [[nodiscard]] const ImageData* atlas() const { return m_built ? &m_atlas : nullptr; } + + /// Number of entries added. + [[nodiscard]] usize entryCount() const { return m_entries.size(); } + + /// Add an image to be packed. Name must be unique. Image is not owned. + void addImage(std::u8string_view name, const ImageData* image) + { + if (image == nullptr) return; + Entry entry; + entry.name = std::u8string(name); + entry.image = image; + m_entries.push_back(std::move(entry)); + } + + /// Pack all added images into a single RGBA8 atlas. Returns true on success. + bool build() + { + if (m_entries.empty()) + { + const u8 emptyPixel[4] = { 0, 0, 0, 0 }; + m_atlas = OwnedImageData(1, 1, PixelFormat::RGBA8, std::span(emptyPixel, 4)); + m_built = true; + return true; + } + + // Sort by height descending for better shelf packing. + sortByHeightDesc(); + + // Try increasing atlas sizes until everything fits. + for (u32 size = m_minSize; size <= m_maxSize; size *= 2) + { + if (tryPack(size, size)) + { + m_built = true; + return true; + } + } + return false; // Couldn't fit in max size. + } + + /// The pixel-space region of a packed image by name (null if not found). + [[nodiscard]] const RectI* getRegion(std::u8string_view name) const { + auto it = m_regions.find(std::u8string(name)); + return it != m_regions.end() ? &it->second : nullptr; + } + + private: + struct Entry + { + std::u8string name; + const ImageData* image = nullptr; + }; + + void sortByHeightDesc() + { + // Insertion sort (entry counts are small); stable-ish, descending height. + for (usize i = 1; i < m_entries.size(); ++i) + { + Entry key = std::move(m_entries[i]); + const u32 keyH = key.image->height(); + usize j = i; + while (j > 0 && m_entries[j - 1].image->height() < keyH) + { + m_entries[j] = std::move(m_entries[j - 1]); + --j; + } + m_entries[j] = std::move(key); + } + } + + bool tryPack(u32 atlasW, u32 atlasH) + { + m_regions.clear(); + + // Shelf packing: place images left-to-right, start a new row when full. + u32 curX = m_padding; + u32 curY = m_padding; + u32 rowHeight = 0; + + for (usize e = 0; e < m_entries.size(); ++e) + { + const Entry& entry = m_entries[e]; + const u32 imgW = entry.image->width(); + const u32 imgH = entry.image->height(); + + if (curX + imgW + m_padding > atlasW) + { + curX = m_padding; + curY += rowHeight + m_padding; + rowHeight = 0; + } + + if (curY + imgH + m_padding > atlasH) + return false; + + m_regions.insert_or_assign(std::u8string(entry.name), + RectI{ static_cast(curX), static_cast(curY), static_cast(imgW), static_cast(imgH) }); + + curX += imgW + m_padding; + rowHeight = std::max(rowHeight, imgH); + } + + // Build the atlas pixel data. + std::vector pixelData; + pixelData.resize(static_cast(atlasW) * atlasH * 4); + std::memset(pixelData.data(), 0, pixelData.size()); + + for (usize e = 0; e < m_entries.size(); ++e) + { + const Entry& entry = m_entries[e]; + auto regionIt = m_regions.find(std::u8string(entry.name)); + const RectI* region = regionIt != m_regions.end() ? ®ionIt->second : nullptr; + if (region == nullptr) continue; + + const ImageData* src = entry.image; + if (src->format() == PixelFormat::RGBA8) + { + const std::span srcData = src->pixelData(); + const u32 srcStride = src->width() * 4; + const u32 dstStride = atlasW * 4; + + for (u32 y = 0; y < src->height(); ++y) + { + const u32 srcOffset = y * srcStride; + const u32 dstOffset = (static_cast(region->y) + y) * dstStride + static_cast(region->x) * 4; + + if (srcOffset + srcStride <= srcData.size() && dstOffset + srcStride <= pixelData.size()) + std::memcpy(pixelData.data() + dstOffset, srcData.data() + srcOffset, srcStride); + } + } + } + + m_atlas = OwnedImageData(atlasW, atlasH, PixelFormat::RGBA8, std::move(pixelData)); + return true; + } + + [[nodiscard]] static u32 nextPowerOf2(u32 v) + { + u32 n = v; + --n; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + ++n; + return std::max(n, 1u); + } + + std::vector m_entries; + OwnedImageData m_atlas; + std::unordered_map m_regions; + u32 m_minSize; + u32 m_maxSize; + u32 m_padding; + bool m_built = false; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/Image/IO/ImageIOModule.cppm b/Engine/cpp/Runtime/Rendering/Image/IO/ImageIOModule.cppm new file mode 100644 index 00000000..c437fcf2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/IO/ImageIOModule.cppm @@ -0,0 +1,112 @@ +/// Concrete image loading and saving via stb_image / stb_image_write. +/// No abstract loader/writer - direct stb dependency. +/// Works with draco::image::Image directly. + +module; + +#include +#include + +#include +#include +#include +#include + +#include "stb_image.h" +#include "stb_image_write.h" + +export module image.io; + +import core; +import image; + +using namespace draco; + +export namespace draco::image::io { + +/// File format for saving. +enum class ImageFileFormat : u32 { PNG, JPG, BMP }; + +/// Load an image from a file path. Returns RGBA8 for LDR, RGBA32F for HDR. +[[nodiscard]] inline Status loadImage(std::u8string_view path, Image& out) { + const std::string cPath(reinterpret_cast(path.data()), path.size()); + int x = 0, y = 0, channels = 0; + constexpr int desired = 4; + + bool isHDR = stbi_is_hdr(cPath.c_str()) != 0; + void* data = nullptr; + if (isHDR) + data = stbi_loadf(cPath.c_str(), &x, &y, &channels, desired); + else + data = stbi_load(cPath.c_str(), &x, &y, &channels, desired); + + if (!data) return ErrorCode::Unknown; + + usize dataSize = isHDR + ? static_cast(x) * y * desired * sizeof(float) + : static_cast(x) * y * desired; + + PixelFormat fmt = isHDR ? PixelFormat::RGBA32F : PixelFormat::RGBA8; + out = Image(static_cast(x), static_cast(y), fmt, + std::span(static_cast(data), dataSize)); + out.setColorSpace(isHDR ? ImageColorSpace::Linear : ImageColorSpace::Srgb); + stbi_image_free(data); + return ErrorCode::Ok; +} + +/// Load an image from a memory buffer. +[[nodiscard]] inline Status loadImageFromMemory(std::span buffer, Image& out) { + int x = 0, y = 0, channels = 0; + constexpr int desired = 4; + + bool isHDR = stbi_is_hdr_from_memory(buffer.data(), static_cast(buffer.size())) != 0; + void* data = nullptr; + if (isHDR) + data = stbi_loadf_from_memory(buffer.data(), static_cast(buffer.size()), + &x, &y, &channels, desired); + else + data = stbi_load_from_memory(buffer.data(), static_cast(buffer.size()), + &x, &y, &channels, desired); + + if (!data) return ErrorCode::Unknown; + + usize dataSize = isHDR + ? static_cast(x) * y * desired * sizeof(float) + : static_cast(x) * y * desired; + + PixelFormat fmt = isHDR ? PixelFormat::RGBA32F : PixelFormat::RGBA8; + out = Image(static_cast(x), static_cast(y), fmt, + std::span(static_cast(data), dataSize)); + out.setColorSpace(isHDR ? ImageColorSpace::Linear : ImageColorSpace::Srgb); + stbi_image_free(data); + return ErrorCode::Ok; +} + +/// Save an image to a file. Only supports 8-bit formats (R8, RG8, RGB8, RGBA8). +[[nodiscard]] inline Status saveImage(const Image& image, std::u8string_view path, + ImageFileFormat format, i32 jpgQuality = 90) { + if (image.width() == 0 || image.height() == 0) return ErrorCode::Unknown; + + switch (image.format()) { + case PixelFormat::R8: case PixelFormat::RG8: + case PixelFormat::RGB8: case PixelFormat::RGBA8: + break; + default: return ErrorCode::Unknown; + } + + const std::string cPath(reinterpret_cast(path.data()), path.size()); + int w = static_cast(image.width()); + int h = static_cast(image.height()); + int ch = static_cast(channelCount(image.format())); + const void* data = image.pixelData().data(); + + int ok = 0; + switch (format) { + case ImageFileFormat::PNG: ok = stbi_write_png(cPath.c_str(), w, h, ch, data, w * ch); break; + case ImageFileFormat::JPG: ok = stbi_write_jpg(cPath.c_str(), w, h, ch, data, jpgQuality); break; + case ImageFileFormat::BMP: ok = stbi_write_bmp(cPath.c_str(), w, h, ch, data); break; + } + return ok != 0 ? ErrorCode::Ok : ErrorCode::Unknown; +} + +} // namespace draco::image::io diff --git a/Engine/cpp/Runtime/Rendering/Image/IO/ImageTests.test.cpp b/Engine/cpp/Runtime/Rendering/Image/IO/ImageTests.test.cpp new file mode 100644 index 00000000..bc398971 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/IO/ImageTests.test.cpp @@ -0,0 +1,35 @@ +#include + +import core; +import image; +import image.io; + +using namespace draco; +using namespace draco::image; + +TEST_CASE("image: procedural create + pixel access") +{ + Image img = Image::createSolidColor(4, 4, Color32{ 10, 20, 30, 255 }); + CHECK(img.width() == 4u); + CHECK(img.height() == 4u); + CHECK(img.format() == PixelFormat::RGBA8); + const Color32 p = img.getPixel(1, 1); + CHECK(p.r == 10); + CHECK(p.g == 20); + CHECK(p.b == 30); +} + +TEST_CASE("image: save PNG and reload via stb") +{ + Image img = Image::createCheckerboard(64); + REQUIRE(img.width() == 64u); + + const std::u8string_view path = u8"/tmp/draco_image_roundtrip.png"; + REQUIRE(io::saveImage(img, path, io::ImageFileFormat::PNG).isOk()); + + Image loaded; + REQUIRE(io::loadImage(path, loaded).isOk()); + CHECK(loaded.width() == 64u); + CHECK(loaded.height() == 64u); + CHECK(loaded.format() == PixelFormat::RGBA8); // stb loads as RGBA8 +} diff --git a/Engine/cpp/Runtime/Rendering/Image/IO/stb_impl.cpp b/Engine/cpp/Runtime/Rendering/Image/IO/stb_impl.cpp new file mode 100644 index 00000000..801634ca --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/IO/stb_impl.cpp @@ -0,0 +1,10 @@ +// Single translation unit for stb implementations. +// Module units cannot define these macros (static symbol collisions), +// so the implementations live in a regular .cpp. + +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_STDIO_WINDOWS_UTF8 0 +#include "stb_image.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" diff --git a/Engine/cpp/Runtime/Rendering/Image/Image.cppm b/Engine/cpp/Runtime/Rendering/Image/Image.cppm new file mode 100644 index 00000000..126345f4 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/Image.cppm @@ -0,0 +1,435 @@ +/// Image - owns a CPU-side pixel buffer with manipulation methods. +/// Implements ImageData so it can be passed to anything accepting the base type. + +module; + +#include + +#include +#include +#include +#include + +export module image:image; + +import core; +import :pixel_format; +import :image_data; + +using namespace draco; + +export namespace draco::image { + +// Pixel access uses the engine's packed byte color, core::Color32 (the image +// library previously defined its own duplicate `Color` - unified away). + +/// Image that owns a pixel buffer. Inherits ImageData for polymorphic use. +/// Supports pixel access, flips, format conversion, and procedural factories. +class Image : public ImageData { +public: + Image() = default; + + Image(u32 w, u32 h, PixelFormat fmt, std::span srcData = {}) + : m_width(w), m_height(h), m_format(fmt) + { + usize needed = dataSize(); + m_data.resize(needed); + if (srcData.size() >= needed) + std::memcpy(m_data.data(), srcData.data(), needed); + else + clear(); + } + + Image(const Image&) = default; + Image(Image&&) noexcept = default; + Image& operator=(const Image&) = default; + Image& operator=(Image&&) noexcept = default; + + // ---- ImageData interface ---- + [[nodiscard]] u32 width() const override { return m_width; } + [[nodiscard]] u32 height() const override { return m_height; } + [[nodiscard]] PixelFormat format() const override { return m_format; } + [[nodiscard]] ImageColorSpace colorSpace() const override { return m_colorSpace; } + [[nodiscard]] std::span pixelData() const override { return { m_data.data(), m_data.size() }; } + + // ---- Mutable access ---- + [[nodiscard]] std::span pixelDataMut() { return { m_data.data(), m_data.size() }; } + [[nodiscard]] u32 pixelCount() const { return m_width * m_height; } + [[nodiscard]] usize dataSize() const { return static_cast(pixelCount()) * bytesPerPixel(m_format); } + void setColorSpace(ImageColorSpace cs) { m_colorSpace = cs; } + + /// Replace dimensions, format, and pixel data in-place (hot-reload). + void replaceData(u32 w, u32 h, PixelFormat fmt, std::span src) { + m_width = w; m_height = h; m_format = fmt; + usize needed = dataSize(); + m_data.resize(needed); + usize copy = std::min(needed, src.size()); + if (copy > 0) std::memcpy(m_data.data(), src.data(), copy); + if (copy < needed) std::memset(m_data.data() + copy, 0, needed - copy); + } + + // ---- Pixel access ---- + + void clear() { + if (draco::image::hasAlpha(m_format)) + fillColor(Color32::Transparent); + else + std::memset(m_data.data(), 0, m_data.size()); + } + + /// Clear the image to a specific color. + void clear(Color32 color) { fillColor(color); } + + /// Bytes per pixel for a format (static helper mirroring Sedulous's API). + [[nodiscard]] static i32 getBytesPerPixel(PixelFormat format) { return static_cast(bytesPerPixel(format)); } + + /// Whether this image's format carries an alpha channel. + [[nodiscard]] bool hasAlpha() const { return draco::image::hasAlpha(m_format); } + /// Number of channels in this image's pixel format. + [[nodiscard]] i32 getChannelCount() const { return static_cast(draco::image::channelCount(m_format)); } + + void fillColor(Color32 c) { + u32 bpp = bytesPerPixel(m_format); + for (usize i = 0; i < m_data.size(); i += bpp) { + switch (m_format) { + case PixelFormat::R8: + m_data[i] = static_cast((c.r + c.g + c.b) / 3); + break; + case PixelFormat::RG8: + m_data[i] = c.r; m_data[i+1] = c.g; + break; + case PixelFormat::RGB8: + m_data[i] = c.r; m_data[i+1] = c.g; m_data[i+2] = c.b; + break; + case PixelFormat::RGBA8: + m_data[i] = c.r; m_data[i+1] = c.g; m_data[i+2] = c.b; m_data[i+3] = c.a; + break; + case PixelFormat::BGR8: + m_data[i] = c.b; m_data[i+1] = c.g; m_data[i+2] = c.r; + break; + case PixelFormat::BGRA8: + m_data[i] = c.b; m_data[i+1] = c.g; m_data[i+2] = c.r; m_data[i+3] = c.a; + break; + default: break; + } + } + } + + [[nodiscard]] Color32 getPixel(u32 x, u32 y) const { + if (x >= m_width || y >= m_height) return Color32::Black; + usize off = pixelOffset(x, y); + switch (m_format) { + case PixelFormat::R8: { u8 g = m_data[off]; return {g,g,g,255}; } + case PixelFormat::RGB8: return {m_data[off], m_data[off+1], m_data[off+2], 255}; + case PixelFormat::RGBA8: return {m_data[off], m_data[off+1], m_data[off+2], m_data[off+3]}; + case PixelFormat::BGR8: return {m_data[off+2], m_data[off+1], m_data[off], 255}; + case PixelFormat::BGRA8: return {m_data[off+2], m_data[off+1], m_data[off], m_data[off+3]}; + default: return Color32::Black; + } + } + + void setPixel(u32 x, u32 y, Color32 c) { + if (x >= m_width || y >= m_height) return; + usize off = pixelOffset(x, y); + switch (m_format) { + case PixelFormat::R8: m_data[off] = static_cast((c.r+c.g+c.b)/3); break; + case PixelFormat::RGB8: m_data[off]=c.r; m_data[off+1]=c.g; m_data[off+2]=c.b; break; + case PixelFormat::RGBA8: m_data[off]=c.r; m_data[off+1]=c.g; m_data[off+2]=c.b; m_data[off+3]=c.a; break; + case PixelFormat::BGR8: m_data[off]=c.b; m_data[off+1]=c.g; m_data[off+2]=c.r; break; + case PixelFormat::BGRA8: m_data[off]=c.b; m_data[off+1]=c.g; m_data[off+2]=c.r; m_data[off+3]=c.a; break; + default: break; + } + } + + // ---- Flips ---- + + void flipVertical() { + u32 rowSize = m_width * bytesPerPixel(m_format); + std::vector tmp(rowSize); + for (u32 y = 0; y < m_height / 2; ++y) { + u8* top = m_data.data() + y * rowSize; + u8* bot = m_data.data() + (m_height - 1 - y) * rowSize; + std::memcpy(tmp.data(), top, rowSize); + std::memcpy(top, bot, rowSize); + std::memcpy(bot, tmp.data(), rowSize); + } + } + + void flipHorizontal() { + u32 bpp = bytesPerPixel(m_format); + std::vector tmp(bpp); + for (u32 y = 0; y < m_height; ++y) { + for (u32 x = 0; x < m_width / 2; ++x) { + u8* left = m_data.data() + pixelOffset(x, y); + u8* right = m_data.data() + pixelOffset(m_width - 1 - x, y); + std::memcpy(tmp.data(), left, bpp); + std::memcpy(left, right, bpp); + std::memcpy(right, tmp.data(), bpp); + } + } + } + + // ---- Format conversion ---- + + [[nodiscard]] Image convertFormat(PixelFormat newFmt) const { + if (newFmt == m_format) return *this; + Image out(m_width, m_height, newFmt); + for (u32 y = 0; y < m_height; ++y) + for (u32 x = 0; x < m_width; ++x) + out.setPixel(x, y, getPixel(x, y)); + return out; + } + + // ---- Factories ---- + + static Image createSolidColor(u32 w, u32 h, Color32 c, PixelFormat fmt = PixelFormat::RGBA8) { + Image img(w, h, fmt); + img.fillColor(c); + return img; + } + + static Image createCheckerboard(u32 size = 256, Color32 c1 = Color32::White, Color32 c2 = Color32::Black, + u32 checkSize = 32, PixelFormat fmt = PixelFormat::RGBA8) { + Image img(size, size, fmt); + for (u32 y = 0; y < size; ++y) + for (u32 x = 0; x < size; ++x) + img.setPixel(x, y, ((x/checkSize + y/checkSize) % 2 == 0) ? c1 : c2); + return img; + } + + static Image createGradient(u32 w, u32 h, Color32 top, Color32 bottom, PixelFormat fmt = PixelFormat::RGBA8) { + Image img(w, h, fmt); + for (u32 y = 0; y < h; ++y) { + f32 t = static_cast(y) / static_cast(h > 1 ? h - 1 : 1); + Color32 c{ + static_cast(top.r + t * (bottom.r - top.r)), + static_cast(top.g + t * (bottom.g - top.g)), + static_cast(top.b + t * (bottom.b - top.b)), + static_cast(top.a + t * (bottom.a - top.a)), + }; + for (u32 x = 0; x < w; ++x) img.setPixel(x, y, c); + } + return img; + } + + // ---- Normal-map factories ---- + // (n*0.5+0.5)*255; the neutral up-normal (0,0,1) -> (128,128,255). + + static Image createFlatNormalMap(u32 width = 256, u32 height = 256, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + image.fillColor(Color32(128, 128, 255, 255)); // (0,0,1) + return image; + } + + static Image createWaveNormalMap(u32 width = 256, u32 height = 256, + f32 waveFrequencyX = 8.0f, f32 waveFrequencyY = 6.0f, + f32 amplitude = 0.3f, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + const f32 fx = static_cast(x) / static_cast(width); + const f32 fy = static_cast(y) / static_cast(height); + const f32 heightValue = math::sin(fx * math::PI * waveFrequencyX) * amplitude + math::sin(fy * math::PI * waveFrequencyY) * amplitude * 0.7f; + const f32 heightRight = math::sin((fx + 1.0f / static_cast(width)) * math::PI * waveFrequencyX) * amplitude + math::sin(fy * math::PI * waveFrequencyY) * amplitude * 0.7f; + const f32 heightDown = math::sin(fx * math::PI * waveFrequencyX) * amplitude + math::sin((fy + 1.0f / static_cast(height)) * math::PI * waveFrequencyY) * amplitude * 0.7f; + const f32 dx = heightRight - heightValue; + const f32 dy = heightDown - heightValue; + image.setPixel(x, y, encodeNormal(math::normalize(math::Vector3{ -dx * 20.0f, -dy * 20.0f, 1.0f }))); + } + } + return image; + } + + static Image createBrickNormalMap(u32 width = 256, u32 height = 256, u32 bricksX = 8, u32 bricksY = 4, + f32 mortarDepth = 0.3f, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + const u32 brickWidth = width / bricksX; + const u32 brickHeight = height / bricksY; + const u32 mortarWidth = std::max(brickWidth / 16u, 2u); + + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + const u32 brickY = y / brickHeight; + u32 adjustedX = x; + if (brickY % 2 == 1) + adjustedX = (x + brickWidth / 2) % width; + + const u32 localX = adjustedX % brickWidth; + const u32 localY = y % brickHeight; + + const bool isHorizontalMortar = localY < mortarWidth || localY >= (brickHeight - mortarWidth); + const bool isVerticalMortar = localX < mortarWidth || localX >= (brickWidth - mortarWidth); + const bool isMortar = isHorizontalMortar || isVerticalMortar; + + math::Vector3 normal; + if (isMortar) { + normal = math::Vector3{ 0.0f, 0.0f, 1.0f - mortarDepth * 2.0f }; + } else { + const f32 brickVariation = math::sin(static_cast(localX) * 0.2f) * math::sin(static_cast(localY) * 0.15f) * 0.1f; + normal = math::Vector3{ 0.0f, 0.0f, 1.0f + brickVariation }; + } + image.setPixel(x, y, encodeNormal(math::normalize(normal))); + } + } + return image; + } + + static Image createCircularBumpNormalMap(u32 width = 256, u32 height = 256, f32 bumpHeight = 0.5f, + f32 falloff = 2.0f, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + const f32 centerX = static_cast(width) * 0.5f; + const f32 centerY = static_cast(height) * 0.5f; + const f32 maxRadius = static_cast(std::min(width, height)) * 0.4f; + + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + const f32 dx = static_cast(x) - centerX; + const f32 dy = static_cast(y) - centerY; + const f32 distance = sqrt(dx * dx + dy * dy); + + math::Vector3 normal; + if (distance < maxRadius && distance > 0.001f) { + const f32 normalizedDist = distance / maxRadius; + const f32 heightDerivative = -falloff * pow(1.0f - normalizedDist, falloff - 1.0f) * bumpHeight * 3.0f / maxRadius; + const f32 nx = (dx / distance) * heightDerivative; + const f32 ny = (dy / distance) * heightDerivative; + normal = math::normalize(math::Vector3{ nx, ny, 1.0f }); + } else { + normal = math::Vector3{ 0.0f, 0.0f, 1.0f }; + } + image.setPixel(x, y, encodeNormal(normal)); + } + } + return image; + } + + static Image createNoiseNormalMap(u32 width = 256, u32 height = 256, f32 scale = 0.1f, + f32 amplitude = 0.2f, i32 seed = 12345, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + + std::vector heightMap; + heightMap.resize(static_cast(width) * height); + + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + f32 noise = 0.0f; + f32 freq = scale; + f32 amp = amplitude; + for (i32 octave = 0; octave < 4; ++octave) { + const f32 fx = static_cast(x) * freq; + const f32 fy = static_cast(y) * freq; + const u32 ix = static_cast(fx); + const u32 iy = static_cast(fy); + const f32 fracX = fx - static_cast(ix); + const f32 fracY = fy - static_cast(iy); + + const f32 a = hashToFloat(seed + static_cast(ix) + static_cast(iy) * 1000 + octave * 10000); + const f32 b = hashToFloat(seed + static_cast(ix + 1) + static_cast(iy) * 1000 + octave * 10000); + const f32 c = hashToFloat(seed + static_cast(ix) + static_cast(iy + 1) * 1000 + octave * 10000); + const f32 d = hashToFloat(seed + static_cast(ix + 1) + static_cast(iy + 1) * 1000 + octave * 10000); + + const f32 smoothX = fracX * fracX * (3.0f - 2.0f * fracX); + const f32 smoothY = fracY * fracY * (3.0f - 2.0f * fracY); + + const f32 i1 = math::lerp(a, b, smoothX); + const f32 i2 = math::lerp(c, d, smoothX); + const f32 value = math::lerp(i1, i2, smoothY); + + noise += value * amp; + freq *= 2.0f; + amp *= 0.5f; + } + heightMap[static_cast(y) * width + x] = noise; + } + } + + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + // Edge-clamped neighbours (avoids the u32 underflow latent in the original). + const u32 xl = (x > 0) ? x - 1 : 0; + const u32 xr = (x + 1 < width) ? x + 1 : width - 1; + const u32 yu = (y > 0) ? y - 1 : 0; + const u32 yd = (y + 1 < height) ? y + 1 : height - 1; + const f32 heightL = heightMap[static_cast(y) * width + xl]; + const f32 heightR = heightMap[static_cast(y) * width + xr]; + const f32 heightU = heightMap[static_cast(yu) * width + x]; + const f32 heightD = heightMap[static_cast(yd) * width + x]; + + const f32 dx = heightR - heightL; + const f32 dy = heightD - heightU; + image.setPixel(x, y, encodeNormal(math::normalize(math::Vector3{ -dx * 8.0f, -dy * 8.0f, 1.0f }))); + } + } + return image; + } + + static Image createTestPatternNormalMap(u32 width = 256, u32 height = 256, PixelFormat format = PixelFormat::RGBA8) { + Image image(width, height, format); + for (u32 y = 0; y < height; ++y) { + for (u32 x = 0; x < width; ++x) { + const f32 fx = static_cast(x) / static_cast(width); + const f32 fy = static_cast(y) / static_cast(height); + + math::Vector3 normal; + if (fx < 0.5f && fy < 0.5f) { + normal = math::Vector3{ 0.0f, 0.0f, 1.0f }; // Top-left: flat. + } else if (fx >= 0.5f && fy < 0.5f) { + const f32 bump = math::sin(fx * math::PI * 16.0f) * 0.5f; // Top-right: X bumps. + normal = math::normalize(math::Vector3{ bump, 0.0f, 1.0f }); + } else if (fx < 0.5f && fy >= 0.5f) { + const f32 bump = math::sin(fy * math::PI * 16.0f) * 0.5f; // Bottom-left: Y bumps. + normal = math::normalize(math::Vector3{ 0.0f, bump, 1.0f }); + } else { + const f32 centerX = 0.75f, centerY = 0.75f; // Bottom-right: circular. + const f32 dx = fx - centerX; + const f32 dy = fy - centerY; + const f32 dist = sqrt(dx * dx + dy * dy); + if (dist < 0.2f) { + const f32 angle = atan2(dy, dx); + normal = math::normalize(math::Vector3{ math::cos(angle) * 0.3f, math::sin(angle) * 0.3f, 1.0f }); + } else { + normal = math::Vector3{ 0.0f, 0.0f, 1.0f }; + } + } + image.setPixel(x, y, encodeNormal(normal)); + } + } + return image; + } + + /// Build a normal from neighbouring heights (heightmap -> normal conversion). + [[nodiscard]] static math::Vector3 calculateNormalFromHeight(f32 heightL, f32 heightR, f32 heightU, f32 heightD, f32 scale = 1.0f) { + const f32 dx = (heightR - heightL) * scale; + const f32 dy = (heightD - heightU) * scale; + return math::normalize(math::Vector3{ -dx, -dy, 1.0f }); + } + +private: + /// Encode a unit normal into a packed RGB color ((n*0.5+0.5)*255), alpha 255. + [[nodiscard]] static Color32 encodeNormal(math::Vector3 n) { + return Color32(static_cast((n.x * 0.5f + 0.5f) * 255.0f), + static_cast((n.y * 0.5f + 0.5f) * 255.0f), + static_cast((n.z * 0.5f + 0.5f) * 255.0f), 255); + } + + /// LCG-style integer hash -> f32 in [-1, 1] (deterministic; for value noise). + [[nodiscard]] static f32 hashToFloat(i32 value) { + u32 hash = static_cast(value); + hash = hash * 1103515245u + 12345u; + hash = (hash >> 16) ^ hash; + hash = hash * 0x85ebca6bu; + hash = (hash >> 13) ^ hash; + return (static_cast(hash & 0x7FFFFFFFu) / static_cast(0x7FFFFFFF)) * 2.0f - 1.0f; + } + + [[nodiscard]] usize pixelOffset(u32 x, u32 y) const { + return static_cast(y * m_width + x) * bytesPerPixel(m_format); + } + + u32 m_width = 0, m_height = 0; + PixelFormat m_format = PixelFormat::RGBA8; + ImageColorSpace m_colorSpace = ImageColorSpace::Srgb; + std::vector m_data; +}; + +} // namespace draco::image diff --git a/Engine/cpp/Runtime/Rendering/Image/ImageData.cppm b/Engine/cpp/Runtime/Rendering/Image/ImageData.cppm new file mode 100644 index 00000000..d46f5127 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/ImageData.cppm @@ -0,0 +1,109 @@ +/// Image data types: ImageColorSpace, OwnedImageData, ImageDataRef. + +module; + +#include + +#include +#include +#include + +export module image:image_data; + +import core; +import :pixel_format; + +using namespace draco; + +export namespace draco::image { + +/// Color space of the pixel data. +enum class ImageColorSpace : u32 { + /// sRGB-encoded (photos, UI). GPU decodes sRGB→linear on sample. + Srgb, + /// Linear data (normal maps, masks, HDR). Sampled as-is. + Linear, +}; + +/// Abstract interface for image data. Implemented by OwnedImageData (owning) +/// and ImageDataRef (non-owning). Allows renderers and other consumers to +/// accept either type through a common base. +class ImageData { +public: + virtual ~ImageData() = default; + [[nodiscard]] virtual u32 width() const = 0; + [[nodiscard]] virtual u32 height() const = 0; + [[nodiscard]] virtual PixelFormat format() const = 0; + [[nodiscard]] virtual std::span pixelData() const = 0; + [[nodiscard]] virtual ImageColorSpace colorSpace() const = 0; +}; + +/// Owns a CPU-side pixel buffer. +class OwnedImageData : public ImageData { +public: + OwnedImageData() = default; + + /// Creates from a copy of the provided data. + OwnedImageData(u32 w, u32 h, PixelFormat fmt, std::span data, + ImageColorSpace cs = ImageColorSpace::Srgb) + : m_width(w), m_height(h), m_format(fmt), m_colorSpace(cs) + { + m_data.resize(data.size()); + if (data.size() > 0) { std::memcpy(m_data.data(), data.data(), data.size()); } + } + + /// Takes ownership of data by move. + OwnedImageData(u32 w, u32 h, PixelFormat fmt, std::vector&& data, + ImageColorSpace cs = ImageColorSpace::Srgb) + : m_width(w), m_height(h), m_format(fmt), m_colorSpace(cs), + m_data(static_cast&&>(data)) {} + + [[nodiscard]] u32 width() const override { return m_width; } + [[nodiscard]] u32 height() const override { return m_height; } + [[nodiscard]] PixelFormat format() const override { return m_format; } + [[nodiscard]] ImageColorSpace colorSpace() const override { return m_colorSpace; } + [[nodiscard]] std::span pixelData() const override { return { m_data.data(), m_data.size() }; } + [[nodiscard]] std::span pixelDataMut() { return { m_data.data(), m_data.size() }; } + [[nodiscard]] u32 dataSize() const { return m_width * m_height * bytesPerPixel(m_format); } + +private: + u32 m_width = 0, m_height = 0; + PixelFormat m_format = PixelFormat::RGBA8; + ImageColorSpace m_colorSpace = ImageColorSpace::Srgb; + std::vector m_data; +}; + +/// References external pixel data (non-owning). +/// Caller must ensure data outlives this reference. +class ImageDataRef : public ImageData { +public: + ImageDataRef() = default; + + /// No pixel data (for GPU-managed textures). + ImageDataRef(u32 w, u32 h, PixelFormat fmt = PixelFormat::RGBA8, + ImageColorSpace cs = ImageColorSpace::Srgb) + : m_width(w), m_height(h), m_format(fmt), m_colorSpace(cs) {} + + /// Points to external pixel data. + ImageDataRef(u32 w, u32 h, PixelFormat fmt, const u8* data, usize length, + ImageColorSpace cs = ImageColorSpace::Srgb) + : m_width(w), m_height(h), m_format(fmt), m_colorSpace(cs), + m_ptr(data), m_length(length) {} + + [[nodiscard]] u32 width() const override { return m_width; } + [[nodiscard]] u32 height() const override { return m_height; } + [[nodiscard]] PixelFormat format() const override { return m_format; } + [[nodiscard]] ImageColorSpace colorSpace() const override { return m_colorSpace; } + [[nodiscard]] std::span pixelData() const override { + return m_ptr ? std::span(m_ptr, m_length) : std::span(); + } + +private: + u32 m_width = 0, m_height = 0; + PixelFormat m_format = PixelFormat::RGBA8; + ImageColorSpace m_colorSpace = ImageColorSpace::Srgb; + const u8* m_ptr = nullptr; + usize m_length = 0; +}; + +} // namespace draco::image diff --git a/Engine/cpp/Runtime/Rendering/Image/ImageDataTests.test.cpp b/Engine/cpp/Runtime/Rendering/Image/ImageDataTests.test.cpp new file mode 100644 index 00000000..d0a0af50 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/ImageDataTests.test.cpp @@ -0,0 +1,249 @@ +// NineSlice, ImageAtlasBuilder, PixelFormat. Mirrors the Sedulous assertions so +// the ported lib inherits that suite's coverage. (Test.Assert -> CHECK; Beef +// scope/new -> stack/Array; nullable RectangleI -> const RectI*.) +#include +#include +import core; +import image; + +using namespace draco; +using namespace draco::image; + +// ============================================================ OwnedImageData + +TEST_CASE("image.owned: construct from span") +{ + u8 pixels[16]; + for (i32 i = 0; i < 16; ++i) pixels[i] = static_cast(i); + + OwnedImageData img(2, 2, PixelFormat::RGBA8, std::span(pixels, 16)); + CHECK(img.width() == 2u); + CHECK(img.height() == 2u); + CHECK(img.format() == PixelFormat::RGBA8); + CHECK(img.pixelData().size() == 16u); + CHECK(img.pixelData().data()[0] == 0); + CHECK(img.pixelData().data()[4] == 4); +} + +TEST_CASE("image.owned: construct from array (move)") +{ + std::vector data; + data.resize(8); + data[0] = 255; + data[7] = 128; + OwnedImageData img(2, 1, PixelFormat::RGBA8, move(data)); + CHECK(img.width() == 2u); + CHECK(img.height() == 1u); + CHECK(img.pixelData().data()[0] == 255); + CHECK(img.pixelData().data()[7] == 128); +} + +TEST_CASE("image.owned: R8 format") +{ + std::vector data; data.resize(4); + OwnedImageData img(2, 2, PixelFormat::R8, move(data)); + CHECK(img.format() == PixelFormat::R8); + CHECK(img.pixelData().size() == 4u); +} + +// ==================================================================== NineSlice + +TEST_CASE("image.nineslice: construct four values") +{ + const NineSlice ns(4, 6, 8, 10); + CHECK(ns.left == 4); + CHECK(ns.top == 6); + CHECK(ns.right == 8); + CHECK(ns.bottom == 10); +} + +TEST_CASE("image.nineslice: construct uniform") +{ + const NineSlice ns(5.0f); + CHECK(ns.left == 5); + CHECK(ns.top == 5); + CHECK(ns.right == 5); + CHECK(ns.bottom == 5); +} + +TEST_CASE("image.nineslice: construct symmetric") +{ + const NineSlice ns(3, 7); + CHECK(ns.left == 3); + CHECK(ns.right == 3); + CHECK(ns.top == 7); + CHECK(ns.bottom == 7); +} + +TEST_CASE("image.nineslice: horizontal border") +{ + const NineSlice ns(4, 0, 6, 0); + CHECK(ns.horizontalBorder() == 10); +} + +TEST_CASE("image.nineslice: vertical border") +{ + const NineSlice ns(0, 3, 0, 5); + CHECK(ns.verticalBorder() == 8); +} + +TEST_CASE("image.nineslice: IsValid non-zero") +{ + const NineSlice ns(1, 0, 0, 0); + CHECK(ns.isValid()); +} + +TEST_CASE("image.nineslice: IsValid all-zero") +{ + const NineSlice ns(0, 0, 0, 0); + CHECK_FALSE(ns.isValid()); +} + +// =========================================================== ImageAtlasBuilder + +namespace +{ + OwnedImageData makeImage(u32 w, u32 h, u8 fill) + { + std::vector data; + data.resize(static_cast(w) * h * 4); + for (usize i = 0; i < data.size(); ++i) data[i] = fill; + return OwnedImageData(w, h, PixelFormat::RGBA8, move(data)); + } + + bool overlaps(const RectI& a, const RectI& b) + { + return a.x < b.x + b.width && a.x + a.width > b.x + && a.y < b.y + b.height && a.y + a.height > b.y; + } + + bool isPow2(u32 v) { return v > 0 && (v & (v - 1)) == 0; } +} + +TEST_CASE("image.atlas: empty build") +{ + ImageAtlasBuilder builder; + CHECK(builder.build()); + REQUIRE(builder.atlas() != nullptr); + CHECK(builder.atlas()->width() >= 1u); + CHECK(builder.atlas()->height() >= 1u); +} + +TEST_CASE("image.atlas: single image") +{ + const OwnedImageData img = makeImage(32, 32, 128); + + ImageAtlasBuilder builder; + builder.addImage(u8"test", &img); + CHECK(builder.build()); + + const RectI* region = builder.getRegion(u8"test"); + REQUIRE(region != nullptr); + CHECK(region->width == 32); + CHECK(region->height == 32); +} + +TEST_CASE("image.atlas: multiple images do not overlap") +{ + const OwnedImageData img1 = makeImage(64, 64, 100); + const OwnedImageData img2 = makeImage(32, 32, 200); + const OwnedImageData img3 = makeImage(48, 48, 150); + + ImageAtlasBuilder builder; + builder.addImage(u8"a", &img1); + builder.addImage(u8"b", &img2); + builder.addImage(u8"c", &img3); + CHECK(builder.build()); + + const RectI* r1 = builder.getRegion(u8"a"); + const RectI* r2 = builder.getRegion(u8"b"); + const RectI* r3 = builder.getRegion(u8"c"); + REQUIRE(r1 != nullptr); + REQUIRE(r2 != nullptr); + REQUIRE(r3 != nullptr); + + CHECK_FALSE(overlaps(*r1, *r2)); + CHECK_FALSE(overlaps(*r1, *r3)); + CHECK_FALSE(overlaps(*r2, *r3)); +} + +TEST_CASE("image.atlas: dimensions are powers of two") +{ + const OwnedImageData img = makeImage(100, 100, 0); + + ImageAtlasBuilder builder; + builder.addImage(u8"big", &img); + CHECK(builder.build()); + + CHECK(isPow2(builder.atlas()->width())); + CHECK(isPow2(builder.atlas()->height())); +} + +TEST_CASE("image.atlas: grows when needed") +{ + ImageAtlasBuilder builder(256, 4096); + std::vector images; + images.reserve(20); + for (i32 i = 0; i < 20; ++i) + images.push_back(makeImage(64, 64, static_cast(i))); + + // Names must outlive build(); build them up front. + for (i32 i = 0; i < 20; ++i) + { + std::u8string name = u8"img"; + const std::string digits = std::to_string(i); + name.append(reinterpret_cast(digits.data()), digits.size()); + builder.addImage(name, &images[static_cast(i)]); + } + + CHECK(builder.build()); + // 20 x 64x64 = 81920 px; 256x256=65536 too small -> should have grown. + CHECK((builder.atlas()->width() >= 512u || builder.atlas()->height() >= 512u)); +} + +TEST_CASE("image.atlas: GetRegion missing") +{ + ImageAtlasBuilder builder; + builder.build(); + CHECK(builder.getRegion(u8"nonexistent") == nullptr); +} + +TEST_CASE("image.atlas: pixel data copied") +{ + const OwnedImageData img = makeImage(4, 4, 42); + + ImageAtlasBuilder builder(256); + builder.addImage(u8"px", &img); + CHECK(builder.build()); + + const RectI* region = builder.getRegion(u8"px"); + REQUIRE(region != nullptr); + const ImageData* atlas = builder.atlas(); + const u32 stride = atlas->width() * 4; + + const usize offset = static_cast(region->y) * stride + static_cast(region->x) * 4; + CHECK(atlas->pixelData().data()[offset] == 42); +} + +TEST_CASE("image.atlas: large image (bigger than min size)") +{ + const OwnedImageData img = makeImage(300, 300, 77); + + ImageAtlasBuilder builder(256, 4096); + builder.addImage(u8"large", &img); + CHECK(builder.build()); + + const RectI* region = builder.getRegion(u8"large"); + REQUIRE(region != nullptr); + CHECK(region->width == 300); + CHECK(region->height == 300); + CHECK(builder.atlas()->width() >= 302u); // 300 + padding +} + +// =================================================================== PixelFormat + +TEST_CASE("image.pixelformat: distinct values") +{ + CHECK(PixelFormat::R8 != PixelFormat::RGBA8); + CHECK(PixelFormat::RGBA8 != PixelFormat::BGRA8); +} diff --git a/Engine/cpp/Runtime/Rendering/Image/ImageManipulationTests.test.cpp b/Engine/cpp/Runtime/Rendering/Image/ImageManipulationTests.test.cpp new file mode 100644 index 00000000..fd533dc0 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/ImageManipulationTests.test.cpp @@ -0,0 +1,240 @@ +// get/set, clear/fill, flips, format conversion, channel helpers. Mirrors the +// Sedulous assertions (Test.Assert -> CHECK; pixel.R/G/B/A -> .r/.g/.b/.a; +// ConvertFormat returns Image directly so no `.Value`; Sedulous Color32.Lime +// (0,255,0) maps to our Color32::Green). +#include +import core; +import image; + +using namespace draco; +using namespace draco::image; + +TEST_CASE("image.manip: creation") +{ + const Image image(64, 64, PixelFormat::RGBA8); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + CHECK(image.format() == PixelFormat::RGBA8); + CHECK(image.pixelCount() == 64u * 64u); + CHECK(image.dataSize() == 64u * 64u * 4u); +} + +TEST_CASE("image.manip: creation with data") +{ + u8 data[4 * 4] = {}; // 2x2 RGBA + data[0] = 255; data[1] = 0; data[2] = 0; data[3] = 255; // pixel 0 = red + data[4] = 0; data[5] = 255; data[6] = 0; data[7] = 255; // pixel 1 = green + + const Image image(2, 2, PixelFormat::RGBA8, std::span(data, 16)); + const Color32 pixel0 = image.getPixel(0, 0); + CHECK((pixel0.r == 255 && pixel0.g == 0 && pixel0.b == 0 && pixel0.a == 255)); + const Color32 pixel1 = image.getPixel(1, 0); + CHECK((pixel1.r == 0 && pixel1.g == 255 && pixel1.b == 0 && pixel1.a == 255)); +} + +TEST_CASE("image.manip: copy") +{ + Image original(4, 4, PixelFormat::RGBA8); + original.setPixel(0, 0, Color32::Red); + original.setPixel(1, 1, Color32::Green); // Sedulous Color32.Lime + + const Image copy(original); + CHECK(copy.width() == original.width()); + CHECK(copy.height() == original.height()); + CHECK(copy.format() == original.format()); + + const Color32 pixel00 = copy.getPixel(0, 0); + CHECK((pixel00.r == 255 && pixel00.g == 0 && pixel00.b == 0)); + const Color32 pixel11 = copy.getPixel(1, 1); + CHECK((pixel11.r == 0 && pixel11.g == 255 && pixel11.b == 0)); +} + +TEST_CASE("image.manip: get/set pixel") +{ + Image image(8, 8, PixelFormat::RGBA8); + image.setPixel(0, 0, Color32::Red); + image.setPixel(1, 0, Color32::Green); // Lime + image.setPixel(2, 0, Color32::Blue); + image.setPixel(3, 0, Color32::White); + image.setPixel(4, 0, Color32::Black); + image.setPixel(5, 0, Color32(128, 64, 32, 200)); + + const Color32 red = image.getPixel(0, 0); + CHECK((red.r == 255 && red.g == 0 && red.b == 0 && red.a == 255)); + const Color32 green = image.getPixel(1, 0); + CHECK((green.r == 0 && green.g == 255 && green.b == 0 && green.a == 255)); + const Color32 blue = image.getPixel(2, 0); + CHECK((blue.r == 0 && blue.g == 0 && blue.b == 255 && blue.a == 255)); + const Color32 white = image.getPixel(3, 0); + CHECK((white.r == 255 && white.g == 255 && white.b == 255 && white.a == 255)); + const Color32 black = image.getPixel(4, 0); + CHECK((black.r == 0 && black.g == 0 && black.b == 0 && black.a == 255)); + const Color32 custom = image.getPixel(5, 0); + CHECK((custom.r == 128 && custom.g == 64 && custom.b == 32 && custom.a == 200)); +} + +TEST_CASE("image.manip: get pixel out of bounds returns black") +{ + const Image image(4, 4, PixelFormat::RGBA8); + CHECK(image.getPixel(10, 10) == Color32::Black); +} + +TEST_CASE("image.manip: set pixel out of bounds is a no-op") +{ + Image image(4, 4, PixelFormat::RGBA8); + image.setPixel(100, 100, Color32::Red); // must not crash/corrupt + + image.setPixel(0, 0, Color32::Green); // Lime + const Color32 pixel = image.getPixel(0, 0); + CHECK((pixel.r == 0 && pixel.g == 255 && pixel.b == 0)); +} + +TEST_CASE("image.manip: clear with color") +{ + Image image(4, 4, PixelFormat::RGBA8); + image.setPixel(0, 0, Color32::Red); + image.setPixel(1, 1, Color32::Green); + + image.clear(Color32::Blue); + for (u32 y = 0; y < 4; ++y) + for (u32 x = 0; x < 4; ++x) + { + const Color32 pixel = image.getPixel(x, y); + CHECK((pixel.r == 0 && pixel.g == 0 && pixel.b == 255 && pixel.a == 255)); + } +} + +TEST_CASE("image.manip: fill color") +{ + Image image(4, 4, PixelFormat::RGBA8); + image.fillColor(Color32(100, 150, 200, 250)); + for (u32 y = 0; y < 4; ++y) + for (u32 x = 0; x < 4; ++x) + { + const Color32 pixel = image.getPixel(x, y); + CHECK((pixel.r == 100 && pixel.g == 150 && pixel.b == 200 && pixel.a == 250)); + } +} + +TEST_CASE("image.manip: flip vertical") +{ + Image image(2, 4, PixelFormat::RGBA8); + image.setPixel(0, 0, Color32::Red); + image.setPixel(1, 0, Color32::Red); + image.setPixel(0, 3, Color32::Blue); + image.setPixel(1, 3, Color32::Blue); + + image.flipVertical(); + + const Color32 topLeft = image.getPixel(0, 0); + CHECK((topLeft.b == 255 && topLeft.r == 0)); + const Color32 bottomLeft = image.getPixel(0, 3); + CHECK((bottomLeft.r == 255 && bottomLeft.b == 0)); +} + +TEST_CASE("image.manip: flip horizontal") +{ + Image image(4, 2, PixelFormat::RGBA8); + image.setPixel(0, 0, Color32::Red); + image.setPixel(0, 1, Color32::Red); + image.setPixel(3, 0, Color32::Blue); + image.setPixel(3, 1, Color32::Blue); + + image.flipHorizontal(); + + const Color32 left = image.getPixel(0, 0); + CHECK((left.b == 255 && left.r == 0)); + const Color32 right = image.getPixel(3, 0); + CHECK((right.r == 255 && right.b == 0)); +} + +TEST_CASE("image.manip: convert format RGBA8 -> RGB8") +{ + Image rgba(4, 4, PixelFormat::RGBA8); + rgba.setPixel(0, 0, Color32::Red); + rgba.setPixel(1, 0, Color32::Green); + rgba.setPixel(2, 0, Color32::Blue); + rgba.setPixel(3, 0, Color32(100, 150, 200, 255)); + + const Image rgb = rgba.convertFormat(PixelFormat::RGB8); + CHECK(rgb.format() == PixelFormat::RGB8); + CHECK((rgb.width() == 4u && rgb.height() == 4u)); + + const Color32 red = rgb.getPixel(0, 0); + CHECK((red.r == 255 && red.g == 0 && red.b == 0)); + const Color32 custom = rgb.getPixel(3, 0); + CHECK((custom.r == 100 && custom.g == 150 && custom.b == 200)); +} + +TEST_CASE("image.manip: convert to same format") +{ + Image original(4, 4, PixelFormat::RGBA8); + original.setPixel(0, 0, Color32::Red); + + const Image converted = original.convertFormat(PixelFormat::RGBA8); + CHECK(converted.format() == PixelFormat::RGBA8); + const Color32 pixel = converted.getPixel(0, 0); + CHECK((pixel.r == 255 && pixel.g == 0 && pixel.b == 0)); +} + +TEST_CASE("image.manip: HasAlpha") +{ + CHECK(Image(4, 4, PixelFormat::RGBA8).hasAlpha()); + CHECK_FALSE(Image(4, 4, PixelFormat::RGB8).hasAlpha()); + CHECK_FALSE(Image(4, 4, PixelFormat::R8).hasAlpha()); + CHECK(Image(4, 4, PixelFormat::BGRA8).hasAlpha()); +} + +TEST_CASE("image.manip: GetChannelCount") +{ + CHECK(Image(4, 4, PixelFormat::R8).getChannelCount() == 1); + CHECK(Image(4, 4, PixelFormat::RG8).getChannelCount() == 2); + CHECK(Image(4, 4, PixelFormat::RGB8).getChannelCount() == 3); + CHECK(Image(4, 4, PixelFormat::RGBA8).getChannelCount() == 4); +} + +TEST_CASE("image.manip: GetBytesPerPixel") +{ + CHECK(Image::getBytesPerPixel(PixelFormat::R8) == 1); + CHECK(Image::getBytesPerPixel(PixelFormat::RG8) == 2); + CHECK(Image::getBytesPerPixel(PixelFormat::RGB8) == 3); + CHECK(Image::getBytesPerPixel(PixelFormat::RGBA8) == 4); + CHECK(Image::getBytesPerPixel(PixelFormat::BGR8) == 3); + CHECK(Image::getBytesPerPixel(PixelFormat::BGRA8) == 4); + CHECK(Image::getBytesPerPixel(PixelFormat::R32F) == 4); + CHECK(Image::getBytesPerPixel(PixelFormat::RGBA32F) == 16); +} + +TEST_CASE("image.manip: RGB8 format preserves RGB, alpha 255") +{ + Image image(4, 4, PixelFormat::RGB8); + image.setPixel(0, 0, Color32(100, 150, 200, 255)); + const Color32 pixel = image.getPixel(0, 0); + CHECK((pixel.r == 100 && pixel.g == 150 && pixel.b == 200 && pixel.a == 255)); +} + +TEST_CASE("image.manip: BGR8 format swaps channels correctly") +{ + Image image(4, 4, PixelFormat::BGR8); + image.setPixel(0, 0, Color32(100, 150, 200, 255)); + const Color32 pixel = image.getPixel(0, 0); + CHECK((pixel.r == 100 && pixel.g == 150 && pixel.b == 200)); +} + +TEST_CASE("image.manip: BGRA8 format") +{ + Image image(4, 4, PixelFormat::BGRA8); + image.setPixel(0, 0, Color32(100, 150, 200, 128)); + const Color32 pixel = image.getPixel(0, 0); + CHECK((pixel.r == 100 && pixel.g == 150 && pixel.b == 200 && pixel.a == 128)); +} + +TEST_CASE("image.manip: R8 format averages to grayscale") +{ + Image image(4, 4, PixelFormat::R8); + image.setPixel(0, 0, Color32(90, 120, 150, 255)); // average = 120 + const Color32 pixel = image.getPixel(0, 0); + CHECK(pixel.r == pixel.g); + CHECK(pixel.g == pixel.b); + CHECK(pixel.a == 255); +} diff --git a/Engine/cpp/Runtime/Rendering/Image/ImageModule.cppm b/Engine/cpp/Runtime/Rendering/Image/ImageModule.cppm new file mode 100644 index 00000000..1b2c99a2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/ImageModule.cppm @@ -0,0 +1,9 @@ +/// Primary module for image. Re-exports all partitions. + +export module image; + +export import :pixel_format; +export import :image_data; +export import :image; +export import :nine_slice; +export import :atlas_builder; diff --git a/Engine/cpp/Runtime/Rendering/Image/NineSlice.cppm b/Engine/cpp/Runtime/Rendering/Image/NineSlice.cppm new file mode 100644 index 00000000..0cf1eacc --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/NineSlice.cppm @@ -0,0 +1,42 @@ +// Draconic::Image - :nine_slice partition. +// +// NineSlice: border insets for 9-slice image scaling - corners stay fixed, +// edges stretch along one axis, the center stretches both ways. Ported from +// Sedulous.Images/NineSlice.bf. + +module; + +export module image:nine_slice; + +import core; + +using namespace draco; + +export namespace draco::image +{ + /// Border insets (in pixels) for 9-slice image scaling. + struct NineSlice + { + f32 left = 0.0f; ///< Left border width. + f32 top = 0.0f; ///< Top border height. + f32 right = 0.0f; ///< Right border width. + f32 bottom = 0.0f; ///< Bottom border height. + + constexpr NineSlice() noexcept = default; + + constexpr NineSlice(f32 inLeft, f32 inTop, f32 inRight, f32 inBottom) noexcept + : left(inLeft), top(inTop), right(inRight), bottom(inBottom) {} + + /// Uniform borders on all sides. + explicit constexpr NineSlice(f32 all) noexcept + : left(all), top(all), right(all), bottom(all) {} + + /// Horizontal and vertical borders. + constexpr NineSlice(f32 horizontal, f32 vertical) noexcept + : left(horizontal), top(vertical), right(horizontal), bottom(vertical) {} + + [[nodiscard]] constexpr f32 horizontalBorder() const noexcept { return left + right; } + [[nodiscard]] constexpr f32 verticalBorder() const noexcept { return top + bottom; } + [[nodiscard]] constexpr bool isValid() const noexcept { return left > 0.0f || top > 0.0f || right > 0.0f || bottom > 0.0f; } + }; +} diff --git a/Engine/cpp/Runtime/Rendering/Image/NormalMapTests.test.cpp b/Engine/cpp/Runtime/Rendering/Image/NormalMapTests.test.cpp new file mode 100644 index 00000000..d7970cf7 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/NormalMapTests.test.cpp @@ -0,0 +1,200 @@ +// generators + CalculateNormalFromHeight. Mirrors the Sedulous assertions +// (Test.Assert -> CHECK; pixel.R/G/B/A -> .r/.g/.b/.a; math::Vector3.X/Y/Z -> .x/.y/.z). +#include +import core; +import image; + +using namespace draco; +using namespace draco::image; + +namespace +{ + // Neutral up-normal (0,0,1) encoded as (128,128,255). + constexpr u8 NeutralX = 128; + constexpr u8 NeutralY = 128; + constexpr u8 NeutralZ = 255; +} + +TEST_CASE("image.normalmap: flat is uniform neutral") +{ + const Image image = Image::createFlatNormalMap(32, 32); + CHECK(image.width() == 32u); + CHECK(image.height() == 32u); + CHECK(image.format() == PixelFormat::RGBA8); + + for (u32 y = 0; y < 32; ++y) + for (u32 x = 0; x < 32; ++x) + { + const Color32 pixel = image.getPixel(x, y); + CHECK(pixel.r == NeutralX); + CHECK(pixel.g == NeutralY); + CHECK(pixel.b == NeutralZ); + CHECK(pixel.a == 255); + } +} + +TEST_CASE("image.normalmap: flat honors format") +{ + const Image image = Image::createFlatNormalMap(16, 16, PixelFormat::RGB8); + CHECK(image.format() == PixelFormat::RGB8); + const Color32 pixel = image.getPixel(8, 8); + CHECK((pixel.r == NeutralX && pixel.g == NeutralY && pixel.b == NeutralZ)); +} + +TEST_CASE("image.normalmap: wave varies in X and Y, Z stays up") +{ + const Image image = Image::createWaveNormalMap(64, 64, 4.0f, 4.0f, 0.3f); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + + bool hasXVariation = false; + bool hasYVariation = false; + for (u32 y = 0; y < 64; ++y) + for (u32 x = 0; x < 64; ++x) + { + const Color32 pixel = image.getPixel(x, y); + if (abs(static_cast(pixel.r) - static_cast(NeutralX)) > 5) hasXVariation = true; + if (abs(static_cast(pixel.g) - static_cast(NeutralY)) > 5) hasYVariation = true; + CHECK(pixel.b >= 128); + } + CHECK(hasXVariation); + CHECK(hasYVariation); +} + +TEST_CASE("image.normalmap: brick has variation, Z stays up") +{ + const Image image = Image::createBrickNormalMap(64, 64, 4, 2, 0.3f); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + + bool hasVariation = false; + for (u32 y = 0; y < 64; ++y) + for (u32 x = 0; x < 64; ++x) + { + const Color32 pixel = image.getPixel(x, y); + if (pixel.r != NeutralX || pixel.g != NeutralY || pixel.b != NeutralZ) hasVariation = true; + CHECK(pixel.b >= 128); + } + CHECK(hasVariation); +} + +TEST_CASE("image.normalmap: circular bump has variation, Z stays up") +{ + const Image image = Image::createCircularBumpNormalMap(64, 64, 0.5f, 2.0f); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + + bool hasVariation = false; + for (u32 y = 0; y < 64 && !hasVariation; ++y) + for (u32 x = 0; x < 64 && !hasVariation; ++x) + { + const Color32 pixel = image.getPixel(x, y); + if (pixel.r != NeutralX || pixel.g != NeutralY) hasVariation = true; + } + CHECK(hasVariation); + + for (u32 y = 0; y < 64; ++y) + for (u32 x = 0; x < 64; ++x) + CHECK(image.getPixel(x, y).b >= 128); +} + +TEST_CASE("image.normalmap: noise varies across most pixels, Z stays up") +{ + const Image image = Image::createNoiseNormalMap(64, 64, 0.1f, 0.2f, 12345); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + + bool hasVariation = false; + i32 variationCount = 0; + for (u32 y = 0; y < 64; ++y) + for (u32 x = 0; x < 64; ++x) + { + const Color32 pixel = image.getPixel(x, y); + if (pixel.r != NeutralX || pixel.g != NeutralY) { hasVariation = true; ++variationCount; } + CHECK(pixel.b >= 128); + } + CHECK(hasVariation); + CHECK(variationCount > 64 * 64 / 2); +} + +TEST_CASE("image.normalmap: noise is deterministic for a seed") +{ + const Image image1 = Image::createNoiseNormalMap(32, 32, 0.1f, 0.2f, 42); + const Image image2 = Image::createNoiseNormalMap(32, 32, 0.1f, 0.2f, 42); + bool identical = true; + for (u32 y = 0; y < 32; ++y) + for (u32 x = 0; x < 32; ++x) + { + const Color32 p1 = image1.getPixel(x, y); + const Color32 p2 = image2.getPixel(x, y); + if (!(p1.r == p2.r && p1.g == p2.g && p1.b == p2.b)) identical = false; + } + CHECK(identical); +} + +TEST_CASE("image.normalmap: different seeds differ") +{ + const Image image1 = Image::createNoiseNormalMap(32, 32, 0.1f, 0.2f, 100); + const Image image2 = Image::createNoiseNormalMap(32, 32, 0.1f, 0.2f, 200); + bool hasDifference = false; + for (u32 y = 0; y < 32 && !hasDifference; ++y) + for (u32 x = 0; x < 32 && !hasDifference; ++x) + { + const Color32 p1 = image1.getPixel(x, y); + const Color32 p2 = image2.getPixel(x, y); + if (p1.r != p2.r || p1.g != p2.g || p1.b != p2.b) hasDifference = true; + } + CHECK(hasDifference); +} + +TEST_CASE("image.normalmap: test pattern varies in X and Y, Z stays up") +{ + const Image image = Image::createTestPatternNormalMap(64, 64); + CHECK(image.width() == 64u); + CHECK(image.height() == 64u); + + bool hasXVariation = false; + bool hasYVariation = false; + for (u32 y = 0; y < 64; ++y) + for (u32 x = 0; x < 64; ++x) + { + const Color32 pixel = image.getPixel(x, y); + if (pixel.r != NeutralX) hasXVariation = true; + if (pixel.g != NeutralY) hasYVariation = true; + CHECK(pixel.b >= 128); + } + CHECK(hasXVariation); + CHECK(hasYVariation); +} + +TEST_CASE("image.normalmap: CalculateNormalFromHeight") +{ + const math::Vector3 flatNormal = Image::calculateNormalFromHeight(0.5f, 0.5f, 0.5f, 0.5f); + CHECK(abs(flatNormal.x) < 0.01f); + CHECK(abs(flatNormal.y) < 0.01f); + CHECK(flatNormal.z > 0.99f); + + const math::Vector3 rightSlope = Image::calculateNormalFromHeight(0.0f, 1.0f, 0.5f, 0.5f); + CHECK(rightSlope.x < 0.0f); // normal points against the slope + CHECK(rightSlope.z > 0.0f); + + const math::Vector3 downSlope = Image::calculateNormalFromHeight(0.5f, 0.5f, 0.0f, 1.0f); + CHECK(downSlope.y < 0.0f); + CHECK(downSlope.z > 0.0f); +} + +TEST_CASE("image.normalmap: all generators keep Z positive (B >= 128)") +{ + std::vector images; + images.push_back(Image::createFlatNormalMap(16, 16)); + images.push_back(Image::createWaveNormalMap(16, 16)); + images.push_back(Image::createBrickNormalMap(16, 16)); + images.push_back(Image::createCircularBumpNormalMap(16, 16)); + images.push_back(Image::createNoiseNormalMap(16, 16)); + images.push_back(Image::createTestPatternNormalMap(16, 16)); + + for (usize i = 0; i < images.size(); ++i) + for (u32 y = 0; y < 16; ++y) + for (u32 x = 0; x < 16; ++x) + CHECK(images[i].getPixel(x, y).b >= 128); +} diff --git a/Engine/cpp/Runtime/Rendering/Image/PixelFormat.cppm b/Engine/cpp/Runtime/Rendering/Image/PixelFormat.cppm new file mode 100644 index 00000000..f18c0412 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Image/PixelFormat.cppm @@ -0,0 +1,55 @@ +/// Pixel format enum for CPU-side image data. + +export module image:pixel_format; + +import core; + +using namespace draco; + +export namespace draco::image { + +enum class PixelFormat : u32 { + R8, RG8, RGB8, RGBA8, + R16F, RG16F, RGB16F, RGBA16F, + R32F, RG32F, RGB32F, RGBA32F, + BGR8, BGRA8, +}; + +[[nodiscard]] constexpr u32 bytesPerPixel(PixelFormat f) { + switch (f) { + case PixelFormat::R8: return 1; + case PixelFormat::RG8: return 2; + case PixelFormat::RGB8: return 3; + case PixelFormat::BGR8: return 3; + case PixelFormat::RGBA8: return 4; + case PixelFormat::BGRA8: return 4; + case PixelFormat::R16F: return 2; + case PixelFormat::RG16F: return 4; + case PixelFormat::RGB16F: return 6; + case PixelFormat::RGBA16F: return 8; + case PixelFormat::R32F: return 4; + case PixelFormat::RG32F: return 8; + case PixelFormat::RGB32F: return 12; + case PixelFormat::RGBA32F: return 16; + } + return 4; +} + +[[nodiscard]] constexpr u32 channelCount(PixelFormat f) { + switch (f) { + case PixelFormat::R8: case PixelFormat::R16F: case PixelFormat::R32F: return 1; + case PixelFormat::RG8: case PixelFormat::RG16F: case PixelFormat::RG32F: return 2; + case PixelFormat::RGB8: case PixelFormat::BGR8: + case PixelFormat::RGB16F: case PixelFormat::RGB32F: return 3; + case PixelFormat::RGBA8: case PixelFormat::BGRA8: + case PixelFormat::RGBA16F: case PixelFormat::RGBA32F: return 4; + } + return 0; +} + +[[nodiscard]] constexpr bool hasAlpha(PixelFormat f) { + return f == PixelFormat::RGBA8 || f == PixelFormat::BGRA8 || + f == PixelFormat::RGBA16F || f == PixelFormat::RGBA32F; +} + +} // namespace draco::image diff --git a/Engine/cpp/Runtime/Rendering/Material/Material.cppm b/Engine/cpp/Runtime/Rendering/Material/Material.cppm deleted file mode 100644 index 20605bd9..00000000 --- a/Engine/cpp/Runtime/Rendering/Material/Material.cppm +++ /dev/null @@ -1,32 +0,0 @@ -module; - -#include - -export module rendering.material; - -import core.stdtypes; -import rendering.rhi; - -export namespace draco::rendering::material -{ - struct Uniform - { - u32 nameHash = 0; - const void* data = nullptr; - u16 count = 1; - }; - - struct Material - { - u32 shaderId = 0; - - rhi::PipelineHandle pipeline = rhi::InvalidPipeline; - - rhi::TextureHandle texture = rhi::InvalidTexture; - rhi::UniformHandle sampler = rhi::InvalidUniform; - - u8 texture_unit = 0; - - std::vector uniforms; - }; -} diff --git a/Engine/cpp/Runtime/Rendering/Materials/Material.cppm b/Engine/cpp/Runtime/Rendering/Materials/Material.cppm new file mode 100644 index 00000000..57a72c2d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/Material.cppm @@ -0,0 +1,142 @@ +/// Material: the shared, immutable material template (`:material` partition). +/// +/// Material: the shared, immutable template - a shader name + variant flags, a list +/// of declared properties, a PipelineConfig, and default values. It is *data*: the +/// MaterialSystem reads the property list to infer the GPU bind-group layout, so a +/// custom material/shader needs no renderer changes. Per-use overrides live in a +/// MaterialInstance. + +module; +#include +#include +#include +#include +#include +#include +#include + +export module materials:material; + +import core; +import rhi; +import shaders; +import :types; +import :pipeline; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +// Shared material template. Properties are declared once; defaults seed every +// instance. Name strings are owned in a stable backing so property views stay valid. +class Material final { +public: + std::u8string name; + std::u8string shaderName; + shaders::ShaderFlags shaderFlags = shaders::ShaderFlags::None; + PipelineConfig pipeline; + + [[nodiscard]] bool isValid() const noexcept { return shaderName.size() > 0; } + [[nodiscard]] usize propertyCount() const noexcept { return m_properties.size(); } + [[nodiscard]] u32 uniformDataSize() const noexcept { return m_uniformDataSize; } + + [[nodiscard]] const MaterialPropertyDef& getProperty(usize index) const noexcept { return m_properties[index]; } + + // Index of the named property, or -1. + [[nodiscard]] isize getPropertyIndex(std::u8string_view n) const noexcept { + for (usize i = 0; i < m_properties.size(); ++i) { + if (m_properties[i].name == n) { return static_cast(i); } + } + return -1; + } + + [[nodiscard]] const MaterialPropertyDef* findProperty(std::u8string_view n) const noexcept { + const isize i = getPropertyIndex(n); + return (i >= 0) ? &m_properties[static_cast(i)] : nullptr; + } + + [[nodiscard]] std::span properties() const noexcept { + return { m_properties.data(), m_properties.size() }; + } + + // Declares a property. The name is cloned into stable backing so the caller's + // string need not outlive the material; uniform size grows to fit uniforms. + void addProperty(const MaterialPropertyDef& prop) { + std::unique_ptr owned = std::make_unique(prop.name); + MaterialPropertyDef d = prop; + d.name = *owned; + m_propertyNames.push_back(std::move(owned)); + m_properties.push_back(d); + + if (d.isUniform()) { + const u32 end = d.offset + d.size; + if (end > m_uniformDataSize) { m_uniformDataSize = end; } + } + } + + // (Re)allocates the default uniform buffer, preserving existing defaults. + void allocateDefaultUniformData() { + if (m_uniformDataSize == 0) { return; } + if (m_defaultUniformData.size() < m_uniformDataSize) { + std::vector grown; + grown.resize(m_uniformDataSize); + for (usize i = 0; i < m_defaultUniformData.size(); ++i) { grown[i] = m_defaultUniformData[i]; } + m_defaultUniformData = std::move(grown); + } + } + + void setDefaultFloat(std::u8string_view n, f32 v) { writeUniform(n, &v, sizeof(v)); } + void setDefaultFloat2(std::u8string_view n, math::Vector2 v) { writeUniform(n, &v, sizeof(v)); } + void setDefaultFloat3(std::u8string_view n, math::Vector3 v) { writeUniform(n, &v, sizeof(v)); } + void setDefaultFloat4(std::u8string_view n, math::Vector4 v) { writeUniform(n, &v, sizeof(v)); } + void setDefaultColor(std::u8string_view n, math::Vector4 c) { setDefaultFloat4(n, c); } + + void setDefaultTexture(std::u8string_view n, rhi::TextureView* tex) { + const isize i = getPropertyIndex(n); + if (i >= 0 && m_properties[static_cast(i)].isTexture()) { m_defaultTextures.insert_or_assign(static_cast(i), tex); } + } + void setDefaultSampler(std::u8string_view n, rhi::Sampler* s) { + const isize i = getPropertyIndex(n); + if (i >= 0 && m_properties[static_cast(i)].isSampler()) { m_defaultSamplers.insert_or_assign(static_cast(i), s); } + } + + [[nodiscard]] std::span defaultUniformData() const noexcept { + return { m_defaultUniformData.data(), m_defaultUniformData.size() }; + } + + // Overlays a raw default-uniform blob (the cooked/serialized defaults) over the + // uniform buffer. Used by the resource factory to restore authored defaults. + void setRawDefaultUniformData(std::span data) { + allocateDefaultUniformData(); + const usize n = data.size() < m_defaultUniformData.size() ? data.size() : m_defaultUniformData.size(); + if (n > 0) { std::memcpy(m_defaultUniformData.data(), data.data(), n); } + } + [[nodiscard]] rhi::TextureView* getDefaultTexture(usize propIndex) const noexcept { + rhi::TextureView* const* p = mapFind(m_defaultTextures, propIndex); + return (p != nullptr) ? *p : nullptr; + } + [[nodiscard]] rhi::Sampler* getDefaultSampler(usize propIndex) const noexcept { + rhi::Sampler* const* p = mapFind(m_defaultSamplers, propIndex); + return (p != nullptr) ? *p : nullptr; + } + +private: + void writeUniform(std::u8string_view n, const void* src, usize bytes) { + const isize i = getPropertyIndex(n); + if (i < 0) { return; } + const MaterialPropertyDef& d = m_properties[static_cast(i)]; + if (!d.isUniform() || d.offset + bytes > m_defaultUniformData.size()) { return; } + std::memcpy(m_defaultUniformData.data() + d.offset, src, bytes); + } + + std::vector m_properties; + std::vector> m_propertyNames; // stable backing for property name views + std::vector m_defaultUniformData; + u32 m_uniformDataSize = 0; + std::unordered_map m_defaultTextures; + std::unordered_map m_defaultSamplers; +}; + + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/MaterialBuilder.cppm b/Engine/cpp/Runtime/Rendering/Materials/MaterialBuilder.cppm new file mode 100644 index 00000000..3b7533d6 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/MaterialBuilder.cppm @@ -0,0 +1,145 @@ +/// Fluent builder for authoring a Material in code (`:builder` partition). +/// +/// Fluent builder for authoring a Material in code: declare the shader, pipeline +/// state, and typed properties; the builder lays out the uniform buffer (std140-ish +/// 16-byte alignment for float3/float4) and assigns binding ordinals. build() hands +/// back the finished Material (a shared_ptr). + +module; +#include +#include +#include + +export module materials:builder; + +import core; +import rhi; +import shaders; +import :types; +import :pipeline; +import :material; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +class MaterialBuilder { +public: + explicit MaterialBuilder(std::u8string_view name) { + m_material = std::make_shared(); + m_material->name = std::u8string(name); + m_material->pipeline = PipelineConfig{}; + } + + MaterialBuilder& shader(std::u8string_view shaderName) { + m_material->shaderName = std::u8string(shaderName); + m_material->pipeline.shaderName = m_material->shaderName; + return *this; + } + MaterialBuilder& flags(shaders::ShaderFlags flags) { + m_material->shaderFlags = flags; + m_material->pipeline.shaderFlags = flags; + return *this; + } + MaterialBuilder& vertexLayout(VertexLayoutType layout) { m_material->pipeline.vertexLayout = layout; return *this; } + MaterialBuilder& blend(BlendMode mode) { m_material->pipeline.blendMode = mode; return *this; } + MaterialBuilder& depth(DepthMode mode) { m_material->pipeline.depthMode = mode; return *this; } + MaterialBuilder& cull(CullModeConfig mode) { m_material->pipeline.cullMode = mode; return *this; } + MaterialBuilder& doubleSided() { m_material->pipeline.cullMode = CullModeConfig::None; return *this; } + MaterialBuilder& transparent() { + m_material->pipeline.blendMode = BlendMode::AlphaBlend; + m_material->pipeline.depthMode = DepthMode::ReadOnly; + return *this; + } + MaterialBuilder& additive() { + m_material->pipeline.blendMode = BlendMode::Additive; + m_material->pipeline.depthMode = DepthMode::ReadOnly; + return *this; + } + + // --- uniform properties (laid out into the material uniform buffer) --- + MaterialBuilder& Float(std::u8string_view name, f32 v = 0.0f) { + addUniform(name, MaterialPropertyType::Float, 4, /*align16*/ false); + m_material->allocateDefaultUniformData(); + m_material->setDefaultFloat(name, v); + return *this; + } + MaterialBuilder& Float2(std::u8string_view name, math::Vector2 v = {}) { + addUniform(name, MaterialPropertyType::Float2, 8, false); + m_material->allocateDefaultUniformData(); + m_material->setDefaultFloat2(name, v); + return *this; + } + MaterialBuilder& Float3(std::u8string_view name, math::Vector3 v = {}) { + addUniform(name, MaterialPropertyType::Float3, 12, /*align16*/ true); // float3 occupies 16 (std140) + m_material->allocateDefaultUniformData(); + m_material->setDefaultFloat3(name, v); + return *this; + } + MaterialBuilder& Float4(std::u8string_view name, math::Vector4 v = {}) { + addUniform(name, MaterialPropertyType::Float4, 16, true); + m_material->allocateDefaultUniformData(); + m_material->setDefaultFloat4(name, v); + return *this; + } + MaterialBuilder& Color(std::u8string_view name, math::Vector4 v = math::Vector4{ 1, 1, 1, 1 }) { return Float4(name, v); } + + // --- resource properties (become bind-group entries) --- + MaterialBuilder& texture(std::u8string_view name, rhi::TextureView* def = nullptr) { + m_material->addProperty(MaterialPropertyDef{ name, MaterialPropertyType::Texture2D, m_binding++, 0, 0 }); + if (def != nullptr) { m_material->setDefaultTexture(name, def); } + return *this; + } + MaterialBuilder& textureCube(std::u8string_view name, rhi::TextureView* def = nullptr) { + m_material->addProperty(MaterialPropertyDef{ name, MaterialPropertyType::TextureCube, m_binding++, 0, 0 }); + if (def != nullptr) { m_material->setDefaultTexture(name, def); } + return *this; + } + MaterialBuilder& sampler(std::u8string_view name, rhi::Sampler* def = nullptr) { + m_material->addProperty(MaterialPropertyDef{ name, MaterialPropertyType::Sampler, m_binding++, 0, 0 }); + if (def != nullptr) { m_material->setDefaultSampler(name, def); } + return *this; + } + + // Finishes and yields the material. The builder is empty afterwards. + [[nodiscard]] std::shared_ptr build() { + m_material->allocateDefaultUniformData(); + return std::move(m_material); + } + +private: + void addUniform(std::u8string_view name, MaterialPropertyType type, u32 size, bool align16) { + if (align16) { m_uniformOffset = (m_uniformOffset + 15u) & ~15u; } + m_material->addProperty(MaterialPropertyDef{ name, type, m_binding, m_uniformOffset, size }); + m_uniformOffset += align16 ? 16u : size; // float3/float4 consume a 16-byte slot + ++m_binding; + } + + std::shared_ptr m_material; + u32 m_uniformOffset = 0; + u32 m_binding = 0; +}; + +// Standard PBR material: the BaseColor/Metallic/Roughness +// uniforms + the five PBR maps + a sampler, in the order the forward set-2 contract expects. The +// importer builds these and assigns the maps; unset maps fall back to neutral defaults in the renderer. +[[nodiscard]] inline std::shared_ptr createPBR(std::u8string_view name, math::Vector4 baseColor = math::Vector4{ 1, 1, 1, 1 }, + f32 metallic = 0.0f, f32 roughness = 0.5f, + std::u8string_view shaderName = u8"forward") { + return MaterialBuilder(name) + .shader(shaderName) + .vertexLayout(VertexLayoutType::Mesh) + .Color(u8"BaseColor", baseColor) + .Float(u8"Metallic", metallic) + .Float(u8"Roughness", roughness) + .texture(u8"AlbedoMap") + .texture(u8"NormalMap") + .texture(u8"MetallicRoughnessMap") + .texture(u8"OcclusionMap") + .texture(u8"EmissiveMap") + .sampler(u8"MainSampler") + .build(); +} + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/MaterialInstance.cppm b/Engine/cpp/Runtime/Rendering/Materials/MaterialInstance.cppm new file mode 100644 index 00000000..e0c01196 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/MaterialInstance.cppm @@ -0,0 +1,177 @@ +/// MaterialInstance: per-use overrides with dirty tracking (`:instance` partition). +/// +/// MaterialInstance: a per-use copy of a Material's properties with overridable +/// values and dirty tracking. Setters write into an override uniform buffer / texture +/// maps and flip dirty flags; on a false->true transition the instance NOTIFIES its +/// sink (the MaterialSystem) so re-prep is O(dirty), not O(all instances). The sink +/// is an interface declared here so :system can depend on :instance (not vice-versa), +/// breaking the partition cycle. + +module; +#include +#include +#include +#include +#include + +export module materials:instance; + +import core; +import rhi; +import :types; +import :material; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +class MaterialInstance; + +// What a MaterialInstance calls back into (implemented by MaterialSystem). Keeps the +// instance->system edge an interface so the partitions don't import each other. +class IMaterialInstanceSink { +public: + virtual ~IMaterialInstanceSink() = default; + virtual void markInstanceDirty(MaterialInstance* instance) = 0; + virtual void releaseInstance(MaterialInstance* instance) = 0; +}; + +// Up-to-128-property override bitset. +struct PropertyOverrideMask { + u64 lo = 0, hi = 0; + void set(usize i) noexcept { if (i < 64) lo |= (1ull << i); else if (i < 128) hi |= (1ull << (i - 64)); } + void clear(usize i) noexcept { if (i < 64) lo &= ~(1ull << i); else if (i < 128) hi &= ~(1ull << (i - 64)); } + [[nodiscard]] bool isSet(usize i) const noexcept { + if (i < 64) return (lo & (1ull << i)) != 0; + if (i < 128) return (hi & (1ull << (i - 64))) != 0; + return false; + } + void reset() noexcept { lo = 0; hi = 0; } + [[nodiscard]] bool hasAny() const noexcept { return lo != 0 || hi != 0; } +}; + +// Per-use material with overridable properties + dirty tracking. +class MaterialInstance { +public: + explicit MaterialInstance(Material* material) : m_material(material) { + if (material != nullptr && material->uniformDataSize() > 0) { + m_uniformData.resize(material->uniformDataSize()); + const std::span defaults = material->defaultUniformData(); + for (usize i = 0; i < defaults.size() && i < m_uniformData.size(); ++i) { m_uniformData[i] = defaults[i]; } + } + } + + ~MaterialInstance() { if (m_sink != nullptr) { m_sink->releaseInstance(this); } } + + MaterialInstance(const MaterialInstance&) = delete; + MaterialInstance& operator=(const MaterialInstance&) = delete; + + [[nodiscard]] Material* getMaterial() const noexcept { return m_material; } + + // --- property setters (write override + mark dirty) --- + void setFloat(std::u8string_view n, f32 v) { writeUniform(n, &v, sizeof(v)); } + void setFloat2(std::u8string_view n, math::Vector2 v){ writeUniform(n, &v, sizeof(v)); } + void setFloat3(std::u8string_view n, math::Vector3 v){ writeUniform(n, &v, sizeof(v)); } + void setFloat4(std::u8string_view n, math::Vector4 v){ writeUniform(n, &v, sizeof(v)); } + void setColor(std::u8string_view n, math::Vector4 c) { setFloat4(n, c); } + + void setTexture(std::u8string_view n, rhi::TextureView* tex) { + const isize i = m_material->getPropertyIndex(n); + if (i < 0 || !m_material->getProperty(static_cast(i)).isTexture()) { return; } + m_textures.insert_or_assign(static_cast(i), tex); + m_overrides.set(static_cast(i)); + setBindGroupDirty(); + } + void setSampler(std::u8string_view n, rhi::Sampler* s) { + const isize i = m_material->getPropertyIndex(n); + if (i < 0 || !m_material->getProperty(static_cast(i)).isSampler()) { return; } + m_samplers.insert_or_assign(static_cast(i), s); + m_overrides.set(static_cast(i)); + setBindGroupDirty(); + } + + // --- effective values (override else material default) --- + [[nodiscard]] rhi::TextureView* getTexture(usize propIndex) const { + if (m_overrides.isSet(propIndex)) { + rhi::TextureView* const* p = mapFind(m_textures, propIndex); + if (p != nullptr) { return *p; } + } + return m_material->getDefaultTexture(propIndex); + } + [[nodiscard]] rhi::Sampler* getSampler(usize propIndex) const { + if (m_overrides.isSet(propIndex)) { + rhi::Sampler* const* p = mapFind(m_samplers, propIndex); + if (p != nullptr) { return *p; } + } + return m_material->getDefaultSampler(propIndex); + } + [[nodiscard]] std::span uniformData() const noexcept { + return m_uniformData.size() > 0 ? std::span{ m_uniformData.data(), m_uniformData.size() } + : m_material->defaultUniformData(); + } + + void resetProperty(std::u8string_view n) { + const isize idx = m_material->getPropertyIndex(n); + if (idx < 0) { return; } + const usize i = static_cast(idx); + const MaterialPropertyDef& d = m_material->getProperty(i); + if (d.isUniform() && m_uniformData.size() >= d.offset + d.size) { + const std::span defaults = m_material->defaultUniformData(); + if (defaults.size() >= d.offset + d.size) { std::memcpy(m_uniformData.data() + d.offset, defaults.data() + d.offset, d.size); } + setUniformDirty(); + } else if (d.isTexture()) { m_textures.erase(i); setBindGroupDirty(); } + else if (d.isSampler()) { m_samplers.erase(i); setBindGroupDirty(); } + m_overrides.clear(i); + } + + // --- dirty-state (driven by MaterialSystem) --- + [[nodiscard]] bool isUniformDirty() const noexcept { return m_uniformDirty; } + [[nodiscard]] bool isBindGroupDirty() const noexcept { return m_bindGroupDirty; } + void clearUniformDirty() noexcept { m_uniformDirty = false; } + void clearBindGroupDirty() noexcept { m_bindGroupDirty = false; } + void markUniformDirty() { setUniformDirty(); } + void markBindGroupDirty() { setBindGroupDirty(); } + + // --- wiring used only by MaterialSystem --- + void setSink(IMaterialInstanceSink* sink) noexcept { m_sink = sink; } + [[nodiscard]] bool isInDirtyList() const noexcept { return m_inDirtyList; } + void setInDirtyList(bool v) noexcept { m_inDirtyList = v; } + [[nodiscard]] rhi::BindGroupLayout* bindGroupLayout() const noexcept { return m_bindGroupLayout; } + void setBindGroupLayout(rhi::BindGroupLayout* l) noexcept { m_bindGroupLayout = l; } + +private: + void writeUniform(std::u8string_view n, const void* src, usize bytes) { + const isize idx = m_material->getPropertyIndex(n); + if (idx < 0) { return; } + const usize i = static_cast(idx); + const MaterialPropertyDef& d = m_material->getProperty(i); + if (!d.isUniform() || m_uniformData.size() < d.offset + bytes) { return; } + std::memcpy(m_uniformData.data() + d.offset, src, bytes); + m_overrides.set(i); + setUniformDirty(); + } + void setUniformDirty() { + if (m_uniformDirty) { return; } + m_uniformDirty = true; + if (!m_inDirtyList && m_sink != nullptr) { m_sink->markInstanceDirty(this); } + } + void setBindGroupDirty() { + if (m_bindGroupDirty) { return; } + m_bindGroupDirty = true; + if (!m_inDirtyList && m_sink != nullptr) { m_sink->markInstanceDirty(this); } + } + + Material* m_material = nullptr; // borrowed (shared template) + IMaterialInstanceSink* m_sink = nullptr; // borrowed (the MaterialSystem) + std::vector m_uniformData; // override uniform buffer + std::unordered_map m_textures; // override textures by property index + std::unordered_map m_samplers; // override samplers by property index + PropertyOverrideMask m_overrides; + rhi::BindGroupLayout* m_bindGroupLayout = nullptr; // set by MaterialSystem for PSO creation + bool m_uniformDirty = true; + bool m_bindGroupDirty = true; + bool m_inDirtyList = false; +}; + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/MaterialSystem.cppm b/Engine/cpp/Runtime/Rendering/Materials/MaterialSystem.cppm new file mode 100644 index 00000000..e4242038 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/MaterialSystem.cppm @@ -0,0 +1,356 @@ +/// MaterialSystem: owns instance GPU resources, infers bind-group layouts (`:system` partition). +/// +/// MaterialSystem: owns material instances' GPU resources and - the key idea - +/// INFERS the bind-group layout from a material's declared property list (uniforms +/// -> one uniform buffer at binding 0; each texture/sampler -> its own entry). A new +/// material or custom shader therefore needs no renderer changes. Layouts are cached +/// by content hash; instance uniform buffers + bind groups are (re)built lazily, and +/// instances notify the system on dirty so PrepareDirtyInstances is O(dirty). + +module; +#include +#include +#include +#include +#include + +export module materials:system; + +import core; +import rhi; +import :types; +import :material; +import :instance; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +class MaterialSystem final : public IMaterialInstanceSink { +public: + MaterialSystem() = default; + ~MaterialSystem() override { shutdown(); } + + MaterialSystem(const MaterialSystem&) = delete; + MaterialSystem& operator=(const MaterialSystem&) = delete; + + // Binds the system to a device + creates default GPU resources (sampler + white + // and flat-normal 1x1 fallback textures used when a texture slot is unbound). + Status initialize(rhi::Device& device) { + m_device = &device; + m_queue = device.getQueue(rhi::QueueType::Graphics); + if (m_queue == nullptr) { return Status{ ErrorCode::Unknown }; } + return createDefaultResources(); + } + + [[nodiscard]] rhi::Device* device() const noexcept { return m_device; } + [[nodiscard]] rhi::Sampler* defaultSampler() const noexcept { return m_defaultSampler; } + [[nodiscard]] rhi::TextureView* whiteTexture() const noexcept { return m_whiteView; } + [[nodiscard]] rhi::TextureView* normalTexture() const noexcept { return m_normalView; } + + // Ensures the instance's uniform buffer is up to date and returns it (null if the material + // declares no uniforms). Lets a renderer assemble its own fixed set-2 layout (UBO + textures) + // while still sourcing the packed uniform data from the material system. + [[nodiscard]] rhi::Buffer* ensureUniformBuffer(MaterialInstance& instance) { + Material* material = instance.getMaterial(); + if (material == nullptr || material->uniformDataSize() == 0) { return nullptr; } + instance.setSink(this); + if (instance.isUniformDirty()) { + if (!updateUniformBuffer(instance)) { return nullptr; } + instance.clearUniformDirty(); + } + rhi::Buffer** buf = mapFind(m_uniformBuffers, &instance); + return (buf != nullptr) ? *buf : nullptr; + } + + // Bind-group layout inferred from the material's property definitions, cached. + [[nodiscard]] rhi::BindGroupLayout* getOrCreateLayout(Material& material) { + const u64 hash = computeLayoutHash(material); + if (rhi::BindGroupLayout** cached = mapFind(m_layoutCache, hash)) { return *cached; } + + std::vector entries; + + bool hasUniforms = false; + for (const MaterialPropertyDef& p : material.properties()) { + if (p.isUniform()) { hasUniforms = true; break; } + } + if (hasUniforms && material.uniformDataSize() > 0) { + entries.push_back(rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Fragment)); + } + + u32 texBinding = 0, sampBinding = 0; + for (const MaterialPropertyDef& p : material.properties()) { + switch (p.type) { + case MaterialPropertyType::Texture2D: + entries.push_back(rhi::BindGroupLayoutEntry::sampledTexture(texBinding++, rhi::ShaderStage::Fragment)); + break; + case MaterialPropertyType::TextureCube: + entries.push_back(rhi::BindGroupLayoutEntry::sampledTexture(texBinding++, rhi::ShaderStage::Fragment, + rhi::TextureViewDimension::TextureCube)); + break; + case MaterialPropertyType::Sampler: + entries.push_back(rhi::BindGroupLayoutEntry::sampler(sampBinding++, rhi::ShaderStage::Fragment)); + break; + default: break; // scalars live in the uniform buffer + } + } + if (entries.empty()) { return nullptr; } + + rhi::BindGroupLayoutDesc desc{}; + desc.entries = std::span{ entries.data(), entries.size() }; + rhi::BindGroupLayout* layout = nullptr; + if (!m_device->createBindGroupLayout(desc, layout).isOk()) { return nullptr; } + m_layoutCache.insert_or_assign(hash, layout); + return layout; + } + + // (Re)builds an instance's uniform buffer + bind group if dirty. Returns its + // bind group, ready to bind for rendering. + [[nodiscard]] rhi::BindGroup* prepareInstance(MaterialInstance& instance, rhi::BindGroupLayout* layout = nullptr) { + Material* material = instance.getMaterial(); + if (material == nullptr) { return nullptr; } + instance.setSink(this); + + rhi::BindGroupLayout* bgLayout = layout; + if (bgLayout == nullptr) { bgLayout = getOrCreateLayout(*material); } + if (bgLayout == nullptr) { return nullptr; } + + if (instance.isUniformDirty() && material->uniformDataSize() > 0) { + if (!updateUniformBuffer(instance)) { return nullptr; } + instance.clearUniformDirty(); + } + instance.setBindGroupLayout(bgLayout); + + if (instance.isBindGroupDirty()) { + if (!updateBindGroup(instance, bgLayout)) { return nullptr; } + instance.clearBindGroupDirty(); + } + rhi::BindGroup** bg = mapFind(m_bindGroups, &instance); + return (bg != nullptr) ? *bg : nullptr; + } + + [[nodiscard]] rhi::BindGroup* getBindGroup(MaterialInstance& instance) { + rhi::BindGroup** bg = mapFind(m_bindGroups, &instance); + return (bg != nullptr) ? *bg : nullptr; + } + + // --- IMaterialInstanceSink: dirty notification + cleanup --- + void markInstanceDirty(MaterialInstance* instance) override { + if (instance == nullptr || instance->isInDirtyList()) { return; } + instance->setInDirtyList(true); + m_dirty.push_back(instance); + } + + // Re-preps every instance dirtied since the last drain (O(dirty)). + void prepareDirtyInstances() { + for (usize i = 0; i < m_dirty.size(); ++i) { + MaterialInstance* inst = m_dirty[i]; + if (inst == nullptr) { continue; } + inst->setInDirtyList(false); + if (inst->isUniformDirty() || inst->isBindGroupDirty()) { (void)prepareInstance(*inst); } + } + m_dirty.clear(); + } + + void releaseInstance(MaterialInstance* instance) override { + if (instance == nullptr) { return; } + if (instance->isInDirtyList()) { + removeFromDirty(instance); + instance->setInDirtyList(false); + } + if (rhi::BindGroup** bg = mapFind(m_bindGroups, instance)) { + rhi::BindGroup* g = *bg; + m_device->destroyBindGroup(g); + m_bindGroups.erase(instance); + } + if (rhi::Buffer** buf = mapFind(m_uniformBuffers, instance)) { + rhi::Buffer* b = *buf; + m_device->destroyBuffer(b); + m_uniformBuffers.erase(instance); + } + } + + // Sampler cache keyed by address/filter combination. + [[nodiscard]] rhi::Sampler* getOrCreateSampler(rhi::AddressMode u, rhi::AddressMode v, + rhi::FilterMode minF = rhi::FilterMode::Linear, + rhi::FilterMode magF = rhi::FilterMode::Linear, + rhi::MipmapFilterMode mip = rhi::MipmapFilterMode::Linear) { + const u64 key = static_cast(u) | (static_cast(v) << 4) | (static_cast(minF) << 8) + | (static_cast(magF) << 12) | (static_cast(mip) << 16); + if (rhi::Sampler** cached = mapFind(m_samplerCache, key)) { return *cached; } + + rhi::SamplerDesc desc{}; + desc.addressU = u; desc.addressV = v; desc.addressW = rhi::AddressMode::Repeat; + desc.minFilter = minF; desc.magFilter = magF; desc.mipmapFilter = mip; + rhi::Sampler* sampler = nullptr; + if (!m_device->createSampler(desc, sampler).isOk()) { return m_defaultSampler; } + m_samplerCache.insert_or_assign(key, sampler); + return sampler; + } + +private: + Status createDefaultResources() { + rhi::SamplerDesc sd{}; + sd.addressU = rhi::AddressMode::ClampToEdge; sd.addressV = rhi::AddressMode::ClampToEdge; + sd.addressW = rhi::AddressMode::ClampToEdge; + if (!m_device->createSampler(sd, m_defaultSampler).isOk()) { return Status{ ErrorCode::Unknown }; } + + if (!createTexture1x1(Color32{ 255, 255, 255, 255 }, m_whiteTex, m_whiteView)) { return Status{ ErrorCode::Unknown }; } + if (!createTexture1x1(Color32{ 128, 128, 255, 255 }, m_normalTex, m_normalView)) { return Status{ ErrorCode::Unknown }; } + return Status{}; + } + + bool createTexture1x1(Color32 color, rhi::Texture*& outTex, rhi::TextureView*& outView) { + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = 1; td.height = 1; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + td.label = u8"1x1"; + if (!m_device->createTexture(td, outTex).isOk()) { return false; } + + const u8 pixel[4] = { color.r, color.g, color.b, color.a }; + rhi::TransferBatch* tb = nullptr; + if (m_queue->createTransferBatch(tb).isOk() && tb != nullptr) { + rhi::TextureDataLayout layout{}; layout.bytesPerRow = 4; layout.rowsPerImage = 1; + tb->writeTexture(outTex, std::span{ pixel, 4 }, layout, rhi::Extent3D{ 1, 1, 1 }); + (void)tb->submit(); + m_queue->destroyTransferBatch(tb); + } + + rhi::TextureViewDesc vd{}; + vd.format = rhi::TextureFormat::RGBA8Unorm; + vd.dimension = rhi::TextureViewDimension::Texture2D; + return m_device->createTextureView(outTex, vd, outView).isOk(); + } + + bool updateUniformBuffer(MaterialInstance& instance) { + Material* material = instance.getMaterial(); + if (material->uniformDataSize() == 0) { return true; } + + rhi::Buffer* buffer = nullptr; + if (rhi::Buffer** existing = mapFind(m_uniformBuffers, &instance)) { + buffer = *existing; + } else { + rhi::BufferDesc bd{}; + // Round up to 16: an HLSL cbuffer is std140-padded to a 16-byte multiple, so the + // bound range must cover that even when the declared props sum to less (e.g. a + // {float4,float,float} PBR block is 24B of data but a 32B cbuffer). + bd.size = (material->uniformDataSize() + 15u) & ~15u; + bd.usage = rhi::BufferUsage::Uniform; + bd.memory = rhi::MemoryLocation::CpuToGpu; + if (!m_device->createBuffer(bd, buffer).isOk()) { return false; } + m_uniformBuffers.insert_or_assign(&instance, buffer); + } + + const std::span data = instance.uniformData(); + if (data.size() > 0) { + if (void* ptr = buffer->map()) { + std::memcpy(ptr, data.data(), data.size()); + buffer->unmap(); + } + } + return true; + } + + bool updateBindGroup(MaterialInstance& instance, rhi::BindGroupLayout* layout) { + Material* material = instance.getMaterial(); + std::vector entries; + + if (rhi::Buffer** buf = mapFind(m_uniformBuffers, &instance)) { + entries.push_back(rhi::BindGroupEntry::bufferEntry(*buf, 0, material->uniformDataSize())); + } + + usize propIndex = 0; + for (const MaterialPropertyDef& p : material->properties()) { + if (p.isTexture()) { + rhi::TextureView* view = instance.getTexture(propIndex); + if (view == nullptr) { + view = (contains(p.name, u8"ormal")) ? m_normalView : m_whiteView; // *N*ormal/*n*ormal fallback + } + if (view != nullptr) { entries.push_back(rhi::BindGroupEntry::textureEntry(view)); } + } else if (p.isSampler()) { + rhi::Sampler* s = instance.getSampler(propIndex); + if (s == nullptr) { s = m_defaultSampler; } + if (s != nullptr) { entries.push_back(rhi::BindGroupEntry::samplerEntry(s)); } + } + ++propIndex; + } + if (entries.empty()) { return false; } + + if (rhi::BindGroup** old = mapFind(m_bindGroups, &instance)) { + rhi::BindGroup* g = *old; + m_device->destroyBindGroup(g); + m_bindGroups.erase(&instance); + } + + rhi::BindGroupDesc desc{}; + desc.layout = layout; + desc.entries = std::span{ entries.data(), entries.size() }; + rhi::BindGroup* bg = nullptr; + if (!m_device->createBindGroup(desc, bg).isOk()) { return false; } + m_bindGroups.insert_or_assign(&instance, bg); + return true; + } + + static u64 computeLayoutHash(Material& material) { + u64 h = 17; + h = h * 31u + material.uniformDataSize(); + for (const MaterialPropertyDef& p : material.properties()) { + h = h * 31u + static_cast(p.type); + h = h * 31u + p.binding; + } + return h; + } + + // Case-insensitive-ish substring check (matches "normal"/"Normal" via "ormal"). + static bool contains(std::u8string_view haystack, std::u8string_view needle) { + if (needle.size() == 0 || haystack.size() < needle.size()) { return false; } + const usize last = haystack.size() - needle.size(); + for (usize i = 0; i <= last; ++i) { + bool match = true; + for (usize j = 0; j < needle.size(); ++j) { + if (haystack[i + j] != needle[j]) { match = false; break; } + } + if (match) { return true; } + } + return false; + } + + void removeFromDirty(MaterialInstance* instance) { + for (usize i = 0; i < m_dirty.size(); ++i) { + if (m_dirty[i] == instance) { m_dirty[i] = nullptr; return; } + } + } + + void shutdown() { + if (m_device == nullptr) { return; } + for (auto& e : m_bindGroups) { m_device->destroyBindGroup(e.second); } + for (auto& e : m_uniformBuffers) { m_device->destroyBuffer(e.second); } + for (auto& e : m_layoutCache) { m_device->destroyBindGroupLayout(e.second); } + for (auto& e : m_samplerCache) { m_device->destroySampler(e.second); } + m_bindGroups.clear(); m_uniformBuffers.clear(); m_layoutCache.clear(); m_samplerCache.clear(); + + if (m_whiteView) { m_device->destroyTextureView(m_whiteView); } + if (m_normalView) { m_device->destroyTextureView(m_normalView); } + if (m_whiteTex) { m_device->destroyTexture(m_whiteTex); } + if (m_normalTex) { m_device->destroyTexture(m_normalTex); } + if (m_defaultSampler) { m_device->destroySampler(m_defaultSampler); } + m_device = nullptr; + } + + rhi::Device* m_device = nullptr; + rhi::Queue* m_queue = nullptr; + rhi::Sampler* m_defaultSampler = nullptr; + rhi::Texture* m_whiteTex = nullptr; rhi::TextureView* m_whiteView = nullptr; + rhi::Texture* m_normalTex = nullptr; rhi::TextureView* m_normalView = nullptr; + + std::unordered_map m_layoutCache; + std::unordered_map m_samplerCache; + std::unordered_map m_uniformBuffers; + std::unordered_map m_bindGroups; + std::vector m_dirty; +}; + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/MaterialsModule.cppm b/Engine/cpp/Runtime/Rendering/Materials/MaterialsModule.cppm new file mode 100644 index 00000000..3397b040 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/MaterialsModule.cppm @@ -0,0 +1,15 @@ +/// Primary module for materials - the data-driven material model. Re-exports all partitions. +/// +/// Aggregates the partitions: value types (:types), render-state + vertex layouts +/// (:pipeline), the shared Material template (:material), the fluent MaterialBuilder +/// (:builder), per-use MaterialInstance (:instance), and the GPU-resource-owning +/// MaterialSystem (:system) that infers bind-group layouts from declared properties. + +export module materials; + +export import :types; +export import :pipeline; +export import :material; +export import :builder; +export import :instance; +export import :system; diff --git a/Engine/cpp/Runtime/Rendering/Materials/MaterialsTests.test.cpp b/Engine/cpp/Runtime/Rendering/Materials/MaterialsTests.test.cpp new file mode 100644 index 00000000..2c1a9f13 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/MaterialsTests.test.cpp @@ -0,0 +1,135 @@ +// The data-driven material model: build a material with the fluent builder, verify +// uniform-buffer layout + declared properties, drive a MaterialSystem (bind-group +// layout inferred from the property list) against the Null RHI, and check instance +// overrides + dirty notification. +#include +#include +#include + +import core; +import rhi; +import rhi.null; +import shaders; +import materials; + +using namespace draco; +using namespace draco::materials; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +TEST_CASE("material builder: lays out uniforms + declares properties") +{ + std::shared_ptr mat = MaterialBuilder(u8"pbr") + .shader(u8"forward") + .flags(shaders::ShaderFlags::NormalMap) + .Color(u8"baseColor", math::Vector4{ 1, 0, 0, 1 }) // float4 -> 16 bytes, offset 0 + .Float(u8"roughness", 0.5f) // float -> 4 bytes, offset 16 + .texture(u8"albedoMap") + .texture(u8"normalMap") + .sampler(u8"linearSampler") + .build(); + + REQUIRE(mat); + CHECK(mat->isValid()); + CHECK(mat->shaderName == u8"forward"); + CHECK(mat->shaderFlags == shaders::ShaderFlags::NormalMap); + CHECK(mat->pipeline.shaderName == u8"forward"); // pipeline mirrors shader id + CHECK(mat->propertyCount() == 5); + CHECK(mat->uniformDataSize() == 20); // 16 (float4) + 4 (float) + + CHECK(mat->getPropertyIndex(u8"roughness") == 1); + CHECK(mat->getPropertyIndex(u8"missing") == -1); + const MaterialPropertyDef* rough = mat->findProperty(u8"roughness"); + REQUIRE(rough != nullptr); + CHECK(rough->offset == 16); + CHECK(rough->isUniform()); + + // default uniform data carries the seeded baseColor (red) at offset 0 + const std::span defaults = mat->defaultUniformData(); + REQUIRE(defaults.size() == 20); + const f32* base = reinterpret_cast(defaults.data()); + CHECK(base[0] == doctest::Approx(1.0f)); + CHECK(base[1] == doctest::Approx(0.0f)); +} + +TEST_CASE("material system: infers bind-group layout from properties + builds instance bind group") +{ + rhi::null::NullDevice device; + MaterialSystem system; + REQUIRE(system.initialize(device).isOk()); + CHECK(system.defaultSampler() != nullptr); + CHECK(system.whiteTexture() != nullptr); + CHECK(system.normalTexture() != nullptr); + + std::shared_ptr mat = MaterialBuilder(u8"lit") + .shader(u8"forward") + .Color(u8"tint", math::Vector4{ 1, 1, 1, 1 }) + .texture(u8"albedoMap") + .sampler(u8"samp") + .build(); + REQUIRE(mat); + + // layout is cached: same material -> same pointer + rhi::BindGroupLayout* l0 = system.getOrCreateLayout(*mat); + rhi::BindGroupLayout* l1 = system.getOrCreateLayout(*mat); + REQUIRE(l0 != nullptr); + CHECK(l0 == l1); + + MaterialInstance inst(mat.get()); + rhi::BindGroup* bg = system.prepareInstance(inst); + REQUIRE(bg != nullptr); + CHECK(inst.bindGroupLayout() == l0); + CHECK_FALSE(inst.isUniformDirty()); + CHECK_FALSE(inst.isBindGroupDirty()); + CHECK(system.getBindGroup(inst) == bg); +} + +TEST_CASE("material instance: overrides notify the system + re-prep is driven by the dirty list") +{ + rhi::null::NullDevice device; + MaterialSystem system; + REQUIRE(system.initialize(device).isOk()); + + std::shared_ptr mat = MaterialBuilder(u8"lit") + .shader(u8"forward") + .Float(u8"roughness", 0.5f) + .texture(u8"albedoMap") + .build(); + REQUIRE(mat); + + MaterialInstance inst(mat.get()); + REQUIRE(system.prepareInstance(inst) != nullptr); // clears dirty + registers sink + + // overriding a uniform marks it dirty and enqueues exactly one dirty entry + inst.setFloat(u8"roughness", 0.9f); + CHECK(inst.isUniformDirty()); + inst.setFloat(u8"roughness", 0.8f); // second set: still one enqueue + + system.prepareDirtyInstances(); + CHECK_FALSE(inst.isUniformDirty()); // drained + re-prepped + + // the override is the effective value (0.8), not the material default (0.5) + const std::span data = inst.uniformData(); + REQUIRE(data.size() == 4); + CHECK(*reinterpret_cast(data.data()) == doctest::Approx(0.8f)); + + // resetting restores the default + inst.resetProperty(u8"roughness"); + system.prepareDirtyInstances(); + CHECK(*reinterpret_cast(inst.uniformData().data()) == doctest::Approx(0.5f)); +} + +TEST_CASE("pipeline config: content hash + equality distinguish render state") +{ + PipelineConfig a = PipelineConfig::forOpaqueMesh(u8"forward"); + PipelineConfig b = PipelineConfig::forOpaqueMesh(u8"forward"); + CHECK(a == b); + CHECK(a.hashCode() == b.hashCode()); + + PipelineConfig c = PipelineConfig::forTransparentMesh(u8"forward"); + CHECK_FALSE(a == c); // blend/depth differ + CHECK(a.hashCode() != c.hashCode()); + + CHECK(VertexLayoutHelper::stride(VertexLayoutType::Mesh) == 48); + CHECK(VertexLayoutHelper::attributes(VertexLayoutType::Mesh).size() == 5); +} diff --git a/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineCacheTests.test.cpp b/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineCacheTests.test.cpp new file mode 100644 index 00000000..ea4fc4da --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineCacheTests.test.cpp @@ -0,0 +1,101 @@ +// The render-side PSO cache: build a pipeline from a PipelineConfig pulling variants +// from the ShaderSystem, verify it caches, and verify the version-polling hot-reload +// path - invalidating the shader makes GetPipeline rebuild and retire the stale PSO. +// Real DXC + Null RHI. +#include +#include +#include + +import core; +import rhi; +import rhi.null; +import shaders; +import shaders.system; +import materials; +import materials.pso; + +using namespace draco; +using namespace draco::materials; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +namespace +{ + constexpr const char8_t* kVtx = u8"float4 main(uint id : SV_VertexID) : SV_Position { return float4(0,0,0,1); }\n"; + constexpr const char8_t* kFrag = u8"float4 main() : SV_Target { return float4(1,0,0,1); }\n"; +} + +TEST_CASE("pso cache: builds + caches a pipeline; shader reload rebuilds via version poll") +{ + shaders::Compiler* compiler = nullptr; + if (!shaders::createCompiler(shaders::CompilerDesc{}, compiler).isOk()) { MESSAGE("DXC unavailable; skipping"); return; } + + rhi::null::NullDevice device; + shaders::ShaderSystem shaderSystem(*compiler, device); + shaderSystem.registerSource(u8"forward", shaders::ShaderStage::Vertex, kVtx); + shaderSystem.registerSource(u8"forward", shaders::ShaderStage::Fragment, kFrag); + + rhi::PipelineLayout* layout = nullptr; + REQUIRE(device.createPipelineLayout(rhi::PipelineLayoutDesc{}, layout).isOk()); + + PipelineStateCache cache(shaderSystem, device); + const PipelineConfig config = PipelineConfig::forOpaqueMesh(u8"forward"); + + rhi::RenderPipeline* p0 = cache.getPipeline(config, layout); + REQUIRE(p0 != nullptr); + CHECK(cache.size() == 1); + + // same request -> same pipeline (cache hit, no rebuild) + rhi::RenderPipeline* p1 = cache.getPipeline(config, layout); + CHECK(p1 == p0); + CHECK(cache.size() == 1); + CHECK(cache.retiredCount() == 0); + + // reload the shader: version bumps -> next GetPipeline rebuilds + retires the old PSO + shaderSystem.invalidateShader(u8"forward"); + rhi::RenderPipeline* p2 = cache.getPipeline(config, layout); + REQUIRE(p2 != nullptr); + CHECK(p2 != p0); // rebuilt against the new shader + CHECK(cache.size() == 1); // same key, replaced in place + CHECK(cache.retiredCount() == 1); // stale pipeline retired, awaiting GPU-safe free + + cache.releaseRetired(); + CHECK(cache.retiredCount() == 0); + + // a distinct render state is a distinct cache entry + rhi::RenderPipeline* pT = cache.getPipeline(PipelineConfig::forTransparentMesh(u8"forward"), layout); + REQUIRE(pT != nullptr); + CHECK(cache.size() == 2); + + cache.clear(); + CHECK(cache.size() == 0); + + device.destroyPipelineLayout(layout); + compiler->destroy(); +} + +TEST_CASE("pso cache: depth-only config builds without a fragment shader") +{ + shaders::Compiler* compiler = nullptr; + if (!shaders::createCompiler(shaders::CompilerDesc{}, compiler).isOk()) { MESSAGE("DXC unavailable; skipping"); return; } + + rhi::null::NullDevice device; + shaders::ShaderSystem shaderSystem(*compiler, device); + shaderSystem.registerSource(u8"shadow", shaders::ShaderStage::Vertex, kVtx); + // deliberately no fragment source registered + + rhi::PipelineLayout* layout = nullptr; + REQUIRE(device.createPipelineLayout(rhi::PipelineLayoutDesc{}, layout).isOk()); + + PipelineStateCache cache(shaderSystem, device); + PipelineConfig config = PipelineConfig::forOpaqueMesh(u8"shadow"); + config.depthOnly = true; + config.vertexLayout = VertexLayoutType::PositionOnly; + + rhi::RenderPipeline* p = cache.getPipeline(config, layout); + CHECK(p != nullptr); // vertex-only pipeline, no fragment fetched + + cache.clear(); + device.destroyPipelineLayout(layout); + compiler->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineStateCache.cppm b/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineStateCache.cppm new file mode 100644 index 00000000..abffc863 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/PSO/PipelineStateCache.cppm @@ -0,0 +1,206 @@ +/// Render-side PSO cache with shader version polling (the `materials.pso` module). +/// +/// The render-side PSO cache: the one piece of the shader/material stack that lives +/// outside the resource system (the "lone exception" from the hot-reload design). It +/// maps a PipelineConfig (× render-target signature) to a compiled RHI RenderPipeline, +/// pulling shader variants from the ShaderSystem and mapping the material's render +/// state to RHI pipeline state. +/// +/// Hot reload is handled by VERSION POLLING at point of use: each cached entry records +/// the ShaderSystem version of its shader at build time; GetPipeline compares against +/// the current version and lazily rebuilds when a shader was invalidated (reloaded). +/// This costs one HashMap lookup + integer compare per draw - the same as a dirty-flag +/// check - with no listener bookkeeping. Superseded pipelines are retired to a +/// graveyard and freed by releaseRetired() once the GPU is done with them. + +module; +#include +#include +#include +#include +#include + +export module materials.pso; + +import core; +import rhi; +import shaders; +import shaders.system; +import materials; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +class PipelineStateCache { +public: + PipelineStateCache(shaders::ShaderSystem& shaderSystem, rhi::Device& device) noexcept + : m_shaders(&shaderSystem), m_device(&device) {} + + ~PipelineStateCache() { clear(); releaseRetired(); } + + PipelineStateCache(const PipelineStateCache&) = delete; + PipelineStateCache& operator=(const PipelineStateCache&) = delete; + + // Returns a RenderPipeline for (config, layout, colorOverride), building it on the + // first request and rebuilding it when the shader has since been reloaded. `layout` + // is the assembled pipeline layout (frame/object/material bind-group layouts); the + // caller owns it. Returns null on build failure. + [[nodiscard]] rhi::RenderPipeline* getPipeline(const PipelineConfig& config, rhi::PipelineLayout* layout, + rhi::TextureFormat colorOverride = rhi::TextureFormat::Undefined) { + const u64 key = keyOf(config, layout, colorOverride); + const u64 version = m_shaders->version(config.shaderName); + + if (Entry* e = mapFind(m_entries, key)) { + if (e->builtVersion == version && e->pipeline != nullptr) { return e->pipeline; } + // shader was reloaded since this PSO was built: retire the stale one + rebuild. + if (e->pipeline != nullptr) { m_retired.push_back(e->pipeline); } + e->pipeline = build(config, layout, colorOverride); + e->builtVersion = version; + return e->pipeline; + } + + Entry entry{}; + entry.pipeline = build(config, layout, colorOverride); + entry.builtVersion = version; + rhi::RenderPipeline* result = entry.pipeline; + m_entries.insert_or_assign(key, entry); + return result; + } + + [[nodiscard]] usize size() const noexcept { return m_entries.size(); } + [[nodiscard]] usize retiredCount() const noexcept { return m_retired.size(); } + + // Frees pipelines superseded by a reload. Call once the frames that may still + // reference them have completed (the GraphicsDevice frame ring is the gate). + void releaseRetired() { + for (rhi::RenderPipeline* p : m_retired) { if (p != nullptr) { m_device->destroyRenderPipeline(p); } } + m_retired.clear(); + } + + // Destroys all live pipelines (retires nothing - call at shutdown). + void clear() { + for (auto& e : m_entries) { if (e.second.pipeline != nullptr) { m_device->destroyRenderPipeline(e.second.pipeline); } } + m_entries.clear(); + } + +private: + struct Entry { + rhi::RenderPipeline* pipeline = nullptr; + u64 builtVersion = 0; + }; + + static u64 keyOf(const PipelineConfig& config, rhi::PipelineLayout* layout, rhi::TextureFormat colorOverride) { + u64 h = config.hashCode(); + h = h * 31u + reinterpret_cast(layout); + h = h * 31u + static_cast(colorOverride); + return h; + } + + rhi::RenderPipeline* build(const PipelineConfig& config, rhi::PipelineLayout* layout, rhi::TextureFormat colorOverride) { + rhi::ShaderModule* vs = m_shaders->getVariant(config.shaderName, shaders::ShaderStage::Vertex, config.shaderFlags); + if (vs == nullptr) { return nullptr; } + + rhi::RenderPipelineDesc desc{}; + desc.layout = layout; + desc.label = config.shaderName; + + // --- vertex --- + // Up to three buffers, in slot order matching the renderer's draw bindings: mesh stream + // (slot 0); the skinning stream (slot 1, joints loc 6 + weights loc 7) when skinned; the + // instance-stepped DataOffsets stream (loc 5) when instanced. Skinned draws are always + // instanced -> [mesh, skin, offsets]; non-skinned instanced -> [mesh, offsets]. + rhi::VertexBufferLayout buffers[3] = { VertexLayoutHelper::bufferLayout(config.vertexLayout), {}, {} }; + u32 bufferCount = (config.vertexLayout != VertexLayoutType::None) ? 1u : 0u; + if (config.vertexLayout == VertexLayoutType::SkinnedMesh) { buffers[bufferCount++] = VertexLayoutHelper::skinningStreamBufferLayout(); } + if (config.instanced) { buffers[bufferCount++] = VertexLayoutHelper::instanceOffsetsBufferLayout(); } + desc.vertex.shader = rhi::ProgrammableStage{ vs, u8"main", rhi::ShaderStage::Vertex }; + if (bufferCount > 0) { desc.vertex.buffers = std::span{ buffers, bufferCount }; } + + // --- fragment (omitted for depth-only passes) --- + // Up to colorTargetCount targets (MRT): target 0 is the shaded color (blended per blendMode + + // format from colorOverride when set, e.g. the per-view HDR/LDR format); targets 1+ are the + // G-buffer aux outputs (view-normal, motion vector) - no blend, formats from config.colorFormats. + rhi::ColorTargetState colorTargets[rhi::maxColorAttachments] = {}; + if (!config.depthOnly) { + rhi::ShaderModule* fs = m_shaders->getVariant(config.shaderName, shaders::ShaderStage::Fragment, config.shaderFlags); + if (fs == nullptr) { return nullptr; } + // Honor colorTargetCount exactly - 0 means a fragment that writes no color (e.g. the masked + // shadow pass: alpha-test discard + depth only). No fallback to 1. + const u32 count = std::min(config.colorTargetCount, rhi::maxColorAttachments); + for (u32 i = 0; i < count; ++i) { + colorTargets[i].format = (i == 0 && colorOverride != rhi::TextureFormat::Undefined) ? colorOverride : config.colorFormats[i]; + colorTargets[i].blend = (i == 0) ? blendFor(config.blendMode) : std::optional{}; + // Aux G-buffer targets (1+) only write when writeAuxTargets is set - transparent draws + // bind them (to match the pass) but leave the opaque normal/velocity underneath intact. + colorTargets[i].writeMask = (i == 0 || config.writeAuxTargets) ? config.colorWriteMask : rhi::ColorWriteMask::None; + } + rhi::FragmentState frag{}; + frag.shader = rhi::ProgrammableStage{ fs, u8"main", rhi::ShaderStage::Fragment }; + frag.targets = std::span{ colorTargets, count }; + desc.fragment = frag; + } + + // --- primitive --- + desc.primitive.topology = config.topology; + desc.primitive.frontFace = config.frontFace; + desc.primitive.cullMode = cullFor(config.cullMode); + desc.primitive.fillMode = config.fillMode; + + // --- depth/stencil --- + if (config.depthMode != DepthMode::Disabled) { + rhi::DepthStencilState ds{}; + ds.format = config.depthFormat; + ds.depthTestEnabled = config.depthMode == DepthMode::ReadWrite || config.depthMode == DepthMode::ReadOnly; + ds.depthWriteEnabled = config.depthMode == DepthMode::ReadWrite || config.depthMode == DepthMode::WriteOnly; + ds.depthCompare = config.depthCompare; + ds.depthBias = config.depthBias; + ds.depthBiasSlopeScale = config.depthBiasSlopeScale; + desc.depthStencil = ds; + } + + // --- multisample --- + desc.multisample.count = config.sampleCount; + + rhi::RenderPipeline* pipeline = nullptr; + if (!m_device->createRenderPipeline(desc, pipeline).isOk()) { return nullptr; } + return pipeline; + } + + static std::optional blendFor(BlendMode mode) { + switch (mode) { + case BlendMode::Opaque: + case BlendMode::Masked: + return {}; // no blending + case BlendMode::AlphaBlend: + return rhi::BlendState::alphaBlend(); + case BlendMode::Additive: + return rhi::BlendState{ { rhi::BlendFactor::One, rhi::BlendFactor::One, rhi::BlendOperation::Add }, + { rhi::BlendFactor::One, rhi::BlendFactor::One, rhi::BlendOperation::Add } }; + case BlendMode::Multiply: + return rhi::BlendState{ { rhi::BlendFactor::Dst, rhi::BlendFactor::Zero, rhi::BlendOperation::Add }, + { rhi::BlendFactor::Dst, rhi::BlendFactor::Zero, rhi::BlendOperation::Add } }; + case BlendMode::PremultipliedAlpha: + return rhi::BlendState{ { rhi::BlendFactor::One, rhi::BlendFactor::OneMinusSrcAlpha, rhi::BlendOperation::Add }, + { rhi::BlendFactor::One, rhi::BlendFactor::OneMinusSrcAlpha, rhi::BlendOperation::Add } }; + } + return {}; + } + + static rhi::CullMode cullFor(CullModeConfig c) { + switch (c) { + case CullModeConfig::None: return rhi::CullMode::None; + case CullModeConfig::Back: return rhi::CullMode::Back; + case CullModeConfig::Front: return rhi::CullMode::Front; + } + return rhi::CullMode::None; + } + + shaders::ShaderSystem* m_shaders; // borrowed + rhi::Device* m_device; // borrowed + std::unordered_map m_entries; + std::vector m_retired; // superseded by reload, awaiting GPU-safe free +}; + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/PipelineConfig.cppm b/Engine/cpp/Runtime/Rendering/Materials/PipelineConfig.cppm new file mode 100644 index 00000000..8d810a9b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/PipelineConfig.cppm @@ -0,0 +1,230 @@ +/// PipelineConfig: full render-state description + PSO-cache key (`:pipeline` partition). +/// +/// PipelineConfig: the full render-state description for a material (shader name + +/// variant flags, vertex layout, primitive/blend/depth state, render-target +/// formats). It is the *key* a PSO cache hashes on - orthogonal to ShaderFlags: +/// flags drive the shader permutation (compile-time #defines), PipelineConfig +/// drives the fixed-function state. All value types so it hashes by content. +/// +/// VertexLayoutHelper maps the predefined VertexLayoutType presets to concrete RHI +/// vertex attributes / strides for the predefined layouts. + +module; +#include +#include +#include + +export module materials:pipeline; + +import core; +import rhi; +import shaders; +import :types; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::materials { + +// Full render-state for a material. Content-hashable PSO-cache key. +struct PipelineConfig { + // --- shader identification --- + std::u8string_view shaderName; + shaders::ShaderFlags shaderFlags = shaders::ShaderFlags::None; + + // --- vertex input --- + VertexLayoutType vertexLayout = VertexLayoutType::Mesh; + u32 customVertexStride = 0; + u8 customAttributeCount = 0; + // When set, the PSO gains a second, instance-stepped vertex buffer: a uint4 DataOffsets + // stream (location 5) whose .x indexes a per-instance StructuredBuffer. Pair with + // ShaderFlags::Instanced (the shader's INSTANCED permutation reads it). + bool instanced = false; + + // --- primitive assembly --- + rhi::PrimitiveTopology topology = rhi::PrimitiveTopology::TriangleList; + CullModeConfig cullMode = CullModeConfig::Back; + rhi::FrontFace frontFace = rhi::FrontFace::CCW; // CCW after Y-flip in projection + rhi::FillMode fillMode = rhi::FillMode::Solid; + + // --- blend --- + BlendMode blendMode = BlendMode::Opaque; + rhi::ColorWriteMask colorWriteMask = rhi::ColorWriteMask::All; + + // --- depth/stencil --- + DepthMode depthMode = DepthMode::ReadWrite; + rhi::CompareFunction depthCompare = rhi::CompareFunction::Less; + rhi::TextureFormat depthFormat = rhi::TextureFormat::Depth32Float; + i16 depthBias = 0; + f32 depthBiasSlopeScale = 0.0f; + + // --- render targets --- + rhi::TextureFormat colorFormats[rhi::maxColorAttachments] = { rhi::TextureFormat::BGRA8Unorm }; + u8 colorTargetCount = 1; + u8 sampleCount = 1; + + // --- flags --- + bool depthOnly = false; + // MRT: write the G-buffer aux targets (normal/velocity, slots 1+). False for transparent/blended + // draws so they don't clobber the opaque G-buffer they blend over (they still bind all targets to + // match the render pass, but with the aux write masks disabled). + bool writeAuxTargets = true; + + // Content hash: name bytes folded with the POD state. Used as the PSO-cache key. + [[nodiscard]] u64 hashCode() const noexcept { + u64 h = std::hash{}(shaderName); + const auto mix = [&h](u64 v) { h = h * 31u + v; }; + mix(static_cast(shaderFlags)); + mix(static_cast(vertexLayout)); + mix(customVertexStride); + mix(customAttributeCount); + mix(instanced ? 1u : 0u); + mix(static_cast(topology)); + mix(static_cast(cullMode)); + mix(static_cast(frontFace)); + mix(static_cast(fillMode)); + mix(static_cast(blendMode)); + mix(static_cast(colorWriteMask)); + mix(static_cast(depthMode)); + mix(static_cast(depthCompare)); + mix(static_cast(depthFormat)); + mix(static_cast(static_cast(depthBias))); + for (u8 i = 0; i < colorTargetCount && i < rhi::maxColorAttachments; ++i) { + mix(static_cast(colorFormats[i])); + } + mix(colorTargetCount); + mix(sampleCount); + mix(depthOnly ? 1u : 0u); + mix(writeAuxTargets ? 1u : 0u); + return h; + } + + [[nodiscard]] bool operator==(const PipelineConfig& o) const noexcept { + if (!(shaderName == o.shaderName && shaderFlags == o.shaderFlags && + vertexLayout == o.vertexLayout && customVertexStride == o.customVertexStride && + customAttributeCount == o.customAttributeCount && instanced == o.instanced && topology == o.topology && + cullMode == o.cullMode && frontFace == o.frontFace && fillMode == o.fillMode && + blendMode == o.blendMode && colorWriteMask == o.colorWriteMask && + depthMode == o.depthMode && depthCompare == o.depthCompare && + depthFormat == o.depthFormat && depthBias == o.depthBias && + depthBiasSlopeScale == o.depthBiasSlopeScale && + colorTargetCount == o.colorTargetCount && sampleCount == o.sampleCount && + depthOnly == o.depthOnly && writeAuxTargets == o.writeAuxTargets)) { + return false; + } + for (u8 i = 0; i < colorTargetCount && i < rhi::maxColorAttachments; ++i) { + if (colorFormats[i] != o.colorFormats[i]) { return false; } + } + return true; + } + + // ---- presets ---- + [[nodiscard]] static PipelineConfig forOpaqueMesh(std::u8string_view shader, shaders::ShaderFlags flags = shaders::ShaderFlags::None) { + PipelineConfig c{}; c.shaderName = shader; c.shaderFlags = flags; + c.vertexLayout = VertexLayoutType::Mesh; c.blendMode = BlendMode::Opaque; c.depthMode = DepthMode::ReadWrite; + return c; + } + [[nodiscard]] static PipelineConfig forTransparentMesh(std::u8string_view shader, shaders::ShaderFlags flags = shaders::ShaderFlags::None) { + PipelineConfig c{}; c.shaderName = shader; c.shaderFlags = flags; + c.vertexLayout = VertexLayoutType::Mesh; c.blendMode = BlendMode::AlphaBlend; c.depthMode = DepthMode::ReadOnly; + return c; + } + [[nodiscard]] static PipelineConfig forSkybox(std::u8string_view shader) { + PipelineConfig c{}; c.shaderName = shader; c.vertexLayout = VertexLayoutType::PositionOnly; + c.depthMode = DepthMode::ReadOnly; c.depthCompare = rhi::CompareFunction::LessEqual; c.cullMode = CullModeConfig::Front; + return c; + } + [[nodiscard]] static PipelineConfig forSprites(std::u8string_view shader) { + PipelineConfig c{}; c.shaderName = shader; c.vertexLayout = VertexLayoutType::PositionUVColor; + c.blendMode = BlendMode::AlphaBlend; c.depthMode = DepthMode::ReadOnly; c.cullMode = CullModeConfig::None; + return c; + } + [[nodiscard]] static PipelineConfig forFullscreen(std::u8string_view shader) { + PipelineConfig c{}; c.shaderName = shader; c.vertexLayout = VertexLayoutType::None; + c.depthMode = DepthMode::Disabled; c.cullMode = CullModeConfig::None; + return c; + } +}; + +// Maps VertexLayoutType -> concrete RHI vertex attributes + stride. +class VertexLayoutHelper { +public: + [[nodiscard]] static u32 stride(VertexLayoutType t) noexcept { + switch (t) { + case VertexLayoutType::None: return 0; + case VertexLayoutType::PositionOnly: return 12; + case VertexLayoutType::PositionUVColor: return 36; + case VertexLayoutType::MeshNoTangent: return 32; + case VertexLayoutType::Mesh: return 48; + // A skinned mesh's buffer 0 IS the static stream (48B) - the skinning data + // is a SEPARATE buffer (see SkinningStream*), matching draco.geometry's + // SkinnedMesh : StaticMesh layout. So a skinned draw binds two vertex buffers. + case VertexLayoutType::SkinnedMesh: return 48; + case VertexLayoutType::Custom: return 0; + } + return 0; + } + + [[nodiscard]] static std::span attributes(VertexLayoutType t) noexcept { + switch (t) { + case VertexLayoutType::PositionOnly: return { kPositionOnly, 1 }; + case VertexLayoutType::PositionUVColor: return { kPositionUVColor, 3 }; + case VertexLayoutType::MeshNoTangent: return { kMeshNoTangent, 3 }; + case VertexLayoutType::Mesh: return { kMesh, 5 }; + case VertexLayoutType::SkinnedMesh: return { kMesh, 5 }; // buffer 0 = static stream + default: return {}; + } + } + + [[nodiscard]] static rhi::VertexBufferLayout bufferLayout(VertexLayoutType t) noexcept { + rhi::VertexBufferLayout l{}; + l.stride = stride(t); + l.stepMode = rhi::VertexStepMode::Vertex; + l.attributes = attributes(t); + return l; + } + + // The instance-stepped vertex buffer an instanced draw binds: a uint4 DataOffsets entry + // per instance (location 5), stride 16. `.x` indexes the per-instance StructuredBuffer; + // hardware instance stepping makes this portable (unlike the SV_InstanceID system value, + // which differs between DX12 and Vulkan). Bound as buffer 1 alongside the mesh stream. + [[nodiscard]] static rhi::VertexBufferLayout instanceOffsetsBufferLayout() noexcept { + rhi::VertexBufferLayout l{}; + l.stride = 16; + l.stepMode = rhi::VertexStepMode::Instance; + l.attributes = { kInstanceOffsets, 1 }; + return l; + } + + // The skinning vertex buffer a skinned draw binds: joints (location 6) + weights (location 7), + // stride 24 - matches draco.geometry::VertexSkinning. Skinned draws are ALWAYS instanced now, so + // the instance-stepped DataOffsets takes location 5 and DXC (which assigns input locations + // sequentially by declaration order, dataOffsets declared before joints/weights) lands these at 6/7. + [[nodiscard]] static u32 skinningStreamStride() noexcept { return 24; } + [[nodiscard]] static std::span skinningStreamAttributes() noexcept { + return { kSkinningStream, 2 }; + } + [[nodiscard]] static rhi::VertexBufferLayout skinningStreamBufferLayout() noexcept { + rhi::VertexBufferLayout l{}; + l.stride = skinningStreamStride(); + l.stepMode = rhi::VertexStepMode::Vertex; + l.attributes = skinningStreamAttributes(); + return l; + } + +private: + using VA = rhi::VertexAttribute; + using VF = rhi::VertexFormat; + static constexpr VA kPositionOnly[1] = { { VF::Float32x3, 0, 0 } }; + static constexpr VA kPositionUVColor[3] = { { VF::Float32x3, 0, 0 }, { VF::Float32x2, 12, 1 }, { VF::Float32x4, 20, 2 } }; + static constexpr VA kMeshNoTangent[3] = { { VF::Float32x3, 0, 0 }, { VF::Float32x3, 12, 1 }, { VF::Float32x2, 24, 2 } }; + static constexpr VA kMesh[5] = { { VF::Float32x3, 0, 0 }, { VF::Float32x3, 12, 1 }, { VF::Float32x2, 24, 2 }, + { VF::Unorm8x4, 32, 3 }, { VF::Float32x3, 36, 4 } }; + // Skinning stream: joints (uint16x4 packed as uint32x2) + weights, at locations 6/7 (DataOffsets + // takes 5; skinned draws are always instanced). + static constexpr VA kSkinningStream[2] = { { VF::Uint32x2, 0, 6 }, { VF::Float32x4, 8, 7 } }; + // Instance offsets stream: a uint4 DataOffsets at location 5. + static constexpr VA kInstanceOffsets[1] = { { VF::Uint32x4, 0, 5 } }; +}; + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Materials/Types.cppm b/Engine/cpp/Runtime/Rendering/Materials/Types.cppm new file mode 100644 index 00000000..26e649c5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Materials/Types.cppm @@ -0,0 +1,94 @@ +/// Value-type vocabulary for the data-driven material model (`:types` partition). +/// +/// Value-type vocabulary for the data-driven material model: property kinds (the +/// declared shape a material exposes to shaders), pipeline-state presets (blend / +/// depth / cull, orthogonal to shader variant flags), and the predefined vertex +/// layouts. A material is *data*: a list of MaterialPropertyDefs + a PipelineConfig +/// + a shader name - the MaterialSystem infers the GPU bind-group layout from the +/// declared properties, so a new material/shader needs no renderer changes. + +module; +#include + +export module materials:types; + +import core; + +using namespace draco; + +export namespace draco::materials { + +// Look up `k` in an associative container, returning a pointer to its value or nullptr +// (a value-or-null find used across the material partitions and PSO cache). +template +[[nodiscard]] auto mapFind(Map& m, const Key& k) -> decltype(&m.find(k)->second) { + auto it = m.find(k); + return it != m.end() ? &it->second : nullptr; +} + +// The kind of a declared material property. Scalars/vectors/matrices pack into a +// uniform buffer; textures and samplers become bind-group entries. +enum class MaterialPropertyType : u8 { + Float, Float2, Float3, Float4, + Int, Int2, Int3, Int4, + Matrix4x4, + Texture2D, TextureCube, Sampler, +}; + +// One declared property: its name, kind, and (for uniforms) its byte offset/size +// in the material uniform buffer. `name` is a view into the owning Material's +// stable name backing (see Material::AddProperty). +struct MaterialPropertyDef { + std::u8string_view name; + MaterialPropertyType type = MaterialPropertyType::Float; + u32 binding = 0; // ordinal in the material's binding space + u32 offset = 0; // byte offset in the uniform buffer (uniforms) + u32 size = 0; // byte size (uniforms) + + [[nodiscard]] bool isTexture() const noexcept { + return type == MaterialPropertyType::Texture2D || type == MaterialPropertyType::TextureCube; + } + [[nodiscard]] bool isSampler() const noexcept { return type == MaterialPropertyType::Sampler; } + [[nodiscard]] bool isUniform() const noexcept { return !isTexture() && !isSampler(); } + + // Packed size of a uniform property kind (0 for textures/samplers). + [[nodiscard]] static u32 sizeOf(MaterialPropertyType t) noexcept { + switch (t) { + case MaterialPropertyType::Float: case MaterialPropertyType::Int: return 4; + case MaterialPropertyType::Float2: case MaterialPropertyType::Int2: return 8; + case MaterialPropertyType::Float3: case MaterialPropertyType::Int3: return 12; + case MaterialPropertyType::Float4: case MaterialPropertyType::Int4: return 16; + case MaterialPropertyType::Matrix4x4: return 64; + default: return 0; // textures / samplers carry no uniform data + } + } +}; + +// Blend preset - resolved to concrete BlendState by the PSO builder. +enum class BlendMode : u8 { + Opaque, Masked, AlphaBlend, Additive, Multiply, PremultipliedAlpha, +}; + +// Depth test/write preset. +enum class DepthMode : u8 { + Disabled, ReadWrite, ReadOnly, WriteOnly, +}; + +// Face-culling preset (distinct from rhi::CullMode so the material layer stays +// RHI-agnostic at the data level; mapped at PSO build time). +enum class CullModeConfig : u8 { + None, Back, Front, +}; + +// Predefined vertex layouts - the byte formats meshes/sprites/etc. supply. +enum class VertexLayoutType : u8 { + None, // procedural (no vertex input) + PositionOnly, // float3 - skybox / shadow depth + PositionUVColor, // float3 + float2 + float4 - sprites / particles + MeshNoTangent, // float3 + float3 + float2 (32 bytes) + Mesh, // + color(ubyte4) + tangent(float3) (48 bytes) + SkinnedMesh, // + joints(uint2) + weights(float4) (72 bytes) + Custom, +}; + +} // namespace draco::materials diff --git a/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cpp b/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cpp deleted file mode 100644 index 3f1e5b60..00000000 --- a/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cpp +++ /dev/null @@ -1,431 +0,0 @@ -module; - -#include -#include -#include - -module rendering.mesh; - -import core.stdtypes; -import core.math.constants; -import core.memory; -import rendering.rhi; -import rendering.rhi.vertex; - -namespace draco::rendering::mesh -{ - using namespace draco::rendering; - - static std::unordered_map g_mesh_cache; - static core::memory::HandleRegistry g_meshes; - static rhi::LayoutHandle g_mesh_layout = rhi::InvalidLayout; - - static usize hashCombine(usize a, usize b) - { - return a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2)); - } - - static usize hashMeshParams(int a, int b = 0, f32 c = 0.0f) - { - usize h1 = std::hash{}(a); - usize h2 = std::hash{}(b); - usize h3 = std::hash{}(c); - - return hashCombine(hashCombine(h1, h2), h3); - } - - static void ensureMeshLayout() - { - if (g_mesh_layout != rhi::InvalidLayout) - return; - - rhi::VertexLayoutDesc desc; - desc.elements = - { - { rhi::Attrib::Position, 3, rhi::AttribType::Float }, - { rhi::Attrib::Normal, 3, rhi::AttribType::Float }, - { rhi::Attrib::TexCoord0,2, rhi::AttribType::Float } - }; - - g_mesh_layout = rhi::createVertexLayout(desc); - } - - MeshHandle create(const void* vertex_data, u32 vertex_size, u32 vertex_count, const std::vector& indices, rhi::LayoutHandle layout) - { - Mesh mesh{}; - - mesh.vbh = rhi::createVertexBuffer(vertex_data, vertex_size, layout); - mesh.ibh = rhi::createIndexBuffer(indices.data(), static_cast(indices.size() * sizeof(u32))); - - mesh.layout = layout; - - mesh.vertexCount = vertex_count; - mesh.indexCount = static_cast(indices.size()); - - mesh.valid = (mesh.vbh != rhi::InvalidBuffer) && (mesh.ibh != rhi::InvalidBuffer); - - if (!mesh.valid) - { - if (mesh.vbh != rhi::InvalidBuffer) rhi::destroyBuffer(mesh.vbh); - if (mesh.ibh != rhi::InvalidBuffer) rhi::destroyBuffer(mesh.ibh); - return {}; - } - return g_meshes.create(mesh); - } - - MeshHandle createCube() - { - ensureMeshLayout(); - - usize key = 1; - - if (auto it = g_mesh_cache.find(key); it != g_mesh_cache.end()) - return it->second; - - auto v = gen::cubeVertices(); - auto i = gen::cubeIndices(); - - MeshHandle h = create(v.data(), v.size()*sizeof(Vertex), (u32)v.size(), i, g_mesh_layout); - - g_mesh_cache[key] = h; - return h; - } - - MeshHandle createPlane(f32 size) - { - ensureMeshLayout(); - - usize key = hashMeshParams(1000, 0, size); - - if (auto it = g_mesh_cache.find(key); it != g_mesh_cache.end()) - return it->second; - - auto v = gen::planeVertices(size); - auto i = gen::planeIndices(); - - MeshHandle h = create(v.data(), v.size()*sizeof(Vertex), (u32)v.size(), i, g_mesh_layout); - - g_mesh_cache[key] = h; - return h; - } - - MeshHandle createSphere(int segments, int rings) - { - if (segments < 3 || rings < 2) - return {}; - - ensureMeshLayout(); - - usize key = hashCombine(std::hash{}(segments), std::hash{}(rings)); - - if (auto it = g_mesh_cache.find(key); it != g_mesh_cache.end()) - return it->second; - - auto v = gen::sphereVertices(segments, rings); - auto i = gen::sphereIndices(segments, rings); - - MeshHandle h = create(v.data(), v.size()*sizeof(Vertex), (u32)v.size(), i, g_mesh_layout); - - g_mesh_cache[key] = h; - return h; - } - - MeshHandle createCylinder(int segments, f32 height) - { - if (segments < 3 || height < 0.0f) - return {}; - - ensureMeshLayout(); - - usize key = hashMeshParams(2000, segments, height); - - if (auto it = g_mesh_cache.find(key); it != g_mesh_cache.end()) - return it->second; - - auto v = gen::cylinderVertices(segments, height); - auto i = gen::cylinderIndices(segments); - - MeshHandle h = create(v.data(), v.size()*sizeof(Vertex), (u32)v.size(), i, g_mesh_layout); - - g_mesh_cache[key] = h; - return h; - } - - MeshHandle createCapsule(int segments, int rings, f32 height) - { - if (segments < 3 || rings < 2 || height < 0.0f) - return {}; - - ensureMeshLayout(); - usize key = hashCombine(hashCombine(std::hash{}(segments), std::hash{}(rings)), std::hash{}(height)); - - if (auto it = g_mesh_cache.find(key); it != g_mesh_cache.end()) - return it->second; - - auto v = gen::capsuleVertices(segments, rings, height); - auto i = gen::capsuleIndices(segments, rings); - - MeshHandle h = create(v.data(), v.size()*sizeof(Vertex), (u32)v.size(), i, g_mesh_layout); - - g_mesh_cache[key] = h; - return h; - } - - void destroy(MeshHandle handle) - { - auto* mesh = g_meshes.get(handle); - if (!mesh) return; - - rhi::destroyBuffer(mesh->vbh); - rhi::destroyBuffer(mesh->ibh); - - // Remove from cache - for (auto it = g_mesh_cache.begin(); it != g_mesh_cache.end(); ) - { - if (it->second == handle) - it = g_mesh_cache.erase(it); - else - ++it; - } - - g_meshes.destroy(handle); - } - - const Mesh* get(MeshHandle handle) - { - return g_meshes.get(handle); - } -} - -namespace draco::rendering::mesh::gen -{ - Vertex make(f32 px, f32 py, f32 pz, f32 nx, f32 ny, f32 nz, f32 u, f32 v) - { - return { px, py, pz, nx, ny, nz, u, v }; - } - - std::vector cubeVertices() - { - return { - make(-1,-1, 1, 0,0,1, 0,0), - make( 1,-1, 1, 0,0,1, 1,0), - make( 1, 1, 1, 0,0,1, 1,1), - make(-1, 1, 1, 0,0,1, 0,1), - - make( 1,-1,-1, 0,0,-1, 0,0), - make(-1,-1,-1, 0,0,-1, 1,0), - make(-1, 1,-1, 0,0,-1, 1,1), - make( 1, 1,-1, 0,0,-1, 0,1), - - make(-1,-1,-1,-1,0,0, 0,0), - make(-1,-1, 1,-1,0,0, 1,0), - make(-1, 1, 1,-1,0,0, 1,1), - make(-1, 1,-1,-1,0,0, 0,1), - - make( 1,-1, 1, 1,0,0, 0,0), - make( 1,-1,-1, 1,0,0, 1,0), - make( 1, 1,-1, 1,0,0, 1,1), - make( 1, 1, 1, 1,0,0, 0,1), - - make(-1, 1, 1, 0,1,0, 0,0), - make( 1, 1, 1, 0,1,0, 1,0), - make( 1, 1,-1, 0,1,0, 1,1), - make(-1, 1,-1, 0,1,0, 0,1), - - make(-1,-1,-1, 0,-1,0, 0,0), - make( 1,-1,-1, 0,-1,0, 1,0), - make( 1,-1, 1, 0,-1,0, 1,1), - make(-1,-1, 1, 0,-1,0, 0,1), - }; - } - - std::vector cubeIndices() - { - return { - 0,1,2, 2,3,0, - 4,5,6, 6,7,4, - 8,9,10, 10,11,8, - 12,13,14, 14,15,12, - 16,17,18, 18,19,16, - 20,21,22, 22,23,20 - }; - } - - std::vector planeVertices(f32 size) - { - f32 s = size * 0.5f; - - return { - make(-s,0,-s, 0,1,0, 0,0), - make( s,0,-s, 0,1,0, 1,0), - make( s,0, s, 0,1,0, 1,1), - make(-s,0, s, 0,1,0, 0,1), - }; - } - - std::vector planeIndices() - { - return { 0,1,2, 2,3,0 }; - } - - std::vector sphereVertices(int segments, int rings) - { - std::vector v; - - for (int y = 0; y <= rings; y++) - { - f32 v01 = (f32)y / rings; - f32 theta = v01 * draco::math::PI; - - for (int x = 0; x <= segments; x++) - { - f32 u01 = (f32)x / segments; - f32 phi = u01 * 2.0f * draco::math::PI; - - f32 px = sinf(theta) * cosf(phi); - f32 py = cosf(theta); - f32 pz = sinf(theta) * sinf(phi); - - v.push_back(make(px,py,pz, px,py,pz, u01,v01)); - } - } - - return v; - } - - std::vector sphereIndices(int segments, int rings) - { - std::vector i; - - for (int y = 0; y < rings; y++) - { - for (int x = 0; x < segments; x++) - { - int a = y * (segments + 1) + x; - int b = a + segments + 1; - - i.push_back(a); - i.push_back(b); - i.push_back(a + 1); - - i.push_back(b); - i.push_back(b + 1); - i.push_back(a + 1); - } - } - - return i; - } - - std::vector cylinderVertices(int segments, f32 height) - { - std::vector v; - f32 half = height * 0.5f; - - // Side walls (Outward normals) - for (int y = 0; y <= 1; y++) { - f32 py = (y ? half : -half); - for (int x = 0; x <= segments; x++) { - f32 t = (f32)x / segments; - f32 a = t * 2.0f * draco::math::PI; - f32 cx = cosf(a); - f32 cz = sinf(a); - // Normal is strictly horizontal for side walls - v.push_back(make(cx, py, cz, cx, 0, cz, t, (f32)y)); - } - } - - // Top cap (Upward normals) - // Center vertex - v.push_back(make(0, half, 0, 0, 1, 0, 0.5f, 0.5f)); - for (int x = 0; x <= segments; x++) { - f32 t = (f32)x / segments; - f32 a = t * 2.0f * draco::math::PI; - v.push_back(make(cosf(a), half, sinf(a), 0, 1, 0, (cosf(a)+1)*0.5f, (sinf(a)+1)*0.5f)); - } - - // Bottom cap (Downward Normals) - // Center vertex - v.push_back(make(0, -half, 0, 0, -1, 0, 0.5f, 0.5f)); - for (int x = 0; x <= segments; x++) { - f32 t = (f32)x / segments; - f32 a = t * 2.0f * draco::math::PI; - v.push_back(make(cosf(a), -half, sinf(a), 0, -1, 0, (cosf(a)+1)*0.5f, (sinf(a)+1)*0.5f)); - } - - return v; - } - - std::vector cylinderIndices(int segments) - { - std::vector i; - int side_start = 0; - int top_start = (segments + 1) * 2; - int bottom_start = top_start + (segments + 2); - - // Sides - for (int s = 0; s < segments; s++) { - int b0 = s; int b1 = s + 1; - int t0 = b0 + segments + 1; int t1 = b1 + segments + 1; - i.push_back(b0); i.push_back(t0); i.push_back(t1); - i.push_back(b0); i.push_back(t1); i.push_back(b1); - } - - // Top Cap (Triangle Fan style) - for (int s = 0; s < segments; s++) { - i.push_back(top_start); // Center - i.push_back(top_start + s + 2); - i.push_back(top_start + s + 1); - } - - // Bottom Cap - for (int s = 0; s < segments; s++) { - i.push_back(bottom_start); // Center - i.push_back(bottom_start + s + 1); - i.push_back(bottom_start + s + 2); - } - return i; - } - - std::vector capsuleVertices(int segments, int rings, f32 height) - { - std::vector v; - f32 half = height * 0.5f; - - // One continuous loop from bottom pole to top pole - // Total rings for a capsule = rings (bottom cap) + rings (top cap) - for (int r = 0; r <= rings; r++) { - f32 v_uv = (f32)r / rings; - f32 theta = v_uv * draco::math::PI; // 0 to draco::math::PI - - // Adjust Y for the cylinder section - f32 y_offset = (theta < draco::math::PI * 0.5f) ? half : -half; - - for (int s = 0; s <= segments; s++) { - f32 u_uv = (f32)s / segments; - f32 phi = u_uv * 2.0f * draco::math::PI; - - f32 nx = sinf(theta) * cosf(phi); - f32 ny = cosf(theta); - f32 nz = sinf(theta) * sinf(phi); - - v.push_back(make(nx, ny + y_offset, nz, nx, ny, nz, u_uv, v_uv)); - } - } - return v; - } - - std::vector capsuleIndices(int segments, int rings) - { - std::vector i; - for (int r = 0; r < rings; r++) { - for (int s = 0; s < segments; s++) { - int a = r * (segments + 1) + s; - int b = a + segments + 1; - i.push_back(a); i.push_back(b); i.push_back(a + 1); - i.push_back(b); i.push_back(b + 1); i.push_back(a + 1); - } - } - return i; - } -} diff --git a/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cppm b/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cppm deleted file mode 100644 index fac985a9..00000000 --- a/Engine/cpp/Runtime/Rendering/Mesh/Mesh.cppm +++ /dev/null @@ -1,71 +0,0 @@ -module; - -#include - -export module rendering.mesh; - -import core.stdtypes; -import core.memory; -import rendering.rhi; - -export namespace draco::rendering::mesh -{ - struct MeshTag {}; - - using MeshHandle = core::memory::Handle; - - struct Vertex - { - f32 px, py, pz; - f32 nx, ny, nz; - f32 u, v; - }; - - struct Mesh - { - rhi::BufferHandle vbh; - rhi::BufferHandle ibh; - - rhi::LayoutHandle layout; - - u32 vertexCount = 0; - u32 indexCount = 0; - - bool valid = false; - }; - - MeshHandle create( - const void* vertexData, - u32 vertexSize, - u32 vertexCount, - const std::vector& indices, - rhi::LayoutHandle layout - ); - - MeshHandle createCube(); - MeshHandle createPlane(float size); - MeshHandle createSphere(int segments, int rings); - MeshHandle createCylinder(int segments, float height); - MeshHandle createCapsule(int segments, int rings, float height); - - void destroy(MeshHandle mesh); - const Mesh* get(MeshHandle mesh); -} - -export namespace draco::rendering::mesh::gen -{ - std::vector cubeVertices(); - std::vector cubeIndices(); - - std::vector planeVertices(float size); - std::vector planeIndices(); - - std::vector sphereVertices(int segments, int rings); - std::vector sphereIndices(int segments, int rings); - - std::vector cylinderVertices(int segments, float height); - std::vector cylinderIndices(int segments); - - std::vector capsuleVertices(int segments, int rings, float height); - std::vector capsuleIndices(int segments, int rings); -} diff --git a/Engine/cpp/Runtime/Rendering/Model/FBX/FbxLoader.cppm b/Engine/cpp/Runtime/Rendering/Model/FBX/FbxLoader.cppm new file mode 100644 index 00000000..2e60080d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/FBX/FbxLoader.cppm @@ -0,0 +1,1176 @@ +/// FBX/OBJ model loader using ufbx. + +module; +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ufbx.h" + +export module model:fbx; + +import core; +import :model; +import :io; +import image; +import image.io; + +export namespace draco::model::fbx { + +using namespace draco; +using namespace draco::model; + +// ufbx hands back char* (UTF-8); the engine std::u8string is UTF-8 too, so these just +// wrap the bytes in an owned std::u8string - no transcoding. +inline std::u8string utf8FromC(const char* s) { + if (!s) return std::u8string{}; + return std::u8string(std::u8string_view(reinterpret_cast(s))); +} +inline std::u8string utf8FromUfbx(const ufbx_string& s) { + return std::u8string(std::u8string_view(reinterpret_cast(s.data), s.length)); +} + +/// Loads FBX and OBJ model files using ufbx. +class FbxLoader : public io::ModelLoader { +public: + FbxLoader() = default; + + ~FbxLoader() override { + if (m_scene) { + ufbx_free_scene(m_scene); + m_scene = nullptr; + } + } + + bool supportsExtension(std::u8string_view ext) const override { + return caseInsensitiveEquals(ext, u8".fbx") || caseInsensitiveEquals(ext, u8".obj"); + } + + // Non-copyable. + FbxLoader(const FbxLoader&) = delete; + FbxLoader& operator=(const FbxLoader&) = delete; + + /// Load an FBX or OBJ file. + ModelLoadResult load(std::u8string_view path, Model& model) override { + // Free previous scene. + if (m_scene) { + ufbx_free_scene(m_scene); + m_scene = nullptr; + } + + // Clear mappings. + m_nodeToBoneIndex.clear(); + m_materialIndexMap.clear(); + m_textureIndexMap.clear(); + m_meshIndexMap.clear(); + m_skinIndexMap.clear(); + + // Extract base path for loading external resources. + const std::u8string narrowStorage = std::u8string(path); + const char* narrowPath = reinterpret_cast(narrowStorage.c_str()); + std::filesystem::path filePath(narrowPath); + m_basePath = filePath.parent_path().string(); + + // Setup load options. + m_loadOpts = {}; + m_loadOpts.target_axes = ufbx_axes_right_handed_y_up; + m_loadOpts.target_unit_meters = 1.0; + m_loadOpts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY; + m_loadOpts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK; + m_loadOpts.generate_missing_normals = true; + m_loadOpts.clean_skin_weights = true; + m_loadOpts.use_blender_pbr_material = true; + + // Parse the file. + ufbx_error error = {}; + m_scene = ufbx_load_file(narrowPath, &m_loadOpts, &error); + + if (!m_scene) { + if (error.type == UFBX_ERROR_FILE_NOT_FOUND) + return ModelLoadResult::FileNotFound; + if (error.type == UFBX_ERROR_UNSUPPORTED_VERSION) + return ModelLoadResult::UnsupportedFormat; + return ModelLoadResult::ParseError; + } + + // With MODIFY_GEOMETRY, ufbx converts everything to Y-up already. + model.originalUpAxis = CoordinateAxis::PositiveY; + + // Convert to Model. + loadMaterials(model); + loadTextures(model); + detectAlphaFromTextures(model); + loadMeshes(model); + loadNodes(model); + loadSkins(model); + loadAnimations(model); + + model.buildBoneHierarchy(); + model.calculateBounds(); + + return ModelLoadResult::Ok; + } + +private: + ufbx_scene* m_scene = nullptr; + std::string m_basePath; + ufbx_load_opts m_loadOpts = {}; + + /// Node typed_id -> model bone index + std::unordered_map m_nodeToBoneIndex; + /// Material typed_id -> model material index + std::unordered_map m_materialIndexMap; + /// Texture typed_id -> model texture index + std::unordered_map m_textureIndexMap; + /// Mesh typed_id -> model mesh index + std::unordered_map m_meshIndexMap; + /// Skin deformer typed_id -> model skin index + std::unordered_map m_skinIndexMap; + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + static bool caseInsensitiveEquals(std::u8string_view a, std::u8string_view b) { + if (a.size() != b.size()) return false; + for (usize i = 0; i < a.size(); ++i) { + if (std::tolower(static_cast(a.data()[i])) != + std::tolower(static_cast(b.data()[i]))) + return false; + } + return true; + } + + static bool endsWithCI(std::u8string_view str, std::u8string_view suffix) { + if (str.size() < suffix.size()) return false; + return caseInsensitiveEquals(str.substr(str.size() - suffix.size(), suffix.size()), suffix); + } + + + + // ----------------------------------------------------------------------- + // Materials + // ----------------------------------------------------------------------- + + void loadMaterials(Model& model) { + for (size_t i = 0; i < m_scene->materials.count; ++i) { + ufbx_material* mat = m_scene->materials.data[i]; + auto* material = new ModelMaterial(); + + if (mat->element.name.data && mat->element.name.length > 0) + material->setName(utf8FromUfbx(mat->element.name)); + + bool hasPBR = mat->features.pbr.enabled; + + if (hasPBR) { + // PBR: base color. + if (mat->pbr.base_color.has_value) { + auto c = mat->pbr.base_color.value_vec4; + material->baseColorFactor = math::Vector4( + static_cast(c.x), static_cast(c.y), + static_cast(c.z), static_cast(c.w)); + } + if (mat->pbr.base_color.texture) { + material->baseColorTextureIndex = getOrCreateTextureIndex( + mat->pbr.base_color.texture, model); + // When using use_blender_pbr_material, the base_color value is often the + // legacy diffuse color (0.8,0.8,0.8) which shouldn't tint the texture. + material->baseColorFactor = math::Vector4(1.0f, 1.0f, 1.0f, 1.0f); + } + + // PBR: metallic + roughness. + if (mat->pbr.metalness.has_value) + material->metallicFactor = static_cast(mat->pbr.metalness.value_real); + if (mat->pbr.roughness.has_value) + material->roughnessFactor = static_cast(mat->pbr.roughness.value_real); + + if (mat->pbr.metalness.texture) + material->metallicRoughnessTextureIndex = getOrCreateTextureIndex( + mat->pbr.metalness.texture, model); + else if (mat->pbr.roughness.texture) + material->metallicRoughnessTextureIndex = getOrCreateTextureIndex( + mat->pbr.roughness.texture, model); + + // PBR: normal map. + if (mat->pbr.normal_map.texture) { + material->normalTextureIndex = getOrCreateTextureIndex( + mat->pbr.normal_map.texture, model); + material->normalScale = 1.0f; + } + + // PBR: ambient occlusion. + if (mat->pbr.ambient_occlusion.texture) { + material->occlusionTextureIndex = getOrCreateTextureIndex( + mat->pbr.ambient_occlusion.texture, model); + material->occlusionStrength = 1.0f; + } + + // PBR: emissive. + if (mat->pbr.emission_color.has_value) { + auto e = mat->pbr.emission_color.value_vec3; + f32 factor = mat->pbr.emission_factor.has_value + ? static_cast(mat->pbr.emission_factor.value_real) : 1.0f; + material->emissiveFactor = math::Vector3( + static_cast(e.x) * factor, + static_cast(e.y) * factor, + static_cast(e.z) * factor); + } + if (mat->pbr.emission_color.texture) + material->emissiveTextureIndex = getOrCreateTextureIndex( + mat->pbr.emission_color.texture, model); + } else { + // Legacy FBX (Lambert/Phong): map to PBR. + if (mat->fbx.diffuse_color.has_value) { + auto c = mat->fbx.diffuse_color.value_vec4; + f32 factor = mat->fbx.diffuse_factor.has_value + ? static_cast(mat->fbx.diffuse_factor.value_real) : 1.0f; + material->baseColorFactor = math::Vector4( + static_cast(c.x) * factor, + static_cast(c.y) * factor, + static_cast(c.z) * factor, 1.0f); + } + if (mat->fbx.diffuse_color.texture) { + material->baseColorTextureIndex = getOrCreateTextureIndex( + mat->fbx.diffuse_color.texture, model); + material->baseColorFactor = math::Vector4(1.0f, 1.0f, 1.0f, 1.0f); + } + + // Normal map. + if (mat->fbx.normal_map.texture) { + material->normalTextureIndex = getOrCreateTextureIndex( + mat->fbx.normal_map.texture, model); + material->normalScale = 1.0f; + } + + // Emissive. + if (mat->fbx.emission_color.has_value) { + auto e = mat->fbx.emission_color.value_vec3; + f32 factor = mat->fbx.emission_factor.has_value + ? static_cast(mat->fbx.emission_factor.value_real) : 1.0f; + material->emissiveFactor = math::Vector3( + static_cast(e.x) * factor, + static_cast(e.y) * factor, + static_cast(e.z) * factor); + } + if (mat->fbx.emission_color.texture) + material->emissiveTextureIndex = getOrCreateTextureIndex( + mat->fbx.emission_color.texture, model); + + // Non-PBR defaults. + material->metallicFactor = 0.0f; + material->roughnessFactor = 0.8f; + } + + // Double-sided: FBX format doesn't reliably carry the DoubleSided flag + // from Blender. When not explicitly set in the file, default to true + // to match the behavior of most GLTF exports and avoid incorrect culling. + if (mat->features.double_sided.is_explicit) + material->doubleSided = mat->features.double_sided.enabled; + else + material->doubleSided = true; + + // Alpha / opacity (scalar values only -- texture-based alpha detection + // is handled by detectAlphaFromTextures after textures are loaded). + if (mat->features.opacity.enabled) { + if (mat->pbr.opacity.has_value && + static_cast(mat->pbr.opacity.value_real) < 1.0f) + material->alphaMode = AlphaMode::Blend; + else if (mat->fbx.transparency_factor.has_value && + static_cast(mat->fbx.transparency_factor.value_real) > 0.0f) + material->alphaMode = AlphaMode::Blend; + } + + i32 matIndex = model.addMaterial(material); + m_materialIndexMap.insert_or_assign(mat->element.typed_id, matIndex); + } + } + + /// Gets or creates a model texture index for a ufbx texture. + /// This allows materials to reference textures before loadTextures runs. + i32 getOrCreateTextureIndex(ufbx_texture* texture, Model& model) { + if (!texture) return -1; + + uint32_t typedId = texture->element.typed_id; + auto it = mapFind(m_textureIndexMap, typedId); + if (it != nullptr) + return *it; + + // Create a placeholder texture entry that loadTextures will populate. + auto* modelTex = new ModelTexture(); + if (texture->element.name.data && texture->element.name.length > 0) + modelTex->setName(utf8FromUfbx(texture->element.name)); + + i32 index = model.addTexture(modelTex); + m_textureIndexMap.insert_or_assign(typedId, index); + return index; + } + + // ----------------------------------------------------------------------- + // Textures + // ----------------------------------------------------------------------- + + void loadTextures(Model& model) { + + for (size_t i = 0; i < m_scene->textures.count; ++i) { + ufbx_texture* tex = m_scene->textures.data[i]; + uint32_t typedId = tex->element.typed_id; + + // Get or create the model texture. + ModelTexture* modelTex = nullptr; + auto it = mapFind(m_textureIndexMap, typedId); + if (it != nullptr) { + modelTex = model.textures()[*it]; + } else { + modelTex = new ModelTexture(); + if (tex->element.name.data && tex->element.name.length > 0) + modelTex->setName(utf8FromUfbx(tex->element.name)); + i32 idx = model.addTexture(modelTex); + m_textureIndexMap.insert_or_assign(typedId, idx); + } + + // Try to load image data. + if (tex->content.size > 0 && tex->content.data) { + // Embedded texture data. + draco::image::Image img; + if (image::io::loadImageFromMemory( + std::span(static_cast(tex->content.data), + tex->content.size), + img) == ErrorCode::Ok) + storeImageData(img, modelTex); + } else if (tex->has_file) { + // External file - try relative path, then walk parent directories, then absolute. + std::string relPath; + if (tex->relative_filename.data && tex->relative_filename.length > 0) + relPath = std::string(tex->relative_filename.data, + tex->relative_filename.length); + + bool loaded = false; + std::string resolvedPath; + + if (!relPath.empty()) { + // Try basePath + relative path first. + auto imagePath = (std::filesystem::path(m_basePath) / relPath).string(); + + if (!std::filesystem::exists(imagePath)) { + // Rel path may be wrong - try just the filename. + auto fileName = std::filesystem::path(relPath).filename().string(); + imagePath = (std::filesystem::path(m_basePath) / fileName).string(); + } + + if (std::filesystem::exists(imagePath)) { + draco::image::Image img; + if (image::io::loadImage(utf8FromC(imagePath.c_str()), img) + == ErrorCode::Ok) { + storeImageData(img, modelTex); + resolvedPath = imagePath; + loaded = true; + } + } else { + // Walk up parent directories (up to 5 levels) looking for the file. + auto searchDir = std::filesystem::path(m_basePath); + for (int depth = 0; depth < 5 && !loaded; ++depth) { + auto parentDir = searchDir.parent_path(); + if (parentDir.empty() || parentDir == searchDir) + break; + + auto candidatePath = (parentDir / relPath).string(); + if (std::filesystem::exists(candidatePath)) { + draco::image::Image img; + if (image::io::loadImage(utf8FromC(candidatePath.c_str()), + img) == ErrorCode::Ok) { + storeImageData(img, modelTex); + resolvedPath = candidatePath; + loaded = true; + } + } + searchDir = parentDir; + } + } + } + + // Fallback: try absolute filename path. + if (!loaded && tex->filename.data && tex->filename.length > 0) { + std::string absPath(tex->filename.data, tex->filename.length); + draco::image::Image img; + if (image::io::loadImage(utf8FromC(absPath.c_str()), img) + == ErrorCode::Ok) { + storeImageData(img, modelTex); + resolvedPath = absPath; + loaded = true; + } + } + + if (loaded) { + // Set the URI so TextureConverter can build the source path for dedup. + modelTex->setUri(utf8FromC(resolvedPath.c_str())); + } + } + + // Sampler / wrap mode. + TextureSampler sampler{}; + sampler.wrapS = convertWrapMode(tex->wrap_u); + sampler.wrapT = convertWrapMode(tex->wrap_v); + i32 samplerIdx = model.addSampler(sampler); + modelTex->samplerIndex = samplerIdx; + } + } + + // ----------------------------------------------------------------------- + // Image helpers + // ----------------------------------------------------------------------- + + static TexturePixelFormat convertPixelFormat(draco::image::PixelFormat fmt) { + switch (fmt) { + case draco::image::PixelFormat::R8: return TexturePixelFormat::R8; + case draco::image::PixelFormat::RG8: return TexturePixelFormat::RG8; + case draco::image::PixelFormat::RGB8: return TexturePixelFormat::RGB8; + case draco::image::PixelFormat::RGBA8: return TexturePixelFormat::RGBA8; + case draco::image::PixelFormat::BGR8: return TexturePixelFormat::BGR8; + case draco::image::PixelFormat::BGRA8: return TexturePixelFormat::BGRA8; + default: return TexturePixelFormat::Unknown; + } + } + + static void storeImageData(const draco::image::Image& image, ModelTexture* texture) { + texture->width = static_cast(image.width()); + texture->height = static_cast(image.height()); + texture->pixelFormat = convertPixelFormat(image.format()); + + auto data = image.pixelData(); + if (data.data() && data.size() > 0) { + auto* copy = new u8[data.size()]; + std::memcpy(copy, data.data(), data.size()); + texture->setData(copy, static_cast(data.size())); + } + } + + static TextureWrap convertWrapMode(ufbx_wrap_mode mode) { + switch (mode) { + case UFBX_WRAP_CLAMP: return TextureWrap::ClampToEdge; + default: return TextureWrap::Repeat; + } + } + + // ----------------------------------------------------------------------- + // Alpha Detection + // ----------------------------------------------------------------------- + + /// Post-process: detect alpha mode from base color texture content. + /// FBX doesn't have an explicit alpha mode like glTF, so we analyze + /// the albedo texture's alpha channel (like Godot does). + static void detectAlphaFromTextures(Model& model) { + for (auto* material : model.materials()) { + // Skip materials that already have an explicit alpha mode. + if (material->alphaMode != AlphaMode::Opaque) + continue; + + if (material->baseColorTextureIndex < 0 || + static_cast(material->baseColorTextureIndex) >= model.textures().size()) + continue; + + auto* texture = model.textures()[material->baseColorTextureIndex]; + if (!texture->getData() || texture->getDataSize() == 0) + continue; + + // Only check RGBA/BGRA textures (4 bytes per pixel with alpha). + i32 bpp = getBytesPerPixel(texture->pixelFormat); + if (bpp != 4) + continue; + + i32 alphaOffset = getAlphaOffset(texture->pixelFormat); + if (alphaOffset < 0) + continue; + + const u8* data = texture->getData(); + i32 pixelCount = texture->width * texture->height; + i32 dataSize = texture->getDataSize(); + + if (pixelCount * bpp > dataSize) + continue; + + // Scan alpha channel and classify pixels. + i32 opaqueCount = 0; + i32 transparentCount = 0; + i32 gradientCount = 0; + + for (i32 p = 0; p < pixelCount; ++p) { + u8 alpha = data[p * bpp + alphaOffset]; + if (alpha == 255) + opaqueCount++; + else if (alpha == 0) + transparentCount++; + else + gradientCount++; + } + (void)opaqueCount; + + if (transparentCount == 0 && gradientCount == 0) + continue; // Fully opaque. + + // Determine alpha mode from pixel distribution. + f32 gradientRatio = static_cast(gradientCount) / static_cast(pixelCount); + if (gradientRatio > 0.25f) { + material->alphaMode = AlphaMode::Blend; + } else if (transparentCount > 0 || gradientCount > 0) { + material->alphaMode = AlphaMode::Mask; + material->alphaCutoff = 0.2f; + } + } + } + + static i32 getBytesPerPixel(TexturePixelFormat fmt) { + switch (fmt) { + case TexturePixelFormat::RGBA8: + case TexturePixelFormat::BGRA8: return 4; + case TexturePixelFormat::RGB8: + case TexturePixelFormat::BGR8: return 3; + case TexturePixelFormat::RG8: return 2; + case TexturePixelFormat::R8: return 1; + default: return 0; + } + } + + static i32 getAlphaOffset(TexturePixelFormat fmt) { + switch (fmt) { + case TexturePixelFormat::RGBA8: return 3; + case TexturePixelFormat::BGRA8: return 3; + default: return -1; + } + } + + // ----------------------------------------------------------------------- + // Meshes + // ----------------------------------------------------------------------- + + void loadMeshes(Model& model) { + for (size_t i = 0; i < m_scene->meshes.count; ++i) { + ufbx_mesh* fbxMesh = m_scene->meshes.data[i]; + auto* mesh = new ModelMesh(); + + if (fbxMesh->element.name.data && fbxMesh->element.name.length > 0) + mesh->setName(utf8FromUfbx(fbxMesh->element.name)); + + // Determine if this mesh is skinned. + bool isSkinned = fbxMesh->skin_deformers.count > 0; + ufbx_skin_deformer* skinDeformer = isSkinned + ? fbxMesh->skin_deformers.data[0] : nullptr; + + // Check for available attributes. + bool hasNormals = fbxMesh->vertex_normal.exists || m_loadOpts.generate_missing_normals; + bool hasUV = fbxMesh->uv_sets.count > 0 && fbxMesh->uv_sets.data[0].vertex_uv.exists; + bool hasTangent = fbxMesh->uv_sets.count > 0 && fbxMesh->uv_sets.data[0].vertex_tangent.exists; + bool hasColor = fbxMesh->color_sets.count > 0 && fbxMesh->color_sets.data[0].vertex_color.exists; + // Also check legacy vertex_color field. + bool hasLegacyColor = fbxMesh->vertex_color.exists; + if (!hasColor && hasLegacyColor) + hasColor = true; + + mesh->setHasNormals(hasNormals); + mesh->setHasTangents(hasTangent); + + // Setup vertex format (matching GltfLoader layout). + i32 stride = 0; + i32 positionOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Position, VertexElementFormat::Float3, positionOffset)); + + i32 normalOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Normal, VertexElementFormat::Float3, normalOffset)); + + i32 texCoordOffset = stride; + stride += static_cast(sizeof(math::Vector2)); + mesh->addVertexElement(VertexElement(VertexSemantic::TexCoord, VertexElementFormat::Float2, texCoordOffset)); + + i32 colorOffset = stride; + stride += static_cast(sizeof(u32)); + mesh->addVertexElement(VertexElement(VertexSemantic::Color, VertexElementFormat::Byte4, colorOffset)); + + i32 tangentOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Tangent, VertexElementFormat::Float3, tangentOffset)); + + i32 jointsOffset = 0; + i32 weightsOffset = 0; + if (isSkinned) { + jointsOffset = stride; + stride += static_cast(sizeof(u16) * 4); + mesh->addVertexElement(VertexElement(VertexSemantic::Joints, VertexElementFormat::UShort4, jointsOffset)); + + weightsOffset = stride; + stride += static_cast(sizeof(math::Vector4)); + mesh->addVertexElement(VertexElement(VertexSemantic::Weights, VertexElementFormat::Float4, weightsOffset)); + } + + // Collect all triangulated vertices across all material parts. + std::vector allVertexBytes; + std::vector allIndices; + std::unordered_map vertexMap; // vertex hash -> index + + // Allocate triangle index buffer for triangulation. + size_t maxTriIndices = fbxMesh->max_face_triangles * 3; + std::vector triIndices(maxTriIndices); + + // Process material parts. + if (fbxMesh->material_parts.count > 0) { + for (size_t p = 0; p < fbxMesh->material_parts.count; ++p) { + ufbx_mesh_part* part = &fbxMesh->material_parts.data[p]; + i32 indexStart = static_cast(allIndices.size()); + i32 indexCount = 0; + + // Get material index. + i32 materialIndex = -1; + if (part->index < static_cast(fbxMesh->materials.count)) { + ufbx_material* matPtr = fbxMesh->materials.data[part->index]; + if (matPtr) { + auto mit = mapFind(m_materialIndexMap, matPtr->element.typed_id); + if (mit != nullptr) + materialIndex = *mit; + } + } + + // Process each face in this material part. + for (size_t fi = 0; fi < part->face_indices.count; ++fi) { + uint32_t faceIdx = part->face_indices.data[fi]; + ufbx_face face = fbxMesh->faces.data[faceIdx]; + + uint32_t numTris = ufbx_triangulate_face( + triIndices.data(), maxTriIndices, fbxMesh, face); + + for (uint32_t ti = 0; ti < numTris * 3; ++ti) { + uint32_t idx = triIndices[ti]; + + size_t hash = buildVertex( + fbxMesh, static_cast(idx), skinDeformer, isSkinned, + hasUV, hasTangent, hasColor, + stride, positionOffset, normalOffset, texCoordOffset, + colorOffset, tangentOffset, jointsOffset, weightsOffset, + allVertexBytes, vertexMap); + + allIndices.push_back(static_cast(*mapFind(vertexMap, hash))); + indexCount++; + } + } + + if (indexCount > 0) + mesh->addPart(ModelMeshPart(indexStart, indexCount, materialIndex)); + } + } else { + // No material parts - process all faces as a single part. + i32 indexCount = 0; + + for (size_t fi = 0; fi < fbxMesh->faces.count; ++fi) { + ufbx_face face = fbxMesh->faces.data[fi]; + uint32_t numTris = ufbx_triangulate_face( + triIndices.data(), maxTriIndices, fbxMesh, face); + + for (uint32_t ti = 0; ti < numTris * 3; ++ti) { + uint32_t idx = triIndices[ti]; + + size_t hash = buildVertex( + fbxMesh, static_cast(idx), skinDeformer, isSkinned, + hasUV, hasTangent, hasColor, + stride, positionOffset, normalOffset, texCoordOffset, + colorOffset, tangentOffset, jointsOffset, weightsOffset, + allVertexBytes, vertexMap); + + allIndices.push_back(static_cast(*mapFind(vertexMap, hash))); + indexCount++; + } + } + + if (indexCount > 0) + mesh->addPart(ModelMeshPart(0, indexCount, -1)); + } + + // Copy vertex data to mesh. + i32 vertexCount = static_cast(allVertexBytes.size() / stride); + if (vertexCount > 0) { + mesh->allocateVertices(vertexCount, stride); + std::memcpy(mesh->getVertexData(), allVertexBytes.data(), allVertexBytes.size()); + } + + // Copy index data to mesh. + i32 idxCount = static_cast(allIndices.size()); + if (idxCount > 0) { + bool use32Bit = idxCount > 65535 || vertexCount > 65535; + mesh->allocateIndices(idxCount, use32Bit); + + if (use32Bit) { + std::memcpy(mesh->getIndexData(), allIndices.data(), + idxCount * sizeof(u32)); + } else { + auto* dst = reinterpret_cast(mesh->getIndexData()); + for (i32 j = 0; j < idxCount; ++j) + dst[j] = static_cast(allIndices[j]); + } + } + + mesh->setTopology(PrimitiveTopology::Triangles); + mesh->calculateBounds(); + + i32 meshIdx = model.addMesh(mesh); + m_meshIndexMap.insert_or_assign(fbxMesh->element.typed_id, meshIdx); + } + } + + /// Builds a vertex at the given face index and adds it to the vertex buffer. + /// Returns the hash for deduplication. + size_t buildVertex( + ufbx_mesh* fbxMesh, int idx, ufbx_skin_deformer* skinDeformer, bool isSkinned, + bool hasUV, bool hasTangent, bool hasColor, + i32 stride, i32 positionOffset, i32 normalOffset, i32 texCoordOffset, + i32 colorOffset, i32 tangentOffset, i32 jointsOffset, i32 weightsOffset, + std::vector& vertexBytes, std::unordered_map& vtxMap) + { + // Read vertex attributes. + ufbx_vec3 pos = readVec3(fbxMesh->vertex_position, idx); + ufbx_vec3 normal = fbxMesh->vertex_normal.exists + ? readVec3(fbxMesh->vertex_normal, idx) + : ufbx_vec3{{{0, 1, 0}}}; + + ufbx_vec2 uv = {}; + if (hasUV) { + uv = readVec2(fbxMesh->uv_sets.data[0].vertex_uv, idx); + // FBX uses bottom-left UV origin (V=0 at bottom), flip to match + // GLTF/renderer convention (V=0 at top). + uv.y = 1.0 - uv.y; + } + + ufbx_vec3 tangent = {{{1, 0, 0}}}; + if (hasTangent) + tangent = readVec3(fbxMesh->uv_sets.data[0].vertex_tangent, idx); + + ufbx_vec4 color = {{{1, 1, 1, 1}}}; + if (hasColor) { + if (fbxMesh->color_sets.count > 0 && fbxMesh->color_sets.data[0].vertex_color.exists) + color = readVec4(fbxMesh->color_sets.data[0].vertex_color, idx); + else if (fbxMesh->vertex_color.exists) + color = readVec4(fbxMesh->vertex_color, idx); + } + + // Skinning data. + u16 joints[4] = {}; + f32 weights[4] = {}; + if (isSkinned && skinDeformer) { + uint32_t vertexIndex = fbxMesh->vertex_indices.data[idx]; + getSkinWeights(skinDeformer, static_cast(vertexIndex), joints, weights); + } + + // Compute hash for deduplication. + size_t hash = hashVertex(pos, normal, uv, tangent, color, isSkinned, joints, weights); + + if (mapFind(vtxMap, hash) != nullptr) + return hash; + + // Add new vertex. + i32 newIndex = static_cast(vertexBytes.size() / stride); + + // Extend buffer. + size_t oldCount = vertexBytes.size(); + vertexBytes.resize(oldCount + stride, 0); + u8* vertex = &vertexBytes[oldCount]; + + // Position. + *reinterpret_cast(vertex + positionOffset) = + math::Vector3(static_cast(pos.x), static_cast(pos.y), static_cast(pos.z)); + + // Normal. + *reinterpret_cast(vertex + normalOffset) = + math::Vector3(static_cast(normal.x), static_cast(normal.y), static_cast(normal.z)); + + // TexCoord. + *reinterpret_cast(vertex + texCoordOffset) = + math::Vector2(static_cast(uv.x), static_cast(uv.y)); + + // Color (pack to RGBA8). + u8 cr = static_cast(std::clamp(static_cast(color.x) * 255.0f, 0.0f, 255.0f)); + u8 cg = static_cast(std::clamp(static_cast(color.y) * 255.0f, 0.0f, 255.0f)); + u8 cb = static_cast(std::clamp(static_cast(color.z) * 255.0f, 0.0f, 255.0f)); + u8 ca = static_cast(std::clamp(static_cast(color.w) * 255.0f, 0.0f, 255.0f)); + *reinterpret_cast(vertex + colorOffset) = + static_cast(cr) | (static_cast(cg) << 8) | + (static_cast(cb) << 16) | (static_cast(ca) << 24); + + // Tangent. + *reinterpret_cast(vertex + tangentOffset) = + math::Vector3(static_cast(tangent.x), static_cast(tangent.y), static_cast(tangent.z)); + + // Skinning. + if (isSkinned) { + auto* jointsDst = reinterpret_cast(vertex + jointsOffset); + jointsDst[0] = joints[0]; jointsDst[1] = joints[1]; + jointsDst[2] = joints[2]; jointsDst[3] = joints[3]; + *reinterpret_cast(vertex + weightsOffset) = + math::Vector4(weights[0], weights[1], weights[2], weights[3]); + } + + vtxMap.insert_or_assign(hash, newIndex); + return hash; + } + + static ufbx_vec3 readVec3(ufbx_vertex_vec3 attrib, int idx) { + uint32_t valueIdx = attrib.indices.data[idx]; + return attrib.values.data[valueIdx]; + } + + static ufbx_vec2 readVec2(ufbx_vertex_vec2 attrib, int idx) { + uint32_t valueIdx = attrib.indices.data[idx]; + return attrib.values.data[valueIdx]; + } + + static ufbx_vec4 readVec4(ufbx_vertex_vec4 attrib, int idx) { + uint32_t valueIdx = attrib.indices.data[idx]; + return attrib.values.data[valueIdx]; + } + + static void getSkinWeights(ufbx_skin_deformer* skin, int vertexIndex, + u16 joints[4], f32 weights[4]) + { + if (static_cast(vertexIndex) >= skin->vertices.count) + return; + + ufbx_skin_vertex* skinVertex = &skin->vertices.data[vertexIndex]; + int numWeights = static_cast(skinVertex->num_weights); + if (numWeights == 0) + return; + + // Collect weights (top 16 max). + int entryCount = std::min(numWeights, 16); + u16 entryJoints[16] = {}; + f32 entryWeights[16] = {}; + + for (int w = 0; w < entryCount; ++w) { + ufbx_skin_weight* skinWeight = + &skin->weights.data[skinVertex->weight_begin + static_cast(w)]; + uint32_t clusterIdx = skinWeight->cluster_index; + + // Use cluster index directly as skeleton joint index. + entryJoints[w] = static_cast(clusterIdx); + entryWeights[w] = static_cast(skinWeight->weight); + } + + // Simple selection of top 4 weights. + int top = std::min(entryCount, 4); + for (int a = 0; a < top; ++a) { + // Find max remaining. + int maxIdx = a; + for (int b = a + 1; b < entryCount; ++b) { + if (entryWeights[b] > entryWeights[maxIdx]) + maxIdx = b; + } + if (maxIdx != a) { + std::swap(entryJoints[a], entryJoints[maxIdx]); + std::swap(entryWeights[a], entryWeights[maxIdx]); + } + + joints[a] = entryJoints[a]; + weights[a] = entryWeights[a]; + } + + // Normalize weights. + f32 sum = weights[0] + weights[1] + weights[2] + weights[3]; + if (sum > 0) { + f32 invSum = 1.0f / sum; + weights[0] *= invSum; + weights[1] *= invSum; + weights[2] *= invSum; + weights[3] *= invSum; + } + } + + static size_t hashVertex(ufbx_vec3 pos, ufbx_vec3 normal, ufbx_vec2 uv, + ufbx_vec3 tangent, ufbx_vec4 color, + bool isSkinned, const u16 joints[4], const f32 weights[4]) + { + size_t hash = 17; + auto hf = [](f32 v) { return std::hash{}(v); }; + hash = hash * 31 + hf(static_cast(pos.x)); + hash = hash * 31 + hf(static_cast(pos.y)); + hash = hash * 31 + hf(static_cast(pos.z)); + hash = hash * 31 + hf(static_cast(normal.x)); + hash = hash * 31 + hf(static_cast(normal.y)); + hash = hash * 31 + hf(static_cast(normal.z)); + hash = hash * 31 + hf(static_cast(uv.x)); + hash = hash * 31 + hf(static_cast(uv.y)); + hash = hash * 31 + hf(static_cast(tangent.x)); + hash = hash * 31 + hf(static_cast(tangent.y)); + hash = hash * 31 + hf(static_cast(tangent.z)); + hash = hash * 31 + hf(static_cast(color.x)); + hash = hash * 31 + hf(static_cast(color.y)); + hash = hash * 31 + hf(static_cast(color.z)); + hash = hash * 31 + hf(static_cast(color.w)); + + if (isSkinned) { + hash = hash * 31 + static_cast(joints[0]); + hash = hash * 31 + static_cast(joints[1]); + hash = hash * 31 + static_cast(joints[2]); + hash = hash * 31 + static_cast(joints[3]); + hash = hash * 31 + hf(weights[0]); + hash = hash * 31 + hf(weights[1]); + hash = hash * 31 + hf(weights[2]); + hash = hash * 31 + hf(weights[3]); + } + + return hash; + } + + // ----------------------------------------------------------------------- + // Nodes + // ----------------------------------------------------------------------- + + void loadNodes(Model& model) { + // Pass 1: create all bones. + for (size_t i = 0; i < m_scene->nodes.count; ++i) { + ufbx_node* node = m_scene->nodes.data[i]; + auto* bone = new ModelBone(); + + if (node->element.name.data && node->element.name.length > 0) + bone->setName(utf8FromUfbx(node->element.name)); + + // Extract TRS from local transform. + ufbx_transform t = node->local_transform; + bone->translation = math::Vector3( + static_cast(t.translation.x), + static_cast(t.translation.y), + static_cast(t.translation.z)); + bone->rotation = math::Quaternion( + static_cast(t.rotation.x), + static_cast(t.rotation.y), + static_cast(t.rotation.z), + static_cast(t.rotation.w)); + bone->scale = math::Vector3( + static_cast(t.scale.x), + static_cast(t.scale.y), + static_cast(t.scale.z)); + bone->updateLocalTransform(); + + // Mesh reference. + if (node->mesh) { + auto mit = mapFind(m_meshIndexMap, node->mesh->element.typed_id); + if (mit != nullptr) + bone->meshIndex = *mit; + } + + // Skin reference. + if (node->mesh && node->mesh->skin_deformers.count > 0) { + ufbx_skin_deformer* skinDef = node->mesh->skin_deformers.data[0]; + auto sit = mapFind(m_skinIndexMap, skinDef->element.typed_id); + if (sit != nullptr) + bone->skinIndex = *sit; + // Note: skin index may be set later when loadSkins runs. + } + + i32 boneIdx = model.addBone(bone); + m_nodeToBoneIndex.insert_or_assign(node->element.typed_id, boneIdx); + } + + // Pass 2: set parent relationships. + for (size_t i = 0; i < m_scene->nodes.count; ++i) { + ufbx_node* node = m_scene->nodes.data[i]; + if (node->parent) { + auto pit = mapFind(m_nodeToBoneIndex, node->parent->element.typed_id); + if (pit != nullptr) + model.bones()[i]->parentIndex = *pit; + } + } + } + + // ----------------------------------------------------------------------- + // Skins + // ----------------------------------------------------------------------- + + void loadSkins(Model& model) { + for (size_t i = 0; i < m_scene->skin_deformers.count; ++i) { + ufbx_skin_deformer* skinDef = m_scene->skin_deformers.data[i]; + auto* skin = new ModelSkin(); + + if (skinDef->element.name.data && skinDef->element.name.length > 0) + skin->setName(utf8FromUfbx(skinDef->element.name)); + + // Process clusters (joints). + for (size_t j = 0; j < skinDef->clusters.count; ++j) { + ufbx_skin_cluster* cluster = skinDef->clusters.data[j]; + if (!cluster || !cluster->bone_node) + continue; + + i32 jointIndex = -1; + auto nit = mapFind(m_nodeToBoneIndex, cluster->bone_node->element.typed_id); + if (nit != nullptr) + jointIndex = *nit; + + if (jointIndex < 0) + continue; + + // Convert geometry_to_bone matrix to our inverse bind matrix. + math::Matrix4 ibm = convertMatrix(cluster->geometry_to_bone); + + skin->addJoint(jointIndex, ibm); + + // Also set on the bone. + if (jointIndex >= 0 && static_cast(jointIndex) < model.bones().size()) + model.bones()[jointIndex]->inverseBindMatrix = ibm; + } + + i32 skinIdx = model.addSkin(skin); + m_skinIndexMap.insert_or_assign(skinDef->element.typed_id, skinIdx); + } + + // Update bone skin indices now that skins are loaded. + for (size_t i = 0; i < m_scene->nodes.count; ++i) { + ufbx_node* node = m_scene->nodes.data[i]; + if (node->mesh && node->mesh->skin_deformers.count > 0) { + ufbx_skin_deformer* skinDef = node->mesh->skin_deformers.data[0]; + auto sit = mapFind(m_skinIndexMap, skinDef->element.typed_id); + if (sit != nullptr) { + if (i < model.bones().size()) + model.bones()[i]->skinIndex = *sit; + } + } + } + } + + // ----------------------------------------------------------------------- + // Animations + // ----------------------------------------------------------------------- + + void loadAnimations(Model& model) { + for (size_t i = 0; i < m_scene->anim_stacks.count; ++i) { + ufbx_anim_stack* stack = m_scene->anim_stacks.data[i]; + auto* animation = new ModelAnimation(); + + if (stack->element.name.data && stack->element.name.length > 0) + animation->setName(utf8FromUfbx(stack->element.name)); + + // Bake the animation - this pre-computes T/R/S keyframes per node + // in the target coordinate system (Y-up), handling Euler-to-quaternion + // conversion and layer blending automatically. + ufbx_bake_opts bakeOpts = {}; + bakeOpts.resample_rate = 30.0; + bakeOpts.minimum_sample_rate = 30.0; + bakeOpts.max_keyframe_segments = 1024; + + ufbx_error bakeError = {}; + ufbx_baked_anim* bakedAnim = ufbx_bake_anim( + m_scene, stack->anim, &bakeOpts, &bakeError); + if (!bakedAnim) { + delete animation; + continue; + } + + for (size_t ni = 0; ni < bakedAnim->nodes.count; ++ni) { + ufbx_baked_node* bakedNode = &bakedAnim->nodes.data[ni]; + + // Map baked node to our bone index. + auto bit = mapFind(m_nodeToBoneIndex, bakedNode->typed_id); + if (bit == nullptr) + continue; + i32 boneIdx = *bit; + + // Translation channel. + if (bakedNode->translation_keys.count > 0 && !bakedNode->constant_translation) { + auto* channel = new AnimationChannel(); + channel->targetBone = boneIdx; + channel->path = AnimationPath::Translation; + channel->interpolation = detectInterpolationVec3(bakedNode->translation_keys); + + for (size_t ki = 0; ki < bakedNode->translation_keys.count; ++ki) { + ufbx_baked_vec3* key = &bakedNode->translation_keys.data[ki]; + channel->addKeyframe(static_cast(key->time), + math::Vector4(static_cast(key->value.x), + static_cast(key->value.y), + static_cast(key->value.z), 0)); + } + animation->addChannel(channel); + } + + // Rotation channel. + if (bakedNode->rotation_keys.count > 0 && !bakedNode->constant_rotation) { + auto* channel = new AnimationChannel(); + channel->targetBone = boneIdx; + channel->path = AnimationPath::Rotation; + channel->interpolation = detectInterpolationQuat(bakedNode->rotation_keys); + + for (size_t ki = 0; ki < bakedNode->rotation_keys.count; ++ki) { + ufbx_baked_quat* key = &bakedNode->rotation_keys.data[ki]; + channel->addKeyframe(static_cast(key->time), + math::Vector4(static_cast(key->value.x), + static_cast(key->value.y), + static_cast(key->value.z), + static_cast(key->value.w))); + } + animation->addChannel(channel); + } + + // Scale channel. + if (bakedNode->scale_keys.count > 0 && !bakedNode->constant_scale) { + auto* channel = new AnimationChannel(); + channel->targetBone = boneIdx; + channel->path = AnimationPath::Scale; + channel->interpolation = detectInterpolationVec3(bakedNode->scale_keys); + + for (size_t ki = 0; ki < bakedNode->scale_keys.count; ++ki) { + ufbx_baked_vec3* key = &bakedNode->scale_keys.data[ki]; + channel->addKeyframe(static_cast(key->time), + math::Vector4(static_cast(key->value.x), + static_cast(key->value.y), + static_cast(key->value.z), 0)); + } + animation->addChannel(channel); + } + } + + ufbx_free_baked_anim(bakedAnim); + + animation->calculateDuration(); + model.addAnimation(animation); + } + } + + /// Detect interpolation mode from baked vec3 keyframe flags. + static AnimationInterpolation detectInterpolationVec3(ufbx_baked_vec3_list keys) { + for (size_t i = 0; i < keys.count; ++i) { + uint32_t flags = keys.data[i].flags; + if ((flags & UFBX_BAKED_KEY_STEP_LEFT) || (flags & UFBX_BAKED_KEY_STEP_RIGHT)) + return AnimationInterpolation::Step; + } + return AnimationInterpolation::Linear; + } + + /// Detect interpolation mode from baked quaternion keyframe flags. + static AnimationInterpolation detectInterpolationQuat(ufbx_baked_quat_list keys) { + for (size_t i = 0; i < keys.count; ++i) { + uint32_t flags = keys.data[i].flags; + if ((flags & UFBX_BAKED_KEY_STEP_LEFT) || (flags & UFBX_BAKED_KEY_STEP_RIGHT)) + return AnimationInterpolation::Step; + } + return AnimationInterpolation::Linear; + } + + // ----------------------------------------------------------------------- + // Matrix Conversion + // ----------------------------------------------------------------------- + + /// Converts a ufbx 3x4 matrix to our 4x4 math::Matrix4. + /// ufbx column -> math::Matrix4 row (same pattern as GltfLoader's column-major transpose). + static math::Matrix4 convertMatrix(ufbx_matrix m) { + return math::Matrix4{{ + { static_cast(m.m00), static_cast(m.m10), static_cast(m.m20), 0 }, + { static_cast(m.m01), static_cast(m.m11), static_cast(m.m21), 0 }, + { static_cast(m.m02), static_cast(m.m12), static_cast(m.m22), 0 }, + { static_cast(m.m03), static_cast(m.m13), static_cast(m.m23), 1 } + }}; + } +}; + +} // namespace draco::model::fbx diff --git a/Engine/cpp/Runtime/Rendering/Model/FBX/ufbx_impl.cpp b/Engine/cpp/Runtime/Rendering/Model/FBX/ufbx_impl.cpp new file mode 100644 index 00000000..a037c017 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/FBX/ufbx_impl.cpp @@ -0,0 +1,18 @@ +// Compile ufbx as C++ so we don't need to enable C in the project. +// ufbx is written as clean C that compiles as C++ without issues. +#ifdef _MSC_VER +#pragma warning(push, 0) +#endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Weverything" +#endif + +#include "ufbx.c" + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/Engine/cpp/Runtime/Rendering/Model/GLTF/GltfLoader.cppm b/Engine/cpp/Runtime/Rendering/Model/GLTF/GltfLoader.cppm new file mode 100644 index 00000000..3ac8409d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/GLTF/GltfLoader.cppm @@ -0,0 +1,855 @@ +/// GLTF/GLB model loader using cgltf. + +module; +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cgltf.h" + +export module model:gltf; + +import core; +import :model; +import :io; +import image; +import image.io; + +export namespace draco::model::gltf { + +using namespace draco; +using namespace draco::model; + +// cgltf hands back char* (UTF-8); the engine std::u8string is UTF-8 too, so this just +// wraps the bytes in an owned std::u8string - no transcoding. +inline std::u8string utf8FromC(const char* s) { + if (!s) return std::u8string{}; + return std::u8string(std::u8string_view(reinterpret_cast(s))); +} + +/// Loads GLTF and GLB model files using cgltf. +class GltfLoader : public io::ModelLoader { +public: + GltfLoader() = default; + + ~GltfLoader() override { + if (m_data) { + cgltf_free(m_data); + m_data = nullptr; + } + } + + bool supportsExtension(std::u8string_view ext) const override { + return caseInsensitiveEquals(ext, u8".gltf") || caseInsensitiveEquals(ext, u8".glb"); + } + + // Non-copyable. + GltfLoader(const GltfLoader&) = delete; + GltfLoader& operator=(const GltfLoader&) = delete; + + /// Load a GLTF or GLB file. + ModelLoadResult load(std::u8string_view path, Model& model) override { + // Free previous data. + if (m_data) { + cgltf_free(m_data); + m_data = nullptr; + } + + // Extract base path for loading external resources. + const std::u8string narrowStorage = std::u8string(path); + const char* narrowPath = reinterpret_cast(narrowStorage.c_str()); + std::filesystem::path filePath(narrowPath); + m_basePath = filePath.parent_path().string(); + + // Parse the file -- explicitly set type for .glb since auto-detect can fail. + cgltf_options options = {}; + + if (endsWithCI(path, u8".glb")) + options.type = cgltf_file_type_glb; + // json_token_count = 0 lets cgltf size the JSON token pool itself (a counting pass first). + // A fixed cap (was 4096) silently fails to parse larger glTFs with invalid_json (result=3) - + // e.g. the Quaternius character (~1MB, >4096 JSON tokens). + options.json_token_count = 0; + + // Read file data ourselves (cgltf's fopen can fail with certain path formats on Windows). + std::ifstream file(narrowPath, std::ios::binary | std::ios::ate); + if (!file.is_open()) + return ModelLoadResult::FileNotFound; + + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector fileBytes(static_cast(fileSize)); + if (!file.read(reinterpret_cast(fileBytes.data()), fileSize)) + return ModelLoadResult::FileNotFound; + file.close(); + + cgltf_result parseResult = cgltf_parse(&options, fileBytes.data(), + fileBytes.size(), &m_data); + if (parseResult != cgltf_result_success) { + fprintf(stderr, " cgltf_parse failed: result=%d, size=%zu, path=%s\n", + static_cast(parseResult), fileBytes.size(), narrowPath); + return ModelLoadResult::ParseError; + } + + // Load buffer data (for external .bin references; GLB has embedded buffers). + cgltf_result loadResult = cgltf_load_buffers(&options, m_data, narrowPath); + if (loadResult != cgltf_result_success) + return ModelLoadResult::InvalidData; + + // Validate (optional -- some exporters produce spec-violating but loadable files). + cgltf_result validateResult = cgltf_validate(m_data); + if (validateResult != cgltf_result_success) { + fprintf(stderr, " cgltf_validate warning: result=%d (continuing anyway)\n", + static_cast(validateResult)); + } + + // GLTF is always Y-up per specification. + model.originalUpAxis = CoordinateAxis::PositiveY; + + // Convert to Model. + loadMaterials(model); + loadTextures(model); + loadMeshes(model); + loadNodes(model); + loadSkins(model); + loadAnimations(model); + + model.buildBoneHierarchy(); + model.calculateBounds(); + + return ModelLoadResult::Ok; + } + +private: + cgltf_data* m_data = nullptr; + std::string m_basePath; + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + static bool caseInsensitiveEquals(std::u8string_view a, std::u8string_view b) { + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); ++i) { + if (std::tolower(static_cast(a[i])) != + std::tolower(static_cast(b[i]))) + return false; + } + return true; + } + + static bool endsWithCI(std::u8string_view str, std::u8string_view suffix) { + if (str.size() < suffix.size()) return false; + return caseInsensitiveEquals(str.substr(str.size() - suffix.size(), suffix.size()), suffix); + } + + // ----------------------------------------------------------------------- + // Materials + // ----------------------------------------------------------------------- + + void loadMaterials(Model& model) { + for (cgltf_size i = 0; i < m_data->materials_count; ++i) { + cgltf_material* mat = &m_data->materials[i]; + auto* material = new ModelMaterial(); + + if (mat->name) + material->setName(utf8FromC(mat->name)); + else { + char buf[64]; + snprintf(buf, sizeof(buf), "m_texture%zu", i); + material->setName(utf8FromC(buf)); + } + + // PBR Metallic Roughness. + if (mat->has_pbr_metallic_roughness) { + cgltf_pbr_metallic_roughness* pbr = &mat->pbr_metallic_roughness; + + material->baseColorFactor = math::Vector4( + pbr->base_color_factor[0], + pbr->base_color_factor[1], + pbr->base_color_factor[2], + pbr->base_color_factor[3] + ); + + if (pbr->base_color_texture.texture) + material->baseColorTextureIndex = static_cast( + cgltf_texture_index(m_data, pbr->base_color_texture.texture)); + + material->metallicFactor = pbr->metallic_factor; + material->roughnessFactor = pbr->roughness_factor; + + if (pbr->metallic_roughness_texture.texture) + material->metallicRoughnessTextureIndex = static_cast( + cgltf_texture_index(m_data, pbr->metallic_roughness_texture.texture)); + } + + // Normal texture. + if (mat->normal_texture.texture) { + material->normalTextureIndex = static_cast( + cgltf_texture_index(m_data, mat->normal_texture.texture)); + material->normalScale = mat->normal_texture.scale; + } + + // Occlusion texture. + if (mat->occlusion_texture.texture) { + material->occlusionTextureIndex = static_cast( + cgltf_texture_index(m_data, mat->occlusion_texture.texture)); + material->occlusionStrength = mat->occlusion_texture.scale; + } + + // Emissive. + material->emissiveFactor = math::Vector3( + mat->emissive_factor[0], + mat->emissive_factor[1], + mat->emissive_factor[2] + ); + + if (mat->emissive_texture.texture) + material->emissiveTextureIndex = static_cast( + cgltf_texture_index(m_data, mat->emissive_texture.texture)); + + // Alpha. + switch (mat->alpha_mode) { + case cgltf_alpha_mode_mask: material->alphaMode = AlphaMode::Mask; break; + case cgltf_alpha_mode_blend: material->alphaMode = AlphaMode::Blend; break; + default: material->alphaMode = AlphaMode::Opaque; break; + } + + material->alphaCutoff = mat->alpha_cutoff; + material->doubleSided = mat->double_sided != 0; + + model.addMaterial(material); + } + } + + // ----------------------------------------------------------------------- + // Textures + // ----------------------------------------------------------------------- + + static TextureWrap wrapModeFromCgltf(cgltf_wrap_mode mode) { + switch (mode) { + case cgltf_wrap_mode_clamp_to_edge: return TextureWrap::ClampToEdge; + case cgltf_wrap_mode_mirrored_repeat: return TextureWrap::MirroredRepeat; + default: return TextureWrap::Repeat; + } + } + + static TextureMinFilter minFilterFromCgltf(cgltf_filter_type filter) { + switch (filter) { + case cgltf_filter_type_nearest: return TextureMinFilter::Nearest; + case cgltf_filter_type_linear: return TextureMinFilter::Linear; + case cgltf_filter_type_nearest_mipmap_nearest: return TextureMinFilter::NearestMipmapNearest; + case cgltf_filter_type_linear_mipmap_nearest: return TextureMinFilter::LinearMipmapNearest; + case cgltf_filter_type_nearest_mipmap_linear: return TextureMinFilter::NearestMipmapLinear; + case cgltf_filter_type_linear_mipmap_linear: return TextureMinFilter::LinearMipmapLinear; + default: return TextureMinFilter::Nearest; + } + } + + static TextureMagFilter magFilterFromCgltf(cgltf_filter_type filter) { + switch (filter) { + case cgltf_filter_type_nearest: + case cgltf_filter_type_nearest_mipmap_nearest: + case cgltf_filter_type_nearest_mipmap_linear: + return TextureMagFilter::Nearest; + case cgltf_filter_type_linear: + case cgltf_filter_type_linear_mipmap_nearest: + case cgltf_filter_type_linear_mipmap_linear: + return TextureMagFilter::Linear; + default: + return TextureMagFilter::Nearest; + } + } + + void loadTextures(Model& model) { + + for (cgltf_size i = 0; i < m_data->textures_count; ++i) { + cgltf_texture* tex = &m_data->textures[i]; + auto* texture = new ModelTexture(); + + if (tex->name) + texture->setName(utf8FromC(tex->name)); + else if (tex->image && tex->image->name) + texture->setName(utf8FromC(tex->image->name)); + + if (tex->sampler) + texture->samplerIndex = static_cast( + cgltf_sampler_index(m_data, tex->sampler)); + + if (tex->image) { + cgltf_image* gltfImage = tex->image; + + if (gltfImage->mime_type) + texture->mimeType = utf8FromC(gltfImage->mime_type); + + if (gltfImage->uri) { + const char* uriC = gltfImage->uri; + texture->setUri(utf8FromC(uriC)); + + if (std::strncmp(uriC, "data:", 5) == 0) { + // Base64 encoded data URI (e.g., "data:image/png;base64,iVBORw0...") + draco::image::Image img; + if (loadImageFromDataUri(uriC, img)) + storeImageData(img, texture); + } else { + // External image file. + std::filesystem::path imagePath = + std::filesystem::path(m_basePath) / uriC; + const std::string imgPath = imagePath.string(); + draco::image::Image img; + if (image::io::loadImage(utf8FromC(imgPath.c_str()), img) == ErrorCode::Ok) + storeImageData(img, texture); + } + } else if (gltfImage->buffer_view) { + // Embedded image data in buffer. + const uint8_t* bufferData = cgltf_buffer_view_data(gltfImage->buffer_view); + size_t size = gltfImage->buffer_view->size; + if (bufferData && size > 0) { + draco::image::Image img; + if (image::io::loadImageFromMemory( + std::span(bufferData, size), img) == ErrorCode::Ok) + storeImageData(img, texture); + } + } + } + + model.addTexture(texture); + } + + // Load samplers. + for (cgltf_size i = 0; i < m_data->samplers_count; ++i) { + cgltf_sampler* samp = &m_data->samplers[i]; + TextureSampler sampler{}; + + sampler.wrapS = wrapModeFromCgltf(samp->wrap_s); + sampler.wrapT = wrapModeFromCgltf(samp->wrap_t); + sampler.minFilter = minFilterFromCgltf(samp->min_filter); + sampler.magFilter = magFilterFromCgltf(samp->mag_filter); + + model.addSampler(sampler); + } + + // Ensure all textures have a valid sampler. + // Per glTF spec, textures without an explicit sampler default to Repeat wrapping. + for (auto* texture : model.textures()) { + if (texture->samplerIndex < 0) { + // Add a default glTF sampler (Repeat/Repeat) and assign it. + TextureSampler defaultSampler{}; // Defaults: WrapS=Repeat, WrapT=Repeat. + texture->samplerIndex = model.addSampler(defaultSampler); + } + } + } + + // ----------------------------------------------------------------------- + // Image helpers + // ----------------------------------------------------------------------- + + static TexturePixelFormat convertPixelFormat(draco::image::PixelFormat fmt) { + switch (fmt) { + case draco::image::PixelFormat::R8: return TexturePixelFormat::R8; + case draco::image::PixelFormat::RG8: return TexturePixelFormat::RG8; + case draco::image::PixelFormat::RGB8: return TexturePixelFormat::RGB8; + case draco::image::PixelFormat::RGBA8: return TexturePixelFormat::RGBA8; + case draco::image::PixelFormat::BGR8: return TexturePixelFormat::BGR8; + case draco::image::PixelFormat::BGRA8: return TexturePixelFormat::BGRA8; + default: return TexturePixelFormat::Unknown; + } + } + + static void storeImageData(const draco::image::Image& image, ModelTexture* texture) { + texture->width = static_cast(image.width()); + texture->height = static_cast(image.height()); + texture->pixelFormat = convertPixelFormat(image.format()); + + auto data = image.pixelData(); + if (data.data() && data.size() > 0) { + auto* copy = new u8[data.size()]; + std::memcpy(copy, data.data(), data.size()); + texture->setData(copy, static_cast(data.size())); + } + } + + bool loadImageFromDataUri(const char* dataUri, draco::image::Image& outImage) { + + // Format: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA... + const char* comma = std::strchr(dataUri, ','); + if (!comma) + return false; + + const char* base64Data = comma + 1; + + // Use cgltf's base64 decoder. + size_t estimatedSize = (std::strlen(base64Data) * 3) / 4; + void* decodedData = nullptr; + cgltf_options options = {}; + + // cgltf_load_buffer_base64 expects a null-terminated string. + if (cgltf_load_buffer_base64(&options, estimatedSize, base64Data, &decodedData) + != cgltf_result_success) + return false; + + bool ok = image::io::loadImageFromMemory( + std::span(static_cast(decodedData), estimatedSize), + outImage) == ErrorCode::Ok; + + std::free(decodedData); + return ok; + } + + // ----------------------------------------------------------------------- + // Meshes + // ----------------------------------------------------------------------- + + void loadMeshes(Model& model) { + for (cgltf_size i = 0; i < m_data->meshes_count; ++i) { + cgltf_mesh* meshData = &m_data->meshes[i]; + auto* mesh = new ModelMesh(); + + if (meshData->name) + mesh->setName(utf8FromC(meshData->name)); + + if (meshData->primitives_count > 0) + loadMeshPrimitives(meshData, mesh); + + mesh->calculateBounds(); + model.addMesh(mesh); + } + } + + /// Loads all primitives of a GLTF mesh, merging their vertex/index data into one ModelMesh. + /// Each primitive becomes a ModelMeshPart with correct index offsets and material index. + void loadMeshPrimitives(cgltf_mesh* meshData, ModelMesh* mesh) { + cgltf_primitive* firstPrim = &meshData->primitives[0]; + + // Phase 1: Detect skinning and available attributes from first primitive. + bool isSkinned = false; + bool hasNormals = false; + bool hasTangents = false; + bool hasTexCoords = false; + for (cgltf_size a = 0; a < firstPrim->attributes_count; ++a) { + cgltf_attribute* attr = &firstPrim->attributes[a]; + if (attr->type == cgltf_attribute_type_joints && attr->index == 0) + isSkinned = true; + if (attr->type == cgltf_attribute_type_normal) + hasNormals = true; + if (attr->type == cgltf_attribute_type_tangent) + hasTangents = true; + if (attr->type == cgltf_attribute_type_texcoord && attr->index == 0) + hasTexCoords = true; + } + (void)hasTexCoords; // Used only for detection, not conditionally. + + mesh->setHasNormals(hasNormals); + mesh->setHasTangents(hasTangents); + + i32 stride = 0; + i32 positionOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Position, VertexElementFormat::Float3, positionOffset)); + + i32 normalOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Normal, VertexElementFormat::Float3, normalOffset)); + + i32 texCoordOffset = stride; + stride += static_cast(sizeof(math::Vector2)); + mesh->addVertexElement(VertexElement(VertexSemantic::TexCoord, VertexElementFormat::Float2, texCoordOffset)); + + i32 colorOffset = stride; + stride += static_cast(sizeof(u32)); + mesh->addVertexElement(VertexElement(VertexSemantic::Color, VertexElementFormat::Byte4, colorOffset)); + + i32 tangentOffset = stride; + stride += static_cast(sizeof(math::Vector3)); + mesh->addVertexElement(VertexElement(VertexSemantic::Tangent, VertexElementFormat::Float3, tangentOffset)); + + i32 jointsOffset = 0; + i32 weightsOffset = 0; + if (isSkinned) { + jointsOffset = stride; + stride += static_cast(sizeof(u16) * 4); + mesh->addVertexElement(VertexElement(VertexSemantic::Joints, VertexElementFormat::UShort4, jointsOffset)); + + weightsOffset = stride; + stride += static_cast(sizeof(math::Vector4)); + mesh->addVertexElement(VertexElement(VertexSemantic::Weights, VertexElementFormat::Float4, weightsOffset)); + } + + // Phase 2: Count total vertices and indices across all primitives. + i32 totalVertexCount = 0; + i32 totalIndexCount = 0; + for (cgltf_size p = 0; p < meshData->primitives_count; ++p) { + cgltf_primitive* prim = &meshData->primitives[p]; + for (cgltf_size a = 0; a < prim->attributes_count; ++a) { + if (prim->attributes[a].type == cgltf_attribute_type_position) { + totalVertexCount += static_cast(prim->attributes[a].data->count); + break; + } + } + if (prim->indices) { + totalIndexCount += static_cast(prim->indices->count); + } else { + // Non-indexed primitive: we will generate sequential indices, one + // per position vertex. GLTF allows non-indexed geometry but we + // require indices for rendering (else the primitive is dropped). + for (cgltf_size a = 0; a < prim->attributes_count; ++a) { + if (prim->attributes[a].type == cgltf_attribute_type_position) { + totalIndexCount += static_cast(prim->attributes[a].data->count); + break; + } + } + } + } + + if (totalVertexCount == 0) + return; + + // Phase 3: Allocate buffers for all primitives combined. + bool use32Bit = totalIndexCount > 65535 || totalVertexCount > 65535; + mesh->allocateVertices(totalVertexCount, stride); + mesh->allocateIndices(totalIndexCount, use32Bit); + + u8* vertexData = mesh->getVertexData(); + u8* indexData = mesh->getIndexData(); + + // Phase 4: Load each primitive's data at correct offset. + i32 vertexOffset = 0; + i32 indexOffset = 0; + + for (cgltf_size p = 0; p < meshData->primitives_count; ++p) { + cgltf_primitive* prim = &meshData->primitives[p]; + + // Find accessors for this primitive. + cgltf_accessor* positionAccessor = nullptr; + cgltf_accessor* normalAccessor = nullptr; + cgltf_accessor* texCoordAccessor = nullptr; + cgltf_accessor* colorAccessor = nullptr; + cgltf_accessor* tangentAccessor = nullptr; + cgltf_accessor* jointsAccessor = nullptr; + cgltf_accessor* weightsAccessor = nullptr; + + for (cgltf_size a = 0; a < prim->attributes_count; ++a) { + cgltf_attribute* attr = &prim->attributes[a]; + switch (attr->type) { + case cgltf_attribute_type_position: positionAccessor = attr->data; break; + case cgltf_attribute_type_normal: normalAccessor = attr->data; break; + case cgltf_attribute_type_texcoord: + if (attr->index == 0) texCoordAccessor = attr->data; + break; + case cgltf_attribute_type_color: + if (attr->index == 0) colorAccessor = attr->data; + break; + case cgltf_attribute_type_tangent: tangentAccessor = attr->data; break; + case cgltf_attribute_type_joints: + if (attr->index == 0) jointsAccessor = attr->data; + break; + case cgltf_attribute_type_weights: + if (attr->index == 0) weightsAccessor = attr->data; + break; + default: break; + } + } + + if (!positionAccessor) + continue; + + i32 primVertexCount = static_cast(positionAccessor->count); + + // Fill vertex data at offset. + for (i32 v = 0; v < primVertexCount; ++v) { + u8* vertex = vertexData + (vertexOffset + v) * stride; + + // Position. + float pos[3] = {}; + cgltf_accessor_read_float(positionAccessor, static_cast(v), pos, 3); + *reinterpret_cast(vertex + positionOffset) = math::Vector3(pos[0], pos[1], pos[2]); + + // Normal. + if (normalAccessor) { + float normal[3] = {}; + cgltf_accessor_read_float(normalAccessor, static_cast(v), normal, 3); + *reinterpret_cast(vertex + normalOffset) = math::Vector3(normal[0], normal[1], normal[2]); + } else { + *reinterpret_cast(vertex + normalOffset) = math::Vector3(0, 1, 0); + } + + // TexCoord. + if (texCoordAccessor) { + float uv[2] = {}; + cgltf_accessor_read_float(texCoordAccessor, static_cast(v), uv, 2); + *reinterpret_cast(vertex + texCoordOffset) = math::Vector2(uv[0], uv[1]); + } + + // Color. + if (colorAccessor) { + float color[4] = { 1, 1, 1, 1 }; + cgltf_accessor_read_float(colorAccessor, static_cast(v), color, 4); + u8 cr = static_cast(std::clamp(color[0] * 255.0f, 0.0f, 255.0f)); + u8 cg = static_cast(std::clamp(color[1] * 255.0f, 0.0f, 255.0f)); + u8 cb = static_cast(std::clamp(color[2] * 255.0f, 0.0f, 255.0f)); + u8 ca = static_cast(std::clamp(color[3] * 255.0f, 0.0f, 255.0f)); + *reinterpret_cast(vertex + colorOffset) = + static_cast(cr) | + (static_cast(cg) << 8) | + (static_cast(cb) << 16) | + (static_cast(ca) << 24); + } else { + *reinterpret_cast(vertex + colorOffset) = 0xFFFFFFFF; + } + + // Tangent. + if (tangentAccessor) { + float tangent[4] = {}; + cgltf_accessor_read_float(tangentAccessor, static_cast(v), tangent, 4); + *reinterpret_cast(vertex + tangentOffset) = math::Vector3(tangent[0], tangent[1], tangent[2]); + } else { + *reinterpret_cast(vertex + tangentOffset) = math::Vector3(1, 0, 0); + } + + // Skinning data. + if (isSkinned && jointsAccessor && weightsAccessor) { + cgltf_uint joints[4] = {}; + cgltf_accessor_read_uint(jointsAccessor, static_cast(v), joints, 4); + auto* jointsDst = reinterpret_cast(vertex + jointsOffset); + jointsDst[0] = static_cast(joints[0]); + jointsDst[1] = static_cast(joints[1]); + jointsDst[2] = static_cast(joints[2]); + jointsDst[3] = static_cast(joints[3]); + + float weights[4] = {}; + cgltf_accessor_read_float(weightsAccessor, static_cast(v), weights, 4); + *reinterpret_cast(vertex + weightsOffset) = math::Vector4(weights[0], weights[1], weights[2], weights[3]); + } + } + + // Fill index data at offset (indices must be remapped by vertexOffset). + i32 primIndexCount = 0; + if (prim->indices) { + primIndexCount = static_cast(prim->indices->count); + + if (use32Bit) { + u32* indices = reinterpret_cast(indexData + indexOffset * 4); + for (i32 idx = 0; idx < primIndexCount; ++idx) + indices[idx] = static_cast(cgltf_accessor_read_index(prim->indices, static_cast(idx))) + static_cast(vertexOffset); + } else { + u16* indices = reinterpret_cast(indexData + indexOffset * 2); + for (i32 idx = 0; idx < primIndexCount; ++idx) + indices[idx] = static_cast(static_cast(cgltf_accessor_read_index(prim->indices, static_cast(idx))) + static_cast(vertexOffset)); + } + } else { + // Non-indexed primitive: generate sequential indices (0, 1, 2, ...). + primIndexCount = primVertexCount; + if (use32Bit) { + u32* indices = reinterpret_cast(indexData + indexOffset * 4); + for (i32 idx = 0; idx < primIndexCount; ++idx) + indices[idx] = static_cast(vertexOffset + idx); + } else { + u16* indices = reinterpret_cast(indexData + indexOffset * 2); + for (i32 idx = 0; idx < primIndexCount; ++idx) + indices[idx] = static_cast(vertexOffset + idx); + } + } + + // Add mesh part with correct offset. + i32 materialIndex = -1; + if (prim->material) + materialIndex = static_cast(cgltf_material_index(m_data, prim->material)); + + if (primIndexCount > 0) + mesh->addPart(ModelMeshPart(indexOffset, primIndexCount, materialIndex)); + + vertexOffset += primVertexCount; + indexOffset += primIndexCount; + } + + // Set topology from first primitive. + switch (firstPrim->type) { + case cgltf_primitive_type_triangles: mesh->setTopology(PrimitiveTopology::Triangles); break; + case cgltf_primitive_type_triangle_strip: mesh->setTopology(PrimitiveTopology::TriangleStrip); break; + case cgltf_primitive_type_lines: mesh->setTopology(PrimitiveTopology::Lines); break; + case cgltf_primitive_type_line_strip: mesh->setTopology(PrimitiveTopology::LineStrip); break; + case cgltf_primitive_type_points: mesh->setTopology(PrimitiveTopology::Points); break; + default: mesh->setTopology(PrimitiveTopology::Triangles); break; + } + } + + // ----------------------------------------------------------------------- + // Nodes + // ----------------------------------------------------------------------- + + void loadNodes(Model& model) { + // First pass: create all bones. + for (cgltf_size i = 0; i < m_data->nodes_count; ++i) { + cgltf_node* node = &m_data->nodes[i]; + auto* bone = new ModelBone(); + + if (node->name) + bone->setName(utf8FromC(node->name)); + + // Translation. + if (node->has_translation) + bone->translation = math::Vector3(node->translation[0], node->translation[1], node->translation[2]); + + // Rotation. + if (node->has_rotation) + bone->rotation = math::Quaternion(node->rotation[0], node->rotation[1], node->rotation[2], node->rotation[3]); + + // Scale. + if (node->has_scale) + bone->scale = math::Vector3(node->scale[0], node->scale[1], node->scale[2]); + + // Matrix. + if (node->has_matrix) { + // GLTF stores column-major for column-vector convention. + // Transpose to row-vector convention by reading flat array directly into row-major storage. + std::memcpy(bone->localTransform.data(), node->matrix, sizeof(float) * 16); + } else { + bone->updateLocalTransform(); + } + + if (node->mesh) + bone->meshIndex = static_cast(cgltf_mesh_index(m_data, node->mesh)); + + if (node->skin) + bone->skinIndex = static_cast(cgltf_skin_index(m_data, node->skin)); + + model.addBone(bone); + } + + // Second pass: set up parent relationships. + for (cgltf_size i = 0; i < m_data->nodes_count; ++i) { + cgltf_node* node = &m_data->nodes[i]; + if (node->parent) + model.bones()[i]->parentIndex = static_cast(cgltf_node_index(m_data, node->parent)); + } + } + + // ----------------------------------------------------------------------- + // Skins + // ----------------------------------------------------------------------- + + void loadSkins(Model& model) { + for (cgltf_size i = 0; i < m_data->skins_count; ++i) { + cgltf_skin* skinData = &m_data->skins[i]; + auto* skin = new ModelSkin(); + + if (skinData->name) + skin->setName(utf8FromC(skinData->name)); + + if (skinData->skeleton) + skin->skeletonRootIndex = static_cast(cgltf_node_index(m_data, skinData->skeleton)); + + for (cgltf_size j = 0; j < skinData->joints_count; ++j) { + cgltf_node* jointNode = skinData->joints[j]; + i32 jointIndex = static_cast(cgltf_node_index(m_data, jointNode)); + + math::Matrix4 ibm = math::Matrix4::identity(); + if (skinData->inverse_bind_matrices) { + float mat[16] = {}; + cgltf_accessor_read_float(skinData->inverse_bind_matrices, j, mat, 16); + // GLTF stores column-major for column-vector convention. + // Transpose to row-vector convention by reading flat array directly into row-major storage. + std::memcpy(ibm.data(), mat, sizeof(float) * 16); + } + + skin->addJoint(jointIndex, ibm); + + // Also set on the bone. + if (jointIndex >= 0 && static_cast(jointIndex) < model.bones().size()) + model.bones()[jointIndex]->inverseBindMatrix = ibm; + } + + model.addSkin(skin); + } + } + + // ----------------------------------------------------------------------- + // Animations + // ----------------------------------------------------------------------- + + void loadAnimations(Model& model) { + for (cgltf_size i = 0; i < m_data->animations_count; ++i) { + cgltf_animation* animData = &m_data->animations[i]; + auto* animation = new ModelAnimation(); + + if (animData->name) + animation->setName(utf8FromC(animData->name)); + + for (cgltf_size c = 0; c < animData->channels_count; ++c) { + cgltf_animation_channel* channelData = &animData->channels[c]; + auto* channel = new AnimationChannel(); + + if (channelData->target_node) + channel->targetBone = static_cast(cgltf_node_index(m_data, channelData->target_node)); + + switch (channelData->target_path) { + case cgltf_animation_path_type_translation: channel->path = AnimationPath::Translation; break; + case cgltf_animation_path_type_rotation: channel->path = AnimationPath::Rotation; break; + case cgltf_animation_path_type_scale: channel->path = AnimationPath::Scale; break; + case cgltf_animation_path_type_weights: channel->path = AnimationPath::Weights; break; + default: break; + } + + if (channelData->sampler) { + cgltf_animation_sampler* sampler = channelData->sampler; + + switch (sampler->interpolation) { + case cgltf_interpolation_type_step: channel->interpolation = AnimationInterpolation::Step; break; + case cgltf_interpolation_type_cubic_spline: channel->interpolation = AnimationInterpolation::CubicSpline; break; + default: channel->interpolation = AnimationInterpolation::Linear; break; + } + + if (sampler->input && sampler->output) { + i32 keyframeCount = static_cast(sampler->input->count); + + for (i32 k = 0; k < keyframeCount; ++k) { + float time = 0; + cgltf_accessor_read_float(sampler->input, static_cast(k), &time, 1); + + math::Vector4 value{}; + switch (channel->path) { + case AnimationPath::Translation: + case AnimationPath::Scale: { + float v3[3] = {}; + cgltf_accessor_read_float(sampler->output, static_cast(k), v3, 3); + value = math::Vector4(v3[0], v3[1], v3[2], 0); + break; + } + case AnimationPath::Rotation: { + float v4[4] = {}; + cgltf_accessor_read_float(sampler->output, static_cast(k), v4, 4); + value = math::Vector4(v4[0], v4[1], v4[2], v4[3]); + break; + } + case AnimationPath::Weights: { + float w = 0; + cgltf_accessor_read_float(sampler->output, static_cast(k), &w, 1); + value.x = w; + break; + } + } + + channel->addKeyframe(time, value); + } + } + } + + animation->addChannel(channel); + } + + animation->calculateDuration(); + model.addAnimation(animation); + } + } +}; + +} // namespace draco::model::gltf diff --git a/Engine/cpp/Runtime/Rendering/Model/GLTF/cgltf_impl.cpp b/Engine/cpp/Runtime/Rendering/Model/GLTF/cgltf_impl.cpp new file mode 100644 index 00000000..ae192cdb --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/GLTF/cgltf_impl.cpp @@ -0,0 +1,3 @@ +// Single translation unit for cgltf implementation. +#define CGLTF_IMPLEMENTATION +#include "cgltf.h" diff --git a/Engine/cpp/Runtime/Rendering/Model/IO/ModelIOModule.cppm b/Engine/cpp/Runtime/Rendering/Model/IO/ModelIOModule.cppm new file mode 100644 index 00000000..34c69135 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/IO/ModelIOModule.cppm @@ -0,0 +1,87 @@ +/// Model IO - loader abstraction and registry. +/// Format-specific loaders (GLTF, FBX) register here. +/// Callers use loadModel(path, model) which selects the right loader by extension. + +module; +#include + +#include +#include + +export module model:io; + +import core; +import :model; + +using namespace draco; + +export namespace draco::model::io { + +/// Abstract base for format-specific model loaders. +class ModelLoader { +public: + virtual ~ModelLoader() = default; + + /// Check if this loader supports the given file extension (e.g. ".gltf"). + [[nodiscard]] virtual bool supportsExtension(std::u8string_view ext) const = 0; + + /// Load a model from a file path. + virtual ModelLoadResult load(std::u8string_view path, Model& model) = 0; +}; + +/// Register a model loader. Does not take ownership. +void registerLoader(ModelLoader* loader); + +/// Unregister a model loader. +void unregisterLoader(ModelLoader* loader); + +/// Load a model from a file, selecting the appropriate loader by extension. +/// Returns UnsupportedFormat if no loader is registered for the extension. +ModelLoadResult loadModel(std::u8string_view path, Model& model); + +/// Check if any loaders are registered. +[[nodiscard]] bool hasLoaders(); + +// ---- Implementation ---- + +namespace detail { + inline std::vector& loaders() { + static std::vector s; + return s; + } + + inline std::u8string_view getExtension(std::u8string_view path) { + for (usize i = path.size(); i > 0; --i) { + if (path.data()[i - 1] == '.') return std::u8string_view(path.data() + i - 1, path.size() - i + 1); + if (path.data()[i - 1] == '/' || path.data()[i - 1] == '\\') break; + } + return {}; + } +} + +inline void registerLoader(ModelLoader* loader) { + if (!loader) return; + auto& v = detail::loaders(); + for (auto* l : v) if (l == loader) return; + v.push_back(loader); +} + +inline void unregisterLoader(ModelLoader* loader) { + auto& v = detail::loaders(); + for (usize i = 0; i < v.size(); ++i) { + if (v[i] == loader) { v.erase(v.begin() + static_cast(i)); return; } + } +} + +inline ModelLoadResult loadModel(std::u8string_view path, Model& model) { + std::u8string_view ext = detail::getExtension(path); + for (auto* loader : detail::loaders()) { + if (loader->supportsExtension(ext)) + return loader->load(path, model); + } + return ModelLoadResult::UnsupportedFormat; +} + +inline bool hasLoaders() { return !detail::loaders().empty(); } + +} // namespace draco::model::io diff --git a/Engine/cpp/Runtime/Rendering/Model/Model.cppm b/Engine/cpp/Runtime/Rendering/Model/Model.cppm new file mode 100644 index 00000000..99fef040 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/Model.cppm @@ -0,0 +1,336 @@ +/// A complete 3D model with meshes, materials, skeleton, and animations. + +module; +#include +#include + +#include +#include +#include + +export module model:model; + +import core; +// Re-export the sub-partitions so anything importing `:model` (the IO layer and the +// FBX/GLTF loaders) sees the full model vocabulary through this one partition. +export import :vertex_format; +export import :model_vertex; +export import :mesh_part; +export import :model_texture; +export import :model_material; +export import :model_bone; +export import :model_mesh; +export import :model_animation; +export import :model_skin; + +using namespace draco; + +export namespace draco::model { + +/// Look up `k` in an associative container, returning a pointer to its value or nullptr +/// (a value-or-null find used by the FBX/GLTF loaders' id->index maps). +template +[[nodiscard]] auto mapFind(Map& m, const Key& k) -> decltype(&m.find(k)->second) { + auto it = m.find(k); + return it != m.end() ? &it->second : nullptr; +} + +/// Result of a model load operation. +enum class ModelLoadResult : u32 { + Ok, + FileNotFound, + ParseError, + UnsupportedFormat, + OutOfMemory, + InvalidData, +}; + +/// The up axis of a coordinate system, as reported by the source file. +enum class CoordinateAxis : u32 { + PositiveX, + NegativeX, + PositiveY, + NegativeY, + PositiveZ, + NegativeZ, +}; + +/// A complete 3D model with all owned containers. +class Model { +public: + Model() = default; + + ~Model() { + for (auto* m : m_meshes) delete m; + for (auto* m : m_materials) delete m; + for (auto* b : m_bones) delete b; + for (auto* s : m_skins) delete s; + for (auto* a : m_animations) delete a; + for (auto* t : m_textures) delete t; + } + + // Non-copyable, movable. + Model(const Model&) = delete; + Model& operator=(const Model&) = delete; + Model(Model&& other) noexcept + : rootBoneIndex(other.rootBoneIndex), + originalUpAxis(other.originalUpAxis), + m_name(static_cast(other.m_name)), + m_meshes(static_cast&&>(other.m_meshes)), + m_materials(static_cast&&>(other.m_materials)), + m_bones(static_cast&&>(other.m_bones)), + m_skins(static_cast&&>(other.m_skins)), + m_animations(static_cast&&>(other.m_animations)), + m_textures(static_cast&&>(other.m_textures)), + m_samplers(static_cast&&>(other.m_samplers)), + m_bounds(other.m_bounds) { + other.m_meshes.clear(); + other.m_materials.clear(); + other.m_bones.clear(); + other.m_skins.clear(); + other.m_animations.clear(); + other.m_textures.clear(); + } + Model& operator=(Model&& other) noexcept { + if (this != &other) { + for (auto* m : m_meshes) delete m; + for (auto* m : m_materials) delete m; + for (auto* b : m_bones) delete b; + for (auto* s : m_skins) delete s; + for (auto* a : m_animations) delete a; + for (auto* t : m_textures) delete t; + + m_name = static_cast(other.m_name); + m_meshes = static_cast&&>(other.m_meshes); + m_materials = static_cast&&>(other.m_materials); + m_bones = static_cast&&>(other.m_bones); + m_skins = static_cast&&>(other.m_skins); + m_animations = static_cast&&>(other.m_animations); + m_textures = static_cast&&>(other.m_textures); + m_samplers = static_cast&&>(other.m_samplers); + m_bounds = other.m_bounds; + rootBoneIndex = other.rootBoneIndex; + originalUpAxis = other.originalUpAxis; + + other.m_meshes.clear(); + other.m_materials.clear(); + other.m_bones.clear(); + other.m_skins.clear(); + other.m_animations.clear(); + other.m_textures.clear(); + } + return *this; + } + + // -- Name -- + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + // -- Meshes -- + + [[nodiscard]] std::span meshes() const { + return std::span(m_meshes.data(), m_meshes.size()); + } + [[nodiscard]] std::span meshes() { + return std::span(m_meshes.data(), m_meshes.size()); + } + + /// Add a mesh (takes ownership). Returns index. + i32 addMesh(ModelMesh* mesh) { + i32 idx = static_cast(m_meshes.size()); + m_meshes.push_back(mesh); + return idx; + } + + // -- Materials -- + + [[nodiscard]] std::span materials() const { + return std::span(m_materials.data(), m_materials.size()); + } + [[nodiscard]] std::span materials() { + return std::span(m_materials.data(), m_materials.size()); + } + + /// Add a material (takes ownership). Returns index. + i32 addMaterial(ModelMaterial* mat) { + i32 idx = static_cast(m_materials.size()); + m_materials.push_back(mat); + return idx; + } + + // -- Bones -- + + [[nodiscard]] std::span bones() const { + return std::span(m_bones.data(), m_bones.size()); + } + [[nodiscard]] std::span bones() { + return std::span(m_bones.data(), m_bones.size()); + } + + /// Add a bone (takes ownership). Sets bone.index. Returns index. + i32 addBone(ModelBone* bone) { + i32 idx = static_cast(m_bones.size()); + bone->index = idx; + m_bones.push_back(bone); + return idx; + } + + // -- Skins -- + + [[nodiscard]] std::span skins() const { + return std::span(m_skins.data(), m_skins.size()); + } + [[nodiscard]] std::span skins() { + return std::span(m_skins.data(), m_skins.size()); + } + + /// Add a skin (takes ownership). Returns index. + i32 addSkin(ModelSkin* skin) { + i32 idx = static_cast(m_skins.size()); + m_skins.push_back(skin); + return idx; + } + + // -- Animations -- + + [[nodiscard]] std::span animations() const { + return std::span(m_animations.data(), m_animations.size()); + } + [[nodiscard]] std::span animations() { + return std::span(m_animations.data(), m_animations.size()); + } + + /// Add an animation (takes ownership). Returns index. + i32 addAnimation(ModelAnimation* anim) { + i32 idx = static_cast(m_animations.size()); + m_animations.push_back(anim); + return idx; + } + + // -- Textures -- + + [[nodiscard]] std::span textures() const { + return std::span(m_textures.data(), m_textures.size()); + } + [[nodiscard]] std::span textures() { + return std::span(m_textures.data(), m_textures.size()); + } + + /// Add a texture (takes ownership). Returns index. + i32 addTexture(ModelTexture* tex) { + i32 idx = static_cast(m_textures.size()); + m_textures.push_back(tex); + return idx; + } + + // -- Samplers -- + + [[nodiscard]] std::span samplers() const { + return std::span(m_samplers.data(), m_samplers.size()); + } + [[nodiscard]] std::span samplers() { + return std::span(m_samplers.data(), m_samplers.size()); + } + + /// Add a sampler. Returns index. + i32 addSampler(TextureSampler sampler) { + i32 idx = static_cast(m_samplers.size()); + m_samplers.push_back(sampler); + return idx; + } + + // -- Bounds -- + + [[nodiscard]] math::AABB bounds() const { return m_bounds; } + + /// Calculate bounds from all meshes. + void calculateBounds() { + if (m_meshes.empty()) { + m_bounds = math::AABB{}; + return; + } + + math::Vector3 bmin(std::numeric_limits::max()); + math::Vector3 bmax(std::numeric_limits::lowest()); + + for (auto* mesh : m_meshes) { + math::AABB mb = mesh->bounds(); + bmin = min(bmin, mb.min); + bmax = max(bmax, mb.max); + } + + m_bounds = math::AABB{bmin, bmax}; + } + + // -- Hierarchy -- + + /// Build bone hierarchy from parent indices. + void buildBoneHierarchy() { + // Clear existing children. + for (auto* bone : m_bones) + bone->clearChildren(); + + // Build hierarchy. + for (usize i = 0; i < m_bones.size(); ++i) { + auto* bone = m_bones[i]; + if (bone->parentIndex >= 0 && + static_cast(bone->parentIndex) < m_bones.size()) { + m_bones[bone->parentIndex]->addChild(bone); + } else if (bone->parentIndex < 0) { + rootBoneIndex = bone->index; + } + } + } + + // -- Lookup by name -- + + /// Get mesh by name (nullptr if not found). + [[nodiscard]] ModelMesh* getMesh(std::u8string_view n) const { + for (auto* m : m_meshes) + if (m->name() == n) return m; + return nullptr; + } + + /// Get material by name (nullptr if not found). + [[nodiscard]] ModelMaterial* getMaterial(std::u8string_view n) const { + for (auto* m : m_materials) + if (m->name() == n) return m; + return nullptr; + } + + /// Get bone by name (nullptr if not found). + [[nodiscard]] ModelBone* getBone(std::u8string_view n) const { + for (auto* b : m_bones) + if (b->name() == n) return b; + return nullptr; + } + + /// Get animation by name (nullptr if not found). + [[nodiscard]] ModelAnimation* getAnimation(std::u8string_view n) const { + for (auto* a : m_animations) + if (a->name() == n) return a; + return nullptr; + } + + // -- Public fields -- + + /// Index of the root bone (-1 if no hierarchy). + i32 rootBoneIndex = -1; + + /// The original up axis of the source file's coordinate system. + CoordinateAxis originalUpAxis = CoordinateAxis::PositiveY; + +private: + std::u8string m_name; + std::vector m_meshes; + std::vector m_materials; + std::vector m_bones; + std::vector m_skins; + std::vector m_animations; + std::vector m_textures; + std::vector m_samplers; + math::AABB m_bounds; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelAnimation.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelAnimation.cppm new file mode 100644 index 00000000..6211c92f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelAnimation.cppm @@ -0,0 +1,182 @@ +/// Animation types: interpolation, channels, keyframes, and animation clips. + +module; +#include +#include + +#include +#include + +export module model:model_animation; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Animation interpolation type. +enum class AnimationInterpolation : u32 { + Linear, + Step, + CubicSpline, +}; + +/// Animation channel target path. +enum class AnimationPath : u32 { + Translation, + Rotation, + Scale, + Weights, // Morph target weights. +}; + +/// Keyframe data for an animation. +struct AnimationKeyframe { + f32 time = 0.0f; + math::Vector4 value{}; // Translation (xyz), Rotation (xyzw), Scale (xyz), or single Weight. + + constexpr AnimationKeyframe() = default; + constexpr AnimationKeyframe(f32 t, math::Vector4 v) : time(t), value(v) {} +}; + +/// An animation channel targeting a specific bone/node property. +class AnimationChannel { +public: + AnimationChannel() = default; + ~AnimationChannel() = default; + + /// Add a keyframe. + void addKeyframe(f32 time, math::Vector4 value) { + m_keyframes.push_back(AnimationKeyframe(time, value)); + } + + [[nodiscard]] std::span keyframes() const { + return std::span(m_keyframes.data(), m_keyframes.size()); + } + [[nodiscard]] std::span keyframes() { + return std::span(m_keyframes.data(), m_keyframes.size()); + } + + /// Sample the animation at a given time. + [[nodiscard]] math::Vector4 sample(f32 time) const { + if (m_keyframes.empty()) return {}; + if (m_keyframes.size() == 1) return m_keyframes[0].value; + + // Clamp to animation bounds. + if (time <= m_keyframes[0].time) + return m_keyframes[0].value; + if (time >= m_keyframes.back().time) + return m_keyframes.back().value; + + // Find surrounding keyframes. + usize i = 0; + while (i < m_keyframes.size() - 1 && m_keyframes[i + 1].time < time) + ++i; + + auto& k0 = m_keyframes[i]; + auto& k1 = m_keyframes[i + 1]; + + f32 t = (time - k0.time) / (k1.time - k0.time); + + switch (interpolation) { + case AnimationInterpolation::Step: + return k0.value; + case AnimationInterpolation::Linear: + if (path == AnimationPath::Rotation) { + // math::Quaternion slerp. + math::Quaternion q0(k0.value.x, k0.value.y, k0.value.z, k0.value.w); + math::Quaternion q1(k1.value.x, k1.value.y, k1.value.z, k1.value.w); + math::Quaternion result = slerp(q0, q1, t); + return math::Vector4(result.x, result.y, result.z, result.w); + } else { + return lerp(k0.value, k1.value, t); + } + case AnimationInterpolation::CubicSpline: + // TODO: Implement cubic spline interpolation. + return lerp(k0.value, k1.value, t); + } + return {}; + } + + // -- Public fields -- + + /// Target bone index. + i32 targetBone = 0; + + /// Property being animated. + AnimationPath path = AnimationPath::Translation; + + /// Interpolation method. + AnimationInterpolation interpolation = AnimationInterpolation::Linear; + +private: + std::vector m_keyframes; +}; + +/// A complete animation clip. +class ModelAnimation { +public: + ModelAnimation() = default; + + ~ModelAnimation() { + for (auto* ch : m_channels) + delete ch; + } + + // Non-copyable, movable. + ModelAnimation(const ModelAnimation&) = delete; + ModelAnimation& operator=(const ModelAnimation&) = delete; + ModelAnimation(ModelAnimation&& other) noexcept + : duration(other.duration), + m_name(static_cast(other.m_name)), + m_channels(static_cast&&>(other.m_channels)) { + other.m_channels.clear(); + } + ModelAnimation& operator=(ModelAnimation&& other) noexcept { + if (this != &other) { + for (auto* ch : m_channels) delete ch; + m_name = static_cast(other.m_name); + m_channels = static_cast&&>(other.m_channels); + duration = other.duration; + other.m_channels.clear(); + } + return *this; + } + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + /// Add a channel (takes ownership of raw pointer). + void addChannel(AnimationChannel* channel) { + m_channels.push_back(channel); + } + + [[nodiscard]] std::span channels() const { + return std::span(m_channels.data(), m_channels.size()); + } + [[nodiscard]] std::span channels() { + return std::span(m_channels.data(), m_channels.size()); + } + + /// Calculate duration from keyframes. + void calculateDuration() { + duration = 0.0f; + for (auto* channel : m_channels) { + for (auto& kf : channel->keyframes()) { + if (kf.time > duration) + duration = kf.time; + } + } + } + + // -- Public fields -- + + /// Duration of the animation in seconds. + f32 duration = 0.0f; + +private: + std::u8string m_name; + std::vector m_channels; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelBone.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelBone.cppm new file mode 100644 index 00000000..0fd474f3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelBone.cppm @@ -0,0 +1,117 @@ +/// A bone/node in the model hierarchy with TRS decomposition. + +module; +#include +#include + +#include +#include +#include + +export module model:model_bone; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// A bone/node in the model skeleton hierarchy. +class ModelBone { +public: + ModelBone() = default; + ~ModelBone() = default; + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + /// Add a child bone (non-owning pointer). + void addChild(ModelBone* child) { m_children.push_back(child); } + + /// Remove all children (does not delete -- children are non-owning). + void clearChildren() { m_children.clear(); } + + /// Non-owning child pointers (owned by Model::m_bones). + [[nodiscard]] std::span children() const { + return std::span(m_children.data(), m_children.size()); + } + [[nodiscard]] std::span children() { + return std::span(m_children.data(), m_children.size()); + } + + /// Update localTransform from translation/rotation/scale (TRS). + /// Order: Scale -> Rotate -> Translate. + void updateLocalTransform() { + // Build scale matrix. + math::Matrix4 s = math::Matrix4::identity(); + s.m[0][0] = scale.x; + s.m[1][1] = scale.y; + s.m[2][2] = scale.z; + + // Build rotation matrix from quaternion. + f32 xx = rotation.x * rotation.x; + f32 yy = rotation.y * rotation.y; + f32 zz = rotation.z * rotation.z; + f32 xy = rotation.x * rotation.y; + f32 xz = rotation.x * rotation.z; + f32 yz = rotation.y * rotation.z; + f32 wx = rotation.w * rotation.x; + f32 wy = rotation.w * rotation.y; + f32 wz = rotation.w * rotation.z; + + math::Matrix4 r = math::Matrix4::identity(); + r.m[0][0] = 1.0f - 2.0f * (yy + zz); + r.m[0][1] = 2.0f * (xy + wz); + r.m[0][2] = 2.0f * (xz - wy); + r.m[1][0] = 2.0f * (xy - wz); + r.m[1][1] = 1.0f - 2.0f * (xx + zz); + r.m[1][2] = 2.0f * (yz + wx); + r.m[2][0] = 2.0f * (xz + wy); + r.m[2][1] = 2.0f * (yz - wx); + r.m[2][2] = 1.0f - 2.0f * (xx + yy); + + // Build translation matrix. + math::Matrix4 t = math::Matrix4::identity(); + t.m[0][3] = translation.x; + t.m[1][3] = translation.y; + t.m[2][3] = translation.z; + + // TRS order: Scale -> Rotate -> Translate. + localTransform = t * (r * s); + } + + // -- Public fields -- + + /// Index of this bone in the model's bone array. + i32 index = 0; + + /// Parent bone index (-1 if root). + i32 parentIndex = -1; + + /// Local transform relative to parent. + math::Matrix4 localTransform = math::Matrix4::identity(); + + /// Inverse bind matrix for skinning (mesh space -> bone space). + math::Matrix4 inverseBindMatrix = math::Matrix4::identity(); + + /// Translation component of local transform. + math::Vector3 translation{}; + + /// Rotation component of local transform (quaternion). + math::Quaternion rotation = math::Quaternion::identity; + + /// Scale component of local transform. + math::Vector3 scale{ 1, 1, 1 }; + + /// Mesh index if this node has a mesh (-1 if none). + i32 meshIndex = -1; + + /// Skin index if this is a skinned mesh node (-1 if none). + i32 skinIndex = -1; + +private: + std::u8string m_name; + std::vector m_children; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelMaterial.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelMaterial.cppm new file mode 100644 index 00000000..cc35d61b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelMaterial.cppm @@ -0,0 +1,64 @@ +/// Material properties for PBR rendering. + +module; +#include + +#include + +export module model:model_material; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Alpha blending mode. +enum class AlphaMode : u32 { + Opaque, + Mask, + Blend, +}; + +/// PBR material properties for a model. +class ModelMaterial { +public: + ModelMaterial() = default; + ~ModelMaterial() = default; + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + // -- Base color -- + math::Vector4 baseColorFactor{ 1, 1, 1, 1 }; + i32 baseColorTextureIndex = -1; + + // -- Metallic-Roughness -- + f32 metallicFactor = 1.0f; + f32 roughnessFactor = 1.0f; + i32 metallicRoughnessTextureIndex = -1; + + // -- Normal map -- + f32 normalScale = 1.0f; + i32 normalTextureIndex = -1; + + // -- Occlusion -- + f32 occlusionStrength = 1.0f; + i32 occlusionTextureIndex = -1; + + // -- Emissive -- + math::Vector3 emissiveFactor{}; + i32 emissiveTextureIndex = -1; + + // -- Alpha -- + AlphaMode alphaMode = AlphaMode::Opaque; + f32 alphaCutoff = 0.5f; + + // -- Double-sided -- + bool doubleSided = false; + +private: + std::u8string m_name; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelMesh.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelMesh.cppm new file mode 100644 index 00000000..46434099 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelMesh.cppm @@ -0,0 +1,284 @@ +/// A mesh within a model containing vertex and index data. + +module; +#include +#include + +#include +#include +#include +#include + +export module model:model_mesh; + +import core; +import :vertex_format; +import :mesh_part; + +using namespace draco; + +export namespace draco::model { + +/// Primitive topology. +enum class PrimitiveTopology : u32 { + Triangles, + TriangleStrip, + Lines, + LineStrip, + Points, +}; + +/// A mesh within a model containing vertex/index buffers, parts, and bounds. +class ModelMesh { +public: + ModelMesh() = default; + ~ModelMesh() = default; + + // -- Name -- + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + // -- Property accessors -- + + [[nodiscard]] i32 vertexCount() const { return m_vertexCount; } + [[nodiscard]] i32 vertexStride() const { return m_vertexStride; } + [[nodiscard]] i32 indexCount() const { return m_indexCount; } + [[nodiscard]] bool use32BitIndices() const { return m_use32BitIndices; } + [[nodiscard]] PrimitiveTopology topology() const { return m_topology; } + [[nodiscard]] math::AABB bounds() const { return m_bounds; } + [[nodiscard]] bool hasNormals() const { return m_hasNormals; } + [[nodiscard]] bool hasTangents() const { return m_hasTangents; } + + void setTopology(PrimitiveTopology t) { m_topology = t; } + void setHasNormals(bool v) { m_hasNormals = v; } + void setHasTangents(bool v) { m_hasTangents = v; } + void setBounds(math::AABB b) { m_bounds = b; } + + // -- Parts -- + + [[nodiscard]] std::span parts() const { + return std::span(m_parts.data(), m_parts.size()); + } + [[nodiscard]] std::span parts() { + return std::span(m_parts.data(), m_parts.size()); + } + void addPart(ModelMeshPart part) { m_parts.push_back(part); } + + // -- Vertex elements -- + + [[nodiscard]] std::span vertexElements() const { + return std::span(m_vertexElements.data(), m_vertexElements.size()); + } + [[nodiscard]] std::span vertexElements() { + return std::span(m_vertexElements.data(), m_vertexElements.size()); + } + void addVertexElement(VertexElement elem) { m_vertexElements.push_back(elem); } + + // -- Vertex data -- + + /// Allocate vertex buffer. + void allocateVertices(i32 count, i32 stride) { + m_vertexCount = count; + m_vertexStride = stride; + m_vertexData.resize(static_cast(count) * stride, 0); + } + + /// Get raw vertex data pointer (nullptr if empty). + [[nodiscard]] const u8* getVertexData() const { + return m_vertexData.empty() ? nullptr : m_vertexData.data(); + } + + [[nodiscard]] u8* getVertexData() { + return m_vertexData.empty() ? nullptr : m_vertexData.data(); + } + + /// Vertex data size in bytes. + [[nodiscard]] i32 getVertexDataSize() const { return m_vertexCount * m_vertexStride; } + + /// Copy typed data into the vertex buffer. + template + void setVertexData(const T* data, usize count) { + usize bytes = count * sizeof(T); + if (m_vertexData.empty() || bytes > m_vertexData.size()) return; + std::memcpy(m_vertexData.data(), data, bytes); + } + + // -- Index data -- + + /// Allocate index buffer. + void allocateIndices(i32 count, bool use32Bit) { + m_indexCount = count; + m_use32BitIndices = use32Bit; + i32 indexSize = use32Bit ? 4 : 2; + m_indexData.resize(static_cast(count) * indexSize, 0); + } + + /// Get raw index data pointer (nullptr if empty). + [[nodiscard]] const u8* getIndexData() const { + return m_indexData.empty() ? nullptr : m_indexData.data(); + } + + [[nodiscard]] u8* getIndexData() { + return m_indexData.empty() ? nullptr : m_indexData.data(); + } + + /// Index data size in bytes. + [[nodiscard]] i32 getIndexDataSize() const { + return m_indexCount * (m_use32BitIndices ? 4 : 2); + } + + /// Copy u16 index data. + void setIndexData(const u16* indices, usize count) { + usize bytes = count * 2; + if (m_indexData.empty() || m_use32BitIndices || bytes > m_indexData.size()) return; + std::memcpy(m_indexData.data(), indices, bytes); + } + + /// Copy u32 index data. + void setIndexData(const u32* indices, usize count) { + usize bytes = count * 4; + if (m_indexData.empty() || !m_use32BitIndices || bytes > m_indexData.size()) return; + std::memcpy(m_indexData.data(), indices, bytes); + } + + // -- Skinning helpers -- + + /// Converts a non-skinned mesh into a skinned one by adding uniform bone weighting. + /// All vertices are assigned to a single joint with weight 1.0. + /// Expands vertex buffer by 24 bytes/vertex (UShort4 joints + Float4 weights). + void addUniformSkinning(i32 jointIndex) { + // Skip if mesh already has joint data. + for (auto& elem : m_vertexElements) + if (elem.semantic == VertexSemantic::Joints) return; + + if (m_vertexData.empty() || m_vertexCount == 0) return; + + i32 oldStride = m_vertexStride; + i32 newStride = oldStride + 24; // +8 (UShort4) +16 (Float4) + std::vector newData(static_cast(m_vertexCount) * newStride, 0); + + for (i32 i = 0; i < m_vertexCount; ++i) { + i32 srcOff = i * oldStride; + i32 dstOff = i * newStride; + + // Copy existing vertex data. + std::memcpy(&newData[dstOff], &m_vertexData[srcOff], oldStride); + + // Write joints: uint16[4] = (jointIndex, 0, 0, 0). + auto* joints = reinterpret_cast(&newData[dstOff + oldStride]); + joints[0] = static_cast(jointIndex); + joints[1] = 0; + joints[2] = 0; + joints[3] = 0; + + // Write weights: float[4] = (1, 0, 0, 0). + auto* weights = reinterpret_cast(&newData[dstOff + oldStride + 8]); + weights[0] = 1.0f; + weights[1] = 0.0f; + weights[2] = 0.0f; + weights[3] = 0.0f; + } + + m_vertexData = static_cast&&>(newData); + m_vertexStride = newStride; + + m_vertexElements.push_back(VertexElement(VertexSemantic::Joints, VertexElementFormat::UShort4, oldStride)); + m_vertexElements.push_back(VertexElement(VertexSemantic::Weights, VertexElementFormat::Float4, oldStride + 8)); + } + + /// Scales vertex positions by a non-uniform scale vector. + void scalePositions(math::Vector3 scale) { + i32 posOffset = -1; + for (auto& elem : m_vertexElements) { + if (elem.semantic == VertexSemantic::Position) { + posOffset = elem.offset; + break; + } + } + if (posOffset < 0 || m_vertexData.empty()) return; + + for (i32 i = 0; i < m_vertexCount; ++i) { + i32 off = i * m_vertexStride + posOffset; + auto* pos = reinterpret_cast(&m_vertexData[off]); + pos->x *= scale.x; + pos->y *= scale.y; + pos->z *= scale.z; + } + } + + /// Remaps vertex joint indices using the provided mapping array. + /// remap[oldJointIndex] = newJointIndex. + void remapJointIndices(const i32* remap, usize remapCount) { + i32 jointsOffset = -1; + for (auto& elem : m_vertexElements) { + if (elem.semantic == VertexSemantic::Joints) { + jointsOffset = elem.offset; + break; + } + } + if (jointsOffset < 0 || m_vertexData.empty()) return; + + for (i32 i = 0; i < m_vertexCount; ++i) { + i32 off = i * m_vertexStride + jointsOffset; + auto* joints = reinterpret_cast(&m_vertexData[off]); + for (int j = 0; j < 4; ++j) { + i32 oldIdx = static_cast(joints[j]); + if (oldIdx >= 0 && static_cast(oldIdx) < remapCount) + joints[j] = static_cast(remap[oldIdx]); + } + } + } + + /// Calculate bounds from position data. + void calculateBounds() { + if (m_vertexData.empty() || m_vertexCount == 0) { + m_bounds = math::AABB{}; + return; + } + + i32 posOffset = -1; + for (auto& elem : m_vertexElements) { + if (elem.semantic == VertexSemantic::Position) { + posOffset = elem.offset; + break; + } + } + if (posOffset < 0) { + m_bounds = math::AABB{}; + return; + } + + math::Vector3 bmin(std::numeric_limits::max()); + math::Vector3 bmax(std::numeric_limits::lowest()); + + for (i32 i = 0; i < m_vertexCount; ++i) { + i32 off = i * m_vertexStride + posOffset; + math::Vector3 pos{}; + std::memcpy(&pos, &m_vertexData[off], sizeof(math::Vector3)); + bmin = min(bmin, pos); + bmax = max(bmax, pos); + } + + m_bounds = math::AABB{bmin, bmax}; + } + +private: + std::u8string m_name; + std::vector m_vertexData; + std::vector m_indexData; + std::vector m_parts; + std::vector m_vertexElements; + + i32 m_vertexCount = 0; + i32 m_vertexStride = 0; + i32 m_indexCount = 0; + bool m_use32BitIndices = false; + PrimitiveTopology m_topology = PrimitiveTopology::Triangles; + math::AABB m_bounds; + + bool m_hasNormals = false; + bool m_hasTangents = false; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelMeshPart.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelMeshPart.cppm new file mode 100644 index 00000000..dcacd664 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelMeshPart.cppm @@ -0,0 +1,22 @@ +/// Defines a portion of a mesh that uses a specific material. + +export module model:mesh_part; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// A sub-range of a mesh's index buffer bound to one material. +struct ModelMeshPart { + i32 indexStart = 0; // Starting index in the index buffer. + i32 indexCount = 0; // Number of indices in this part. + i32 materialIndex = -1; // Material index (-1 for no material). + + constexpr ModelMeshPart() = default; + constexpr ModelMeshPart(i32 start, i32 count, i32 matIdx = -1) + : indexStart(start), indexCount(count), materialIndex(matIdx) {} +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelModule.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelModule.cppm new file mode 100644 index 00000000..c18fee5f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelModule.cppm @@ -0,0 +1,17 @@ +/// Primary module for the model library. Re-exports all partitions. + +export module model; + +export import :vertex_format; +export import :model_vertex; +export import :mesh_part; +export import :model_texture; +export import :model_material; +export import :model_bone; +export import :model_mesh; +export import :model_animation; +export import :model_skin; +export import :model; +export import :io; +export import :fbx; +export import :gltf; diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelSkin.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelSkin.cppm new file mode 100644 index 00000000..86ed62b4 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelSkin.cppm @@ -0,0 +1,58 @@ +/// Skin data for skeletal animation. + +module; +#include +#include + +#include +#include + +export module model:model_skin; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Skin data binding joints to inverse bind matrices. +class ModelSkin { +public: + ModelSkin() = default; + ~ModelSkin() = default; + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + void setName(std::u8string_view n) { m_name = std::u8string(n); } + + /// Add a joint to the skin. + void addJoint(i32 boneIndex, math::Matrix4 inverseBindMatrix) { + m_joints.push_back(boneIndex); + m_inverseBindMatrices.push_back(inverseBindMatrix); + } + + [[nodiscard]] std::span joints() const { + return std::span(m_joints.data(), m_joints.size()); + } + [[nodiscard]] std::span joints() { + return std::span(m_joints.data(), m_joints.size()); + } + + [[nodiscard]] std::span inverseBindMatrices() const { + return std::span(m_inverseBindMatrices.data(), m_inverseBindMatrices.size()); + } + [[nodiscard]] std::span inverseBindMatrices() { + return std::span(m_inverseBindMatrices.data(), m_inverseBindMatrices.size()); + } + + // -- Public fields -- + + /// Index of the skeleton root bone (-1 if not specified). + i32 skeletonRootIndex = -1; + +private: + std::u8string m_name; + std::vector m_joints; + std::vector m_inverseBindMatrices; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelTests.test.cpp b/Engine/cpp/Runtime/Rendering/Model/ModelTests.test.cpp new file mode 100644 index 00000000..da9f7eff --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelTests.test.cpp @@ -0,0 +1,17 @@ +#include + +import core; +import model; + +using namespace draco; +using namespace draco::model; + +TEST_CASE("model: core data types - names round-trip as UTF-8 strings") +{ + ModelMaterial mat; + mat.setName(u8"steel"); + CHECK(mat.name() == u8"steel"); + + Model model; + CHECK(model.meshes().size() == 0u); +} diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelTexture.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelTexture.cppm new file mode 100644 index 00000000..354c3c82 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelTexture.cppm @@ -0,0 +1,160 @@ +/// Texture-related enums and the ModelTexture class. + +module; +#include + +#include +#include + +export module model:model_texture; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Texture wrapping mode. +enum class TextureWrap : u32 { + Repeat, + ClampToEdge, + MirroredRepeat, +}; + +/// Texture minification filter. +enum class TextureMinFilter : u32 { + Nearest, + Linear, + NearestMipmapNearest, + LinearMipmapNearest, + NearestMipmapLinear, + LinearMipmapLinear, +}; + +/// Texture magnification filter. +enum class TextureMagFilter : u32 { + Nearest, + Linear, +}; + +/// Texture sampler settings. +struct TextureSampler { + TextureWrap wrapS = TextureWrap::Repeat; + TextureWrap wrapT = TextureWrap::Repeat; + TextureMinFilter minFilter = TextureMinFilter::LinearMipmapLinear; + TextureMagFilter magFilter = TextureMagFilter::Linear; +}; + +/// Pixel format for decoded texture data. +enum class TexturePixelFormat : u32 { + Unknown, + R8, // 1 byte per pixel (grayscale) + RG8, // 2 bytes per pixel + RGB8, // 3 bytes per pixel + RGBA8, // 4 bytes per pixel + BGR8, // 3 bytes per pixel (BGR order) + BGRA8, // 4 bytes per pixel (BGRA order) +}; + +/// A texture reference in a model. +class ModelTexture { +public: + ModelTexture() = default; + ~ModelTexture() { + delete[] m_data; + } + + // Non-copyable, movable. + ModelTexture(const ModelTexture&) = delete; + ModelTexture& operator=(const ModelTexture&) = delete; + ModelTexture(ModelTexture&& other) noexcept + : mimeType(static_cast(other.mimeType)), + samplerIndex(other.samplerIndex), + width(other.width), height(other.height), + pixelFormat(other.pixelFormat), + m_name(static_cast(other.m_name)), + m_uri(static_cast(other.m_uri)), + m_data(other.m_data), m_dataSize(other.m_dataSize) { + other.m_data = nullptr; + other.m_dataSize = 0; + } + ModelTexture& operator=(ModelTexture&& other) noexcept { + if (this != &other) { + delete[] m_data; + m_name = static_cast(other.m_name); + m_uri = static_cast(other.m_uri); + m_data = other.m_data; + m_dataSize = other.m_dataSize; + mimeType = static_cast(other.mimeType); + samplerIndex = other.samplerIndex; + width = other.width; + height = other.height; + pixelFormat = other.pixelFormat; + other.m_data = nullptr; + other.m_dataSize = 0; + } + return *this; + } + + // -- Accessors -- + + [[nodiscard]] std::u8string_view name() const { return std::u8string_view(m_name.data(), m_name.size()); } + [[nodiscard]] std::u8string_view uri() const { return std::u8string_view(m_uri.data(), m_uri.size()); } + + void setName(std::u8string_view n) { m_name = std::u8string(n); } + void setUri(std::u8string_view u) { m_uri = std::u8string(u); } + + /// Set embedded image data (copies). + void setData(const u8* ptr, usize length) { + delete[] m_data; + if (ptr && length > 0) { + m_data = new u8[length]; + std::memcpy(m_data, ptr, length); + m_dataSize = static_cast(length); + } else { + m_data = nullptr; + m_dataSize = 0; + } + } + + /// Set embedded image data (takes ownership of raw pointer). + void setData(u8* ptr, i32 size) { + delete[] m_data; + m_data = ptr; + m_dataSize = size; + } + + /// Get embedded image data pointer (nullptr if empty). + [[nodiscard]] const u8* getData() const { return m_data; } + + /// Get embedded image data size. + [[nodiscard]] i32 getDataSize() const { return m_dataSize; } + + /// Whether texture has embedded data. + [[nodiscard]] bool hasEmbeddedData() const { return m_data != nullptr && m_dataSize > 0; } + + // -- Public fields -- + + /// Image data format (e.g., "image/png", "image/jpeg"). + std::u8string mimeType; + + /// Sampler index (-1 for default sampler). + i32 samplerIndex = -1; + + /// Width in pixels (0 if not yet loaded). + i32 width = 0; + + /// Height in pixels (0 if not yet loaded). + i32 height = 0; + + /// Pixel format of decoded data. + TexturePixelFormat pixelFormat = TexturePixelFormat::Unknown; + +private: + std::u8string m_name; + std::u8string m_uri; + u8* m_data = nullptr; + i32 m_dataSize = 0; +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/ModelVertex.cppm b/Engine/cpp/Runtime/Rendering/Model/ModelVertex.cppm new file mode 100644 index 00000000..552da9f1 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/ModelVertex.cppm @@ -0,0 +1,47 @@ +/// Standard and skinned vertex structs for model data. + +export module model:model_vertex; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Standard vertex format for static models (48 bytes). +struct ModelVertex { + math::Float3 position{}; // 12 bytes + math::Float3 normal{ 0, 1, 0 }; // 12 bytes + math::Float2 texCoord{}; // 8 bytes + u32 color = 0xFFFFFFFF; // 4 bytes (packed RGBA) + math::Float3 tangent{ 1, 0, 0 }; // 12 bytes + // Total: 48 bytes + + constexpr ModelVertex() = default; + constexpr ModelVertex(math::Vector3 pos, math::Vector3 nrm, math::Vector2 uv, u32 col = 0xFFFFFFFF, math::Vector3 tan = { 1, 0, 0 }) + : position(pos), normal(nrm), texCoord(uv), color(col), tangent(tan) {} +}; + +static_assert(sizeof(ModelVertex) == 48, "ModelVertex must be 48 bytes"); + +/// Skinned vertex format for animated models (72 bytes). +struct SkinnedModelVertex { + math::Float3 position{}; // 12 bytes + math::Float3 normal{ 0, 1, 0 }; // 12 bytes + math::Float2 texCoord{}; // 8 bytes + u32 color = 0xFFFFFFFF; // 4 bytes (packed RGBA) + math::Float3 tangent{ 1, 0, 0 }; // 12 bytes + u16 joints[4] = { 0, 0, 0, 0 }; // 8 bytes (up to 4 bone indices) + math::Float4 weights{ 1, 0, 0, 0 }; // 16 bytes (bone weights) + // Total: 72 bytes + + constexpr SkinnedModelVertex() = default; + constexpr SkinnedModelVertex(math::Vector3 pos, math::Vector3 nrm, math::Vector2 uv, u32 col, math::Vector3 tan, + u16 j0, u16 j1, u16 j2, u16 j3, math::Vector4 wt) + : position(pos), normal(nrm), texCoord(uv), color(col), tangent(tan), + joints{ j0, j1, j2, j3 }, weights(wt) {} +}; + +static_assert(sizeof(SkinnedModelVertex) == 72, "SkinnedModelVertex must be 72 bytes"); + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/Model/VertexFormat.cppm b/Engine/cpp/Runtime/Rendering/Model/VertexFormat.cppm new file mode 100644 index 00000000..17918830 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Model/VertexFormat.cppm @@ -0,0 +1,59 @@ +/// Vertex element semantics, formats, and descriptors. + +export module model:vertex_format; + +import core; + +using namespace draco; + +export namespace draco::model { + +/// Vertex element semantic types. +enum class VertexSemantic : u32 { + Position, + Normal, + TexCoord, + Color, + Tangent, + Joints, + Weights, +}; + +/// Vertex element data formats. +enum class VertexElementFormat : u32 { + Float, + Float2, + Float3, + Float4, + Byte4, + UShort2, + UShort4, +}; + +/// Describes a single element in a vertex layout. +struct VertexElement { + VertexSemantic semantic = VertexSemantic::Position; + VertexElementFormat format = VertexElementFormat::Float3; + i32 offset = 0; + i32 semanticIndex = 0; // For multiple UV channels, etc. + + constexpr VertexElement() = default; + constexpr VertexElement(VertexSemantic sem, VertexElementFormat fmt, i32 off, i32 semIdx = 0) + : semantic(sem), format(fmt), offset(off), semanticIndex(semIdx) {} + + /// Size of this element in bytes. + [[nodiscard]] constexpr i32 size() const { + switch (format) { + case VertexElementFormat::Float: return 4; + case VertexElementFormat::Float2: return 8; + case VertexElementFormat::Float3: return 12; + case VertexElementFormat::Float4: return 16; + case VertexElementFormat::Byte4: return 4; + case VertexElementFormat::UShort2: return 4; + case VertexElementFormat::UShort4: return 8; + } + return 0; + } +}; + +} // namespace draco::model diff --git a/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cpp b/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cpp deleted file mode 100644 index ff67e855..00000000 --- a/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cpp +++ /dev/null @@ -1,186 +0,0 @@ -module; - -#include -#include - -#include -#include - -module rendering.quad; - -import core.stdtypes; -import rendering.rhi; -import rendering.rhi.vertex; -import rendering.rendergraph; - -namespace draco::rendering::quad { - - static constexpr f32 QuadUV[4][2] = { - {0.0f, 0.0f}, - {1.0f, 0.0f}, - {1.0f, 1.0f}, - {0.0f, 1.0f} - }; - - void QuadRenderer::init(rhi::PipelineHandle pipeline) - { - using namespace draco::rendering::rhi; - - VertexLayoutDesc layout{}; - layout.elements.push_back({Attrib::Position, 3, AttribType::Float}); - layout.elements.push_back({Attrib::TexCoord0, 2, AttribType::Float}); - layout.elements.push_back({Attrib::Color0, 4, AttribType::Uint8, true}); - - m_pipeline = pipeline; - m_layout = createVertexLayout(layout); - - // Allocating dynamic streaming buffers - m_vb = createDynamicVertexBuffer(sizeof(TexturedVertex) * MaxVertices, m_layout); - - // Pass BGFX_BUFFER_NONE implicitly to match tracking - m_ib = createDynamicIndexBuffer(MaxIndices * sizeof(u16), BGFX_BUFFER_NONE); - - m_sampler = createUniform("s_texColor", UniformType::Sampler); - } - - void QuadRenderer::begin() - { - m_vertices.clear(); - m_indices.clear(); - - m_quad_count = 0; - - m_batch_key = {}; - } - - void QuadRenderer::submit(const QuadCommand& cmd) - { - if (m_quad_count >= MaxQuads) - return; - - BatchKey new_key{cmd.texture, m_pipeline, draco::rendering::rhi::InvalidSampler}; - - if (m_batch_key.texture == draco::rendering::rhi::InvalidTexture) - { - m_batch_key = new_key; - } - - bool state_change = !(new_key == m_batch_key); - - if (state_change) - { - // TODO: Flush current batch automatically - - return; - } - - pushQuad(cmd); - - m_quad_count++; - } - - void QuadRenderer::pushQuad(const QuadCommand& cmd) - { - f32 hw = cmd.width * 0.5f; - f32 hh = cmd.height * 0.5f; - - f32 c = cosf(cmd.rotation); - f32 s = sinf(cmd.rotation); - - f32 corners[4][2] = { - {-hw, -hh}, - { hw, -hh}, - { hw, hh}, - {-hw, hh} - }; - - u16 start = static_cast(m_vertices.size()); - - for (int i = 0; i < 4; i++) - { - f32 rx = corners[i][0] * c - corners[i][1] * s; - - f32 ry = corners[i][0] * s + corners[i][1] * c; - - draco::rendering::rhi::TexturedVertex v{}; - - v.x = cmd.x + rx; - v.y = cmd.y + ry; - v.z = cmd.z; - - v.u = QuadUV[i][0]; - v.v = QuadUV[i][1]; - - v.color = cmd.color; - - m_vertices.push_back(v); - } - - m_indices.push_back(start + 0); - m_indices.push_back(start + 1); - m_indices.push_back(start + 2); - - m_indices.push_back(start + 2); - m_indices.push_back(start + 3); - m_indices.push_back(start + 0); - } - - void QuadRenderer::flushToPass(draco::rendering::rendergraph::Pass& pass) - { - using namespace draco::rendering::rhi; - - if (m_vertices.empty()) - return; - - // Upload only the exact slices we are using this frame - updateDynamicVertexBuffer(m_vb, 0, m_vertices.data(), static_cast(m_vertices.size() * sizeof(TexturedVertex))); - updateDynamicIndexBuffer(m_ib, 0, m_indices.data(), static_cast(m_indices.size() * sizeof(u16))); - - RenderPacket pkt{}; - pkt.vertexBuffer = m_vb; - pkt.indexBuffer = m_ib; - pkt.pipeline = m_pipeline; - pkt.textureHandle = m_batch_key.texture; - pkt.samplerUniform = m_sampler; - - pkt.vertexCount = static_cast(m_vertices.size()); - pkt.indexCount = static_cast(m_indices.size()); - - pkt.sortKey = makeSortKey(0, 0, static_cast(m_pipeline.value), static_cast(m_batch_key.texture.value), 0); - - bx::mtxIdentity(pkt.model); - - pass.packets.push_back(pkt); - - m_vertices.clear(); - m_indices.clear(); - } - - void QuadRenderer::shutdown() - { - using namespace draco::rendering::rhi; - - destroyBuffer(m_vb); - destroyBuffer(m_ib); - - destroyUniform(m_sampler); - } - - void QuadRenderer::buildOrtho(OrthoCamera& cam, f32 width, f32 height) - { - using namespace draco::rendering::rhi; - - identityMatrix(cam.view); - identityMatrix(cam.proj); - - f32 rl = std::max(width, 1.0f); - f32 tb = std::max(height, 1.0f); - - cam.proj[0] = 2.0f / rl; - cam.proj[5] = -2.0f / tb; - cam.proj[10] = -1.0f; - - cam.proj[12] = -1.0f; - cam.proj[13] = 1.0f; - } -} diff --git a/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cppm b/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cppm deleted file mode 100644 index e3f72bc8..00000000 --- a/Engine/cpp/Runtime/Rendering/QuadRenderer/QuadRenderer.cppm +++ /dev/null @@ -1,81 +0,0 @@ -module; - -#include - -export module rendering.quad; - -import core.stdtypes; -import rendering.rhi; -import rendering.rhi.vertex; -import rendering.rendergraph; - -export namespace draco::rendering::quad { - - struct BatchKey { - rhi::TextureHandle texture = rhi::InvalidTexture; - - rhi::PipelineHandle pipeline = rhi::InvalidPipeline; - - rhi::SamplerHandle sampler = rhi::InvalidSampler; - - [[nodiscard]] bool operator==(const BatchKey&) const = default; - }; - - struct QuadCommand { - rhi::TextureHandle texture = rhi::InvalidTexture; - - f32 x = 0.0f; - f32 y = 0.0f; - f32 z = 0.0f; - - f32 width = 1.0f; - f32 height = 1.0f; - - f32 rotation = 0.0f; - - u32 color = 0xffffffff; - }; - - struct OrthoCamera { - f32 view[16]; - f32 proj[16]; - - f32 x = 0.0f; - f32 y = 0.0f; - f32 zoom = 1.0f; - }; - - class QuadRenderer { - public: - static constexpr u32 MaxQuads = 10000; - static constexpr u32 MaxVertices = MaxQuads * 4; - static constexpr u32 MaxIndices = MaxQuads * 6; - - void init(draco::rendering::rhi::PipelineHandle pipeline); - - void begin(); - - void submit(const QuadCommand& cmd); - - void flushToPass(rendergraph::Pass& pass); - - void shutdown(); - - static void buildOrtho(OrthoCamera& cam, f32 width, f32 height); - - private: - void pushQuad(const QuadCommand& cmd); - - BatchKey m_batch_key{}; - - std::vector m_vertices; - std::vector m_indices; - - rhi::BufferHandle m_vb = rhi::InvalidBuffer; - rhi::BufferHandle m_ib = rhi::InvalidBuffer; - rhi::LayoutHandle m_layout = rhi::InvalidLayout; - rhi::PipelineHandle m_pipeline = rhi::InvalidPipeline; - rhi::UniformHandle m_sampler = rhi::InvalidUniform; - u32 m_quad_count = 0; - }; -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Buffers.cpp b/Engine/cpp/Runtime/Rendering/RHI/Buffers.cpp deleted file mode 100644 index fa65dac5..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Buffers.cpp +++ /dev/null @@ -1,102 +0,0 @@ -module; - -#include -#include "macros.h" - -module rendering.rhi; - -import core.stdtypes; -import core.math.constants; - -namespace draco::rendering::rhi -{ - BufferHandle createVertexBuffer(const void* data, u32 size, LayoutHandle layout_h) - { - RHI_ASSERT(data != nullptr, "Vertex buffer data is null"); - RHI_ASSERT(size > 0, "Vertex buffer size is zero"); - - auto* layout = getChecked(g_layouts, layout_h, "Layout"); - - RHI_ASSERT(layout, "Invalid vertex layout"); - - auto vbh = bgfx::createVertexBuffer(bgfx::copy(data, size), layout->layout); - - Buffer buf; - buf.vbh = vbh; - - return g_buffers.create(buf); - } - - BufferHandle createIndexBuffer(const void* data, u32 size) - { - RHI_ASSERT(data != nullptr, "Index buffer data is null"); - RHI_ASSERT(size > 0, "Index buffer size is zero"); - - bgfx::IndexBufferHandle ibh = bgfx::createIndexBuffer(bgfx::copy(data, size), BGFX_BUFFER_INDEX32); - - Buffer buf; // Idk why I named it this, it just sounds funny ;) - buf.ibh = ibh; - buf.isIndex = true; - - return g_buffers.create(buf); - } - - BufferHandle createDynamicVertexBuffer(u32 size, LayoutHandle layout_h) - { - auto* layout = getChecked(g_layouts, layout_h, "Layout"); - RHI_ASSERT(layout, "Invalid layout"); - - bgfx::DynamicVertexBufferHandle dvbh = bgfx::createDynamicVertexBuffer(size, layout->layout); - - RHI_ASSERT(bgfx::isValid(dvbh), "Failed to create dynamic vertex buffer"); - - Buffer buf; - buf.dvbh = dvbh; - buf.isDynamic = true; - - return g_buffers.create(buf); - } - - void updateDynamicVertexBuffer(BufferHandle handle, u32 start_vertex, const void* data, u32 size) - { - auto* buf = getChecked(g_buffers, handle, "Buffer"); - - if (!buf) - return; - - RHI_ASSERT(buf->isDynamic && !buf->isIndex, "Not a dynamic vertex buffer"); - RHI_ASSERT(bgfx::isValid(buf->dvbh), "Invalid dynamic vertex buffer handle"); - - const bgfx::Memory* mem = bgfx::copy(data, size); - - bgfx::update(buf->dvbh, start_vertex, mem); - } - - BufferHandle createDynamicIndexBuffer(u32 size, u16 flags) - { - bgfx::DynamicIndexBufferHandle ibh = bgfx::createDynamicIndexBuffer(size, flags); - - RHI_ASSERT(bgfx::isValid(ibh), "Invalid dynamic index buffer handle"); - - Buffer buf{}; - buf.isDynamic = true; - buf.isIndex = true; - buf.dibh = ibh; - - return g_buffers.create(buf); - } - - void updateDynamicIndexBuffer(BufferHandle handle, u32 start_index, const void* data, u32 size) - { - auto* buf = getChecked(g_buffers, handle, "DynamicIndexBuffer"); - - if (!buf) - return; - - RHI_ASSERT(buf->isDynamic && buf->isIndex, "Not a dynamic index buffer"); - - const bgfx::Memory* mem = bgfx::copy(data, size); - - bgfx::update(buf->dibh, start_index, mem); - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Commands.cpp b/Engine/cpp/Runtime/Rendering/RHI/Commands.cpp deleted file mode 100644 index c751d14c..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Commands.cpp +++ /dev/null @@ -1,150 +0,0 @@ -module; - -#include -#include -#include -#include "macros.h" - -module rendering.rhi; - -import core.stdtypes; -import core.math.constants; - -namespace draco::rendering::rhi -{ - void perspective(f32* out, f32 fov, f32 aspect, f32 nearp, f32 farp) - { - bx::mtxProj(out, fov, aspect, nearp, farp, bgfx::getCaps()->homogeneousDepth); - } - - void lookAt(f32* out, const f32* eye, const f32* at, const f32* up) - { - bx::Vec3 eye_v { eye[0], eye[1], eye[2] }; - bx::Vec3 at_v { at[0], at[1], at[2] }; - bx::Vec3 up_v { up[0], up[1], up[2] }; - - bx::mtxLookAt(out, eye_v, at_v, up_v); - } - - // Note: Internal use only, use apply_view() instead - void setViewRect(ViewID view, u16 x, u16 y, u16 w, u16 h) - { - bgfx::setViewRect(view, x, y, w, h); - } - - // Note: Internal use only, use apply_view() instead - void setViewFramebuffer(ViewID view, FramebufferHandle h) - { - auto* fb = getChecked(g_framebuffers, h, "Framebuffer"); - - if (!fb) - return; - - bgfx::setViewFrameBuffer(view, fb->fbh); - } - - void setViewProjection(ViewID view, const f32* view_mtx, const f32* proj_mtx) - { - bgfx::setViewTransform(view, view_mtx, proj_mtx); - } - - void setScissor(const ScissorRect& r) - { - if (!r.enabled) - bgfx::setScissor(math::UINT16_MAX_VAL); - else - bgfx::setScissor(r.x, r.y, r.w, r.h); - } - - void setStencil(u32 fstencil, u32 bstencil) - { - bgfx::setStencil(fstencil, bstencil); - } - - void applyView(ViewID view, const ViewDesc& desc) - { - if (desc.fb != InvalidFramebuffer) - { - auto* fb = getChecked(g_framebuffers, desc.fb, "Framebuffer"); - - if (fb && bgfx::isValid(fb->fbh)) - { - bgfx::setViewFrameBuffer(view, fb->fbh); - } - else - { - RHI_WARN(false, "Framebuffer invalid at apply_view"); - } - } - - bgfx::setViewRect(view, desc.x, desc.y, desc.w, desc.h); - - if (desc.clearFlags != 0) - { - bgfx::setViewClear(view, desc.clearFlags, desc.clearColor); - } - } - - void identityMatrix(f32* mtx) - { - bx::mtxIdentity(mtx); - } - - void submit(const RenderPacket& p, ViewID view) - { - auto* pipeline = getChecked(g_pipelines, p.pipeline, "Pipeline"); - auto* vb = getChecked(g_buffers, p.vertexBuffer, "VertexBuffer"); - Buffer* ib = nullptr; - - if (!pipeline || !vb) - return; - - if (p.indexBuffer != InvalidBuffer) - ib = getChecked(g_buffers, p.indexBuffer, "IndexBuffer"); - - // Transform matrix (model) - bgfx::setTransform(p.model); - - // Vertex buffer binding with explicit range control - if (vb->isDynamic) - { - // If count is UINT32_MAX, bgfx will fallback to drawing the full buffer automatically - bgfx::setVertexBuffer(0, vb->dvbh, 0, p.vertexCount); - } else { - bgfx::setVertexBuffer(0, vb->vbh, 0, p.vertexCount); - } - - // Index buffer binding with explicit range control - if (ib && ib->isIndex) - { - if (ib->isDynamic) - { - bgfx::setIndexBuffer(ib->dibh, 0, p.indexCount); - } else { - bgfx::setIndexBuffer(ib->ibh, 0, p.indexCount); - } - } - - // Uniforms - for (const auto& u : p.uniforms) - { - if (auto* handle = getChecked(g_uniforms, u.handle, "UniformBind")) - { - bgfx::setUniform(*handle, u.data, u.num); - } - } - - // Texture binding - if (auto* tex = getChecked(g_textures, p.textureHandle, "Texture")) - { - if (auto* sampler = getChecked(g_uniforms, p.samplerUniform, "Sampler")) - { - bgfx::setTexture(p.textureUnit, *sampler, *tex, p.samplerFlags); - } - } - - // Apply pipeline state & submit draw call - bgfx::setState(pipeline->state); - bgfx::submit(view, pipeline->program); - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Commands.cppm b/Engine/cpp/Runtime/Rendering/RHI/Commands.cppm new file mode 100644 index 00000000..c576a7de --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Commands.cppm @@ -0,0 +1,236 @@ +/// Abstract command recording interfaces: CommandPool, CommandEncoder, +/// RenderPassEncoder, ComputePassEncoder, TransferBatch. + +module; + +#include +#include + +export module rhi:commands; + +import core.stdtypes; +import core.status; +import :forward; +import :enums; +import :types; +import :descriptors; +import :resources; + +using namespace draco; + +export namespace draco::rhi { + +// ---- Render Command Encoder (shared draw-recording surface) ---- + +/// The draw-recording commands common to a render pass and a render bundle. A `Renderer` that +/// takes a `RenderCommandEncoder*` records identically whether it targets a live pass (inline) +/// or an off-thread `RenderBundleEncoder` - which is what makes parallel command recording +/// fall out (split a draw list into N bundles recorded on N threads, then executeBundles). +/// This is exactly the subset valid inside a WebGPU render bundle: no pass-level dynamic state +/// (viewport / scissor / blend constant / stencil ref are inherited from the pass), no queries. +class RenderCommandEncoder { +public: + virtual ~RenderCommandEncoder() = default; + + /// Bind a graphics (rasterization) pipeline. + virtual void setPipeline(RenderPipeline* pipeline) = 0; + /// Bind a resource group at the given index. + virtual void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets = {}) = 0; + /// Upload push constant data. + virtual void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) = 0; + + /// Bind a vertex buffer to a slot. + virtual void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset = 0) = 0; + /// Bind an index buffer. + virtual void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset = 0) = 0; + + /// Issue a non-indexed draw call. + virtual void draw(u32 vertexCount, u32 instanceCount = 1, u32 firstVertex = 0, u32 firstInstance = 0) = 0; + /// Issue an indexed draw call. + virtual void drawIndexed(u32 indexCount, u32 instanceCount = 1, u32 firstIndex = 0, i32 baseVertex = 0, u32 firstInstance = 0) = 0; + /// Issue an indirect draw call. + virtual void drawIndirect(Buffer* buffer, u64 offset, u32 drawCount = 1, u32 stride = 0) = 0; + /// Issue an indexed indirect draw call. + virtual void drawIndexedIndirect(Buffer* buffer, u64 offset, u32 drawCount = 1, u32 stride = 0) = 0; +}; + +// ---- Render Pass Encoder ---- + +/// Records draw commands within a render pass: the shared recording surface plus the pass-level +/// dynamic state, queries, bundle execution, and pass control. +class RenderPassEncoder : public RenderCommandEncoder { +public: + /// Cross-query for the mesh-shader extension (replacement for a sideways + /// dynamic_cast). Encoders that support it return `this`. + [[nodiscard]] virtual MeshShaderPassExt* asMeshShaderExt() noexcept { return nullptr; } + + /// Set the viewport rectangle and depth range. + virtual void setViewport(f32 x, f32 y, f32 w, f32 h, f32 minDepth = 0.0f, f32 maxDepth = 1.0f) = 0; + /// Set the scissor rectangle. + virtual void setScissor(i32 x, i32 y, u32 w, u32 h) = 0; + /// Set the blend constant color. + virtual void setBlendConstant(f32 r, f32 g, f32 b, f32 a) = 0; + /// Set the stencil reference value. + virtual void setStencilReference(u32 reference) = 0; + + /// Execute pre-recorded render bundles in order (replays their draws into this pass). The + /// pass must have been begun with RenderPassContents::SecondaryCommandBuffers. + virtual void executeBundles(std::span bundles) = 0; + + /// Write a timestamp query. + virtual void writeTimestamp(QuerySet* querySet, u32 index) = 0; + /// Begin an occlusion query. + virtual void beginOcclusionQuery(QuerySet* querySet, u32 index) = 0; + /// End an occlusion query. + virtual void endOcclusionQuery(QuerySet* querySet, u32 index) = 0; + + /// End the render pass. + virtual void end() = 0; +}; + +// ---- Render Bundle ---- + +/// An immutable, pre-recorded sequence of draw commands, replayable into any compatible render +/// pass (matching attachment formats) via RenderPassEncoder::executeBundles. Valid until its +/// owning command pool is reset. Recorded off the main thread for parallel command recording. +class RenderBundle { +public: + virtual ~RenderBundle() = default; +}; + +/// Records draws into a render bundle (the shared recording surface only - no pass-level state). +class RenderBundleEncoder : public RenderCommandEncoder { +public: + /// Finish recording and return the immutable bundle (owned by the command pool). + [[nodiscard]] virtual RenderBundle* finish() = 0; +}; + +// ---- Compute Pass Encoder ---- + +/// Records compute dispatch commands within a compute pass. +class ComputePassEncoder { +public: + virtual ~ComputePassEncoder() = default; + + virtual void setPipeline(ComputePipeline* pipeline) = 0; + virtual void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets = {}) = 0; + virtual void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) = 0; + + /// Dispatch compute work groups. + virtual void dispatch(u32 x, u32 y = 1, u32 z = 1) = 0; + /// Dispatch compute work groups via an indirect buffer. + virtual void dispatchIndirect(Buffer* buffer, u64 offset) = 0; + + /// Insert a compute-to-compute memory barrier. + virtual void computeBarrier() = 0; + + virtual void writeTimestamp(QuerySet* querySet, u32 index) = 0; + virtual void end() = 0; +}; + +// ---- Command Encoder ---- + +/// Records GPU commands: render/compute passes, barriers, copies, queries. +class CommandEncoder { +public: + virtual ~CommandEncoder() = default; + + /// Cross-query for the ray-tracing extension (replacement for a sideways + /// dynamic_cast). Encoders that support it return `this`. + [[nodiscard]] virtual RayTracingEncoderExt* asRayTracingExt() noexcept { return nullptr; } + + /// Begin a render pass. Returns the encoder for recording draw commands. + [[nodiscard]] virtual RenderPassEncoder* beginRenderPass(const RenderPassDesc& desc) = 0; + /// Begin a compute pass. + [[nodiscard]] virtual ComputePassEncoder* beginComputePass(std::u8string_view label = {}) = 0; + /// Begin recording a render bundle (a reusable, off-thread-recordable draw sequence + /// replayable into passes matching `desc`'s attachment signature). Returns null if the + /// backend does not support bundles. + [[nodiscard]] virtual RenderBundleEncoder* createRenderBundleEncoder(const RenderBundleDesc& desc) = 0; + + /// Insert resource barriers. + virtual void barrier(const BarrierGroup& group) = 0; + + /// Convenience: transition a single texture between resource states. + void transitionTexture(Texture* tex, ResourceState oldState, ResourceState newState) { + TextureBarrier tb{}; tb.texture = tex; tb.oldState = oldState; tb.newState = newState; + BarrierGroup g{}; g.textureBarriers = std::span(&tb, 1); + barrier(g); + } + + /// Convenience: transition a single buffer between resource states. + void transitionBuffer(Buffer* buf, ResourceState oldState, ResourceState newState) { + BufferBarrier bb{}; bb.buffer = buf; bb.oldState = oldState; bb.newState = newState; + BarrierGroup g{}; g.bufferBarriers = std::span(&bb, 1); + barrier(g); + } + + /// Copy operations. + virtual void copyBufferToBuffer(Buffer* src, u64 srcOffset, Buffer* dst, u64 dstOffset, u64 size) = 0; + virtual void copyBufferToTexture(Buffer* src, Texture* dst, const BufferTextureCopyRegion& region) = 0; + virtual void copyTextureToBuffer(Texture* src, Buffer* dst, const BufferTextureCopyRegion& region) = 0; + virtual void copyTextureToTexture(Texture* src, Texture* dst, const TextureCopyRegion& region) = 0; + + /// Blit (scaled copy) from one texture to another. + virtual void blit(Texture* src, Texture* dst) = 0; + /// Generate mipmaps for a texture. + virtual void generateMipmaps(Texture* texture) = 0; + /// Resolve a multisampled texture to a single-sampled texture. + virtual void resolveTexture(Texture* src, Texture* dst) = 0; + + /// Query operations. + virtual void resetQuerySet(QuerySet* querySet, u32 first, u32 count) = 0; + virtual void writeTimestamp(QuerySet* querySet, u32 index) = 0; + virtual void resolveQuerySet(QuerySet* querySet, u32 first, u32 count, Buffer* dst, u64 dstOffset) = 0; + + /// Debug labels. + virtual void beginDebugLabel(std::u8string_view label, f32 r = 0, f32 g = 0, f32 b = 0, f32 a = 1) = 0; + virtual void endDebugLabel() = 0; + virtual void insertDebugLabel(std::u8string_view label, f32 r = 0, f32 g = 0, f32 b = 0, f32 a = 1) = 0; + + /// Finish recording and return an immutable command buffer. + [[nodiscard]] virtual CommandBuffer* finish() = 0; +}; + +// ---- Command Pool ---- + +/// Manages command buffer memory for a single queue type. +/// One pool per thread per queue type. +class CommandPool { +public: + virtual ~CommandPool() = default; + + /// Create a new command encoder for recording. + virtual Status createEncoder(CommandEncoder*& out) = 0; + /// Destroy a command encoder. + virtual void destroyEncoder(CommandEncoder*& encoder) = 0; + /// Reset all command buffers allocated from this pool. + virtual void reset() = 0; +}; + +// ---- Transfer Batch ---- + +/// Batches staging upload operations (CPU->GPU buffer/texture writes). +class TransferBatch { +public: + virtual ~TransferBatch() = default; + + /// Stage a buffer write. + virtual void writeBuffer(Buffer* dst, u64 dstOffset, std::span data) = 0; + /// Stage a texture write. + virtual void writeTexture(Texture* dst, std::span data, + const TextureDataLayout& layout, Extent3D extent, + u32 mipLevel = 0, u32 arrayLayer = 0) = 0; + + /// Submit all staged writes synchronously. + virtual Status submit() = 0; + /// Submit all staged writes, signaling a fence on completion. + virtual Status submitAsync(Fence* fence, u64 signalValue) = 0; + + /// Reset the batch for reuse. + virtual void reset() = 0; + /// Destroy the batch. + virtual void destroy() = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Core.cpp b/Engine/cpp/Runtime/Rendering/RHI/Core.cpp deleted file mode 100644 index e8529435..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Core.cpp +++ /dev/null @@ -1,334 +0,0 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include "macros.h" - -module rendering.rhi; - -import core.stdtypes; -import core.math.constants; - -namespace draco::rendering::rhi -{ - using namespace draco::core::memory; - - HandleRegistry g_buffers; - HandleRegistry g_pipelines; - HandleRegistry g_uniforms; - HandleRegistry g_textures; - HandleRegistry g_framebuffers; - HandleRegistry g_shaders; - HandleRegistry g_layouts; - - std::vector g_deletion_queue; - u16 g_width = 0; - u16 g_height = 0; - - void queueDestruction(std::function cb) - { - g_deletion_queue.push_back({ - bgfx::getStats()->gpuFrameNum, - std::move(cb) - }); - } - - // Explicit overloads for each bgfx resource - void destroyLater(bgfx::ShaderHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::UniformHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::VertexBufferHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::IndexBufferHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::DynamicVertexBufferHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::DynamicIndexBufferHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::TextureHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void destroyLater(bgfx::FrameBufferHandle handle) - { - queueDestruction([handle]() { - bgfx::destroy(handle); - }); - } - - void processDeletions() - { - u64 frame = bgfx::getStats()->gpuFrameNum; - - std::erase_if(g_deletion_queue, [frame](const auto& d) - { - if (frame >= d.frame + 2) - { - d.cleanup(); - return true; - } - return false; - }); - } - - bool init(void* display_type, void* window_handle, NativeWindowType window_type, u16 width, u16 height) - { - g_width = width; - g_height = height; - - bgfx::Init init{}; - init.type = bgfx::RendererType::Count; - - init.platformData.ndt = display_type; - init.platformData.nwh = window_handle; - - // Map our internal window type to bgfx's native window handle type - if (window_type == NativeWindowType::Wayland) - { - init.platformData.type = bgfx::NativeWindowHandleType::Wayland; - } - else - { - // Others can work fine with the default type - init.platformData.type = bgfx::NativeWindowHandleType::Default; - } - - init.resolution.width = width; - init.resolution.height = height; - init.resolution.reset = BGFX_RESET_VSYNC; - - if (!bgfx::init(init)) - { - RHI_WARN(false, "bgfx initialization failed"); - return false; - } - - bgfx::setDebug(BGFX_DEBUG_TEXT); - return true; - } - - void resize(u16 width, u16 height) - { - if(width == 0 || height == 0) - return; // Minimized window safety - - if(width == g_width && height == g_height) - return; // No need to resize - - g_width = width; - g_height = height; - - bgfx::reset(width, height, BGFX_RESET_VSYNC); - } - - void shutdown() - { - // Walk all registries and destroy live GPU objects - for (auto& slot : g_buffers.internal().raw()) - { - if (!slot.alive) continue; - - if (bgfx::isValid(slot.value.vbh)) - bgfx::destroy(slot.value.vbh); - - if (bgfx::isValid(slot.value.ibh)) - bgfx::destroy(slot.value.ibh); - - if (bgfx::isValid(slot.value.dvbh)) - bgfx::destroy(slot.value.dvbh); - } - - for (auto& slot : g_pipelines.internal().raw()) - { - if (!slot.alive) continue; - - if (bgfx::isValid(slot.value.program)) - bgfx::destroy(slot.value.program); - } - - for (auto& slot : g_uniforms.internal().raw()) - { - if (!slot.alive) continue; - - if (bgfx::isValid(slot.value)) - bgfx::destroy(slot.value); - } - - for (auto& slot : g_textures.internal().raw()) - { - if (!slot.alive) continue; - - if (bgfx::isValid(slot.value)) - bgfx::destroy(slot.value); - } - - bgfx::shutdown(); - } - - void destroyBuffer(BufferHandle h) - { - auto* buf = getChecked(g_buffers, h, "Buffer"); - - if (!buf) - return; - - if (bgfx::isValid(buf->vbh)) - destroyLater(buf->vbh); - - if (bgfx::isValid(buf->ibh)) - destroyLater(buf->ibh); - - if (bgfx::isValid(buf->dvbh)) - destroyLater(buf->dvbh); - - if (bgfx::isValid(buf->dibh)) - destroyLater(buf->dibh); - - g_buffers.destroy(h); - } - - u64 mapState(PipelineState s, BlendMode blend, DepthTest depth, CullMode cull, bool depth_write) - { - u64 state = 0; - - if ((s & PipelineState::WriteRGB) != PipelineState::Default) - state |= BGFX_STATE_WRITE_RGB; - - if ((s & PipelineState::WriteAlpha) != PipelineState::Default) - state |= BGFX_STATE_WRITE_A; - - if (depth_write) - state |= BGFX_STATE_WRITE_Z; - - switch (depth) - { - case DepthTest::Less: state |= BGFX_STATE_DEPTH_TEST_LESS; break; - case DepthTest::Equal: state |= BGFX_STATE_DEPTH_TEST_EQUAL; break; - case DepthTest::Always: state |= BGFX_STATE_DEPTH_TEST_ALWAYS; break; - case DepthTest::None: break; - } - - - switch (cull) - { - case CullMode::CW: state |= BGFX_STATE_CULL_CW; break; - case CullMode::CCW: state |= BGFX_STATE_CULL_CCW; break; - case CullMode::None: break; - } - - switch (blend) - { - case BlendMode::Alpha: - state |= BGFX_STATE_BLEND_ALPHA; - break; - - case BlendMode::Additive: - state |= BGFX_STATE_BLEND_ADD; - break; - - case BlendMode::Multiply: - state |= BGFX_STATE_BLEND_MULTIPLY; - break; - - case BlendMode::None: - break; - } - - if ((s & PipelineState::MSAA) != PipelineState::Default) - state |= BGFX_STATE_MSAA; - - if ((s & PipelineState::PrimitiveTriStrip) != PipelineState::Default) - state |= BGFX_STATE_PT_TRISTRIP; - - return state; - } - - bgfx::UniformType::Enum map_uniform_type(UniformType t) - { - switch (t) - { - case UniformType::Sampler: return bgfx::UniformType::Sampler; - case UniformType::Vec4: return bgfx::UniformType::Vec4; - case UniformType::Mat3: return bgfx::UniformType::Mat3; - case UniformType::Mat4: return bgfx::UniformType::Mat4; - } - return bgfx::UniformType::Vec4; - } - - bgfx::Attrib::Enum map_attrib(Attrib a) - { - switch (a) - { - case Attrib::Position: return bgfx::Attrib::Position; - case Attrib::Color0: return bgfx::Attrib::Color0; - case Attrib::TexCoord0: return bgfx::Attrib::TexCoord0; - case Attrib::Normal: return bgfx::Attrib::Normal; - case Attrib::Tangent: return bgfx::Attrib::Tangent; - } - - return bgfx::Attrib::Position; - } - - bgfx::AttribType::Enum map_attrib_type(AttribType t) - { - switch (t) - { - case AttribType::Float: return bgfx::AttribType::Float; - case AttribType::Uint8: return bgfx::AttribType::Uint8; - } - - return bgfx::AttribType::Float; - } - - void beginFrame() - { - // Clean up GPU resources safely - processDeletions(); - } - - void endFrame() - { - // Submit frame to GPU - bgfx::frame(); - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAccelStruct.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAccelStruct.cppm new file mode 100644 index 00000000..358c7d83 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAccelStruct.cppm @@ -0,0 +1,58 @@ +/// DX12 implementation of AccelStruct. +/// Wraps a D3D12 buffer resource in RAYTRACING_ACCELERATION_STRUCTURE state. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:accel_struct; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxAccelStructImpl : public AccelStruct { +public: + Status init(ID3D12Device* device, const AccelStructDesc& d) { + m_type = d.type; + + D3D12_HEAP_PROPERTIES hp{}; hp.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + rd.Width = 256 * 1024; // 256 KB default, grown at build time + rd.Height = 1; + rd.DepthOrArraySize = 1; + rd.MipLevels = 1; + rd.Format = DXGI_FORMAT_UNKNOWN; + rd.SampleDesc = { 1, 0 }; + rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + rd.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + + HRESULT hr = device->CreateCommittedResource( + &hp, D3D12_HEAP_FLAG_NONE, &rd, + D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE, nullptr, + IID_PPV_ARGS(&m_resource)); + if (FAILED(hr)) return ErrorCode::Unknown; + + m_gpuAddr = m_resource->GetGPUVirtualAddress(); + return ErrorCode::Ok; + } + + AccelStructType type() const override { return m_type; } + u64 deviceAddress() const override { return m_gpuAddr; } + + void cleanup() { m_resource.Reset(); } + + [[nodiscard]] ID3D12Resource* handle() const { return m_resource.Get(); } + +private: + AccelStructType m_type = AccelStructType::BottomLevel; + u64 m_gpuAddr = 0; + ComPtr m_resource; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAdapter.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAdapter.cppm new file mode 100644 index 00000000..0f9bd638 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxAdapter.cppm @@ -0,0 +1,122 @@ +/// DX12 implementation of Adapter. +/// Wraps IDXGIAdapter1, queries device features, creates DxDevice. + +module; + +#include "DxIncludes.h" + +#include +#include + +export module rhi.dx12:adapter; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward + +/// Convert a null-terminated UTF-16 string (as DXGI reports device names) to UTF-8. +inline std::u8string wideToUtf8(const wchar_t* w) { + if (!w) return {}; + const int len = ::WideCharToMultiByte(CP_UTF8, 0, w, -1, nullptr, 0, nullptr, nullptr); + if (len <= 1) return {}; + std::u8string out(static_cast(len - 1), u8'\0'); + ::WideCharToMultiByte(CP_UTF8, 0, w, -1, reinterpret_cast(out.data()), len, nullptr, nullptr); + return out; +} + +class DxAdapterImpl : public Adapter { +public: + DxAdapterImpl(IDXGIAdapter1* adapter, IDXGIFactory4* factory) + : m_adapter(adapter), m_factory(factory) + { + m_adapter->GetDesc1(&m_desc); + } + + ~DxAdapterImpl() override { + if (m_adapter) { m_adapter->Release(); m_adapter = nullptr; } + } + + // ---- Adapter interface ---- + + void getInfo(AdapterInfo& out) override { + // DXGI Description is a WCHAR[] (UTF-16) - transcode to UTF-8. + out.name = wideToUtf8(m_desc.Description); + out.vendorId = m_desc.VendorId; + out.deviceId = m_desc.DeviceId; + out.type = (m_desc.DedicatedVideoMemory > 0) ? AdapterType::DiscreteGpu : AdapterType::IntegratedGpu; + out.supportedFeatures = buildFeatures(); + } + + DeviceFeatures buildFeatures() { + // Create a temporary device to query features. + ComPtr tempDevice; + HRESULT hr = D3D12CreateDevice(m_adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&tempDevice)); + if (FAILED(hr) || !tempDevice) return {}; + + DeviceFeatures f{}; + + // Check feature support. + D3D12_FEATURE_DATA_D3D12_OPTIONS options{}; + if (SUCCEEDED(tempDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS, &options, sizeof(options)))) { + f.bindlessDescriptors = true; // DX12 always supports descriptor indexing + f.timestampQueries = true; + f.multiDrawIndirect = true; + f.depthClamp = true; + f.fillModeWireframe = true; + f.textureCompressionBC = true; + f.textureCompressionASTC = false; + f.independentBlend = true; + f.multiViewport = true; + f.pipelineStatisticsQueries = true; + } + + // Check mesh shader support. + D3D12_FEATURE_DATA_D3D12_OPTIONS7 options7{}; + if (SUCCEEDED(tempDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS7, &options7, sizeof(options7)))) { + f.meshShaders = (options7.MeshShaderTier != D3D12_MESH_SHADER_TIER_NOT_SUPPORTED); + } + + // Check ray tracing support. + D3D12_FEATURE_DATA_D3D12_OPTIONS5 options5{}; + if (SUCCEEDED(tempDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &options5, sizeof(options5)))) { + f.rayTracing = (options5.RaytracingTier != D3D12_RAYTRACING_TIER_NOT_SUPPORTED); + } + + // Conservative limits for D3D12 feature level 12.0. + f.maxBindGroups = 32; + f.maxBindingsPerGroup = 1000000; + f.maxPushConstantSize = 128; + f.maxTextureDimension2D = 16384; + f.maxTextureArrayLayers = 2048; + f.maxComputeWorkgroupSizeX = 1024; + f.maxComputeWorkgroupSizeY = 1024; + f.maxComputeWorkgroupSizeZ = 64; + f.maxComputeWorkgroupsPerDimension = 65535; + f.maxBufferSize = static_cast(m_desc.DedicatedVideoMemory); + f.minUniformBufferOffsetAlignment = 256; + f.minStorageBufferOffsetAlignment = 16; + f.timestampPeriodNs = 1; // DX12 timestamps in ticks, period queried at runtime + + return f; + } + + Status createDevice(const DeviceDesc& desc, Device*& out) override; + + // ---- Internal ---- + [[nodiscard]] IDXGIAdapter1* handle() const { return m_adapter; } + [[nodiscard]] IDXGIFactory4* factory() const { return m_factory; } + [[nodiscard]] DXGI_ADAPTER_DESC1 adapterDesc() const { return m_desc; } + +private: + IDXGIAdapter1* m_adapter = nullptr; // owned, released in destructor + IDXGIFactory4* m_factory = nullptr; // not owned (Backend owns it) + DXGI_ADAPTER_DESC1 m_desc{}; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBackend.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBackend.cppm new file mode 100644 index 00000000..56e56084 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBackend.cppm @@ -0,0 +1,138 @@ +/// DX12 implementation of Backend. +/// Creates DXGI factory, enumerates adapters, creates surfaces. + +module; + +#include "DxIncludes.h" +#include +#include + +#include + +export module rhi.dx12:backend; + +import core.stdtypes; +import core.status; +import rhi; +import :surface; +import :adapter; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +/// Configuration for DX12 backend creation. +struct DxBackendDesc { + bool enableValidation = false; +}; + +/// DX12 implementation of Backend. +class DxBackendImpl : public Backend { +public: + ~DxBackendImpl() override { destroyImpl(); } + + // ---- Backend interface ---- + + std::span enumerateAdapters() override { + return std::span(m_adapterPtrs.data(), m_adapterPtrs.size()); + } + + Status createSurface(void* windowHandle, void* /*displayHandle*/, Surface*& out) override { + out = nullptr; + if (!windowHandle) { + logError("DxBackend: window handle is null"); + return ErrorCode::InvalidArgument; + } + out = new DxSurfaceImpl(reinterpret_cast(windowHandle)); + return ErrorCode::Ok; + } + + void destroy() override { + destroyImpl(); + delete this; + } + + // ---- Internal ---- + [[nodiscard]] IDXGIFactory4* factory() const { return m_factory.Get(); } + [[nodiscard]] bool validationEnabled() const { return m_validationEnabled; } + +private: + friend Status createDxBackend(const DxBackendDesc& desc, Backend*& out); + + Status init(bool enableValidation) { + m_validationEnabled = enableValidation; + + // Enable debug layer before device creation. + if (m_validationEnabled) { + ComPtr debugController; + if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { + debugController->EnableDebugLayer(); + } + } + + // Create DXGI factory. + UINT factoryFlags = m_validationEnabled ? DXGI_CREATE_FACTORY_DEBUG : 0; + HRESULT hr = CreateDXGIFactory2(factoryFlags, IID_PPV_ARGS(&m_factory)); + if (FAILED(hr)) { + logErrorf("DxBackend: CreateDXGIFactory2 failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + enumerateAdaptersInternal(); + isInitialized = true; + return ErrorCode::Ok; + } + + void enumerateAdaptersInternal() { + ComPtr adapter; + for (UINT i = 0; m_factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) { + DXGI_ADAPTER_DESC1 desc{}; + adapter->GetDesc1(&desc); + + // Skip software adapters. + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { + adapter.Reset(); + continue; + } + + // Check D3D12 feature level 12.0 support. + if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, __uuidof(ID3D12Device), nullptr))) { + auto* a = new DxAdapterImpl(adapter.Detach(), m_factory.Get()); + m_adapters.push_back(a); + m_adapterPtrs.push_back(a); + } + + adapter.Reset(); + } + + // Expose adapters best-GPU-first; callers take [0]. See Backend::enumerateAdapters. + sortAdaptersByPreference(m_adapterPtrs); + } + + void destroyImpl() { + for (auto* a : m_adapters) delete a; + m_adapters.clear(); + m_adapterPtrs.clear(); + m_factory.Reset(); + } + + ComPtr m_factory; + bool m_validationEnabled = false; + std::vector m_adapters; + std::vector m_adapterPtrs; +}; + +/// Creates a DX12 backend. Caller owns the returned pointer - dispose via destroy(). +[[nodiscard]] Status createDxBackend(const DxBackendDesc& desc, Backend*& out) { + out = nullptr; + auto* b = new DxBackendImpl(); + Status r = b->init(desc.enableValidation); + if (r != ErrorCode::Ok) { + delete b; + return r; + } + out = b; + return ErrorCode::Ok; +} + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroup.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroup.cppm new file mode 100644 index 00000000..8ffde992 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroup.cppm @@ -0,0 +1,213 @@ +/// DX12 implementation of BindGroup. +/// Allocates contiguous descriptor ranges in CPU-visible heaps and writes descriptors. + +module; + +#include "DxIncludes.h" +#include +#include + +export module rhi.dx12:bind_group; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :bind_group_layout; +import :gpu_descriptor_heap; +import :buffer; +import :texture; +import :texture_view; +import :sampler; +import :accel_struct; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxAccelStructImpl; // forward + +class DxBindGroupImpl : public BindGroup { +public: + Status init(ID3D12Device* device, const BindGroupDesc& d, + DxGpuDescriptorHeap* cpuSrvHeap, DxGpuDescriptorHeap* cpuSamplerHeap) { + m_device = device; + m_layout = static_cast(d.layout); + m_cpuSrvHeap = cpuSrvHeap; + m_cpuSamplerHeap = cpuSamplerHeap; + if (!m_layout) return ErrorCode::Unknown; + + // Cache counts so cleanup doesn't need to access m_layout (which may be destroyed first). + m_cachedCbvSrvUavCount = m_layout->cbvSrvUavCount(); + m_cachedSamplerCount = m_layout->samplerCount(); + + if (m_cachedCbvSrvUavCount > 0) { + m_cbvSrvUavOffset = cpuSrvHeap->allocate(m_cachedCbvSrvUavCount); + if (m_cbvSrvUavOffset < 0) return ErrorCode::Unknown; + } + if (m_cachedSamplerCount > 0) { + m_samplerOffset = cpuSamplerHeap->allocate(m_cachedSamplerCount); + if (m_samplerOffset < 0) return ErrorCode::Unknown; + } + + writeDescriptors(d); + return ErrorCode::Ok; + } + + BindGroupLayout* layout() override { return m_layout; } + + void updateBindless(std::span entries) override { + auto ranges = m_layout->ranges(); + for (usize i = 0; i < entries.size(); ++i) { + const auto& e = entries[i]; + if (e.layoutIndex >= static_cast(ranges.size())) continue; + const auto& r = ranges[e.layoutIndex]; + + BindGroupEntry bgEntry{}; + bgEntry.buffer = e.buffer; + bgEntry.bufferOffset= e.bufferOffset; + bgEntry.bufferSize = e.bufferSize; + bgEntry.textureView = e.textureView; + bgEntry.sampler = e.sampler; + + if (r.isSampler) + writeSampler(bgEntry, r, e.arrayIndex); + else + writeCbvSrvUav(bgEntry, r, e.arrayIndex); + } + } + + void cleanup() { + if (m_cbvSrvUavOffset >= 0 && m_cachedCbvSrvUavCount > 0) + m_cpuSrvHeap->free(static_cast(m_cbvSrvUavOffset), m_cachedCbvSrvUavCount); + if (m_samplerOffset >= 0 && m_cachedSamplerCount > 0) + m_cpuSamplerHeap->free(static_cast(m_samplerOffset), m_cachedSamplerCount); + m_cbvSrvUavOffset = -1; m_samplerOffset = -1; + } + + [[nodiscard]] i32 cbvSrvUavOffset() const { return m_cbvSrvUavOffset; } + [[nodiscard]] i32 samplerOffset() const { return m_samplerOffset; } + [[nodiscard]] std::span dynamicGpuAddresses() const { return { m_dynAddrs.data(), m_dynAddrs.size() }; } + +private: + void writeDescriptors(const BindGroupDesc& d) { + auto ranges = m_layout->ranges(); + usize entryIdx = 0; + for (usize i = 0; i < ranges.size(); ++i) { + const auto& r = ranges[i]; + switch (r.type) { + case BindingType::BindlessTextures: case BindingType::BindlessSamplers: + case BindingType::BindlessStorageBuffers: case BindingType::BindlessStorageTextures: + continue; + default: break; + } + if (entryIdx >= d.entries.size()) break; + const auto& e = d.entries[entryIdx++]; + + if (r.hasDynamicOffset) { + if (auto* buf = static_cast(e.buffer)) + m_dynAddrs.push_back(buf->gpuAddress() + e.bufferOffset); + else + m_dynAddrs.push_back(0); + continue; + } + if (r.isSampler) writeSampler(e, r); + else writeCbvSrvUav(e, r); + } + } + + void writeCbvSrvUav(const BindGroupEntry& e, const DxBindingRangeInfo& r, u32 arrayIdx = 0) { + u32 off = static_cast(m_cbvSrvUavOffset) + r.heapOffset + arrayIdx; + D3D12_CPU_DESCRIPTOR_HANDLE dest = m_cpuSrvHeap->getCpuHandle(off); + + switch (r.type) { + case BindingType::UniformBuffer: + if (auto* buf = static_cast(e.buffer)) { + D3D12_CONSTANT_BUFFER_VIEW_DESC cbv{}; + cbv.BufferLocation = buf->gpuAddress() + e.bufferOffset; + u64 sz = (e.bufferSize > 0) ? e.bufferSize : buf->desc.size; + cbv.SizeInBytes = static_cast((sz + 255) & ~u64(255)); + m_device->CreateConstantBufferView(&cbv, dest); + } + break; + case BindingType::StorageBufferReadOnly: + if (auto* buf = static_cast(e.buffer)) { + u64 sz = (e.bufferSize > 0) ? e.bufferSize : buf->desc.size; + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.ViewDimension = D3D12_SRV_DIMENSION_BUFFER; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + if (r.storageBufferStride > 0) { + srv.Format = DXGI_FORMAT_UNKNOWN; + srv.Buffer.FirstElement = static_cast(e.bufferOffset / r.storageBufferStride); + srv.Buffer.NumElements = static_cast(sz / r.storageBufferStride); + srv.Buffer.StructureByteStride = r.storageBufferStride; + } else { + srv.Format = DXGI_FORMAT_R32_TYPELESS; + srv.Buffer.FirstElement = static_cast(e.bufferOffset / 4); + srv.Buffer.NumElements = static_cast(sz / 4); + srv.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_RAW; + } + m_device->CreateShaderResourceView(buf->handle(), &srv, dest); + } + break; + case BindingType::StorageBufferReadWrite: + if (auto* buf = static_cast(e.buffer)) { + u64 sz = (e.bufferSize > 0) ? e.bufferSize : buf->desc.size; + D3D12_UNORDERED_ACCESS_VIEW_DESC uav{}; + uav.ViewDimension = D3D12_UAV_DIMENSION_BUFFER; + if (r.storageBufferStride > 0) { + uav.Format = DXGI_FORMAT_UNKNOWN; + uav.Buffer.FirstElement = static_cast(e.bufferOffset / r.storageBufferStride); + uav.Buffer.NumElements = static_cast(sz / r.storageBufferStride); + uav.Buffer.StructureByteStride = r.storageBufferStride; + } else { + uav.Format = DXGI_FORMAT_R32_TYPELESS; + uav.Buffer.FirstElement = static_cast(e.bufferOffset / 4); + uav.Buffer.NumElements = static_cast(sz / 4); + uav.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW; + } + m_device->CreateUnorderedAccessView(buf->handle(), nullptr, &uav, dest); + } + break; + case BindingType::SampledTexture: case BindingType::BindlessTextures: + if (auto* v = static_cast(e.textureView)) + m_device->CopyDescriptorsSimple(1, dest, v->getSrv(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + break; + case BindingType::StorageTextureReadOnly: case BindingType::StorageTextureReadWrite: + case BindingType::BindlessStorageTextures: + if (auto* v = static_cast(e.textureView)) + m_device->CopyDescriptorsSimple(1, dest, v->getUav(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + break; + case BindingType::AccelerationStructure: + if (auto* dxAs = static_cast(e.accelStruct)) { + D3D12_SHADER_RESOURCE_VIEW_DESC asSrv{}; + asSrv.Format = DXGI_FORMAT_UNKNOWN; + asSrv.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE; + asSrv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + asSrv.RaytracingAccelerationStructure.Location = dxAs->deviceAddress(); + m_device->CreateShaderResourceView(nullptr, &asSrv, dest); + } + break; + default: break; + } + } + + void writeSampler(const BindGroupEntry& e, const DxBindingRangeInfo& r, u32 arrayIdx = 0) { + u32 off = static_cast(m_samplerOffset) + r.heapOffset + arrayIdx; + D3D12_CPU_DESCRIPTOR_HANDLE dest = m_cpuSamplerHeap->getCpuHandle(off); + if (auto* s = static_cast(e.sampler)) + m_device->CopyDescriptorsSimple(1, dest, s->handle(), D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER); + } + + ID3D12Device* m_device = nullptr; + DxBindGroupLayoutImpl* m_layout = nullptr; + DxGpuDescriptorHeap* m_cpuSrvHeap = nullptr; + DxGpuDescriptorHeap* m_cpuSamplerHeap = nullptr; + u32 m_cachedCbvSrvUavCount = 0; + u32 m_cachedSamplerCount = 0; + i32 m_cbvSrvUavOffset = -1; + i32 m_samplerOffset = -1; + std::vector m_dynAddrs; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroupLayout.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroupLayout.cppm new file mode 100644 index 00000000..d889eaf3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBindGroupLayout.cppm @@ -0,0 +1,86 @@ +/// DX12 implementation of BindGroupLayout. +/// Tracks descriptor range definitions and heap offset info. + +module; + +#include +#include + +#include + +export module rhi.dx12:bind_group_layout; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +struct DxBindingRangeInfo { + u32 binding = 0; + BindingType type = BindingType::UniformBuffer; + u32 count = 0; + u32 heapOffset = 0; + bool isSampler = false; + bool hasDynamicOffset = false; + u32 storageBufferStride = 0; +}; + +class DxBindGroupLayoutImpl : public BindGroupLayout { +public: + Status init(const BindGroupLayoutDesc& d) { + u32 csvUavOff = 0, sampOff = 0; + + for (usize i = 0; i < d.entries.size(); ++i) { + const auto& e = d.entries[i]; + m_entries.push_back(e); + + bool sampler = isSamplerBinding(e.type); + u32 cnt = e.count; + if (cnt == ~0u) { cnt = 1024 * 16; m_hasBindless = true; } + + DxBindingRangeInfo r{}; + r.binding = e.binding; + r.type = e.type; + r.count = cnt; + r.isSampler = sampler; + r.hasDynamicOffset = e.hasDynamicOffset; + r.storageBufferStride = e.storageBufferStride; + + if (e.hasDynamicOffset) { + r.heapOffset = 0; + ++m_dynamicOffsetCount; + } else if (sampler) { + r.heapOffset = sampOff; + sampOff += cnt; + } else { + r.heapOffset = csvUavOff; + csvUavOff += cnt; + } + m_ranges.push_back(r); + } + m_cbvSrvUavCount = csvUavOff; + m_samplerCount = sampOff; + return ErrorCode::Ok; + } + + [[nodiscard]] std::span entries() const { return { m_entries.data(), m_entries.size() }; } + [[nodiscard]] std::span ranges() const { return { m_ranges.data(), m_ranges.size() }; } + [[nodiscard]] u32 cbvSrvUavCount() const { return m_cbvSrvUavCount; } + [[nodiscard]] u32 samplerCount() const { return m_samplerCount; } + [[nodiscard]] u32 dynamicOffsetCount()const { return m_dynamicOffsetCount; } + [[nodiscard]] bool hasBindless() const { return m_hasBindless; } + +private: + std::vector m_entries; + std::vector m_ranges; + u32 m_cbvSrvUavCount = 0; + u32 m_samplerCount = 0; + u32 m_dynamicOffsetCount= 0; + bool m_hasBindless = false; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBuffer.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBuffer.cppm new file mode 100644 index 00000000..d2745a48 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxBuffer.cppm @@ -0,0 +1,94 @@ +/// DX12 implementation of Buffer. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:buffer; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward + +class DxBufferImpl : public Buffer { +public: + Status init(ID3D12Device* device, const BufferDesc& d) { + desc = d; + + auto heapType = toHeapType(d.memory); + auto flags = toBufferResourceFlags(d.usage); + + // Initial state based on heap type. + m_state = D3D12_RESOURCE_STATE_COMMON; + if (heapType == D3D12_HEAP_TYPE_UPLOAD) m_state = D3D12_RESOURCE_STATE_GENERIC_READ; + if (heapType == D3D12_HEAP_TYPE_READBACK) m_state = D3D12_RESOURCE_STATE_COPY_DEST; + + u64 alignedSize = (d.size + 255) & ~u64(255); // 256-byte alignment for CBVs + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = heapType; + + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + rd.Width = alignedSize; + rd.Height = 1; + rd.DepthOrArraySize = 1; + rd.MipLevels = 1; + rd.Format = DXGI_FORMAT_UNKNOWN; + rd.SampleDesc = { 1, 0 }; + rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + rd.Flags = flags; + + HRESULT hr = device->CreateCommittedResource( + &heapProps, D3D12_HEAP_FLAG_NONE, + &rd, m_state, nullptr, + IID_PPV_ARGS(&m_resource)); + if (FAILED(hr)) { + logErrorf("DxBuffer: CreateCommittedResource failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + // Persistently map upload/readback buffers. + if (heapType == D3D12_HEAP_TYPE_UPLOAD || heapType == D3D12_HEAP_TYPE_READBACK) + m_resource->Map(0, nullptr, &m_persistentMap); + + return ErrorCode::Ok; + } + + void* map() override { + if (m_persistentMap) return m_persistentMap; + void* ptr = nullptr; + if (SUCCEEDED(m_resource->Map(0, nullptr, &ptr))) return ptr; + return nullptr; + } + + void unmap() override { + if (m_persistentMap) return; // don't unmap persistently mapped buffers + m_resource->Unmap(0, nullptr); + } + + void cleanup() { + if (m_persistentMap) { m_resource->Unmap(0, nullptr); m_persistentMap = nullptr; } + m_resource.Reset(); + } + + // ---- Internal ---- + [[nodiscard]] ID3D12Resource* handle() const { return m_resource.Get(); } + [[nodiscard]] D3D12_RESOURCE_STATES currentState() const { return m_state; } + void setState(D3D12_RESOURCE_STATES s) { m_state = s; } + [[nodiscard]] D3D12_GPU_VIRTUAL_ADDRESS gpuAddress() const { return m_resource ? m_resource->GetGPUVirtualAddress() : 0; } + +private: + ComPtr m_resource; + D3D12_RESOURCE_STATES m_state = D3D12_RESOURCE_STATE_COMMON; + void* m_persistentMap = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandBuffer.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandBuffer.cppm new file mode 100644 index 00000000..723bb906 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandBuffer.cppm @@ -0,0 +1,32 @@ +/// DX12 implementation of CommandBuffer. +/// Simple wrapper for a closed ID3D12GraphicsCommandList. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:command_buffer; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxCommandBufferImpl : public CommandBuffer { +public: + explicit DxCommandBufferImpl(ID3D12GraphicsCommandList* cmdList) : m_cmdList(cmdList) {} + + [[nodiscard]] ID3D12GraphicsCommandList* handle() const { return m_cmdList; } + + void release() { + if (m_cmdList) { m_cmdList->Release(); m_cmdList = nullptr; } + } + +private: + ID3D12GraphicsCommandList* m_cmdList = nullptr; // raw, released via release() +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandEncoder.cppm new file mode 100644 index 00000000..f85c4cd9 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandEncoder.cppm @@ -0,0 +1,927 @@ +/// DX12 implementation of CommandEncoder + RayTracingEncoderExt. +/// Wraps an ID3D12GraphicsCommandList for recording commands. + +module; + +#include "DxIncludes.h" +#include +#include +#include + +#include +#include + +export module rhi.dx12:command_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :texture; +import :texture_view; +import :bind_group; +import :bind_group_layout; +import :pipeline_layout; +import :query_set; +import :command_buffer; +import :command_pool; +import :descriptor_heap; +import :gpu_descriptor_heap; +import :descriptor_staging; +import :render_pipeline; +import :render_bundle_encoder; +import :compute_pipeline; +import :accel_struct; +import :ray_tracing_pipeline; +import :render_pass_encoder; +import :compute_pass_encoder; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward + +/// DX12 implementation of CommandEncoder and RayTracingEncoderExt. +/// Wraps an ID3D12GraphicsCommandList for recording commands outside of +/// render/compute passes (barriers, copies, queries, RT builds). +class DxCommandEncoderImpl : public CommandEncoder, public RayTracingEncoderExt { +public: + RayTracingEncoderExt* asRayTracingExt() noexcept override { return this; } + DxCommandEncoderImpl(DxDeviceImpl* device, ID3D12GraphicsCommandList* cmdList, + DxCommandPoolImpl* pool, const DxRenderPassContext& rpeCtx, + const DxComputePassContext& cpeCtx) + : m_device(device), m_cmdList(cmdList), m_pool(pool), + m_gpuSrvHeap(rpeCtx.gpuSrvHeap), m_gpuSamplerHeap(rpeCtx.gpuSamplerHeap), + m_rpe(rpeCtx), m_cpe(cpeCtx), m_rpeCtx(rpeCtx) {} + + ~DxCommandEncoderImpl() override { for (auto* e : m_bundleEncoders) delete e; } + + // ================================================================ + // CommandEncoder interface + // ================================================================ + + // ---- Render Pass ---- + + RenderPassEncoder* beginRenderPass(const RenderPassDesc& desc) override { + ensureDescriptorHeaps(); + + // Timestamp at pass begin. + if (desc.timestampQuerySet) { + if (auto* qs = static_cast(desc.timestampQuerySet)) + m_cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_TIMESTAMP, desc.beginTimestampIndex); + } + + // Collect render target views. + D3D12_CPU_DESCRIPTOR_HANDLE rtvHandles[8]{}; + u32 rtvCount = 0; + const auto& colorAtts = desc.colorAttachments; + for (usize i = 0; i < colorAtts.size() && i < 8; ++i) { + const auto& att = colorAtts[i]; + if (auto* dxView = static_cast(att.view)) { + rtvHandles[i] = dxView->getRtv(); + ++rtvCount; + } + } + + // Depth/stencil view. + D3D12_CPU_DESCRIPTOR_HANDLE dsvStorage{}; + D3D12_CPU_DESCRIPTOR_HANDLE* dsvPtr = nullptr; + if (desc.depthStencilAttachment.has_value()) { + const auto& dsAttach = *desc.depthStencilAttachment; + if (auto* dxView = static_cast(dsAttach.view)) { + dsvStorage = dxView->getDsv(); + dsvPtr = &dsvStorage; + } + } + + m_cmdList->OMSetRenderTargets(rtvCount, rtvHandles, FALSE, dsvPtr); + + // Clear render targets. + for (usize i = 0; i < colorAtts.size() && i < 8; ++i) { + const auto& att = colorAtts[i]; + if (att.loadOp == LoadOp::Clear) { + FLOAT color[4] = { att.clearValue.r, att.clearValue.g, att.clearValue.b, att.clearValue.a }; + m_cmdList->ClearRenderTargetView(rtvHandles[i], color, 0, nullptr); + } + } + + // Clear depth/stencil. + if (desc.depthStencilAttachment.has_value() && dsvPtr) { + const auto& dsAttach = *desc.depthStencilAttachment; + D3D12_CLEAR_FLAGS clearFlags = static_cast(0); + bool needsClear = false; + if (dsAttach.depthLoadOp == LoadOp::Clear) { + clearFlags = static_cast( + static_cast(clearFlags) | static_cast(D3D12_CLEAR_FLAG_DEPTH)); + needsClear = true; + } + if (dsAttach.stencilLoadOp == LoadOp::Clear) { + clearFlags = static_cast( + static_cast(clearFlags) | static_cast(D3D12_CLEAR_FLAG_STENCIL)); + needsClear = true; + } + if (needsClear) + m_cmdList->ClearDepthStencilView(*dsvPtr, clearFlags, + dsAttach.depthClearValue, static_cast(dsAttach.stencilClearValue), 0, nullptr); + } + + m_rpe.begin(desc); + return &m_rpe; + } + + // ---- Compute Pass ---- + + ComputePassEncoder* beginComputePass(std::u8string_view) override { + ensureDescriptorHeaps(); + m_cpe.begin(); + return &m_cpe; + } + + RenderBundleEncoder* createRenderBundleEncoder(const RenderBundleDesc& /*desc*/) override { + ComPtr dev; + m_cmdList->GetDevice(IID_PPV_ARGS(&dev)); + if (!dev) return nullptr; + ComPtr alloc; + if (FAILED(dev->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_BUNDLE, IID_PPV_ARGS(&alloc)))) return nullptr; + ComPtr list; + if (FAILED(dev->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_BUNDLE, alloc.Get(), nullptr, IID_PPV_ARGS(&list)))) + return nullptr; + + // Bundles inherit the parent's descriptor heaps; bind matching heaps on the bundle. + ensureDescriptorHeaps(); + ID3D12DescriptorHeap* heaps[2]; UINT n = 0; + if (m_gpuSrvHeap) heaps[n++] = m_gpuSrvHeap->heap(); + if (m_gpuSamplerHeap) heaps[n++] = m_gpuSamplerHeap->heap(); + if (n > 0) list->SetDescriptorHeaps(n, heaps); + + DxRenderPassContext ctx = m_rpeCtx; + ctx.cmdList = list.Get(); + auto* enc = new DxRenderBundleEncoderImpl(ctx, list, alloc); + m_bundleEncoders.push_back(enc); // owned: freed in this encoder's destructor + return enc; + } + + // ---- Barriers ---- + + void barrier(const BarrierGroup& group) override { + usize totalBarriers = group.bufferBarriers.size() + group.textureBarriers.size() + + group.memoryBarriers.size(); + if (totalBarriers == 0) return; + + std::vector dxBarriers; + dxBarriers.reserve(totalBarriers); + + // Buffer barriers. + for (usize i = 0; i < group.bufferBarriers.size(); ++i) { + const auto& bb = group.bufferBarriers[i]; + auto* dxBuf = static_cast(bb.buffer); + if (!dxBuf) continue; + + auto oldState = toResourceStates(bb.oldState); + auto newState = toResourceStates(bb.newState); + if (oldState == newState) continue; + + D3D12_RESOURCE_BARRIER b{}; + b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + b.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + b.Transition.pResource = dxBuf->handle(); + b.Transition.StateBefore = oldState; + b.Transition.StateAfter = newState; + b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + dxBuf->setState(newState); + dxBarriers.push_back(b); + } + + // Texture barriers: coalesce chained transitions per (resource, subresource). + // The barrier solver can emit multiple barriers for the same texture in one + // batch (e.g. ParticlePass declares both ReadDepth and ReadTexture on SceneDepth, + // producing DEPTH_WRITE->DEPTH_READ then DEPTH_READ->SHADER_READ). D3D12 processes + // all barriers in one ResourceBarrier() call simultaneously, so the second barrier's + // StateBefore won't match reality. Fix: coalesce A->B, B->C into a single A->C. + { + struct CoalescedEntry { + ID3D12Resource* resource; + u32 subresource; + D3D12_RESOURCE_STATES firstBefore; + D3D12_RESOURCE_STATES lastAfter; + }; + std::vector coalesced; + coalesced.reserve(group.textureBarriers.size()); + + for (usize i = 0; i < group.textureBarriers.size(); ++i) { + const auto& tb = group.textureBarriers[i]; + auto* dxTex = static_cast(tb.texture); + if (!dxTex) continue; + + auto newState = toResourceStates(tb.newState, dxTex->desc.format); + bool isWholeResource = (tb.mipLevelCount == ~0u) && (tb.arrayLayerCount == ~0u); + + if (isWholeResource) { + auto resolvedOldState = dxTex->currentState(); + if (resolvedOldState == newState) continue; + + // Check if we already have an entry for this resource+subresource. + bool found = false; + for (auto& entry : coalesced) { + if (entry.resource == dxTex->handle() + && entry.subresource == D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES) { + entry.lastAfter = newState; + found = true; + break; + } + } + if (!found) + coalesced.push_back({ dxTex->handle(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + resolvedOldState, newState }); + + dxTex->setState(newState); + } else { + // Per-subresource: expand range and coalesce each subresource individually. + u32 mipCount = dxTex->desc.mipLevelCount; + u32 layerCount = std::max( + (dxTex->desc.dimension == TextureDimension::Texture3D) + ? dxTex->desc.depth : dxTex->desc.arrayLayerCount, 1u); + u32 baseMip = tb.baseMipLevel; + u32 mipEnd = std::min(baseMip + tb.mipLevelCount, mipCount); + u32 baseLayer= tb.baseArrayLayer; + u32 layerEnd = std::min(baseLayer + tb.arrayLayerCount, layerCount); + + for (u32 layer = baseLayer; layer < layerEnd; ++layer) { + for (u32 mip = baseMip; mip < mipEnd; ++mip) { + auto resolvedOldState = dxTex->getSubresourceState(mip, layer); + u32 sub = mip + layer * mipCount; + if (resolvedOldState == newState) continue; + + bool found = false; + for (auto& entry : coalesced) { + if (entry.resource == dxTex->handle() && entry.subresource == sub) { + entry.lastAfter = newState; + found = true; + break; + } + } + if (!found) + coalesced.push_back({ dxTex->handle(), sub, resolvedOldState, newState }); + } + } + dxTex->setSubresourceState(baseMip, tb.mipLevelCount, baseLayer, tb.arrayLayerCount, newState); + } + } + + // Phase 2: emit one D3D12 barrier per coalesced entry. + for (const auto& entry : coalesced) { + if (entry.firstBefore == entry.lastAfter) continue; // A->B->A cancelled out + D3D12_RESOURCE_BARRIER b{}; + b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + b.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + b.Transition.pResource = entry.resource; + b.Transition.StateBefore = entry.firstBefore; + b.Transition.StateAfter = entry.lastAfter; + b.Transition.Subresource = entry.subresource; + dxBarriers.push_back(b); + } + } + + // Memory barriers -> UAV barriers. + for (usize i = 0; i < group.memoryBarriers.size(); ++i) { + D3D12_RESOURCE_BARRIER b{}; + b.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV; + b.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + b.UAV.pResource = nullptr; // global UAV barrier + dxBarriers.push_back(b); + } + + if (!dxBarriers.empty()) + m_cmdList->ResourceBarrier(static_cast(dxBarriers.size()), dxBarriers.data()); + } + + // ---- Copy Operations ---- + + void copyBufferToBuffer(Buffer* src, u64 srcOffset, Buffer* dst, u64 dstOffset, u64 size) override { + auto* dxSrc = static_cast(src); + auto* dxDst = static_cast(dst); + if (!dxSrc || !dxDst) return; + m_cmdList->CopyBufferRegion(dxDst->handle(), dstOffset, dxSrc->handle(), srcOffset, size); + } + + void copyBufferToTexture(Buffer* src, Texture* dst, const BufferTextureCopyRegion& region) override { + auto* dxSrc = static_cast(src); + auto* dxTex = static_cast(dst); + if (!dxSrc || !dxTex) return; + + u32 subresource = region.textureMipLevel + region.textureArrayLayer * dxTex->desc.mipLevelCount; + + D3D12_TEXTURE_COPY_LOCATION srcLoc{}; + srcLoc.pResource = dxSrc->handle(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLoc.PlacedFootprint.Offset = region.bufferOffset; + srcLoc.PlacedFootprint.Footprint.Format = toDxgiFormat(dxTex->desc.format); + srcLoc.PlacedFootprint.Footprint.Width = region.textureExtent.width; + srcLoc.PlacedFootprint.Footprint.Height = region.textureExtent.height; + srcLoc.PlacedFootprint.Footprint.Depth = region.textureExtent.depth; + srcLoc.PlacedFootprint.Footprint.RowPitch = region.bytesPerRow; + + D3D12_TEXTURE_COPY_LOCATION dstLoc{}; + dstLoc.pResource = dxTex->handle(); + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLoc.SubresourceIndex = subresource; + + m_cmdList->CopyTextureRegion(&dstLoc, + region.textureOrigin.x, region.textureOrigin.y, region.textureOrigin.z, + &srcLoc, nullptr); + } + + void copyTextureToBuffer(Texture* src, Buffer* dst, const BufferTextureCopyRegion& region) override { + auto* dxTex = static_cast(src); + auto* dxDst = static_cast(dst); + if (!dxTex || !dxDst) return; + + u32 subresource = region.textureMipLevel + region.textureArrayLayer * dxTex->desc.mipLevelCount; + + D3D12_TEXTURE_COPY_LOCATION srcLoc{}; + srcLoc.pResource = dxTex->handle(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + srcLoc.SubresourceIndex = subresource; + + D3D12_TEXTURE_COPY_LOCATION dstLoc{}; + dstLoc.pResource = dxDst->handle(); + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + dstLoc.PlacedFootprint.Offset = region.bufferOffset; + dstLoc.PlacedFootprint.Footprint.Format = toDxgiFormat(dxTex->desc.format); + dstLoc.PlacedFootprint.Footprint.Width = region.textureExtent.width; + dstLoc.PlacedFootprint.Footprint.Height = region.textureExtent.height; + dstLoc.PlacedFootprint.Footprint.Depth = region.textureExtent.depth; + dstLoc.PlacedFootprint.Footprint.RowPitch = region.bytesPerRow; + + D3D12_BOX srcBox{}; + srcBox.left = region.textureOrigin.x; + srcBox.top = region.textureOrigin.y; + srcBox.front = region.textureOrigin.z; + srcBox.right = region.textureOrigin.x + region.textureExtent.width; + srcBox.bottom = region.textureOrigin.y + region.textureExtent.height; + srcBox.back = region.textureOrigin.z + region.textureExtent.depth; + + m_cmdList->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, &srcBox); + } + + void copyTextureToTexture(Texture* src, Texture* dst, const TextureCopyRegion& region) override { + auto* dxSrc = static_cast(src); + auto* dxDst = static_cast(dst); + if (!dxSrc || !dxDst) return; + + u32 srcSub = region.srcMipLevel + region.srcArrayLayer * dxSrc->desc.mipLevelCount; + u32 dstSub = region.dstMipLevel + region.dstArrayLayer * dxDst->desc.mipLevelCount; + + D3D12_TEXTURE_COPY_LOCATION srcLoc{}; + srcLoc.pResource = dxSrc->handle(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + srcLoc.SubresourceIndex = srcSub; + + D3D12_TEXTURE_COPY_LOCATION dstLoc{}; + dstLoc.pResource = dxDst->handle(); + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLoc.SubresourceIndex = dstSub; + + D3D12_BOX srcBox{}; + srcBox.left = 0; + srcBox.top = 0; + srcBox.front = 0; + srcBox.right = region.extent.width; + srcBox.bottom = region.extent.height; + srcBox.back = region.extent.depth; + + m_cmdList->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, &srcBox); + } + + // ---- Blit & Mipmap Generation ---- + + void blit(Texture* src, Texture* dst) override { + auto* dxSrc = static_cast(src); + auto* dxDst = static_cast(dst); + if (!dxSrc || !dxDst) return; + + DXGI_FORMAT dxgiFormat = toDxgiFormat(dxDst->desc.format); + + // Caller has set textures to CopySrc/CopyDst. Internally transition to SRV/RTV for blit. + D3D12_RESOURCE_BARRIER barriers[2]{}; + + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = dxSrc->handle(); + barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = dxDst->handle(); + barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + + m_cmdList->ResourceBarrier(2, barriers); + + blitSubresource(dxSrc, 0, dxDst, 0, dxDst->desc.width, dxDst->desc.height, dxgiFormat); + + // Transition back to CopySrc/CopyDst. + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + + m_cmdList->ResourceBarrier(2, barriers); + } + + void generateMipmaps(Texture* texture) override { + auto* dxTex = static_cast(texture); + if (!dxTex) return; + + const auto& d = dxTex->desc; + if (d.mipLevelCount <= 1) return; + + DXGI_FORMAT dxgiFormat = toDxgiFormat(d.format); + + // Texture enters in CopySrc+CopyDst state (DX12: all subresources in COPY_SOURCE). + // For each mip: transition src->SRV, dst->RTV, blit, restore both->COPY_SOURCE. + for (u32 mip = 1; mip < d.mipLevelCount; ++mip) { + u32 dstWidth = std::max(1u, d.width >> mip); + u32 dstHeight = std::max(1u, d.height >> mip); + + D3D12_RESOURCE_BARRIER barriers[2]{}; + + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = dxTex->handle(); + barriers[0].Transition.Subresource = mip - 1; + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = dxTex->handle(); + barriers[1].Transition.Subresource = mip; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + + m_cmdList->ResourceBarrier(2, barriers); + + blitSubresource(dxTex, mip - 1, dxTex, mip, dstWidth, dstHeight, dxgiFormat); + + // Post-blit: restore both to COPY_SOURCE. + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + + m_cmdList->ResourceBarrier(2, barriers); + } + } + + // ---- MSAA Resolve ---- + + void resolveTexture(Texture* src, Texture* dst) override { + auto* dxSrc = static_cast(src); + auto* dxDst = static_cast(dst); + if (!dxSrc || !dxDst) return; + + DXGI_FORMAT dxgiFormat = toDxgiFormat(dxDst->desc.format); + + // Transition src -> RESOLVE_SOURCE, dst -> RESOLVE_DEST. + D3D12_RESOURCE_BARRIER barriers[2]{}; + + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = dxSrc->handle(); + barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_RESOLVE_SOURCE; + + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = dxDst->handle(); + barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RESOLVE_DEST; + + m_cmdList->ResourceBarrier(2, barriers); + + m_cmdList->ResolveSubresource(dxDst->handle(), 0, dxSrc->handle(), 0, dxgiFormat); + + // Transition back. + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_RESOLVE_SOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RESOLVE_DEST; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + + m_cmdList->ResourceBarrier(2, barriers); + } + + // ---- Queries ---- + + void resetQuerySet(QuerySet*, u32, u32) override { + // DX12 does not require explicit query reset -- queries are implicitly reset when written. + } + + void writeTimestamp(QuerySet* querySet, u32 index) override { + if (auto* qs = static_cast(querySet)) + m_cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_TIMESTAMP, index); + } + + void resolveQuerySet(QuerySet* querySet, u32 first, u32 count, Buffer* dst, u64 dstOffset) override { + auto* qs = static_cast(querySet); + auto* dxDst = static_cast(dst); + if (!qs || !dxDst) return; + m_cmdList->ResolveQueryData(qs->handle(), DxQuerySetImpl::toDxQueryType(qs->type), + first, count, dxDst->handle(), dstOffset); + } + + // ---- Debug Labels ---- + + void beginDebugLabel(std::u8string_view, f32, f32, f32, f32) override { + // PIX events would go here; no-op without PIX runtime. + } + + void endDebugLabel() override {} + + void insertDebugLabel(std::u8string_view, f32, f32, f32, f32) override {} + + // ---- Finish ---- + + CommandBuffer* finish() override { + m_cmdList->Close(); + auto* cb = new DxCommandBufferImpl(m_cmdList); + m_pool->trackCommandBuffer(cb); + return cb; + } + + // ================================================================ + // RayTracingEncoderExt interface + // ================================================================ + + void buildBottomLevelAccelStruct(AccelStruct* dst, Buffer* scratchBuffer, u64 scratchOffset, + std::span tris, + std::span aabbs) override + { + auto* dxAs = static_cast(dst); + auto* dxScratch = static_cast(scratchBuffer); + if (!dxAs || !dxScratch) return; + + // Query ID3D12GraphicsCommandList4 for RT support. + ComPtr cmdList4; + if (FAILED(m_cmdList->QueryInterface(IID_PPV_ARGS(&cmdList4))) || !cmdList4) return; + + usize totalGeoms = tris.size() + aabbs.size(); + std::vector geomDescs(totalGeoms); + usize idx = 0; + + for (usize i = 0; i < tris.size(); ++i) { + const auto& t = tris[i]; + geomDescs[idx] = {}; + geomDescs[idx].Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES; + geomDescs[idx].Flags = toGeometryFlags(t.flags); + + auto& tri = geomDescs[idx].Triangles; + + if (auto* vb = static_cast(t.vertexBuffer)) { + tri.VertexBuffer.StartAddress = vb->gpuAddress() + t.vertexOffset; + tri.VertexBuffer.StrideInBytes = t.vertexStride; + tri.VertexCount = t.vertexCount; + tri.VertexFormat = toDxgiVertexFormat(t.vertexFormat); + } + + if (t.indexBuffer) { + if (auto* ib = static_cast(t.indexBuffer)) { + tri.IndexBuffer = ib->gpuAddress() + t.indexOffset; + tri.IndexCount = t.indexCount; + tri.IndexFormat = (t.indexFormat == IndexFormat::UInt16) + ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT; + } + } else { + tri.IndexFormat = DXGI_FORMAT_UNKNOWN; + } + + if (t.transformBuffer) { + if (auto* tb = static_cast(t.transformBuffer)) + tri.Transform3x4 = tb->gpuAddress() + t.transformOffset; + } + + ++idx; + } + + for (usize i = 0; i < aabbs.size(); ++i) { + const auto& a = aabbs[i]; + geomDescs[idx] = {}; + geomDescs[idx].Type = D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS; + geomDescs[idx].Flags = toGeometryFlags(a.flags); + + if (auto* ab = static_cast(a.aabbBuffer)) { + geomDescs[idx].AABBs.AABBs.StartAddress = ab->gpuAddress() + a.offset; + geomDescs[idx].AABBs.AABBs.StrideInBytes = a.stride; + geomDescs[idx].AABBs.AABBCount = a.count; + } + + ++idx; + } + + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC buildDesc{}; + buildDesc.DestAccelerationStructureData = dxAs->deviceAddress(); + buildDesc.ScratchAccelerationStructureData = dxScratch->gpuAddress() + scratchOffset; + buildDesc.Inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL; + buildDesc.Inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE; + buildDesc.Inputs.NumDescs = static_cast(totalGeoms); + buildDesc.Inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY; + buildDesc.Inputs.pGeometryDescs = geomDescs.data(); + + cmdList4->BuildRaytracingAccelerationStructure(&buildDesc, 0, nullptr); + } + + void buildTopLevelAccelStruct(AccelStruct* dst, Buffer* scratchBuffer, u64 scratchOffset, + Buffer* instanceBuffer, u64 instanceOffset, u32 instanceCount) override + { + auto* dxAs = static_cast(dst); + auto* dxScratch = static_cast(scratchBuffer); + auto* dxInstances = static_cast(instanceBuffer); + if (!dxAs || !dxScratch || !dxInstances) return; + + ComPtr cmdList4; + if (FAILED(m_cmdList->QueryInterface(IID_PPV_ARGS(&cmdList4))) || !cmdList4) return; + + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC buildDesc{}; + buildDesc.DestAccelerationStructureData = dxAs->deviceAddress(); + buildDesc.ScratchAccelerationStructureData = dxScratch->gpuAddress() + scratchOffset; + buildDesc.Inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL; + buildDesc.Inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE; + buildDesc.Inputs.NumDescs = instanceCount; + buildDesc.Inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY; + buildDesc.Inputs.InstanceDescs = dxInstances->gpuAddress() + instanceOffset; + + cmdList4->BuildRaytracingAccelerationStructure(&buildDesc, 0, nullptr); + } + + void setRayTracingPipeline(RayTracingPipeline* pipeline) override { + m_currentRtPipeline = static_cast(pipeline); + if (!m_currentRtPipeline) return; + + ensureDescriptorHeaps(); + + ComPtr cmdList4; + if (SUCCEEDED(m_cmdList->QueryInterface(IID_PPV_ARGS(&cmdList4))) && cmdList4) + cmdList4->SetPipelineState1(m_currentRtPipeline->handle()); + + // RT uses compute root signature binding. + if (auto* layout = m_currentRtPipeline->pipelineLayout()) + m_cmdList->SetComputeRootSignature(layout->handle()); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets) override { + auto* dxGroup = static_cast(group); + if (!dxGroup || !m_currentRtPipeline) return; + + auto* layout = m_currentRtPipeline->pipelineLayout(); + if (!layout) return; + + auto* dxLayout = static_cast(dxGroup->layout()); + + // RT uses compute root signature binding -- copy-on-bind staging. + if (dxGroup->cbvSrvUavOffset() >= 0 && dxLayout && dxLayout->cbvSrvUavCount() > 0) { + i32 rootIdx = layout->getCbvSrvUavRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_pool->srvStaging()->copyFrom( + static_cast(dxGroup->cbvSrvUavOffset()), dxLayout->cbvSrvUavCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_gpuSrvHeap->getGpuHandle(static_cast(stagedOffset)); + m_cmdList->SetComputeRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + if (dxGroup->samplerOffset() >= 0 && dxLayout && dxLayout->samplerCount() > 0) { + i32 rootIdx = layout->getSamplerRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_pool->samplerStaging()->copyFrom( + static_cast(dxGroup->samplerOffset()), dxLayout->samplerCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_gpuSamplerHeap->getGpuHandle(static_cast(stagedOffset)); + m_cmdList->SetComputeRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + // Dynamic root entries. + auto dynEntries = layout->dynamicRootEntries(); + auto dynAddrs = dxGroup->dynamicGpuAddresses(); + usize dynOffsetIdx = 0; + for (usize i = 0; i < dynEntries.size(); ++i) { + const auto& entry = dynEntries[i]; + if (entry.groupIndex != index) continue; + if (entry.dynamicIndex >= dynAddrs.size()) continue; + + u64 gpuAddr = dynAddrs[entry.dynamicIndex]; + if (dynOffsetIdx < dynamicOffsets.size()) + gpuAddr += static_cast(dynamicOffsets[dynOffsetIdx]); + ++dynOffsetIdx; + + switch (entry.paramType) { + case D3D12_ROOT_PARAMETER_TYPE_CBV: + m_cmdList->SetComputeRootConstantBufferView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_SRV: + m_cmdList->SetComputeRootShaderResourceView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_UAV: + m_cmdList->SetComputeRootUnorderedAccessView(static_cast(entry.rootParamIndex), gpuAddr); + break; + default: break; + } + } + } + + void setPushConstants(ShaderStage, u32 offset, u32 size, const void* data) override { + if (!m_currentRtPipeline) return; + auto* layout = m_currentRtPipeline->pipelineLayout(); + if (!layout || layout->pushConstantRootIndex() < 0) return; + + m_cmdList->SetComputeRoot32BitConstants( + static_cast(layout->pushConstantRootIndex()), + size / 4, data, offset / 4); + } + + void traceRays(Buffer* raygenSBT, u64 raygenOffset, u64 raygenStride, + Buffer* missSBT, u64 missOffset, u64 missStride, + Buffer* hitSBT, u64 hitOffset, u64 hitStride, + u32 width, u32 height, u32 depth) override + { + ComPtr cmdList4; + if (FAILED(m_cmdList->QueryInterface(IID_PPV_ARGS(&cmdList4))) || !cmdList4) return; + + D3D12_DISPATCH_RAYS_DESC dispatchDesc{}; + + // Raygen -- single record. + if (auto* dxBuf = static_cast(raygenSBT)) { + dispatchDesc.RayGenerationShaderRecord.StartAddress = dxBuf->gpuAddress() + raygenOffset; + dispatchDesc.RayGenerationShaderRecord.SizeInBytes = raygenStride; + } + + // Miss. + if (missSBT) { + if (auto* dxBuf = static_cast(missSBT)) { + dispatchDesc.MissShaderTable.StartAddress = dxBuf->gpuAddress() + missOffset; + dispatchDesc.MissShaderTable.StrideInBytes = missStride; + dispatchDesc.MissShaderTable.SizeInBytes = missStride; // assume single entry + } + } + + // Hit group. + if (hitSBT) { + if (auto* dxBuf = static_cast(hitSBT)) { + dispatchDesc.HitGroupTable.StartAddress = dxBuf->gpuAddress() + hitOffset; + dispatchDesc.HitGroupTable.StrideInBytes = hitStride; + dispatchDesc.HitGroupTable.SizeInBytes = hitStride; // assume single entry + } + } + + dispatchDesc.Width = width; + dispatchDesc.Height = height; + dispatchDesc.Depth = depth; + + cmdList4->DispatchRays(&dispatchDesc); + } + + // ================================================================ + // Internal accessors + // ================================================================ + + [[nodiscard]] ID3D12GraphicsCommandList* cmdList() const { return m_cmdList; } + [[nodiscard]] DxDeviceImpl* ownerDevice() const { return m_device; } + [[nodiscard]] DxDescriptorStaging* srvStaging() { return m_pool->srvStaging(); } + [[nodiscard]] DxDescriptorStaging* samplerStaging() { return m_pool->samplerStaging(); } + + // ================================================================ + // Static helpers + // ================================================================ + + /// Convert RHI ResourceState to D3D12_RESOURCE_STATES. + static D3D12_RESOURCE_STATES toResourceStates(ResourceState state) { + return toResourceStates(state, TextureFormat::Undefined); + } + + /// Convert RHI ResourceState to D3D12_RESOURCE_STATES, considering the texture format. + /// For depth formats, ShaderRead maps to DEPTH_READ instead of PIXEL_SHADER_RESOURCE. + static D3D12_RESOURCE_STATES toResourceStates(ResourceState state, TextureFormat format) { + if (state == ResourceState::Undefined) + return D3D12_RESOURCE_STATE_COMMON; + + D3D12_RESOURCE_STATES result = D3D12_RESOURCE_STATE_COMMON; + + if (hasFlag(state, ResourceState::VertexBuffer)) + result = static_cast(result | D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER); + if (hasFlag(state, ResourceState::IndexBuffer)) + result = static_cast(result | D3D12_RESOURCE_STATE_INDEX_BUFFER); + if (hasFlag(state, ResourceState::UniformBuffer)) + result = static_cast(result | D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER); + if (hasFlag(state, ResourceState::ShaderRead)) { + // Depth textures use DEPTH_READ when sampled, not PIXEL_SHADER_RESOURCE. + if (isDepthFormat(format)) + result = static_cast(result | D3D12_RESOURCE_STATE_DEPTH_READ); + else + result = static_cast(result | D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE + | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + if (hasFlag(state, ResourceState::ShaderWrite)) + result = static_cast(result | D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + if (hasFlag(state, ResourceState::RenderTarget)) + result = static_cast(result | D3D12_RESOURCE_STATE_RENDER_TARGET); + if (hasFlag(state, ResourceState::DepthStencilWrite)) + result = static_cast(result | D3D12_RESOURCE_STATE_DEPTH_WRITE); + if (hasFlag(state, ResourceState::DepthStencilRead)) + result = static_cast(result | D3D12_RESOURCE_STATE_DEPTH_READ); + if (hasFlag(state, ResourceState::IndirectArgument)) + result = static_cast(result | D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT); + if (hasFlag(state, ResourceState::CopySrc)) + result = static_cast(result | D3D12_RESOURCE_STATE_COPY_SOURCE); + if (hasFlag(state, ResourceState::CopyDst)) + result = static_cast(result | D3D12_RESOURCE_STATE_COPY_DEST); + if (hasFlag(state, ResourceState::Present)) + result = static_cast(result | D3D12_RESOURCE_STATE_PRESENT); + if (hasFlag(state, ResourceState::General)) + result = static_cast(result | D3D12_RESOURCE_STATE_COMMON); + if (hasFlag(state, ResourceState::AccelStructRead)) + result = static_cast(result | D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE); + if (hasFlag(state, ResourceState::AccelStructWrite)) + result = static_cast(result | D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + + return result; + } + +private: + // ---- Blit helper ---- + + /// Blits one subresource using the device's fullscreen triangle pipeline. + /// Expects src subresource in PIXEL_SHADER_RESOURCE state, dst subresource in RENDER_TARGET state. + /// Implementation defined out-of-line (requires DxDeviceImpl for blit PSO and descriptor heaps). + void blitSubresource(DxTextureImpl* srcTex, u32 srcMip, DxTextureImpl* dstTex, u32 dstMip, + u32 dstWidth, u32 dstHeight, DXGI_FORMAT dxgiFormat); + + // ---- Descriptor heap management ---- + + void ensureDescriptorHeaps() { + if (m_descriptorHeapsSet) return; + m_descriptorHeapsSet = true; + + ID3D12DescriptorHeap* heaps[2] = { + m_gpuSrvHeap->heap(), + m_gpuSamplerHeap->heap() + }; + m_cmdList->SetDescriptorHeaps(2, heaps); + } + + static D3D12_RAYTRACING_GEOMETRY_FLAGS toGeometryFlags(GeometryFlags flags) { + D3D12_RAYTRACING_GEOMETRY_FLAGS result = D3D12_RAYTRACING_GEOMETRY_FLAG_NONE; + if (static_cast(flags) & static_cast(GeometryFlags::Opaque)) + result = static_cast( + result | D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE); + if (static_cast(flags) & static_cast(GeometryFlags::NoDuplicateAnyHitInvocation)) + result = static_cast( + result | D3D12_RAYTRACING_GEOMETRY_FLAG_NO_DUPLICATE_ANYHIT_INVOCATION); + return result; + } + + static bool hasFlag(ResourceState state, ResourceState flag) { + return (static_cast(state) & static_cast(flag)) != 0; + } + + // ---- Members ---- + + DxDeviceImpl* m_device = nullptr; + ID3D12GraphicsCommandList* m_cmdList = nullptr; + DxCommandPoolImpl* m_pool = nullptr; + DxRayTracingPipelineImpl* m_currentRtPipeline = nullptr; + bool m_descriptorHeapsSet = false; + + // GPU descriptor heaps (cached from device at construction). + DxGpuDescriptorHeap* m_gpuSrvHeap = nullptr; + DxGpuDescriptorHeap* m_gpuSamplerHeap = nullptr; + + // Embedded sub-encoders using context-based decoupling (see DxRenderPassEncoder.cppm, + // DxComputePassEncoder.cppm). Contexts carry the pointers the sub-encoders need. + DxRenderPassEncoderImpl m_rpe; + DxComputePassEncoderImpl m_cpe; + DxRenderPassContext m_rpeCtx; // cloned for bundle encoders + std::vector m_bundleEncoders; // owned wrappers (freed in dtor) +}; + +// ---- Deferred DxCommandPoolImpl method implementations ---- + +// createEncoder and destroyEncoder are defined out-of-line here because they +// need the full DxCommandEncoderImpl definition. The DxDeviceImpl will call +// pool->init() with the GPU descriptor heap pointers so the pool can build +// the context structs. For now, these are left as declarations to be resolved +// when DxDeviceImpl is defined. +// +// The DxDeviceImpl (not yet ported) will wire up: +// DxRenderPassContext rpeCtx { cmdList, device, srvStaging, samplerStaging, +// gpuSrvHeap, gpuSamplerHeap, drawSig, drawIdxSig, meshSig }; +// DxComputePassContext cpeCtx { cmdList, srvStaging, samplerStaging, +// gpuSrvHeap, gpuSamplerHeap, dispatchSig }; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandPool.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandPool.cppm new file mode 100644 index 00000000..d9a0d115 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxCommandPool.cppm @@ -0,0 +1,93 @@ +/// DX12 implementation of CommandPool. +/// Wraps an ID3D12CommandAllocator. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:command_pool; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :command_buffer; +import :descriptor_staging; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward +class DxCommandEncoderImpl; // forward + +class DxCommandPoolImpl : public CommandPool { +public: + Status init(DxDeviceImpl* device, ID3D12Device* d3dDevice, QueueType queueType, + DxGpuDescriptorHeap* cpuSrvHeap, DxGpuDescriptorHeap* gpuSrvHeap, + DxGpuDescriptorHeap* cpuSamplerHeap, DxGpuDescriptorHeap* gpuSamplerHeap) { + m_device = device; + m_d3dDevice = d3dDevice; + m_type = toCommandListType(queueType); + + HRESULT hr = d3dDevice->CreateCommandAllocator(m_type, IID_PPV_ARGS(&m_allocator)); + if (FAILED(hr)) return ErrorCode::Unknown; + + // Create descriptor staging (shared by all encoders from this pool). + m_srvStaging.init(cpuSrvHeap, gpuSrvHeap, d3dDevice, + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 1024); + m_samplerStaging.init(cpuSamplerHeap, gpuSamplerHeap, d3dDevice, + D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, 64); + + return ErrorCode::Ok; + } + + // ---- CommandPool interface ---- + Status createEncoder(CommandEncoder*& out) override; + void destroyEncoder(CommandEncoder*& encoder) override; + + void reset() override { + releaseCommandBuffers(); + // Reset descriptor staging -- GPU is done (fence waited), so staging + // bump pointers can safely return to start. + m_srvStaging.reset(); + m_samplerStaging.reset(); + m_allocator->Reset(); + } + + void cleanup() { + releaseCommandBuffers(); + m_srvStaging.destroy(); + m_samplerStaging.destroy(); + m_allocator.Reset(); + } + + // ---- Internal ---- + [[nodiscard]] ID3D12CommandAllocator* handle() const { return m_allocator.Get(); } + [[nodiscard]] DxDeviceImpl* ownerDevice() const { return m_device; } + [[nodiscard]] DxDescriptorStaging* srvStaging() { return &m_srvStaging; } + [[nodiscard]] DxDescriptorStaging* samplerStaging() { return &m_samplerStaging; } + + /// Called by DxCommandEncoderImpl::finish() to register a command buffer with this pool. + void trackCommandBuffer(DxCommandBufferImpl* cb) { m_trackedBuffers.push_back(cb); } + +private: + void releaseCommandBuffers() { + for (auto* cb : m_trackedBuffers) { + cb->release(); + delete cb; + } + m_trackedBuffers.clear(); + } + + ComPtr m_allocator; + ID3D12Device* m_d3dDevice = nullptr; + DxDeviceImpl* m_device = nullptr; + D3D12_COMMAND_LIST_TYPE m_type = D3D12_COMMAND_LIST_TYPE_DIRECT; + std::vector m_trackedBuffers; + DxDescriptorStaging m_srvStaging; + DxDescriptorStaging m_samplerStaging; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePassEncoder.cppm new file mode 100644 index 00000000..f869eb1c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePassEncoder.cppm @@ -0,0 +1,177 @@ +/// DX12 implementation of ComputePassEncoder. +/// Records compute dispatch commands into the parent command encoder's command list. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:compute_pass_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :bind_group; +import :bind_group_layout; +import :compute_pipeline; +import :pipeline_layout; +import :query_set; +import :descriptor_staging; +import :gpu_descriptor_heap; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +/// Pointers needed by the compute pass encoder, provided by the command encoder. +struct DxComputePassContext { + ID3D12GraphicsCommandList* cmdList = nullptr; + DxDescriptorStaging* srvStaging = nullptr; + DxDescriptorStaging* samplerStaging = nullptr; + DxGpuDescriptorHeap* gpuSrvHeap = nullptr; + DxGpuDescriptorHeap* gpuSamplerHeap = nullptr; + // Indirect command signature (cached on device). + ID3D12CommandSignature* dispatchSig = nullptr; +}; + +class DxComputePassEncoderImpl : public ComputePassEncoder { +public: + explicit DxComputePassEncoderImpl(const DxComputePassContext& ctx) + : m_ctx(ctx) {} + + void begin() { + m_currentPipeline = nullptr; + } + + // ---- Pipeline & Binding ---- + + void setPipeline(ComputePipeline* pipeline) override { + auto* dxPipeline = static_cast(pipeline); + if (!dxPipeline) return; + m_currentPipeline = dxPipeline; + + auto* cmdList = m_ctx.cmdList; + cmdList->SetPipelineState(dxPipeline->handle()); + cmdList->SetComputeRootSignature(dxPipeline->pipelineLayout()->handle()); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets) override { + auto* dxGroup = static_cast(group); + if (!dxGroup || !m_currentPipeline) return; + + auto* layout = m_currentPipeline->pipelineLayout(); + if (!layout) return; + + auto* cmdList = m_ctx.cmdList; + auto* dxLayout = static_cast(dxGroup->layout()); + + // Copy-on-bind: copy into encoder's staging region, bind from staging offset. + if (dxGroup->cbvSrvUavOffset() >= 0 && dxLayout && dxLayout->cbvSrvUavCount() > 0) { + i32 rootIdx = layout->getCbvSrvUavRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_ctx.srvStaging->copyFrom( + static_cast(dxGroup->cbvSrvUavOffset()), dxLayout->cbvSrvUavCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_ctx.gpuSrvHeap->getGpuHandle(static_cast(stagedOffset)); + cmdList->SetComputeRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + if (dxGroup->samplerOffset() >= 0 && dxLayout && dxLayout->samplerCount() > 0) { + i32 rootIdx = layout->getSamplerRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_ctx.samplerStaging->copyFrom( + static_cast(dxGroup->samplerOffset()), dxLayout->samplerCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_ctx.gpuSamplerHeap->getGpuHandle(static_cast(stagedOffset)); + cmdList->SetComputeRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + // Bind dynamic offset root descriptors (not staged -- uses GPU virtual addresses). + auto dynAddrs = dxGroup->dynamicGpuAddresses(); + usize dynOffsetIdx = 0; + for (usize i = 0; i < layout->dynamicRootEntries().size(); ++i) { + const auto& entry = layout->dynamicRootEntries()[i]; + if (entry.groupIndex != index) continue; + if (entry.dynamicIndex >= static_cast(dynAddrs.size())) continue; + + u64 gpuAddr = dynAddrs[entry.dynamicIndex]; + if (dynOffsetIdx < dynamicOffsets.size()) + gpuAddr += static_cast(dynamicOffsets[dynOffsetIdx]); + ++dynOffsetIdx; + + switch (entry.paramType) { + case D3D12_ROOT_PARAMETER_TYPE_CBV: + cmdList->SetComputeRootConstantBufferView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_SRV: + cmdList->SetComputeRootShaderResourceView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_UAV: + cmdList->SetComputeRootUnorderedAccessView(static_cast(entry.rootParamIndex), gpuAddr); + break; + default: break; + } + } + } + + void setPushConstants(ShaderStage /*stages*/, u32 offset, u32 size, const void* data) override { + if (!m_currentPipeline) return; + auto* layout = m_currentPipeline->pipelineLayout(); + if (!layout || layout->pushConstantRootIndex() < 0) return; + + m_ctx.cmdList->SetComputeRoot32BitConstants( + static_cast(layout->pushConstantRootIndex()), + size / 4, data, offset / 4); + } + + // ---- Dispatch ---- + + void dispatch(u32 x, u32 y, u32 z) override { + m_ctx.cmdList->Dispatch(x, y, z); + } + + void dispatchIndirect(Buffer* buffer, u64 offset) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf) return; + + auto* sig = m_ctx.dispatchSig; + if (!sig) return; + + m_ctx.cmdList->ExecuteIndirect(sig, 1, dxBuf->handle(), offset, nullptr, 0); + } + + // ---- Barrier ---- + + void computeBarrier() override { + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.UAV.pResource = nullptr; // global UAV barrier + m_ctx.cmdList->ResourceBarrier(1, &barrier); + } + + // ---- Queries ---- + + void writeTimestamp(QuerySet* querySet, u32 index) override { + auto* qs = static_cast(querySet); + if (qs) m_ctx.cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_TIMESTAMP, index); + } + + // ---- End ---- + + void end() override { + m_currentPipeline = nullptr; + } + +private: + DxComputePassContext m_ctx; + DxComputePipelineImpl* m_currentPipeline = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePipeline.cppm new file mode 100644 index 00000000..5aac0e8e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxComputePipeline.cppm @@ -0,0 +1,50 @@ +/// DX12 implementation of ComputePipeline. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:compute_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :pipeline_layout; +import :shader_module; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxComputePipelineImpl : public ComputePipeline { +public: + Status init(ID3D12Device* device, const ComputePipelineDesc& d) { + m_layout = static_cast(d.layout); + if (!m_layout) return ErrorCode::Unknown; + auto* csMod = static_cast(d.compute.module); + if (!csMod) return ErrorCode::Unknown; + + auto cs = csMod->bytecode(); + D3D12_COMPUTE_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = m_layout->handle(); + pso.CS = { cs.data(), cs.size() }; + + HRESULT hr = device->CreateComputePipelineState(&pso, IID_PPV_ARGS(&m_pipelineState)); + if (FAILED(hr)) { + logErrorf("DxComputePipeline: CreateComputePipelineState failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + return ErrorCode::Ok; + } + + void cleanup() { m_pipelineState.Reset(); } + + [[nodiscard]] ID3D12PipelineState* handle() const { return m_pipelineState.Get(); } + [[nodiscard]] DxPipelineLayoutImpl* pipelineLayout() const { return m_layout; } + +private: + ComPtr m_pipelineState; + DxPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxConversions.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxConversions.cppm new file mode 100644 index 00000000..85afec72 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxConversions.cppm @@ -0,0 +1,380 @@ +/// Conversion utilities between draco::rhi enums and DX12/DXGI enums. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:conversions; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +inline DXGI_FORMAT toDxgiFormat(TextureFormat f) { + switch (f) { + case TextureFormat::Undefined: return DXGI_FORMAT_UNKNOWN; + case TextureFormat::R8Unorm: return DXGI_FORMAT_R8_UNORM; + case TextureFormat::R8Snorm: return DXGI_FORMAT_R8_SNORM; + case TextureFormat::R8Uint: return DXGI_FORMAT_R8_UINT; + case TextureFormat::R8Sint: return DXGI_FORMAT_R8_SINT; + case TextureFormat::R16Uint: return DXGI_FORMAT_R16_UINT; + case TextureFormat::R16Sint: return DXGI_FORMAT_R16_SINT; + case TextureFormat::R16Float: return DXGI_FORMAT_R16_FLOAT; + case TextureFormat::RG8Unorm: return DXGI_FORMAT_R8G8_UNORM; + case TextureFormat::RG8Snorm: return DXGI_FORMAT_R8G8_SNORM; + case TextureFormat::RG8Uint: return DXGI_FORMAT_R8G8_UINT; + case TextureFormat::RG8Sint: return DXGI_FORMAT_R8G8_SINT; + case TextureFormat::R32Uint: return DXGI_FORMAT_R32_UINT; + case TextureFormat::R32Sint: return DXGI_FORMAT_R32_SINT; + case TextureFormat::R32Float: return DXGI_FORMAT_R32_FLOAT; + case TextureFormat::RG16Uint: return DXGI_FORMAT_R16G16_UINT; + case TextureFormat::RG16Sint: return DXGI_FORMAT_R16G16_SINT; + case TextureFormat::RG16Float: return DXGI_FORMAT_R16G16_FLOAT; + case TextureFormat::RGBA8Unorm: return DXGI_FORMAT_R8G8B8A8_UNORM; + case TextureFormat::RGBA8UnormSrgb: return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + case TextureFormat::RGBA8Snorm: return DXGI_FORMAT_R8G8B8A8_SNORM; + case TextureFormat::RGBA8Uint: return DXGI_FORMAT_R8G8B8A8_UINT; + case TextureFormat::RGBA8Sint: return DXGI_FORMAT_R8G8B8A8_SINT; + case TextureFormat::BGRA8Unorm: return DXGI_FORMAT_B8G8R8A8_UNORM; + case TextureFormat::BGRA8UnormSrgb: return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; + case TextureFormat::RGB10A2Unorm: return DXGI_FORMAT_R10G10B10A2_UNORM; + case TextureFormat::RGB10A2Uint: return DXGI_FORMAT_R10G10B10A2_UINT; + case TextureFormat::RG11B10Float: return DXGI_FORMAT_R11G11B10_FLOAT; + case TextureFormat::RGB9E5Float: return DXGI_FORMAT_R9G9B9E5_SHAREDEXP; + case TextureFormat::RG32Uint: return DXGI_FORMAT_R32G32_UINT; + case TextureFormat::RG32Sint: return DXGI_FORMAT_R32G32_SINT; + case TextureFormat::RG32Float: return DXGI_FORMAT_R32G32_FLOAT; + case TextureFormat::RGBA16Uint: return DXGI_FORMAT_R16G16B16A16_UINT; + case TextureFormat::RGBA16Sint: return DXGI_FORMAT_R16G16B16A16_SINT; + case TextureFormat::RGBA16Float: return DXGI_FORMAT_R16G16B16A16_FLOAT; + case TextureFormat::RGBA16Unorm: return DXGI_FORMAT_R16G16B16A16_UNORM; + case TextureFormat::RGBA16Snorm: return DXGI_FORMAT_R16G16B16A16_SNORM; + case TextureFormat::RGBA32Uint: return DXGI_FORMAT_R32G32B32A32_UINT; + case TextureFormat::RGBA32Sint: return DXGI_FORMAT_R32G32B32A32_SINT; + case TextureFormat::RGBA32Float: return DXGI_FORMAT_R32G32B32A32_FLOAT; + case TextureFormat::Depth16Unorm: return DXGI_FORMAT_D16_UNORM; + case TextureFormat::Depth24Plus: return DXGI_FORMAT_D24_UNORM_S8_UINT; + case TextureFormat::Depth24PlusStencil8:return DXGI_FORMAT_D24_UNORM_S8_UINT; + case TextureFormat::Depth32Float: return DXGI_FORMAT_D32_FLOAT; + case TextureFormat::Depth32FloatStencil8:return DXGI_FORMAT_D32_FLOAT_S8X24_UINT; + case TextureFormat::Stencil8: return DXGI_FORMAT_R8_UINT; + case TextureFormat::BC1RGBAUnorm: return DXGI_FORMAT_BC1_UNORM; + case TextureFormat::BC1RGBAUnormSrgb: return DXGI_FORMAT_BC1_UNORM_SRGB; + case TextureFormat::BC2RGBAUnorm: return DXGI_FORMAT_BC2_UNORM; + case TextureFormat::BC2RGBAUnormSrgb: return DXGI_FORMAT_BC2_UNORM_SRGB; + case TextureFormat::BC3RGBAUnorm: return DXGI_FORMAT_BC3_UNORM; + case TextureFormat::BC3RGBAUnormSrgb: return DXGI_FORMAT_BC3_UNORM_SRGB; + case TextureFormat::BC4RUnorm: return DXGI_FORMAT_BC4_UNORM; + case TextureFormat::BC4RSnorm: return DXGI_FORMAT_BC4_SNORM; + case TextureFormat::BC5RGUnorm: return DXGI_FORMAT_BC5_UNORM; + case TextureFormat::BC5RGSnorm: return DXGI_FORMAT_BC5_SNORM; + case TextureFormat::BC6HRGBUfloat: return DXGI_FORMAT_BC6H_UF16; + case TextureFormat::BC6HRGBFloat: return DXGI_FORMAT_BC6H_SF16; + case TextureFormat::BC7RGBAUnorm: return DXGI_FORMAT_BC7_UNORM; + case TextureFormat::BC7RGBAUnormSrgb: return DXGI_FORMAT_BC7_UNORM_SRGB; + default: return DXGI_FORMAT_UNKNOWN; + } +} + +inline TextureFormat fromDxgiFormat(DXGI_FORMAT f) { + switch (f) { + case DXGI_FORMAT_R8G8B8A8_UNORM: return TextureFormat::RGBA8Unorm; + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return TextureFormat::RGBA8UnormSrgb; + case DXGI_FORMAT_B8G8R8A8_UNORM: return TextureFormat::BGRA8Unorm; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return TextureFormat::BGRA8UnormSrgb; + case DXGI_FORMAT_R16G16B16A16_FLOAT: return TextureFormat::RGBA16Float; + case DXGI_FORMAT_R10G10B10A2_UNORM: return TextureFormat::RGB10A2Unorm; + case DXGI_FORMAT_R32G32B32A32_FLOAT: return TextureFormat::RGBA32Float; + default: return TextureFormat::Undefined; + } +} + +inline DXGI_FORMAT toDxgiVertexFormat(VertexFormat f) { + switch (f) { + case VertexFormat::Uint8x2: return DXGI_FORMAT_R8G8_UINT; + case VertexFormat::Uint8x4: return DXGI_FORMAT_R8G8B8A8_UINT; + case VertexFormat::Sint8x2: return DXGI_FORMAT_R8G8_SINT; + case VertexFormat::Sint8x4: return DXGI_FORMAT_R8G8B8A8_SINT; + case VertexFormat::Unorm8x2: return DXGI_FORMAT_R8G8_UNORM; + case VertexFormat::Unorm8x4: return DXGI_FORMAT_R8G8B8A8_UNORM; + case VertexFormat::Snorm8x2: return DXGI_FORMAT_R8G8_SNORM; + case VertexFormat::Snorm8x4: return DXGI_FORMAT_R8G8B8A8_SNORM; + case VertexFormat::Uint16x2: return DXGI_FORMAT_R16G16_UINT; + case VertexFormat::Uint16x4: return DXGI_FORMAT_R16G16B16A16_UINT; + case VertexFormat::Sint16x2: return DXGI_FORMAT_R16G16_SINT; + case VertexFormat::Sint16x4: return DXGI_FORMAT_R16G16B16A16_SINT; + case VertexFormat::Unorm16x2: return DXGI_FORMAT_R16G16_UNORM; + case VertexFormat::Unorm16x4: return DXGI_FORMAT_R16G16B16A16_UNORM; + case VertexFormat::Snorm16x2: return DXGI_FORMAT_R16G16_SNORM; + case VertexFormat::Snorm16x4: return DXGI_FORMAT_R16G16B16A16_SNORM; + case VertexFormat::Float16x2: return DXGI_FORMAT_R16G16_FLOAT; + case VertexFormat::Float16x4: return DXGI_FORMAT_R16G16B16A16_FLOAT; + case VertexFormat::Float32: return DXGI_FORMAT_R32_FLOAT; + case VertexFormat::Float32x2: return DXGI_FORMAT_R32G32_FLOAT; + case VertexFormat::Float32x3: return DXGI_FORMAT_R32G32B32_FLOAT; + case VertexFormat::Float32x4: return DXGI_FORMAT_R32G32B32A32_FLOAT; + case VertexFormat::Uint32: return DXGI_FORMAT_R32_UINT; + case VertexFormat::Uint32x2: return DXGI_FORMAT_R32G32_UINT; + case VertexFormat::Uint32x3: return DXGI_FORMAT_R32G32B32_UINT; + case VertexFormat::Uint32x4: return DXGI_FORMAT_R32G32B32A32_UINT; + case VertexFormat::Sint32: return DXGI_FORMAT_R32_SINT; + case VertexFormat::Sint32x2: return DXGI_FORMAT_R32G32_SINT; + case VertexFormat::Sint32x3: return DXGI_FORMAT_R32G32B32_SINT; + case VertexFormat::Sint32x4: return DXGI_FORMAT_R32G32B32A32_SINT; + default: return DXGI_FORMAT_UNKNOWN; + } +} + +inline DXGI_FORMAT toDxgiIndexFormat(IndexFormat f) { + switch (f) { + case IndexFormat::UInt16: return DXGI_FORMAT_R16_UINT; + case IndexFormat::UInt32: return DXGI_FORMAT_R32_UINT; + } + return DXGI_FORMAT_R16_UINT; +} + +inline D3D12_COMMAND_LIST_TYPE toCommandListType(QueueType t) { + switch (t) { + case QueueType::Graphics: return D3D12_COMMAND_LIST_TYPE_DIRECT; + case QueueType::Compute: return D3D12_COMMAND_LIST_TYPE_COMPUTE; + case QueueType::Transfer: return D3D12_COMMAND_LIST_TYPE_COPY; + } + return D3D12_COMMAND_LIST_TYPE_DIRECT; +} + +inline D3D12_HEAP_TYPE toHeapType(MemoryLocation loc) { + switch (loc) { + case MemoryLocation::GpuOnly: return D3D12_HEAP_TYPE_DEFAULT; + case MemoryLocation::CpuToGpu: return D3D12_HEAP_TYPE_UPLOAD; + case MemoryLocation::GpuToCpu: return D3D12_HEAP_TYPE_READBACK; + case MemoryLocation::Auto: return D3D12_HEAP_TYPE_DEFAULT; + } + return D3D12_HEAP_TYPE_DEFAULT; +} + +inline D3D12_RESOURCE_FLAGS toTextureResourceFlags(TextureUsage usage) { + D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE; + if (static_cast(usage & TextureUsage::RenderTarget)) flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + if (static_cast(usage & TextureUsage::DepthStencil)) flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + if (static_cast(usage & TextureUsage::Storage)) flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + return flags; +} + +inline D3D12_RESOURCE_FLAGS toBufferResourceFlags(BufferUsage usage) { + D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE; + if (static_cast(usage & BufferUsage::Storage)) flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + if (static_cast(usage & BufferUsage::AccelStructScratch)) flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + return flags; +} + +inline D3D12_RESOURCE_DIMENSION toResourceDimension(TextureDimension d) { + switch (d) { + case TextureDimension::Texture1D: return D3D12_RESOURCE_DIMENSION_TEXTURE1D; + case TextureDimension::Texture2D: return D3D12_RESOURCE_DIMENSION_TEXTURE2D; + case TextureDimension::Texture3D: return D3D12_RESOURCE_DIMENSION_TEXTURE3D; + } + return D3D12_RESOURCE_DIMENSION_TEXTURE2D; +} + +inline D3D12_COMPARISON_FUNC toComparisonFunc(CompareFunction f) { + switch (f) { + case CompareFunction::Never: return D3D12_COMPARISON_FUNC_NEVER; + case CompareFunction::Less: return D3D12_COMPARISON_FUNC_LESS; + case CompareFunction::Equal: return D3D12_COMPARISON_FUNC_EQUAL; + case CompareFunction::LessEqual: return D3D12_COMPARISON_FUNC_LESS_EQUAL; + case CompareFunction::Greater: return D3D12_COMPARISON_FUNC_GREATER; + case CompareFunction::NotEqual: return D3D12_COMPARISON_FUNC_NOT_EQUAL; + case CompareFunction::GreaterEqual: return D3D12_COMPARISON_FUNC_GREATER_EQUAL; + case CompareFunction::Always: return D3D12_COMPARISON_FUNC_ALWAYS; + } + return D3D12_COMPARISON_FUNC_ALWAYS; +} + +inline D3D12_TEXTURE_ADDRESS_MODE toAddressMode(AddressMode m) { + switch (m) { + case AddressMode::Repeat: return D3D12_TEXTURE_ADDRESS_MODE_WRAP; + case AddressMode::MirrorRepeat: return D3D12_TEXTURE_ADDRESS_MODE_MIRROR; + case AddressMode::ClampToEdge: return D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + case AddressMode::ClampToBorder: return D3D12_TEXTURE_ADDRESS_MODE_BORDER; + } + return D3D12_TEXTURE_ADDRESS_MODE_WRAP; +} + +inline D3D12_FILTER toFilter(FilterMode min, FilterMode mag, MipmapFilterMode mip, bool comparison) { + bool minL = min == FilterMode::Linear; + bool magL = mag == FilterMode::Linear; + bool mipL = mip == MipmapFilterMode::Linear; + if (comparison) { + if (!minL && !magL && !mipL) return D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT; + if (!minL && !magL && mipL) return D3D12_FILTER_COMPARISON_MIN_MAG_POINT_MIP_LINEAR; + if (!minL && magL && !mipL) return D3D12_FILTER_COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT; + if (!minL && magL && mipL) return D3D12_FILTER_COMPARISON_MIN_POINT_MAG_MIP_LINEAR; + if ( minL && !magL && !mipL) return D3D12_FILTER_COMPARISON_MIN_LINEAR_MAG_MIP_POINT; + if ( minL && !magL && mipL) return D3D12_FILTER_COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR; + if ( minL && magL && !mipL) return D3D12_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT; + return D3D12_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR; + } + if (!minL && !magL && !mipL) return D3D12_FILTER_MIN_MAG_MIP_POINT; + if (!minL && !magL && mipL) return D3D12_FILTER_MIN_MAG_POINT_MIP_LINEAR; + if (!minL && magL && !mipL) return D3D12_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT; + if (!minL && magL && mipL) return D3D12_FILTER_MIN_POINT_MAG_MIP_LINEAR; + if ( minL && !magL && !mipL) return D3D12_FILTER_MIN_LINEAR_MAG_MIP_POINT; + if ( minL && !magL && mipL) return D3D12_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR; + if ( minL && magL && !mipL) return D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT; + return D3D12_FILTER_MIN_MAG_MIP_LINEAR; +} + +inline DXGI_FORMAT toTypelessDepthFormat(TextureFormat f) { + switch (f) { + case TextureFormat::Depth16Unorm: return DXGI_FORMAT_R16_TYPELESS; + case TextureFormat::Depth24Plus: + case TextureFormat::Depth24PlusStencil8: return DXGI_FORMAT_R24G8_TYPELESS; + case TextureFormat::Depth32Float: return DXGI_FORMAT_R32_TYPELESS; + case TextureFormat::Depth32FloatStencil8:return DXGI_FORMAT_R32G8X24_TYPELESS; + default: return toDxgiFormat(f); + } +} + +inline DXGI_FORMAT toDepthSrvFormat(TextureFormat f) { + switch (f) { + case TextureFormat::Depth16Unorm: return DXGI_FORMAT_R16_UNORM; + case TextureFormat::Depth24Plus: + case TextureFormat::Depth24PlusStencil8: return DXGI_FORMAT_R24_UNORM_X8_TYPELESS; + case TextureFormat::Depth32Float: return DXGI_FORMAT_R32_FLOAT; + case TextureFormat::Depth32FloatStencil8:return DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS; + default: return toDxgiFormat(f); + } +} + +inline DXGI_FORMAT toStencilSrvFormat(TextureFormat f) { + switch (f) { + case TextureFormat::Depth24PlusStencil8: return DXGI_FORMAT_X24_TYPELESS_G8_UINT; + case TextureFormat::Depth32FloatStencil8:return DXGI_FORMAT_X32_TYPELESS_G8X24_UINT; + default: return toDxgiFormat(f); + } +} + +inline D3D12_PRIMITIVE_TOPOLOGY_TYPE toPrimitiveTopologyType(PrimitiveTopology t) { + switch (t) { + case PrimitiveTopology::PointList: return D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT; + case PrimitiveTopology::LineList: + case PrimitiveTopology::LineStrip: return D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE; + case PrimitiveTopology::TriangleList: + case PrimitiveTopology::TriangleStrip: return D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + } + return D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; +} + +inline D3D_PRIMITIVE_TOPOLOGY toPrimitiveTopology(PrimitiveTopology t) { + switch (t) { + case PrimitiveTopology::PointList: return D3D_PRIMITIVE_TOPOLOGY_POINTLIST; + case PrimitiveTopology::LineList: return D3D_PRIMITIVE_TOPOLOGY_LINELIST; + case PrimitiveTopology::LineStrip: return D3D_PRIMITIVE_TOPOLOGY_LINESTRIP; + case PrimitiveTopology::TriangleList: return D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; + case PrimitiveTopology::TriangleStrip: return D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP; + } + return D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; +} + +inline D3D12_CULL_MODE toCullMode(CullMode m) { + switch (m) { + case CullMode::None: return D3D12_CULL_MODE_NONE; + case CullMode::Front: return D3D12_CULL_MODE_FRONT; + case CullMode::Back: return D3D12_CULL_MODE_BACK; + } + return D3D12_CULL_MODE_NONE; +} + +inline D3D12_FILL_MODE toFillMode(FillMode m) { + switch (m) { + case FillMode::Solid: return D3D12_FILL_MODE_SOLID; + case FillMode::Wireframe: return D3D12_FILL_MODE_WIREFRAME; + } + return D3D12_FILL_MODE_SOLID; +} + +inline D3D12_BLEND toBlendFactor(BlendFactor f) { + switch (f) { + case BlendFactor::Zero: return D3D12_BLEND_ZERO; + case BlendFactor::One: return D3D12_BLEND_ONE; + case BlendFactor::Src: return D3D12_BLEND_SRC_COLOR; + case BlendFactor::OneMinusSrc: return D3D12_BLEND_INV_SRC_COLOR; + case BlendFactor::SrcAlpha: return D3D12_BLEND_SRC_ALPHA; + case BlendFactor::OneMinusSrcAlpha: return D3D12_BLEND_INV_SRC_ALPHA; + case BlendFactor::Dst: return D3D12_BLEND_DEST_COLOR; + case BlendFactor::OneMinusDst: return D3D12_BLEND_INV_DEST_COLOR; + case BlendFactor::DstAlpha: return D3D12_BLEND_DEST_ALPHA; + case BlendFactor::OneMinusDstAlpha: return D3D12_BLEND_INV_DEST_ALPHA; + case BlendFactor::SrcAlphaSaturated: return D3D12_BLEND_SRC_ALPHA_SAT; + case BlendFactor::Constant: return D3D12_BLEND_BLEND_FACTOR; + case BlendFactor::OneMinusConstant: return D3D12_BLEND_INV_BLEND_FACTOR; + } + return D3D12_BLEND_ONE; +} + +inline D3D12_BLEND_OP toBlendOp(BlendOperation op) { + switch (op) { + case BlendOperation::Add: return D3D12_BLEND_OP_ADD; + case BlendOperation::Subtract: return D3D12_BLEND_OP_SUBTRACT; + case BlendOperation::ReverseSubtract: return D3D12_BLEND_OP_REV_SUBTRACT; + case BlendOperation::Min: return D3D12_BLEND_OP_MIN; + case BlendOperation::Max: return D3D12_BLEND_OP_MAX; + } + return D3D12_BLEND_OP_ADD; +} + +inline D3D12_STENCIL_OP toStencilOp(StencilOperation op) { + switch (op) { + case StencilOperation::Keep: return D3D12_STENCIL_OP_KEEP; + case StencilOperation::Zero: return D3D12_STENCIL_OP_ZERO; + case StencilOperation::Replace: return D3D12_STENCIL_OP_REPLACE; + case StencilOperation::IncrementClamp: return D3D12_STENCIL_OP_INCR_SAT; + case StencilOperation::DecrementClamp: return D3D12_STENCIL_OP_DECR_SAT; + case StencilOperation::Invert: return D3D12_STENCIL_OP_INVERT; + case StencilOperation::IncrementWrap: return D3D12_STENCIL_OP_INCR; + case StencilOperation::DecrementWrap: return D3D12_STENCIL_OP_DECR; + } + return D3D12_STENCIL_OP_KEEP; +} + +inline D3D12_DESCRIPTOR_RANGE_TYPE toDescriptorRangeType(BindingType t) { + switch (t) { + case BindingType::UniformBuffer: return D3D12_DESCRIPTOR_RANGE_TYPE_CBV; + case BindingType::StorageBufferReadOnly: return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + case BindingType::StorageBufferReadWrite: return D3D12_DESCRIPTOR_RANGE_TYPE_UAV; + case BindingType::SampledTexture: return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + case BindingType::StorageTextureReadOnly: return D3D12_DESCRIPTOR_RANGE_TYPE_UAV; + case BindingType::StorageTextureReadWrite: return D3D12_DESCRIPTOR_RANGE_TYPE_UAV; + case BindingType::Sampler: return D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER; + case BindingType::ComparisonSampler: return D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER; + case BindingType::BindlessTextures: return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + case BindingType::BindlessSamplers: return D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER; + case BindingType::BindlessStorageBuffers: return D3D12_DESCRIPTOR_RANGE_TYPE_UAV; + case BindingType::BindlessStorageTextures: return D3D12_DESCRIPTOR_RANGE_TYPE_UAV; + case BindingType::AccelerationStructure: return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + } + return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; +} + +inline bool isSamplerBinding(BindingType t) { + return t == BindingType::Sampler || t == BindingType::ComparisonSampler || t == BindingType::BindlessSamplers; +} + +/// Strips sRGB from a DXGI format (needed for DXGI flip model swap chains). +inline DXGI_FORMAT stripSrgb(DXGI_FORMAT f) { + switch (f) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + case DXGI_FORMAT_BC1_UNORM_SRGB: return DXGI_FORMAT_BC1_UNORM; + case DXGI_FORMAT_BC2_UNORM_SRGB: return DXGI_FORMAT_BC2_UNORM; + case DXGI_FORMAT_BC3_UNORM_SRGB: return DXGI_FORMAT_BC3_UNORM; + case DXGI_FORMAT_BC7_UNORM_SRGB: return DXGI_FORMAT_BC7_UNORM; + default: return f; + } +} + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorHeap.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorHeap.cppm new file mode 100644 index 00000000..1aaf3a80 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorHeap.cppm @@ -0,0 +1,80 @@ +/// Simple CPU-side descriptor heap allocator with free-list. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:descriptor_heap; + +import core.stdtypes; +import core.status; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDescriptorHeapAllocator { +public: + DxDescriptorHeapAllocator() = default; + + Status init(ID3D12Device* device, D3D12_DESCRIPTOR_HEAP_TYPE type, u32 maxCount, + D3D12_DESCRIPTOR_HEAP_FLAGS flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE) { + m_maxCount = maxCount; + m_alive.resize(maxCount, static_cast(0)); + + D3D12_DESCRIPTOR_HEAP_DESC hd{}; + hd.Type = type; + hd.NumDescriptors = maxCount; + hd.Flags = flags; + HRESULT hr = device->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&m_heap)); + if (FAILED(hr)) return ErrorCode::Unknown; + + m_heapStart = m_heap->GetCPUDescriptorHandleForHeapStart(); + m_descriptorSize = device->GetDescriptorHandleIncrementSize(type); + return ErrorCode::Ok; + } + + D3D12_CPU_DESCRIPTOR_HANDLE allocate() { + for (u32 i = 0; i < m_maxCount; ++i) { + u32 idx = (m_searchStart + i) % m_maxCount; + if (!m_alive[idx]) { + m_alive[idx] = true; + ++m_allocCount; + m_searchStart = (idx + 1) % m_maxCount; + D3D12_CPU_DESCRIPTOR_HANDLE h{}; + h.ptr = m_heapStart.ptr + static_cast(idx) * m_descriptorSize; + return h; + } + } + return {}; // heap full + } + + void free(D3D12_CPU_DESCRIPTOR_HANDLE handle) { + if (handle.ptr < m_heapStart.ptr) return; + u32 offset = static_cast((handle.ptr - m_heapStart.ptr) / m_descriptorSize); + if (offset < m_maxCount && m_alive[offset]) { + m_alive[offset] = false; + --m_allocCount; + } + } + + void destroy() { + m_heap.Reset(); + m_alive.clear(); + } + + [[nodiscard]] ID3D12DescriptorHeap* heap() const { return m_heap.Get(); } + [[nodiscard]] u32 descriptorSize() const { return m_descriptorSize; } + +private: + ComPtr m_heap; + D3D12_CPU_DESCRIPTOR_HANDLE m_heapStart{}; + u32 m_descriptorSize = 0; + u32 m_maxCount = 0; + u32 m_allocCount = 0; + u32 m_searchStart = 0; + std::vector m_alive; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorStaging.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorStaging.cppm new file mode 100644 index 00000000..86e79661 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDescriptorStaging.cppm @@ -0,0 +1,98 @@ +/// Bump-allocating staging region within a GPU-visible descriptor heap. +/// Copies bind group descriptors from CPU heap into GPU heap at bind time. + +module; + +#include "DxIncludes.h" +#include + +#include + +export module rhi.dx12:descriptor_staging; + +import core.stdtypes; +import core.status; +import :gpu_descriptor_heap; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDescriptorStaging { +public: + DxDescriptorStaging() = default; + + void init(DxGpuDescriptorHeap* cpuHeap, DxGpuDescriptorHeap* gpuHeap, + ID3D12Device* device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, u32 initialCapacity) { + m_cpuHeap = cpuHeap; + m_gpuHeap = gpuHeap; + m_device = device; + m_heapType = heapType; + m_capacity = initialCapacity; + } + + /// Copies `count` descriptors from `srcOffset` in CPU heap into GPU staging. + /// Returns the staging offset in the GPU heap, or -1 on failure. + i32 copyFrom(u32 srcOffset, u32 count) { + if (count == 0) return -1; + + // Lazy allocation. + if (m_blockOffset < 0) { + m_blockOffset = m_gpuHeap->allocate(m_capacity); + if (m_blockOffset < 0) return -1; + m_current = 0; + } + + // Grow if needed: retire current block, allocate bigger. + if (m_current + count > m_capacity) { + u32 newCap = std::max(m_capacity * 2, m_current + count); + i32 newBlock = m_gpuHeap->allocate(newCap); + if (newBlock < 0) return -1; + m_retiredBlocks.push_back({ m_blockOffset, m_capacity }); + m_blockOffset = newBlock; + m_capacity = newCap; + m_current = 0; + } + + u32 dstOffset = static_cast(m_blockOffset) + m_current; + m_device->CopyDescriptorsSimple(count, + m_gpuHeap->getCpuHandle(dstOffset), + m_cpuHeap->getCpuHandle(srcOffset), + m_heapType); + m_current += count; + return static_cast(dstOffset); + } + + /// Resets bump pointer. Called when command pool resets after fence wait. + void reset() { + m_current = 0; + for (auto& b : m_retiredBlocks) + m_gpuHeap->free(static_cast(b.offset), b.capacity); + m_retiredBlocks.clear(); + } + + /// Frees all blocks. + void destroy() { + if (m_blockOffset >= 0) { + m_gpuHeap->free(static_cast(m_blockOffset), m_capacity); + m_blockOffset = -1; + } + for (auto& b : m_retiredBlocks) + m_gpuHeap->free(static_cast(b.offset), b.capacity); + m_retiredBlocks.clear(); + } + +private: + struct RetiredBlock { i32 offset; u32 capacity; }; + + DxGpuDescriptorHeap* m_cpuHeap = nullptr; + DxGpuDescriptorHeap* m_gpuHeap = nullptr; + ID3D12Device* m_device = nullptr; + D3D12_DESCRIPTOR_HEAP_TYPE m_heapType = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + i32 m_blockOffset = -1; + u32 m_capacity = 0; + u32 m_current = 0; + std::vector m_retiredBlocks; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDevice.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDevice.cppm new file mode 100644 index 00000000..36e5e678 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxDevice.cppm @@ -0,0 +1,926 @@ +/// DX12 implementation of Device. +/// Creates ID3D12Device, manages descriptor heaps, queues, command signatures, +/// and an internal blit pipeline for texture copy / mipmap generation. + +module; + +#include "DxIncludes.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +export module rhi.dx12:device; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :adapter; +import :surface; +import :descriptor_heap; +import :gpu_descriptor_heap; +import :buffer; +import :texture; +import :texture_view; +import :sampler; +import :shader_module; +import :fence; +import :query_set; +import :bind_group_layout; +import :bind_group; +import :pipeline_layout; +import :pipeline_cache; +import :render_pipeline; +import :compute_pipeline; +import :mesh_pipeline; +import :accel_struct; +import :ray_tracing_pipeline; +import :command_pool; +import :command_encoder; +import :render_pass_encoder; +import :compute_pass_encoder; +import :queue; +import :swap_chain; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl : public Device { +public: + Status init(DxAdapterImpl* adapter, const DeviceDesc& desc) { + m_adapter = adapter; + + // Create device at feature level 12.0. + HRESULT hr = D3D12CreateDevice( + adapter->handle(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&m_device)); + if (FAILED(hr)) { + logErrorf("DxDevice: D3D12CreateDevice failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + // Suppress noisy debug layer warnings. + { + ComPtr infoQueue; + if (SUCCEEDED(m_device->QueryInterface(IID_PPV_ARGS(&infoQueue)))) { + D3D12_MESSAGE_ID suppressIds[] = { + D3D12_MESSAGE_ID_CLEARRENDERTARGETVIEW_MISMATCHINGCLEARVALUE, + D3D12_MESSAGE_ID_CLEARDEPTHSTENCILVIEW_MISMATCHINGCLEARVALUE, + }; + D3D12_INFO_QUEUE_FILTER filter{}; + filter.DenyList.NumIDs = static_cast(std::size(suppressIds)); + filter.DenyList.pIDList = suppressIds; + infoQueue->AddStorageFilterEntries(&filter); + m_infoQueue = infoQueue; + } + } + + // --- Descriptor heap allocators (CPU-side, for staging) --- + m_rtvHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 256); + m_dsvHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 64); + m_srvHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 4096); + m_samplerHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, 256); + + // --- GPU-visible descriptor heaps (shader-visible) --- + m_gpuSrvHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 65536, true); + m_gpuSamplerHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, 2048, true); + + // --- CPU-visible descriptor heaps (non-shader-visible, bind groups write here) --- + m_cpuSrvHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 65536, false); + m_cpuSamplerHeap.init(m_device.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, 2048, false); + + // --- Create queues --- + u32 graphicsCount = std::max(desc.graphicsQueueCount, 1u); + for (u32 i = 0; i < graphicsCount; ++i) { + auto* q = new DxQueueImpl(); + if (q->init(m_device.Get(), QueueType::Graphics, this) != ErrorCode::Ok) { + delete q; break; + } + m_graphicsQueues.push_back(q); + } + for (u32 i = 0; i < desc.computeQueueCount; ++i) { + auto* q = new DxQueueImpl(); + if (q->init(m_device.Get(), QueueType::Compute, this) != ErrorCode::Ok) { + delete q; break; + } + m_computeQueues.push_back(q); + } + for (u32 i = 0; i < desc.transferQueueCount; ++i) { + auto* q = new DxQueueImpl(); + if (q->init(m_device.Get(), QueueType::Transfer, this) != ErrorCode::Ok) { + delete q; break; + } + m_transferQueues.push_back(q); + } + + // --- Cached command signatures for indirect execution --- + createIndirectCommandSignatures(); + + // --- Internal blit pipeline --- + createBlitPipeline(); + + // --- Detect mesh shader & ray tracing support --- + detectExtensionSupport(); + + // --- Populate features --- + type = DeviceType::DX12; + features = adapter->buildFeatures(); + + // --- RT handle properties (DX12 constants) --- + if (m_rtEnabled) { + shaderGroupHandleSize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES; // 32 + shaderGroupHandleAlignment = D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT; // 32 + shaderGroupBaseAlignment = D3D12_RAYTRACING_SHADER_TABLE_BYTE_ALIGNMENT; // 64 + } + + return ErrorCode::Ok; + } + + // ================================================================== + // Device interface -- Queues + // ================================================================== + + Queue* getQueue(QueueType t, u32 index) override { + switch (t) { + case QueueType::Graphics: return index < m_graphicsQueues.size() ? m_graphicsQueues[index] : nullptr; + case QueueType::Compute: return index < m_computeQueues.size() ? m_computeQueues[index] : nullptr; + case QueueType::Transfer: return index < m_transferQueues.size() ? m_transferQueues[index] : nullptr; + } + return nullptr; + } + + u32 getQueueCount(QueueType t) override { + switch (t) { + case QueueType::Graphics: return static_cast(m_graphicsQueues.size()); + case QueueType::Compute: return static_cast(m_computeQueues.size()); + case QueueType::Transfer: return static_cast(m_transferQueues.size()); + } + return 0; + } + + FormatSupport getFormatSupport(TextureFormat /*format*/) override { + // DX12 supports D24_S8 on all hardware and most formats broadly. + // A full implementation would call CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT). + return FormatSupport::Texture | FormatSupport::ColorAttachment | + FormatSupport::DepthStencil | FormatSupport::Buffer | + FormatSupport::VertexBuffer | FormatSupport::BlendableColor | + FormatSupport::LinearFilter; + } + + // ================================================================== + // Device interface -- Resource creation + // ================================================================== + + Status createBuffer(const BufferDesc& d, Buffer*& out) override { + auto* b = new DxBufferImpl(); + if (b->init(m_device.Get(), d) != ErrorCode::Ok) { delete b; out = nullptr; return ErrorCode::Unknown; } + setDebugName(b->handle(), d.label); + out = b; + return ErrorCode::Ok; + } + + Status createTexture(const TextureDesc& d, Texture*& out) override { + auto* t = new DxTextureImpl(); + if (t->init(m_device.Get(), d) != ErrorCode::Ok) { delete t; out = nullptr; return ErrorCode::Unknown; } + setDebugName(t->handle(), d.label); + out = t; + return ErrorCode::Ok; + } + + Status createTextureView(Texture* tex, const TextureViewDesc& d, TextureView*& out) override { + auto* dxTex = static_cast(tex); + if (!dxTex) { + logError("DxDevice: cast to DxTextureImpl failed"); + out = nullptr; + return ErrorCode::Unknown; + } + auto* v = new DxTextureViewImpl(); + if (v->init(m_device.Get(), dxTex, d, &m_srvHeap, &m_rtvHeap, &m_dsvHeap) != ErrorCode::Ok) { + delete v; out = nullptr; return ErrorCode::Unknown; + } + out = v; + return ErrorCode::Ok; + } + + Status createSampler(const SamplerDesc& d, Sampler*& out) override { + auto* s = new DxSamplerImpl(); + if (s->init(m_device.Get(), d, &m_samplerHeap) != ErrorCode::Ok) { + delete s; out = nullptr; return ErrorCode::Unknown; + } + out = s; + return ErrorCode::Ok; + } + + Status createShaderModule(const ShaderModuleDesc& d, ShaderModule*& out) override { + auto* m = new DxShaderModuleImpl(); + if (m->init(d) != ErrorCode::Ok) { delete m; out = nullptr; return ErrorCode::Unknown; } + out = m; + return ErrorCode::Ok; + } + + // ================================================================== + // Binding & Pipelines + // ================================================================== + + Status createBindGroupLayout(const BindGroupLayoutDesc& d, BindGroupLayout*& out) override { + auto* l = new DxBindGroupLayoutImpl(); + if (l->init(d) != ErrorCode::Ok) { delete l; out = nullptr; return ErrorCode::Unknown; } + out = l; + return ErrorCode::Ok; + } + + Status createBindGroup(const BindGroupDesc& d, BindGroup*& out) override { + auto* g = new DxBindGroupImpl(); + if (g->init(m_device.Get(), d, &m_cpuSrvHeap, &m_cpuSamplerHeap) != ErrorCode::Ok) { + delete g; out = nullptr; return ErrorCode::Unknown; + } + out = g; + return ErrorCode::Ok; + } + + Status createPipelineLayout(const PipelineLayoutDesc& d, PipelineLayout*& out) override { + auto* l = new DxPipelineLayoutImpl(); + if (l->init(m_device.Get(), d) != ErrorCode::Ok) { + logError("DxDevice: createPipelineLayout failed"); + delete l; out = nullptr; return ErrorCode::Unknown; + } + setDebugName(l->handle(), d.label); + out = l; + return ErrorCode::Ok; + } + + Status createPipelineCache(const PipelineCacheDesc& d, PipelineCache*& out) override { + auto* c = new DxPipelineCacheImpl(); + if (c->init(m_device.Get(), d) != ErrorCode::Ok) { delete c; out = nullptr; return ErrorCode::Unknown; } + if (c->handle()) setDebugName(c->handle(), d.label); + out = c; + return ErrorCode::Ok; + } + + Status createRenderPipeline(const RenderPipelineDesc& d, RenderPipeline*& out) override { + auto* p = new DxRenderPipelineImpl(); + if (p->init(m_device.Get(), d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + setDebugName(p->handle(), d.label); + out = p; + return ErrorCode::Ok; + } + + Status createComputePipeline(const ComputePipelineDesc& d, ComputePipeline*& out) override { + auto* p = new DxComputePipelineImpl(); + if (p->init(m_device.Get(), d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + setDebugName(p->handle(), d.label); + out = p; + return ErrorCode::Ok; + } + + // ================================================================== + // Mesh shader (folded in) + // ================================================================== + + Status createMeshPipeline(const MeshPipelineDesc& d, MeshPipeline*& out) override { + if (!m_meshEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* p = new DxMeshPipelineImpl(); + if (p->init(m_device.Get(), d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + setDebugName(p->handle(), d.label); + out = p; + return ErrorCode::Ok; + } + + void destroyMeshPipeline(MeshPipeline*& p) override { + if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } + } + + // ================================================================== + // Ray tracing (folded in) + // ================================================================== + + Status createAccelStruct(const AccelStructDesc& d, AccelStruct*& out) override { + if (!m_rtEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* a = new DxAccelStructImpl(); + if (a->init(m_device.Get(), d) != ErrorCode::Ok) { delete a; out = nullptr; return ErrorCode::Unknown; } + out = a; + return ErrorCode::Ok; + } + + void destroyAccelStruct(AccelStruct*& a) override { + if (a) { static_cast(a)->cleanup(); delete a; a = nullptr; } + } + + Status createRayTracingPipeline(const RayTracingPipelineDesc& d, RayTracingPipeline*& out) override { + if (!m_rtEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* p = new DxRayTracingPipelineImpl(); + if (p->init(m_device.Get(), d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; + return ErrorCode::Ok; + } + + void destroyRayTracingPipeline(RayTracingPipeline*& p) override { + if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } + } + + Status getShaderGroupHandles(RayTracingPipeline* pipeline, u32 firstGroup, + u32 groupCount, std::span outData) override { + if (!m_rtEnabled) return ErrorCode::NotSupported; + auto* dxPipeline = static_cast(pipeline); + if (!dxPipeline || !dxPipeline->properties()) { + logError("DxDevice: pipeline or properties is null"); + return ErrorCode::Unknown; + } + + constexpr u32 handleSize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES; // 32 + if (outData.size() < static_cast(groupCount * handleSize)) { + logError("DxDevice: output buffer too small for shader group handles"); + return ErrorCode::Unknown; + } + + auto exportNames = dxPipeline->groupExportNames(); + for (u32 i = 0; i < groupCount; ++i) { + u32 groupIdx = firstGroup + i; + if (groupIdx >= exportNames.size()) { + logError("DxDevice: shader group index out of range"); + return ErrorCode::Unknown; + } + const auto& exportName = exportNames[groupIdx]; + void* identifier = dxPipeline->properties()->GetShaderIdentifier(exportName.c_str()); + if (!identifier) { + logError("DxDevice: GetShaderIdentifier returned null"); + return ErrorCode::Unknown; + } + std::memcpy(outData.data() + (i * handleSize), identifier, handleSize); + } + return ErrorCode::Ok; + } + + // ================================================================== + // Commands + // ================================================================== + + Status createCommandPool(QueueType qt, CommandPool*& out) override { + auto* p = new DxCommandPoolImpl(); + if (p->init(this, m_device.Get(), qt, + &m_cpuSrvHeap, &m_gpuSrvHeap, + &m_cpuSamplerHeap, &m_gpuSamplerHeap) != ErrorCode::Ok) { + delete p; out = nullptr; return ErrorCode::Unknown; + } + out = p; + return ErrorCode::Ok; + } + + // ================================================================== + // Synchronization + // ================================================================== + + Status createFence(u64 initialValue, Fence*& out) override { + auto* f = new DxFenceImpl(); + if (f->init(m_device.Get(), initialValue) != ErrorCode::Ok) { delete f; out = nullptr; return ErrorCode::Unknown; } + out = f; + return ErrorCode::Ok; + } + + // ================================================================== + // Queries + // ================================================================== + + Status createQuerySet(const QuerySetDesc& d, QuerySet*& out) override { + auto* q = new DxQuerySetImpl(); + if (q->init(m_device.Get(), d) != ErrorCode::Ok) { delete q; out = nullptr; return ErrorCode::Unknown; } + setDebugName(q->handle(), d.label); + out = q; + return ErrorCode::Ok; + } + + // ================================================================== + // Presentation + // ================================================================== + + Status createSwapChain(Surface* surface, const SwapChainDesc& d, SwapChain*& out) override { + auto* dxSurface = static_cast(surface); + if (!dxSurface) { + logError("DxDevice: cast to DxSurfaceImpl failed"); + out = nullptr; + return ErrorCode::Unknown; + } + + // Need a graphics queue for swap chain. + if (m_graphicsQueues.empty()) { out = nullptr; return ErrorCode::Unknown; } + + auto* sc = new DxSwapChainImpl(); + if (sc->init(m_device.Get(), m_adapter->factory(), + m_graphicsQueues[0]->handle(), + dxSurface, d, + &m_srvHeap, &m_rtvHeap, &m_dsvHeap) != ErrorCode::Ok) { + delete sc; out = nullptr; return ErrorCode::Unknown; + } + out = sc; + return ErrorCode::Ok; + } + + // ================================================================== + // Resource destruction + // ================================================================== + + void destroyBuffer(Buffer*& b) override { if (b) { static_cast(b)->cleanup(); delete b; b = nullptr; } } + void destroyTexture(Texture*& t) override { if (t) { static_cast(t)->cleanup(); delete t; t = nullptr; } } + void destroyTextureView(TextureView*& v) override { if (v) { static_cast(v)->cleanup(); delete v; v = nullptr; } } + void destroySampler(Sampler*& s) override { if (s) { static_cast(s)->cleanup(); delete s; s = nullptr; } } + void destroyShaderModule(ShaderModule*& m) override { if (m) { static_cast(m)->cleanup(); delete m; m = nullptr; } } + void destroyBindGroupLayout(BindGroupLayout*& l) override { if (l) { delete l; l = nullptr; } } + void destroyBindGroup(BindGroup*& g) override { if (g) { static_cast(g)->cleanup(); delete g; g = nullptr; } } + void destroyPipelineLayout(PipelineLayout*& l) override { if (l) { static_cast(l)->cleanup(); delete l; l = nullptr; } } + void destroyPipelineCache(PipelineCache*& c) override { if (c) { static_cast(c)->cleanup(); delete c; c = nullptr; } } + void destroyRenderPipeline(RenderPipeline*& p) override { if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } } + void destroyComputePipeline(ComputePipeline*& p) override { if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } } + void destroyCommandPool(CommandPool*& p) override { if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } } + void destroyFence(Fence*& f) override { if (f) { static_cast(f)->cleanup(); delete f; f = nullptr; } } + void destroyQuerySet(QuerySet*& q) override { if (q) { static_cast(q)->cleanup(); delete q; q = nullptr; } } + void destroySwapChain(SwapChain*& sc) override { if (sc) { static_cast(sc)->cleanup(); delete sc; sc = nullptr; } } + void destroySurface(Surface*& s) override { if (s) { delete s; s = nullptr; } } + + // ================================================================== + // Lifecycle + // ================================================================== + + void waitIdle() override { + for (auto* q : m_graphicsQueues) q->waitIdle(); + for (auto* q : m_computeQueues) q->waitIdle(); + for (auto* q : m_transferQueues) q->waitIdle(); + drainDebugMessages(); + } + + void drainDebugMessages() { + if (!m_infoQueue) return; + UINT64 count = m_infoQueue->GetNumStoredMessages(); + for (UINT64 i = 0; i < count; ++i) { + SIZE_T len = 0; + m_infoQueue->GetMessage(i, nullptr, &len); + if (len == 0) continue; + auto* msg = static_cast(std::malloc(len)); + if (m_infoQueue->GetMessage(i, msg, &len) == S_OK) { + if (msg->Severity <= D3D12_MESSAGE_SEVERITY_WARNING) + std::fprintf(stderr, "[DX12 %s] %.*s\n", + msg->Severity == D3D12_MESSAGE_SEVERITY_ERROR ? "ERROR" : + msg->Severity == D3D12_MESSAGE_SEVERITY_WARNING ? "WARN" : "CORRUPT", + static_cast(msg->DescriptionByteLength), msg->pDescription); + } + std::free(msg); + } + m_infoQueue->ClearStoredMessages(); + } + + void destroy() override { + waitIdle(); + + // Queues. + for (auto* q : m_graphicsQueues) { q->cleanup(); delete q; } + for (auto* q : m_computeQueues) { q->cleanup(); delete q; } + for (auto* q : m_transferQueues) { q->cleanup(); delete q; } + m_graphicsQueues.clear(); + m_computeQueues.clear(); + m_transferQueues.clear(); + + // Blit pipeline. + for (auto& [fmt, pso] : m_blitPsoCache) + pso.Reset(); + m_blitPsoCache.clear(); + m_blitVsBlob.Reset(); + m_blitPsBlob.Reset(); + m_blitRootSignature.Reset(); + + // Command signatures. + m_drawSignature.Reset(); + m_drawIndexedSignature.Reset(); + m_dispatchSignature.Reset(); + m_dispatchMeshSignature.Reset(); + + // Descriptor heaps. + m_cpuSrvHeap.destroy(); + m_cpuSamplerHeap.destroy(); + m_gpuSrvHeap.destroy(); + m_gpuSamplerHeap.destroy(); + m_rtvHeap.destroy(); + m_dsvHeap.destroy(); + m_srvHeap.destroy(); + m_samplerHeap.destroy(); + + // Report live objects in debug builds. +#ifdef _DEBUG + { + ComPtr debugDevice; + if (SUCCEEDED(m_device->QueryInterface(IID_PPV_ARGS(&debugDevice)))) { + debugDevice->ReportLiveDeviceObjects( + static_cast(D3D12_RLDO_DETAIL | D3D12_RLDO_IGNORE_INTERNAL)); + } + } +#endif + + m_device.Reset(); + delete this; + } + + // ================================================================== + // Internal accessors (encoders, swap chain, etc. need these) + // ================================================================== + + [[nodiscard]] ID3D12Device* handle() const { return m_device.Get(); } + [[nodiscard]] DxAdapterImpl* adapter() const { return m_adapter; } + + [[nodiscard]] DxDescriptorHeapAllocator* rtvHeap() { return &m_rtvHeap; } + [[nodiscard]] DxDescriptorHeapAllocator* dsvHeap() { return &m_dsvHeap; } + [[nodiscard]] DxDescriptorHeapAllocator* srvHeap() { return &m_srvHeap; } + [[nodiscard]] DxDescriptorHeapAllocator* samplerHeap() { return &m_samplerHeap; } + + [[nodiscard]] DxGpuDescriptorHeap* gpuSrvHeap() { return &m_gpuSrvHeap; } + [[nodiscard]] DxGpuDescriptorHeap* gpuSamplerHeap() { return &m_gpuSamplerHeap; } + [[nodiscard]] DxGpuDescriptorHeap* cpuSrvHeap() { return &m_cpuSrvHeap; } + [[nodiscard]] DxGpuDescriptorHeap* cpuSamplerHeap() { return &m_cpuSamplerHeap; } + + [[nodiscard]] ID3D12CommandSignature* drawSignature() const { return m_drawSignature.Get(); } + [[nodiscard]] ID3D12CommandSignature* drawIndexedSignature() const { return m_drawIndexedSignature.Get(); } + [[nodiscard]] ID3D12CommandSignature* dispatchSignature() const { return m_dispatchSignature.Get(); } + [[nodiscard]] ID3D12CommandSignature* dispatchMeshSignature() const { return m_dispatchMeshSignature.Get(); } + [[nodiscard]] ID3D12RootSignature* blitRootSignature() const { return m_blitRootSignature.Get(); } + + [[nodiscard]] bool meshEnabled() const { return m_meshEnabled; } + [[nodiscard]] bool rtEnabled() const { return m_rtEnabled; } + + /// Gets or creates a blit PSO for the given render target format. + ID3D12PipelineState* getOrCreateBlitPSO(DXGI_FORMAT format) { + if (!m_blitRootSignature) return nullptr; + + std::lock_guard lock(m_blitMutex); + + auto it = m_blitPsoCache.find(format); + if (it != m_blitPsoCache.end()) return it->second.Get(); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC psd{}; + psd.pRootSignature = m_blitRootSignature.Get(); + psd.VS = m_blitVsBytecode; + psd.PS = m_blitPsBytecode; + psd.InputLayout = { nullptr, 0 }; + psd.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + psd.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + psd.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + psd.RasterizerState.DepthClipEnable = FALSE; + psd.BlendState.RenderTarget[0].BlendEnable = FALSE; + psd.BlendState.RenderTarget[0].RenderTargetWriteMask = 0x0F; + psd.DepthStencilState.DepthEnable = FALSE; + psd.DepthStencilState.StencilEnable = FALSE; + psd.DSVFormat = DXGI_FORMAT_UNKNOWN; + psd.NumRenderTargets = 1; + psd.RTVFormats[0] = format; + psd.SampleDesc.Count = 1; + psd.SampleMask = UINT_MAX; + + ComPtr newPso; + if (SUCCEEDED(m_device->CreateGraphicsPipelineState(&psd, IID_PPV_ARGS(&newPso)))) { + auto* raw = newPso.Get(); + m_blitPsoCache[format] = std::move(newPso); + return raw; + } + return nullptr; + } + + /// Sets a debug name on a DX12 object (visible in PIX, VS Graphics Debugger, etc.). + /// Works with any type that inherits from ID3D12Object (Resource, PSO, QueryHeap, etc.). + template + static void setDebugName(T* obj, std::u8string_view name) { + if (!obj || name.empty()) return; + // Convert narrow to wide. + std::wstring wide; + wide.reserve(name.size()); + for (usize i = 0; i < name.size(); ++i) + wide.push_back(static_cast(name[i])); + obj->SetName(wide.c_str()); + } + +private: + // ------------------------------------------------------------------ + // Indirect command signatures + // ------------------------------------------------------------------ + + void createIndirectCommandSignatures() { + D3D12_INDIRECT_ARGUMENT_DESC argDesc{}; + D3D12_COMMAND_SIGNATURE_DESC sigDesc{}; + sigDesc.NumArgumentDescs = 1; + sigDesc.pArgumentDescs = &argDesc; + sigDesc.NodeMask = 0; + + // Draw. + argDesc.Type = D3D12_INDIRECT_ARGUMENT_TYPE_DRAW; + sigDesc.ByteStride = 16; // sizeof(D3D12_DRAW_ARGUMENTS): 4 x uint32 + m_device->CreateCommandSignature(&sigDesc, nullptr, IID_PPV_ARGS(&m_drawSignature)); + + // DrawIndexed. + argDesc.Type = D3D12_INDIRECT_ARGUMENT_TYPE_DRAW_INDEXED; + sigDesc.ByteStride = 20; // sizeof(D3D12_DRAW_INDEXED_ARGUMENTS): 5 x uint32 + m_device->CreateCommandSignature(&sigDesc, nullptr, IID_PPV_ARGS(&m_drawIndexedSignature)); + + // Dispatch. + argDesc.Type = D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH; + sigDesc.ByteStride = 12; // sizeof(D3D12_DISPATCH_ARGUMENTS): 3 x uint32 + m_device->CreateCommandSignature(&sigDesc, nullptr, IID_PPV_ARGS(&m_dispatchSignature)); + } + + // ------------------------------------------------------------------ + // Extension support detection (mesh shader, ray tracing) + // ------------------------------------------------------------------ + + void detectExtensionSupport() { + // Mesh shaders -- requires D3D12_OPTIONS7. + D3D12_FEATURE_DATA_D3D12_OPTIONS7 options7{}; + HRESULT hr = m_device->CheckFeatureSupport( + D3D12_FEATURE_D3D12_OPTIONS7, &options7, sizeof(options7)); + if (SUCCEEDED(hr) && options7.MeshShaderTier != D3D12_MESH_SHADER_TIER_NOT_SUPPORTED) { + m_meshEnabled = true; + + // DispatchMesh command signature. + D3D12_INDIRECT_ARGUMENT_DESC argDesc{}; + argDesc.Type = D3D12_INDIRECT_ARGUMENT_TYPE_DISPATCH_MESH; + D3D12_COMMAND_SIGNATURE_DESC sigDesc{}; + sigDesc.ByteStride = 12; // sizeof(D3D12_DISPATCH_MESH_ARGUMENTS): 3 x uint32 + sigDesc.NumArgumentDescs = 1; + sigDesc.pArgumentDescs = &argDesc; + sigDesc.NodeMask = 0; + m_device->CreateCommandSignature(&sigDesc, nullptr, IID_PPV_ARGS(&m_dispatchMeshSignature)); + } + + // Ray tracing -- requires D3D12_OPTIONS5. + D3D12_FEATURE_DATA_D3D12_OPTIONS5 options5{}; + hr = m_device->CheckFeatureSupport( + D3D12_FEATURE_D3D12_OPTIONS5, &options5, sizeof(options5)); + if (SUCCEEDED(hr) && options5.RaytracingTier != D3D12_RAYTRACING_TIER_NOT_SUPPORTED) { + m_rtEnabled = true; + } + } + + // ------------------------------------------------------------------ + // Internal blit pipeline (fullscreen triangle VS + texture sample PS) + // ------------------------------------------------------------------ + + void createBlitPipeline() { + // TODO: Blit pipeline requires D3DCompile from d3dcompiler.lib. + // Add d3dcompiler to target_link_libraries and uncomment the code below + // once d3dcompiler linkage is available in this project. + // + // The blit pipeline is used for Blit and GenerateMipmaps operations. + const char vsSource[] = R"( + struct VSOutput { + float4 Position : SV_Position; + float2 UV : TEXCOORD0; + }; + VSOutput main(uint vertexId : SV_VertexID) { + VSOutput output; + output.UV = float2((vertexId << 1) & 2, vertexId & 2); + output.Position = float4(output.UV * float2(2, -2) + float2(-1, 1), 0, 1); + return output; + } + )"; + + const char psSource[] = R"( + Texture2D srcTexture : register(t0); + SamplerState srcSampler : register(s0); + float4 main(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target { + return srcTexture.Sample(srcSampler, uv); + } + )"; + + ComPtr errorBlob; + + // Compile VS. + HRESULT hr = D3DCompile(vsSource, sizeof(vsSource) - 1, nullptr, nullptr, nullptr, + "main", "vs_5_0", 0, 0, &m_blitVsBlob, &errorBlob); + if (FAILED(hr)) { + if (errorBlob) logErrorf("DxDevice: blit VS compile error: %s", + static_cast(errorBlob->GetBufferPointer())); + return; + } + errorBlob.Reset(); + + // Compile PS. + hr = D3DCompile(psSource, sizeof(psSource) - 1, nullptr, nullptr, nullptr, + "main", "ps_5_0", 0, 0, &m_blitPsBlob, &errorBlob); + if (FAILED(hr)) { + if (errorBlob) logErrorf("DxDevice: blit PS compile error: %s", + static_cast(errorBlob->GetBufferPointer())); + m_blitVsBlob.Reset(); + return; + } + errorBlob.Reset(); + + m_blitVsBytecode = { m_blitVsBlob->GetBufferPointer(), m_blitVsBlob->GetBufferSize() }; + m_blitPsBytecode = { m_blitPsBlob->GetBufferPointer(), m_blitPsBlob->GetBufferSize() }; + + // Root signature: 1 SRV descriptor table (t0) + 1 static linear sampler (s0). + D3D12_DESCRIPTOR_RANGE srvRange{}; + srvRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + srvRange.NumDescriptors = 1; + srvRange.BaseShaderRegister = 0; + srvRange.RegisterSpace = 0; + srvRange.OffsetInDescriptorsFromTableStart = 0; + + D3D12_ROOT_PARAMETER rootParam{}; + rootParam.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + rootParam.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + rootParam.DescriptorTable.NumDescriptorRanges = 1; + rootParam.DescriptorTable.pDescriptorRanges = &srvRange; + + D3D12_STATIC_SAMPLER_DESC staticSampler{}; + staticSampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + staticSampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + staticSampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + staticSampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + staticSampler.MaxAnisotropy = 1; + staticSampler.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER; + staticSampler.MinLOD = 0; + staticSampler.MaxLOD = D3D12_FLOAT32_MAX; + staticSampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + D3D12_ROOT_SIGNATURE_DESC rsDesc{}; + rsDesc.NumParameters = 1; + rsDesc.pParameters = &rootParam; + rsDesc.NumStaticSamplers = 1; + rsDesc.pStaticSamplers = &staticSampler; + + ComPtr signatureBlob; + hr = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, + &signatureBlob, &errorBlob); + if (FAILED(hr)) { + if (errorBlob) logErrorf("DxDevice: blit root sig serialize error: %s", + static_cast(errorBlob->GetBufferPointer())); + return; + } + + m_device->CreateRootSignature(0, signatureBlob->GetBufferPointer(), + signatureBlob->GetBufferSize(), + IID_PPV_ARGS(&m_blitRootSignature)); + } + + // ------------------------------------------------------------------ + // Member data + // ------------------------------------------------------------------ + + ComPtr m_device; + ComPtr m_infoQueue; + DxAdapterImpl* m_adapter = nullptr; + + // Queues. + std::vector m_graphicsQueues; + std::vector m_computeQueues; + std::vector m_transferQueues; + + // Descriptor heap allocators (CPU-side for staging). + DxDescriptorHeapAllocator m_rtvHeap; + DxDescriptorHeapAllocator m_dsvHeap; + DxDescriptorHeapAllocator m_srvHeap; + DxDescriptorHeapAllocator m_samplerHeap; + + // GPU-visible descriptor heaps (shader-visible, for command buffer binding). + DxGpuDescriptorHeap m_gpuSrvHeap; + DxGpuDescriptorHeap m_gpuSamplerHeap; + + // CPU-visible descriptor heaps (non-shader-visible, bind groups write here). + DxGpuDescriptorHeap m_cpuSrvHeap; + DxGpuDescriptorHeap m_cpuSamplerHeap; + + // Cached command signatures for indirect execution. + ComPtr m_drawSignature; + ComPtr m_drawIndexedSignature; + ComPtr m_dispatchSignature; + ComPtr m_dispatchMeshSignature; + + // Internal blit pipeline. + ComPtr m_blitRootSignature; + D3D12_SHADER_BYTECODE m_blitVsBytecode{}; + D3D12_SHADER_BYTECODE m_blitPsBytecode{}; + ComPtr m_blitVsBlob; + ComPtr m_blitPsBlob; + std::unordered_map> m_blitPsoCache; + std::mutex m_blitMutex; + + // Extension flags. + bool m_meshEnabled = false; + bool m_rtEnabled = false; +}; + +// ================================================================== +// Adapter::CreateDevice implementation +// ================================================================== + +Status DxAdapterImpl::createDevice(const DeviceDesc& desc, Device*& out) { + auto* dev = new DxDeviceImpl(); + if (dev->init(this, desc) != ErrorCode::Ok) { + delete dev; out = nullptr; return ErrorCode::Unknown; + } + out = dev; + return ErrorCode::Ok; +} + +// ---- CommandEncoder out-of-line: blitSubresource (needs DxDeviceImpl) ---- + +void DxCommandEncoderImpl::blitSubresource(DxTextureImpl* srcTex, u32 srcMip, + DxTextureImpl* dstTex, u32 dstMip, u32 dstWidth, u32 dstHeight, DXGI_FORMAT dxgiFormat) { + + auto* blitRootSig = m_device->blitRootSignature(); + if (!blitRootSig) return; + auto* blitPso = m_device->getOrCreateBlitPSO(dxgiFormat); + if (!blitPso) return; + + // Allocate temp RTV for destination mip. + auto rtvHandle = m_device->rtvHeap()->allocate(); + + D3D12_RENDER_TARGET_VIEW_DESC rtvDesc{}; + rtvDesc.Format = dxgiFormat; + rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = dstMip; + m_device->handle()->CreateRenderTargetView(dstTex->handle(), &rtvDesc, rtvHandle); + + // Allocate temp SRV in CPU heap, write, then stage-copy to GPU heap. + i32 tempSrvOff = m_device->cpuSrvHeap()->allocate(1); + if (tempSrvOff < 0) { m_device->rtvHeap()->free(rtvHandle); return; } + + auto tempCpuHandle = m_device->cpuSrvHeap()->getCpuHandle(static_cast(tempSrvOff)); + + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = dxgiFormat; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.Texture2D.MostDetailedMip = srcMip; + srvDesc.Texture2D.MipLevels = 1; + m_device->handle()->CreateShaderResourceView(srcTex->handle(), &srvDesc, tempCpuHandle); + + // Copy from CPU heap into GPU staging, then free CPU temp slot. + i32 stagedOff = m_pool->srvStaging()->copyFrom(static_cast(tempSrvOff), 1); + m_device->cpuSrvHeap()->free(static_cast(tempSrvOff), 1); + if (stagedOff < 0) { m_device->rtvHeap()->free(rtvHandle); return; } + + auto srvGpuHandle = m_device->gpuSrvHeap()->getGpuHandle(static_cast(stagedOff)); + + ensureDescriptorHeaps(); + + // Set blit pipeline. + m_cmdList->SetGraphicsRootSignature(blitRootSig); + m_cmdList->SetPipelineState(blitPso); + m_cmdList->SetGraphicsRootDescriptorTable(0, srvGpuHandle); + m_cmdList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); + + D3D12_VIEWPORT vp{}; vp.Width = static_cast(dstWidth); vp.Height = static_cast(dstHeight); vp.MaxDepth = 1.0f; + m_cmdList->RSSetViewports(1, &vp); + D3D12_RECT sc{}; sc.right = static_cast(dstWidth); sc.bottom = static_cast(dstHeight); + m_cmdList->RSSetScissorRects(1, &sc); + + m_cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + m_cmdList->DrawInstanced(3, 1, 0, 0); + + m_device->rtvHeap()->free(rtvHandle); +} + +// ---- CommandPool out-of-line methods (need DxCommandEncoderImpl + context structs) ---- + +Status DxCommandPoolImpl::createEncoder(CommandEncoder*& out) { + out = nullptr; + + ComPtr d3dDev; + m_allocator->GetDevice(IID_PPV_ARGS(&d3dDev)); + if (!d3dDev) return ErrorCode::Unknown; + + ID3D12GraphicsCommandList* cmdList = nullptr; + HRESULT hr = d3dDev->CreateCommandList(0, m_type, + m_allocator.Get(), nullptr, IID_PPV_ARGS(&cmdList)); + if (FAILED(hr)) return ErrorCode::Unknown; + + DxRenderPassContext rpeCtx{}; + rpeCtx.cmdList = cmdList; + rpeCtx.srvStaging = &m_srvStaging; + rpeCtx.samplerStaging = &m_samplerStaging; + rpeCtx.gpuSrvHeap = m_device->gpuSrvHeap(); + rpeCtx.gpuSamplerHeap = m_device->gpuSamplerHeap(); + rpeCtx.drawSig = m_device->drawSignature(); + rpeCtx.drawIndexedSig = m_device->drawIndexedSignature(); + rpeCtx.dispatchMeshSig = m_device->dispatchMeshSignature(); + + DxComputePassContext cpeCtx{}; + cpeCtx.cmdList = cmdList; + cpeCtx.srvStaging = &m_srvStaging; + cpeCtx.samplerStaging = &m_samplerStaging; + cpeCtx.gpuSrvHeap = m_device->gpuSrvHeap(); + cpeCtx.gpuSamplerHeap = m_device->gpuSamplerHeap(); + cpeCtx.dispatchSig = m_device->dispatchSignature(); + + auto* enc = new DxCommandEncoderImpl(m_device, cmdList, this, rpeCtx, cpeCtx); + out = enc; + return ErrorCode::Ok; +} + +void DxCommandPoolImpl::destroyEncoder(CommandEncoder*& encoder) { + if (auto* dx = static_cast(encoder)) delete dx; + encoder = nullptr; +} + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxFence.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxFence.cppm new file mode 100644 index 00000000..f10855b3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxFence.cppm @@ -0,0 +1,57 @@ +/// DX12 implementation of Fence using ID3D12Fence. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:fence; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxFenceImpl : public Fence { +public: + Status init(ID3D12Device* device, u64 initialValue) { + HRESULT hr = device->CreateFence(initialValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)); + if (FAILED(hr)) { + logErrorf("DxFence: CreateFence failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + m_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + return ErrorCode::Ok; + } + + u64 completedValue() override { + return m_fence->GetCompletedValue(); + } + + bool wait(u64 value, u64 timeoutNs) override { + if (m_fence->GetCompletedValue() >= value) return true; + m_fence->SetEventOnCompletion(value, m_event); + DWORD timeoutMs = (timeoutNs == ~0ull) ? INFINITE : static_cast(timeoutNs / 1000000); + return WaitForSingleObject(m_event, timeoutMs) == WAIT_OBJECT_0; + } + + void cleanup() { + if (m_event) { CloseHandle(m_event); m_event = nullptr; } + m_fence.Reset(); + } + + // ---- Internal ---- + [[nodiscard]] ID3D12Fence* handle() const { return m_fence.Get(); } + + void signal(ID3D12CommandQueue* queue, u64 value) { + queue->Signal(m_fence.Get(), value); + } + +private: + ComPtr m_fence; + HANDLE m_event = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxGpuDescriptorHeap.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxGpuDescriptorHeap.cppm new file mode 100644 index 00000000..30499d15 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxGpuDescriptorHeap.cppm @@ -0,0 +1,108 @@ +/// GPU-visible descriptor heap with contiguous block allocation. +/// Used for CBV/SRV/UAV and Sampler heaps that are shader-visible. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:gpu_descriptor_heap; + +import core.stdtypes; +import core.status; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxGpuDescriptorHeap { +public: + DxGpuDescriptorHeap() = default; + + Status init(ID3D12Device* device, D3D12_DESCRIPTOR_HEAP_TYPE type, u32 capacity, + bool shaderVisible = true) { + m_capacity = capacity; + + D3D12_DESCRIPTOR_HEAP_DESC hd{}; + hd.Type = type; + hd.NumDescriptors = capacity; + hd.Flags = shaderVisible ? D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE + : D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + HRESULT hr = device->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&m_heap)); + if (FAILED(hr)) return ErrorCode::Unknown; + + m_cpuStart = m_heap->GetCPUDescriptorHandleForHeapStart(); + if (shaderVisible) + m_gpuStart = m_heap->GetGPUDescriptorHandleForHeapStart(); + m_incrementSize = device->GetDescriptorHandleIncrementSize(type); + return ErrorCode::Ok; + } + + /// Allocate a contiguous block. Returns offset or -1. + i32 allocate(u32 count) { + if (count == 0) return -1; + // First-fit from free list. + for (usize i = 0; i < m_freeBlocks.size(); ++i) { + auto& b = m_freeBlocks[i]; + if (b.count >= count) { + u32 off = b.offset; + if (b.count == count) + m_freeBlocks.erase(m_freeBlocks.begin() + i); + else + b = { b.offset + count, b.count - count }; + return static_cast(off); + } + } + // Bump allocate. + if (m_nextFree + count <= m_capacity) { + u32 off = m_nextFree; + m_nextFree += count; + return static_cast(off); + } + return -1; + } + + /// Free a block with coalescing. + void free(u32 offset, u32 count) { + if (count == 0) return; + u32 mOff = offset, mCnt = count; + for (usize i = 0; i < m_freeBlocks.size(); ) { + if (m_freeBlocks[i].offset + m_freeBlocks[i].count == mOff) { + mOff = m_freeBlocks[i].offset; mCnt += m_freeBlocks[i].count; + m_freeBlocks.erase(m_freeBlocks.begin() + i); + } else if (mOff + mCnt == m_freeBlocks[i].offset) { + mCnt += m_freeBlocks[i].count; + m_freeBlocks.erase(m_freeBlocks.begin() + i); + } else ++i; + } + if (mOff + mCnt == m_nextFree) + m_nextFree = mOff; + else + m_freeBlocks.push_back({ mOff, mCnt }); + } + + D3D12_CPU_DESCRIPTOR_HANDLE getCpuHandle(u32 offset) const { + D3D12_CPU_DESCRIPTOR_HANDLE h{}; h.ptr = m_cpuStart.ptr + static_cast(offset) * m_incrementSize; return h; + } + D3D12_GPU_DESCRIPTOR_HANDLE getGpuHandle(u32 offset) const { + D3D12_GPU_DESCRIPTOR_HANDLE h{}; h.ptr = m_gpuStart.ptr + static_cast(offset) * m_incrementSize; return h; + } + + void destroy() { m_heap.Reset(); m_freeBlocks.clear(); } + + [[nodiscard]] ID3D12DescriptorHeap* heap() const { return m_heap.Get(); } + [[nodiscard]] u32 incrementSize() const { return m_incrementSize; } + +private: + struct FreeBlock { u32 offset; u32 count; }; + + ComPtr m_heap; + D3D12_CPU_DESCRIPTOR_HANDLE m_cpuStart{}; + D3D12_GPU_DESCRIPTOR_HANDLE m_gpuStart{}; + u32 m_incrementSize = 0; + u32 m_capacity = 0; + u32 m_nextFree = 0; + std::vector m_freeBlocks; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxIncludes.h b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxIncludes.h new file mode 100644 index 00000000..b984153b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxIncludes.h @@ -0,0 +1,31 @@ +#ifndef DRACO_RHI_DX12_INCLUDES_H_ +#define DRACO_RHI_DX12_INCLUDES_H_ + +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +# define NOMINMAX +#endif + +// DX12 headers use __uuidof (MSVC extension) via IID_PPV_ARGS. Clang supports +// it but warns under -Wlanguage-extension-token; suppress for all DX12 code. +// NOTE: We push but do NOT pop here - the suppression must remain active for +// IID_PPV_ARGS expansions in our .cppm files. The diagnostic state is scoped +// to each translation unit, so it won't leak. +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wlanguage-extension-token" +#endif + +#include +#include // ComPtr +#include +#include +#include + +// Helper alias. +template +using ComPtr = Microsoft::WRL::ComPtr; + +#endif // DRACO_RHI_DX12_INCLUDES_H_ diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxMeshPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxMeshPipeline.cppm new file mode 100644 index 00000000..0e6225f9 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxMeshPipeline.cppm @@ -0,0 +1,221 @@ +/// DX12 implementation of MeshPipeline. +/// Uses pipeline state stream (ID3D12Device2::CreatePipelineState) since +/// mesh shader pipelines cannot use the traditional D3D12_GRAPHICS_PIPELINE_STATE_DESC. + +module; + +#include "DxIncludes.h" + +#include +#include +#include + +export module rhi.dx12:mesh_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :pipeline_layout; +import :shader_module; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxMeshPipelineImpl : public MeshPipeline { +public: + Status init(ID3D12Device* device, const MeshPipelineDesc& desc) { + m_layout = static_cast(desc.layout); + if (!m_layout) { + logErrorf("DxMeshPipeline: pipeline layout is null"); + return ErrorCode::Unknown; + } + layout = desc.layout; + + // Query ID3D12Device2 for CreatePipelineState (pipeline state stream API). + ComPtr device2; + HRESULT hr = device->QueryInterface(IID_PPV_ARGS(&device2)); + if (FAILED(hr) || !device2) { + logErrorf("DxMeshPipeline: QueryInterface for ID3D12Device2 failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + // Build pipeline state stream as raw bytes. + // Subobjects packed sequentially: root sig, MS, [AS], [PS], blend, + // sample mask, rasterizer, [depth/stencil], [DS format], RT formats, sample desc. + alignas(8) u8 streamBuffer[2048]{}; + usize offset = 0; + + // Root signature (manual - pointer type cannot use writeSubobject). + { + offset = (offset + 7) & ~usize(7); + std::memcpy(&streamBuffer[offset], &(const D3D12_PIPELINE_STATE_SUBOBJECT_TYPE&) + (D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE), sizeof(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE)); + offset += sizeof(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE); + offset = (offset + 7) & ~usize(7); // pointer-align + auto* rootSig = m_layout->handle(); + std::memcpy(&streamBuffer[offset], &rootSig, sizeof(rootSig)); + offset += sizeof(rootSig); + } + + // Mesh shader (required). + auto* msMod = static_cast(desc.mesh.module); + if (!msMod) { + logErrorf("DxMeshPipeline: mesh shader module is null"); + return ErrorCode::Unknown; + } + { + auto msCode = msMod->bytecode(); + D3D12_SHADER_BYTECODE msBC{ msCode.data(), msCode.size() }; + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_MS, msBC); + } + + // Task/amplification shader (optional). + if (desc.task.has_value()) { + if (auto* asMod = static_cast(desc.task->module)) { + auto asCode = asMod->bytecode(); + D3D12_SHADER_BYTECODE asBC{ asCode.data(), asCode.size() }; + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_AS, asBC); + } + } + + // Fragment/pixel shader (optional). + if (desc.fragment.has_value()) { + if (auto* psMod = static_cast(desc.fragment->shader.module)) { + auto psCode = psMod->bytecode(); + D3D12_SHADER_BYTECODE psBC{ psCode.data(), psCode.size() }; + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS, psBC); + } + } + + // Blend state. + auto colorTargets = desc.colorTargets; + D3D12_BLEND_DESC blendDesc{}; + blendDesc.AlphaToCoverageEnable = desc.multisample.alphaToCoverageEnabled ? TRUE : FALSE; + blendDesc.IndependentBlendEnable = (colorTargets.size() > 1) ? TRUE : FALSE; + for (usize i = 0; i < colorTargets.size() && i < 8; ++i) { + const auto& t = colorTargets[i]; + auto& rt = blendDesc.RenderTarget[i]; + rt.RenderTargetWriteMask = static_cast(t.writeMask); + if (t.blend.has_value()) { + rt.BlendEnable = TRUE; + rt.SrcBlend = toBlendFactor(t.blend->color.srcFactor); + rt.DestBlend = toBlendFactor(t.blend->color.dstFactor); + rt.BlendOp = toBlendOp(t.blend->color.operation); + rt.SrcBlendAlpha = toBlendFactor(t.blend->alpha.srcFactor); + rt.DestBlendAlpha = toBlendFactor(t.blend->alpha.dstFactor); + rt.BlendOpAlpha = toBlendOp(t.blend->alpha.operation); + } + } + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_BLEND, blendDesc); + + // Sample mask. + UINT sampleMask = (desc.multisample.mask != 0) ? desc.multisample.mask : ~0u; + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_MASK, sampleMask); + + // Rasterizer state. + D3D12_RASTERIZER_DESC rasterDesc{}; + rasterDesc.FillMode = toFillMode(desc.primitive.fillMode); + rasterDesc.CullMode = toCullMode(desc.primitive.cullMode); + rasterDesc.FrontCounterClockwise = (desc.primitive.frontFace == FrontFace::CCW) ? TRUE : FALSE; + rasterDesc.DepthClipEnable = desc.primitive.depthClipEnabled ? TRUE : FALSE; + rasterDesc.MultisampleEnable = (desc.multisample.count > 1) ? TRUE : FALSE; + + if (desc.depthStencil.has_value()) { + rasterDesc.DepthBias = desc.depthStencil->depthBias; + rasterDesc.DepthBiasClamp = desc.depthStencil->depthBiasClamp; + rasterDesc.SlopeScaledDepthBias = desc.depthStencil->depthBiasSlopeScale; + } + + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RASTERIZER, rasterDesc); + + // Depth/stencil state. + if (desc.depthStencil.has_value()) { + const auto& ds = *desc.depthStencil; + + D3D12_DEPTH_STENCIL_DESC dsDesc{}; + dsDesc.DepthEnable = ds.depthTestEnabled ? TRUE : FALSE; + dsDesc.DepthWriteMask = ds.depthWriteEnabled ? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO; + dsDesc.DepthFunc = toComparisonFunc(ds.depthCompare); + dsDesc.StencilEnable = ds.stencilEnabled ? TRUE : FALSE; + dsDesc.StencilReadMask = ds.stencilReadMask; + dsDesc.StencilWriteMask = ds.stencilWriteMask; + + auto& ff = dsDesc.FrontFace; + ff.StencilFailOp = toStencilOp(ds.stencilFront.failOp); + ff.StencilDepthFailOp = toStencilOp(ds.stencilFront.depthFailOp); + ff.StencilPassOp = toStencilOp(ds.stencilFront.passOp); + ff.StencilFunc = toComparisonFunc(ds.stencilFront.compare); + + auto& bf = dsDesc.BackFace; + bf.StencilFailOp = toStencilOp(ds.stencilBack.failOp); + bf.StencilDepthFailOp = toStencilOp(ds.stencilBack.depthFailOp); + bf.StencilPassOp = toStencilOp(ds.stencilBack.passOp); + bf.StencilFunc = toComparisonFunc(ds.stencilBack.compare); + + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL, dsDesc); + + // Depth/stencil format. + DXGI_FORMAT dsFormat = toDxgiFormat(ds.format); + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DEPTH_STENCIL_FORMAT, dsFormat); + } + + // Render target formats. + D3D12_RT_FORMAT_ARRAY rtFormats{}; + rtFormats.NumRenderTargets = static_cast(std::min(colorTargets.size(), usize(8))); + for (usize i = 0; i < colorTargets.size() && i < 8; ++i) + rtFormats.RTFormats[i] = toDxgiFormat(colorTargets[i].format); + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_RENDER_TARGET_FORMATS, rtFormats); + + // Sample desc. + DXGI_SAMPLE_DESC sampleDesc{}; + sampleDesc.Count = std::max(desc.multisample.count, 1u); + sampleDesc.Quality = 0; + writeSubobject(streamBuffer, offset, D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_SAMPLE_DESC, sampleDesc); + + // Create pipeline state via stream API. + D3D12_PIPELINE_STATE_STREAM_DESC streamDesc{}; + streamDesc.SizeInBytes = static_cast(offset); + streamDesc.pPipelineStateSubobjectStream = streamBuffer; + + hr = device2->CreatePipelineState(&streamDesc, IID_PPV_ARGS(&m_pipelineState)); + if (FAILED(hr)) { + logErrorf("DxMeshPipeline: CreatePipelineState failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + return ErrorCode::Ok; + } + + void cleanup() { m_pipelineState.Reset(); } + + [[nodiscard]] ID3D12PipelineState* handle() const { return m_pipelineState.Get(); } + [[nodiscard]] DxPipelineLayoutImpl* pipelineLayout() const { return m_layout; } + +private: + /// Writes a pipeline state stream subobject into the buffer. + /// Each subobject is: { type (aligned to 8), padding to value alignment, value }. + template + static void writeSubobject(u8* buffer, usize& offset, + D3D12_PIPELINE_STATE_SUBOBJECT_TYPE type, const T& value) { + // Align subobject start to pointer size (8 bytes on 64-bit). + offset = (offset + 7) & ~usize(7); + + // Write type. + std::memcpy(&buffer[offset], &type, sizeof(type)); + offset += sizeof(D3D12_PIPELINE_STATE_SUBOBJECT_TYPE); + + // Pad to natural alignment of value within the subobject. + constexpr usize valueAlign = alignof(T); + offset = (offset + valueAlign - 1) & ~(valueAlign - 1); + + // Write value. + std::memcpy(&buffer[offset], &value, sizeof(T)); + offset += sizeof(T); + } + + ComPtr m_pipelineState; + DxPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxModule.cppm new file mode 100644 index 00000000..a84256c3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxModule.cppm @@ -0,0 +1,38 @@ +/// Primary module for rhi.dx12. Re-exports all partitions. +/// DX12 backend - Windows only. + +export module rhi.dx12; + +export import :conversions; +export import :surface; +export import :descriptor_heap; +export import :adapter; +export import :backend; +export import :buffer; +export import :texture; +export import :texture_view; +export import :sampler; +export import :shader_module; +export import :fence; +export import :query_set; +export import :gpu_descriptor_heap; +export import :descriptor_staging; +export import :bind_group_layout; +export import :pipeline_cache; +export import :pipeline_layout; +export import :bind_group; +export import :render_pipeline; +export import :compute_pipeline; +export import :command_buffer; +export import :command_pool; +export import :render_pass_encoder; +export import :render_bundle_encoder; +export import :compute_pass_encoder; +export import :command_encoder; +export import :transfer_batch; +export import :swap_chain; +export import :queue; +export import :accel_struct; +export import :mesh_pipeline; +export import :ray_tracing_pipeline; +export import :device; diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineCache.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineCache.cppm new file mode 100644 index 00000000..d83470cd --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineCache.cppm @@ -0,0 +1,53 @@ +/// DX12 implementation of PipelineCache via ID3D12PipelineLibrary. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:pipeline_cache; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxPipelineCacheImpl : public PipelineCache { +public: + Status init(ID3D12Device* device, const PipelineCacheDesc& d) { + ComPtr device1; + if (FAILED(device->QueryInterface(IID_PPV_ARGS(&device1)))) return ErrorCode::Unknown; + + HRESULT hr; + if (d.initialData.size() > 0) + hr = device1->CreatePipelineLibrary(d.initialData.data(), d.initialData.size(), IID_PPV_ARGS(&m_library)); + else + hr = device1->CreatePipelineLibrary(nullptr, 0, IID_PPV_ARGS(&m_library)); + + return SUCCEEDED(hr) ? ErrorCode::Ok : ErrorCode::Unknown; + } + + u32 getDataSize() override { + if (!m_library) return 0; + return static_cast(m_library->GetSerializedSize()); + } + + Status getData(std::span outData) override { + if (!m_library) return ErrorCode::Unknown; + auto size = m_library->GetSerializedSize(); + if (outData.size() < size) return ErrorCode::Unknown; + return SUCCEEDED(m_library->Serialize(outData.data(), size)) ? ErrorCode::Ok : ErrorCode::Unknown; + } + + void cleanup() { m_library.Reset(); } + + [[nodiscard]] ID3D12PipelineLibrary* handle() const { return m_library.Get(); } + +private: + ComPtr m_library; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineLayout.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineLayout.cppm new file mode 100644 index 00000000..65b3c54b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxPipelineLayout.cppm @@ -0,0 +1,154 @@ +/// DX12 implementation of PipelineLayout. +/// Creates ID3D12RootSignature from bind group layouts + push constant ranges. + +module; + +#include "DxIncludes.h" +#include +#include + +export module rhi.dx12:pipeline_layout; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :bind_group_layout; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +struct DynamicRootEntry { + u32 groupIndex; + u32 dynamicIndex; + i32 rootParamIndex; + D3D12_ROOT_PARAMETER_TYPE paramType; +}; + +class DxPipelineLayoutImpl : public PipelineLayout { +public: + Status init(ID3D12Device* device, const PipelineLayoutDesc& d) { + m_numBindGroups = static_cast(d.bindGroupLayouts.size()); + + std::vector rootParams; + // Storage for descriptor ranges (must outlive SerializeRootSignature). + std::vector> rangeStorage; + + m_rootParamMap.resize(d.bindGroupLayouts.size() * 2, -1); + + for (usize gi = 0; gi < d.bindGroupLayouts.size(); ++gi) { + auto* layout = static_cast(d.bindGroupLayouts[gi]); + if (!layout) return ErrorCode::Unknown; + + std::vector csvRanges, sampRanges; + u32 dynIdx = 0; + + auto ranges = layout->ranges(); + for (usize ri = 0; ri < ranges.size(); ++ri) { + const auto& r = ranges[ri]; + + if (r.hasDynamicOffset) { + D3D12_ROOT_PARAMETER_TYPE pt; + switch (r.type) { + case BindingType::UniformBuffer: pt = D3D12_ROOT_PARAMETER_TYPE_CBV; break; + case BindingType::StorageBufferReadOnly: pt = D3D12_ROOT_PARAMETER_TYPE_SRV; break; + case BindingType::StorageBufferReadWrite: pt = D3D12_ROOT_PARAMETER_TYPE_UAV; break; + default: continue; + } + D3D12_ROOT_PARAMETER p{}; p.ParameterType = pt; + p.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + p.Descriptor.ShaderRegister = r.binding; + p.Descriptor.RegisterSpace = static_cast(gi); + + m_dynamicRootEntries.push_back({ static_cast(gi), dynIdx, + static_cast(rootParams.size()), pt }); + rootParams.push_back(p); + ++dynIdx; + continue; + } + + D3D12_DESCRIPTOR_RANGE dr{}; + dr.RangeType = toDescriptorRangeType(r.type); + dr.NumDescriptors = r.count; + dr.BaseShaderRegister = r.binding; + dr.RegisterSpace = static_cast(gi); + dr.OffsetInDescriptorsFromTableStart = r.heapOffset; + + if (r.isSampler) sampRanges.push_back(dr); + else csvRanges.push_back(dr); + } + + if (!csvRanges.empty()) { + rangeStorage.push_back(static_cast&&>(csvRanges)); + auto& stored = rangeStorage.back(); + D3D12_ROOT_PARAMETER p{}; p.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + p.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + p.DescriptorTable.NumDescriptorRanges = static_cast(stored.size()); + p.DescriptorTable.pDescriptorRanges = stored.data(); + m_rootParamMap[gi * 2] = static_cast(rootParams.size()); + rootParams.push_back(p); + } + if (!sampRanges.empty()) { + rangeStorage.push_back(static_cast&&>(sampRanges)); + auto& stored = rangeStorage.back(); + D3D12_ROOT_PARAMETER p{}; p.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + p.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + p.DescriptorTable.NumDescriptorRanges = static_cast(stored.size()); + p.DescriptorTable.pDescriptorRanges = stored.data(); + m_rootParamMap[gi * 2 + 1] = static_cast(rootParams.size()); + rootParams.push_back(p); + } + } + + // Push constants → root 32-bit constants. + for (usize i = 0; i < d.pushConstantRanges.size(); ++i) { + const auto& pc = d.pushConstantRanges[i]; + if (m_pushConstantRootIndex < 0) + m_pushConstantRootIndex = static_cast(rootParams.size()); + + D3D12_ROOT_PARAMETER p{}; p.ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + p.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + p.Constants.ShaderRegister = pc.offset / 4; + p.Constants.RegisterSpace = m_numBindGroups; + p.Constants.Num32BitValues = pc.size / 4; + rootParams.push_back(p); + } + + // Serialize and create root signature. + D3D12_ROOT_SIGNATURE_DESC rsDesc{}; + rsDesc.NumParameters = static_cast(rootParams.size()); + rsDesc.pParameters = rootParams.data(); + rsDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + + ComPtr sigBlob, errBlob; + HRESULT hr = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, + &sigBlob, &errBlob); + if (FAILED(hr)) { + if (errBlob) logErrorf("DxPipelineLayout: %s", static_cast(errBlob->GetBufferPointer())); + return ErrorCode::Unknown; + } + + hr = device->CreateRootSignature(0, sigBlob->GetBufferPointer(), sigBlob->GetBufferSize(), + IID_PPV_ARGS(&m_rootSig)); + return SUCCEEDED(hr) ? ErrorCode::Ok : ErrorCode::Unknown; + } + + void cleanup() { m_rootSig.Reset(); } + + [[nodiscard]] ID3D12RootSignature* handle() const { return m_rootSig.Get(); } + [[nodiscard]] i32 getCbvSrvUavRootIndex(u32 gi) const { return (gi*2 < m_rootParamMap.size()) ? m_rootParamMap[gi*2] : -1; } + [[nodiscard]] i32 getSamplerRootIndex(u32 gi) const { return (gi*2+1 < m_rootParamMap.size()) ? m_rootParamMap[gi*2+1] : -1; } + [[nodiscard]] i32 pushConstantRootIndex() const { return m_pushConstantRootIndex; } + [[nodiscard]] u32 numBindGroups() const { return m_numBindGroups; } + [[nodiscard]] std::span dynamicRootEntries() const { return { m_dynamicRootEntries.data(), m_dynamicRootEntries.size() }; } + +private: + ComPtr m_rootSig; + std::vector m_rootParamMap; + std::vector m_dynamicRootEntries; + i32 m_pushConstantRootIndex = -1; + u32 m_numBindGroups = 0; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQuerySet.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQuerySet.cppm new file mode 100644 index 00000000..3f0e9bfd --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQuerySet.cppm @@ -0,0 +1,60 @@ +/// DX12 implementation of QuerySet. Wraps ID3D12QueryHeap. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:query_set; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxQuerySetImpl : public QuerySet { +public: + Status init(ID3D12Device* device, const QuerySetDesc& d) { + type = d.type; + count = d.count; + + D3D12_QUERY_HEAP_DESC hd{}; + hd.Type = toQueryHeapType(d.type); + hd.Count = d.count; + HRESULT hr = device->CreateQueryHeap(&hd, IID_PPV_ARGS(&m_heap)); + if (FAILED(hr)) { + logErrorf("DxQuerySet: CreateQueryHeap failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + return ErrorCode::Ok; + } + + void cleanup() { m_heap.Reset(); } + + [[nodiscard]] ID3D12QueryHeap* handle() const { return m_heap.Get(); } + + static D3D12_QUERY_HEAP_TYPE toQueryHeapType(QueryType t) { + switch (t) { + case QueryType::Timestamp: return D3D12_QUERY_HEAP_TYPE_TIMESTAMP; + case QueryType::Occlusion: return D3D12_QUERY_HEAP_TYPE_OCCLUSION; + case QueryType::PipelineStatistics: return D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS; + } + return D3D12_QUERY_HEAP_TYPE_TIMESTAMP; + } + + static D3D12_QUERY_TYPE toDxQueryType(QueryType t) { + switch (t) { + case QueryType::Timestamp: return D3D12_QUERY_TYPE_TIMESTAMP; + case QueryType::Occlusion: return D3D12_QUERY_TYPE_OCCLUSION; + case QueryType::PipelineStatistics: return D3D12_QUERY_TYPE_PIPELINE_STATISTICS; + } + return D3D12_QUERY_TYPE_TIMESTAMP; + } + +private: + ComPtr m_heap; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQueue.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQueue.cppm new file mode 100644 index 00000000..c12766d0 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxQueue.cppm @@ -0,0 +1,127 @@ +/// DX12 implementation of Queue. + +module; + +#include "DxIncludes.h" +#include +#include + +export module rhi.dx12:queue; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :command_buffer; +import :fence; +import :transfer_batch; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward + +class DxQueueImpl : public Queue { +public: + Status init(ID3D12Device* device, QueueType type, DxDeviceImpl* owner) { + queueType = type; + m_device = owner; + m_d3dDevice = device; + + D3D12_COMMAND_QUEUE_DESC qd{}; + qd.Type = toCommandListType(type); + HRESULT hr = device->CreateCommandQueue(&qd, IID_PPV_ARGS(&m_queue)); + if (FAILED(hr)) return ErrorCode::Unknown; + + hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_internalFence)); + if (FAILED(hr)) return ErrorCode::Unknown; + m_fenceEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + + // Query timestamp frequency. + UINT64 freq = 0; + m_queue->GetTimestampFrequency(&freq); + m_tsPeriod = (freq > 0) ? (1e9f / static_cast(freq)) : 1.0f; + + return ErrorCode::Ok; + } + + // ---- Queue interface ---- + + void submit(std::span cmdBufs) override { + if (cmdBufs.size() == 0) return; + std::vector lists(cmdBufs.size()); + for (usize i = 0; i < cmdBufs.size(); ++i) { + if (auto* dxCb = static_cast(cmdBufs[i])) + lists[i] = dxCb->handle(); + } + m_queue->ExecuteCommandLists(static_cast(lists.size()), lists.data()); + } + + void submit(std::span cmdBufs, Fence* signalFence, u64 signalValue) override { + submit(cmdBufs); + if (auto* f = static_cast(signalFence)) + m_queue->Signal(f->handle(), signalValue); + } + + void submit(std::span cmdBufs, + std::span waitFences, std::span waitValues, + Fence* signalFence, u64 signalValue) override { + for (usize i = 0; i < waitFences.size(); ++i) + if (auto* f = static_cast(waitFences[i])) + m_queue->Wait(f->handle(), waitValues[i]); + submit(cmdBufs); + if (auto* f = static_cast(signalFence)) + m_queue->Signal(f->handle(), signalValue); + } + + void waitIdle() override { + ++m_fenceValue; + m_queue->Signal(m_internalFence.Get(), m_fenceValue); + if (m_internalFence->GetCompletedValue() < m_fenceValue) { + m_internalFence->SetEventOnCompletion(m_fenceValue, m_fenceEvent); + WaitForSingleObject(m_fenceEvent, INFINITE); + } + } + + Status createTransferBatch(TransferBatch*& out) override { + auto* batch = new DxTransferBatchImpl(); + if (batch->init(m_d3dDevice, m_queue.Get(), queueType) != ErrorCode::Ok) { + delete batch; + return ErrorCode::Unknown; + } + out = batch; + return ErrorCode::Ok; + } + + void destroyTransferBatch(TransferBatch*& batch) override { + if (auto* dx = static_cast(batch)) { + dx->destroy(); + delete dx; + } + batch = nullptr; + } + + f32 timestampPeriod() const override { return m_tsPeriod; } + + void cleanup() { + if (m_fenceEvent) { CloseHandle(m_fenceEvent); m_fenceEvent = nullptr; } + m_internalFence.Reset(); + m_queue.Reset(); + } + + // ---- Internal ---- + [[nodiscard]] ID3D12CommandQueue* handle() const { return m_queue.Get(); } + [[nodiscard]] DxDeviceImpl* owner() const { return m_device; } + +private: + ComPtr m_queue; + ComPtr m_internalFence; + HANDLE m_fenceEvent = nullptr; + u64 m_fenceValue = 0; + f32 m_tsPeriod = 1.0f; + DxDeviceImpl* m_device = nullptr; + ID3D12Device* m_d3dDevice = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRayTracingPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRayTracingPipeline.cppm new file mode 100644 index 00000000..92140aa0 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRayTracingPipeline.cppm @@ -0,0 +1,236 @@ +/// DX12 implementation of RayTracingPipeline. +/// Wraps a D3D12 state object created via ID3D12Device5::CreateStateObject. + +module; + +#include "DxIncludes.h" +#include +#include + +#include +#include + +export module rhi.dx12:ray_tracing_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :shader_module; +import :pipeline_layout; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxRayTracingPipelineImpl : public RayTracingPipeline { +public: + Status init(ID3D12Device* device, const RayTracingPipelineDesc& desc) { + m_layout = static_cast(desc.layout); + if (!m_layout) { + logErrorf("DxRayTracingPipeline: pipeline layout is null"); + return ErrorCode::Unknown; + } + layout = desc.layout; + + // Query ID3D12Device5 for CreateStateObject. + ComPtr device5; + HRESULT hr = device->QueryInterface(IID_PPV_ARGS(&device5)); + if (FAILED(hr) || !device5) { + logErrorf("DxRayTracingPipeline: QueryInterface for ID3D12Device5 failed (0x%08X)", + static_cast(hr)); + return ErrorCode::Unknown; + } + + // Collect entry point names and per-stage export descriptors. + // Export names are UTF-8 std::u8string_view; DX12 wants LPCWSTR, so widen them + // (ASCII-safe for shader export names). + std::vector entryWide; + std::vector exports; + + for (usize i = 0; i < desc.stages.size(); ++i) { + const auto& stage = desc.stages[i]; + auto* dxMod = static_cast(stage.module); + if (!dxMod) continue; + + entryWide.emplace_back( + reinterpret_cast(stage.entryPoint.data()), + stage.entryPoint.size()); + } + + exports.resize(entryWide.size()); + for (usize i = 0; i < entryWide.size(); ++i) { + exports[i] = {}; + exports[i].Name = entryWide[i].c_str(); + exports[i].Flags = D3D12_EXPORT_FLAG_NONE; + } + + // Build one DXIL library subobject per stage. + std::vector libraries; + for (usize i = 0; i < desc.stages.size(); ++i) { + auto* dxMod = static_cast(desc.stages[i].module); + if (!dxMod) continue; + + auto bc = dxMod->bytecode(); + D3D12_DXIL_LIBRARY_DESC lib{}; + lib.DXILLibrary.pShaderBytecode = bc.data(); + lib.DXILLibrary.BytecodeLength = bc.size(); + lib.NumExports = 1; + lib.pExports = &exports[i]; + libraries.push_back(lib); + } + + // Count hit groups. + u32 numHitGroups = 0; + for (usize i = 0; i < desc.groups.size(); ++i) { + if (desc.groups[i].type == RayTracingShaderGroup::Type::TrianglesHitGroup || + desc.groups[i].type == RayTracingShaderGroup::Type::ProceduralHitGroup) + ++numHitGroups; + } + + // Total subobjects: libraries + hit groups + shader config + pipeline config + global root sig. + usize subobjectCount = libraries.size() + numHitGroups + 3; + std::vector subobjects(subobjectCount); + usize soIdx = 0; + + // --- DXIL library subobjects --- + for (usize i = 0; i < libraries.size(); ++i) { + subobjects[soIdx].Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY; + subobjects[soIdx].pDesc = &libraries[i]; + ++soIdx; + } + + // --- Hit groups --- + std::vector hitGroups; + std::vector hitGroupNamesWide; + + for (usize i = 0; i < desc.groups.size(); ++i) { + const auto& group = desc.groups[i]; + if (group.type != RayTracingShaderGroup::Type::TrianglesHitGroup && + group.type != RayTracingShaderGroup::Type::ProceduralHitGroup) + continue; + + // Format "HitGroupN" where N is the group index. + std::wstring hgName = L"HitGroup" + std::to_wstring(i); + hitGroupNamesWide.push_back(hgName); + + D3D12_HIT_GROUP_DESC hg{}; + hg.HitGroupExport = hitGroupNamesWide.back().c_str(); + hg.Type = (group.type == RayTracingShaderGroup::Type::TrianglesHitGroup) + ? D3D12_HIT_GROUP_TYPE_TRIANGLES + : D3D12_HIT_GROUP_TYPE_PROCEDURAL_PRIMITIVE; + + if (group.closestHitShaderIndex != ~0u && group.closestHitShaderIndex < entryWide.size()) + hg.ClosestHitShaderImport = entryWide[group.closestHitShaderIndex].c_str(); + + if (group.anyHitShaderIndex != ~0u && group.anyHitShaderIndex < entryWide.size()) + hg.AnyHitShaderImport = entryWide[group.anyHitShaderIndex].c_str(); + + if (group.intersectionShaderIndex != ~0u && group.intersectionShaderIndex < entryWide.size()) + hg.IntersectionShaderImport = entryWide[group.intersectionShaderIndex].c_str(); + + hitGroups.push_back(hg); + } + + for (usize i = 0; i < hitGroups.size(); ++i) { + subobjects[soIdx].Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP; + subobjects[soIdx].pDesc = &hitGroups[i]; + ++soIdx; + } + + // --- Build group-to-export-name mapping --- + // For each group in desc.Groups order, store the DX12 export name + // used to retrieve its shader identifier. + for (usize i = 0; i < desc.groups.size(); ++i) { + const auto& group = desc.groups[i]; + if (group.type == RayTracingShaderGroup::Type::General) { + // General groups (raygen/miss/callable) use the entry point name. + if (group.generalShaderIndex != ~0u && group.generalShaderIndex < desc.stages.size()) { + auto ep = desc.stages[group.generalShaderIndex].entryPoint; + m_groupExportNames.emplace_back( + reinterpret_cast(ep.data()), ep.size()); + } else { + m_groupExportNames.emplace_back(); + } + } else { + // Hit groups use "HitGroupN" where N is the group index. + m_groupExportNames.push_back(L"HitGroup" + std::to_wstring(i)); + } + } + + // --- Shader config --- + D3D12_RAYTRACING_SHADER_CONFIG shaderConfig{}; + shaderConfig.MaxPayloadSizeInBytes = (desc.maxPayloadSize > 0) ? desc.maxPayloadSize : 32; + shaderConfig.MaxAttributeSizeInBytes = (desc.maxAttributeSize > 0) ? desc.maxAttributeSize : 8; + subobjects[soIdx].Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG; + subobjects[soIdx].pDesc = &shaderConfig; + ++soIdx; + + // --- Pipeline config --- + D3D12_RAYTRACING_PIPELINE_CONFIG pipelineConfig{}; + pipelineConfig.MaxTraceRecursionDepth = desc.maxRecursionDepth; + subobjects[soIdx].Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG; + subobjects[soIdx].pDesc = &pipelineConfig; + ++soIdx; + + // --- Global root signature --- + D3D12_GLOBAL_ROOT_SIGNATURE globalRootSig{}; + globalRootSig.pGlobalRootSignature = m_layout->handle(); + subobjects[soIdx].Type = D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE; + subobjects[soIdx].pDesc = &globalRootSig; + ++soIdx; + + // --- Create state object --- + D3D12_STATE_OBJECT_DESC stateObjDesc{}; + stateObjDesc.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE; + stateObjDesc.NumSubobjects = static_cast(soIdx); + stateObjDesc.pSubobjects = subobjects.data(); + + hr = device5->CreateStateObject(&stateObjDesc, IID_PPV_ARGS(&m_stateObject)); + if (FAILED(hr) || !m_stateObject) { + logErrorf("DxRayTracingPipeline: CreateStateObject failed (0x%08X)", + static_cast(hr)); + return ErrorCode::Unknown; + } + + // Query properties for shader identifier lookup. + hr = m_stateObject->QueryInterface(IID_PPV_ARGS(&m_properties)); + if (FAILED(hr)) m_properties.Reset(); + + return ErrorCode::Ok; + } + + /// Gets the shader identifier for an export name. + /// Returns a pointer to D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES (32) bytes, + /// or nullptr on failure. + [[nodiscard]] void* getShaderIdentifier(std::u8string_view exportName) const { + if (!m_properties) return nullptr; + + // Convert narrow string to wide (ASCII-safe for shader entry points). + std::wstring wide; + wide.reserve(exportName.size()); + for (usize i = 0; i < exportName.size(); ++i) + wide.push_back(static_cast(exportName[i])); + + return m_properties->GetShaderIdentifier(wide.c_str()); + } + + void cleanup() { + m_properties.Reset(); + m_stateObject.Reset(); + } + + [[nodiscard]] ID3D12StateObject* handle() const { return m_stateObject.Get(); } + [[nodiscard]] ID3D12StateObjectProperties* properties() const { return m_properties.Get(); } + [[nodiscard]] DxPipelineLayoutImpl* pipelineLayout() const { return m_layout; } + [[nodiscard]] std::span groupExportNames() const { + return { m_groupExportNames.data(), m_groupExportNames.size() }; + } + +private: + ComPtr m_stateObject; + ComPtr m_properties; + DxPipelineLayoutImpl* m_layout = nullptr; + std::vector m_groupExportNames; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderBundleEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderBundleEncoder.cppm new file mode 100644 index 00000000..d55bb089 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderBundleEncoder.cppm @@ -0,0 +1,69 @@ +/// DX12 implementation of RenderBundleEncoder + RenderBundle. +/// +/// A render bundle is an ID3D12GraphicsCommandList of type D3D12_COMMAND_LIST_TYPE_BUNDLE, +/// recorded once and replayed into a direct command list via ExecuteBundle +/// (RenderPassEncoder::ExecuteBundles). DX12 bundles inherit the parent's descriptor heaps, +/// viewport, scissor, and render targets - but NOT pipeline state / topology, which the bundle +/// sets itself (handled by SetPipeline). Draw recording is delegated to a DxRenderPassEncoderImpl +/// whose context points at the bundle list, reusing the full root-signature / descriptor-table +/// binding logic. +/// +/// BEST-EFFORT (authored without a Windows toolchain to compile against - shake out on Windows): +/// the structure + DX12 bundle semantics are in place; details (heap inheritance, signature +/// caching, lifetime vs frames-in-flight) may need adjustment. + +module; + +#include "DxIncludes.h" +#include +#include + +export module rhi.dx12:render_bundle_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :render_pass_encoder; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +// DxRenderBundleImpl lives in :render_pass_encoder (so ExecuteBundles can use it without a +// module cycle); this partition imports it from there. + +// Records draws into a bundle command list by delegating to a render-pass encoder pointed at it. +class DxRenderBundleEncoderImpl : public RenderBundleEncoder { +public: + DxRenderBundleEncoderImpl(const DxRenderPassContext& ctx, + ComPtr list, ComPtr alloc) + : m_rec(ctx), m_list(std::move(list)), m_alloc(std::move(alloc)) { + m_rec.begin(RenderPassDesc{}); // reset pipeline-tracking state (no pass attachments needed) + } + ~DxRenderBundleEncoderImpl() override { delete m_bundle; } + + void setPipeline(RenderPipeline* p) override { m_rec.setPipeline(p); } + void setBindGroup(u32 i, BindGroup* g, std::span d) override { m_rec.setBindGroup(i, g, d); } + void setPushConstants(ShaderStage s, u32 o, u32 sz, const void* d) override { m_rec.setPushConstants(s, o, sz, d); } + void setVertexBuffer(u32 slot, Buffer* b, u64 o) override { m_rec.setVertexBuffer(slot, b, o); } + void setIndexBuffer(Buffer* b, IndexFormat f, u64 o) override { m_rec.setIndexBuffer(b, f, o); } + void draw(u32 v, u32 inst, u32 fv, u32 fi) override { m_rec.draw(v, inst, fv, fi); } + void drawIndexed(u32 ic, u32 inst, u32 fi, i32 bv, u32 finst) override { m_rec.drawIndexed(ic, inst, fi, bv, finst); } + void drawIndirect(Buffer* b, u64 o, u32 dc, u32 st) override { m_rec.drawIndirect(b, o, dc, st); } + void drawIndexedIndirect(Buffer* b, u64 o, u32 dc, u32 st) override { m_rec.drawIndexedIndirect(b, o, dc, st); } + + RenderBundle* finish() override { + if (m_bundle) return m_bundle; + m_list->Close(); + m_bundle = new DxRenderBundleImpl(m_list, m_alloc, m_rec.currentRootSig(), m_rec.currentPso()); + return m_bundle; + } + +private: + DxRenderPassEncoderImpl m_rec; + ComPtr m_list; + ComPtr m_alloc; + DxRenderBundleImpl* m_bundle = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPassEncoder.cppm new file mode 100644 index 00000000..22b364b7 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPassEncoder.cppm @@ -0,0 +1,444 @@ +/// DX12 implementation of RenderPassEncoder + MeshShaderPassExt. +/// Records render pass commands into the parent command encoder's command list. + +module; + +#include "DxIncludes.h" +#include +#include + +export module rhi.dx12:render_pass_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :bind_group; +import :bind_group_layout; +import :render_pipeline; +import :pipeline_layout; +import :query_set; +import :texture; +import :texture_view; +import :descriptor_staging; +import :gpu_descriptor_heap; +import :mesh_pipeline; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +// A recorded bundle command list (+ its allocator, which must outlive GPU execution). Lives +// here (not in :render_bundle_encoder) so ExecuteBundles can use it without a module cycle. +class DxRenderBundleImpl : public RenderBundle { +public: + DxRenderBundleImpl(ComPtr list, ComPtr alloc, + ID3D12RootSignature* rootSig = nullptr, ID3D12PipelineState* pso = nullptr) + : m_list(std::move(list)), m_alloc(std::move(alloc)), m_rootSig(rootSig), m_pso(pso) {} + [[nodiscard]] ID3D12GraphicsCommandList* handle() const { return m_list.Get(); } + [[nodiscard]] ID3D12RootSignature* rootSig() const { return m_rootSig; } + [[nodiscard]] ID3D12PipelineState* pso() const { return m_pso; } +private: + ComPtr m_list; + ComPtr m_alloc; + ID3D12RootSignature* m_rootSig = nullptr; + ID3D12PipelineState* m_pso = nullptr; +}; + +/// Pointers needed by the render pass encoder, provided by the command encoder. +/// Avoids coupling to DxCommandEncoderImpl directly. +struct DxRenderPassContext { + ID3D12GraphicsCommandList* cmdList = nullptr; + DxDescriptorStaging* srvStaging = nullptr; + DxDescriptorStaging* samplerStaging = nullptr; + DxGpuDescriptorHeap* gpuSrvHeap = nullptr; + DxGpuDescriptorHeap* gpuSamplerHeap = nullptr; + // Indirect command signatures (cached on device). + ID3D12CommandSignature* drawSig = nullptr; + ID3D12CommandSignature* drawIndexedSig = nullptr; + ID3D12CommandSignature* dispatchMeshSig = nullptr; +}; + +class DxRenderPassEncoderImpl : public RenderPassEncoder, public MeshShaderPassExt { +public: + MeshShaderPassExt* asMeshShaderExt() noexcept override { return this; } + explicit DxRenderPassEncoderImpl(const DxRenderPassContext& ctx) + : m_ctx(ctx) {} + + void begin(const RenderPassDesc& desc) { + m_desc = desc; + m_currentPipeline = nullptr; + m_currentMeshPipeline = nullptr; + } + + // ---- RenderPassEncoder: Pipeline & Binding ---- + + void setPipeline(RenderPipeline* pipeline) override { + auto* dxPipeline = static_cast(pipeline); + if (!dxPipeline) return; + m_currentPipeline = dxPipeline; + m_currentMeshPipeline = nullptr; + + auto* cmdList = m_ctx.cmdList; + cmdList->SetPipelineState(dxPipeline->handle()); + cmdList->SetGraphicsRootSignature(dxPipeline->pipelineLayout()->handle()); + cmdList->IASetPrimitiveTopology(dxPipeline->topology()); + + // Re-apply cached vertex buffers with correct strides from the new pipeline. + // DX12 vertex buffer views include stride, which comes from the pipeline. + // If setVertexBuffer was called before setPipeline, the stride was 0. + for (u32 slot = 0; slot < m_cachedVbCount; ++slot) { + if (m_cachedVbs[slot].BufferLocation != 0) { + m_cachedVbs[slot].StrideInBytes = dxPipeline->getVertexStride(slot); + cmdList->IASetVertexBuffers(slot, 1, &m_cachedVbs[slot]); + } + } + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets) override { + auto* dxGroup = static_cast(group); + if (!dxGroup) return; + + auto* layout = getCurrentLayout(); + if (!layout) return; + + auto* cmdList = m_ctx.cmdList; + auto* dxLayout = static_cast(dxGroup->layout()); + + // Copy-on-bind: copy bind group's descriptors into encoder's staging region, + // then bind from the staging offset. This makes bind group destruction safe + // during command recording -- the GPU only references the staging copy. + + // Bind CBV/SRV/UAV table (staged). + if (dxGroup->cbvSrvUavOffset() >= 0 && dxLayout && dxLayout->cbvSrvUavCount() > 0) { + i32 rootIdx = layout->getCbvSrvUavRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_ctx.srvStaging->copyFrom( + static_cast(dxGroup->cbvSrvUavOffset()), dxLayout->cbvSrvUavCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_ctx.gpuSrvHeap->getGpuHandle(static_cast(stagedOffset)); + cmdList->SetGraphicsRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + // Bind sampler table (staged). + if (dxGroup->samplerOffset() >= 0 && dxLayout && dxLayout->samplerCount() > 0) { + i32 rootIdx = layout->getSamplerRootIndex(index); + if (rootIdx >= 0) { + i32 stagedOffset = m_ctx.samplerStaging->copyFrom( + static_cast(dxGroup->samplerOffset()), dxLayout->samplerCount()); + if (stagedOffset >= 0) { + auto gpuHandle = m_ctx.gpuSamplerHeap->getGpuHandle(static_cast(stagedOffset)); + cmdList->SetGraphicsRootDescriptorTable(static_cast(rootIdx), gpuHandle); + } + } + } + + // Bind dynamic offset root descriptors (not staged -- uses GPU virtual addresses). + auto dynAddrs = dxGroup->dynamicGpuAddresses(); + usize dynOffsetIdx = 0; + for (usize i = 0; i < layout->dynamicRootEntries().size(); ++i) { + const auto& entry = layout->dynamicRootEntries()[i]; + if (entry.groupIndex != index) continue; + if (entry.dynamicIndex >= static_cast(dynAddrs.size())) continue; + + u64 gpuAddr = dynAddrs[entry.dynamicIndex]; + if (dynOffsetIdx < dynamicOffsets.size()) + gpuAddr += static_cast(dynamicOffsets[dynOffsetIdx]); + ++dynOffsetIdx; + + switch (entry.paramType) { + case D3D12_ROOT_PARAMETER_TYPE_CBV: + cmdList->SetGraphicsRootConstantBufferView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_SRV: + cmdList->SetGraphicsRootShaderResourceView(static_cast(entry.rootParamIndex), gpuAddr); + break; + case D3D12_ROOT_PARAMETER_TYPE_UAV: + cmdList->SetGraphicsRootUnorderedAccessView(static_cast(entry.rootParamIndex), gpuAddr); + break; + default: break; + } + } + } + + void setPushConstants(ShaderStage /*stages*/, u32 offset, u32 size, const void* data) override { + auto* layout = getCurrentLayout(); + if (!layout || layout->pushConstantRootIndex() < 0) return; + + m_ctx.cmdList->SetGraphicsRoot32BitConstants( + static_cast(layout->pushConstantRootIndex()), + size / 4, data, offset / 4); + } + + // ---- Vertex & Index Buffers ---- + + void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf || slot >= 8) return; + + u32 stride = m_currentPipeline ? m_currentPipeline->getVertexStride(slot) : 0; + + D3D12_VERTEX_BUFFER_VIEW view{}; + view.BufferLocation = dxBuf->gpuAddress() + offset; + view.SizeInBytes = static_cast(dxBuf->desc.size - offset); + view.StrideInBytes = stride; + + // Cache for re-application when pipeline changes. + m_cachedVbs[slot] = view; + if (slot >= m_cachedVbCount) m_cachedVbCount = slot + 1; + + m_ctx.cmdList->IASetVertexBuffers(slot, 1, &view); + } + + void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf) return; + + D3D12_INDEX_BUFFER_VIEW view{}; + view.BufferLocation = dxBuf->gpuAddress() + offset; + view.SizeInBytes = static_cast(dxBuf->desc.size - offset); + view.Format = toDxgiIndexFormat(format); + + m_ctx.cmdList->IASetIndexBuffer(&view); + } + + // ---- Dynamic State ---- + + void setViewport(f32 x, f32 y, f32 w, f32 h, f32 minDepth, f32 maxDepth) override { + D3D12_VIEWPORT viewport{}; + viewport.TopLeftX = x; + viewport.TopLeftY = y; + viewport.Width = w; + viewport.Height = h; + viewport.MinDepth = minDepth; + viewport.MaxDepth = maxDepth; + + m_ctx.cmdList->RSSetViewports(1, &viewport); + } + + void setScissor(i32 x, i32 y, u32 w, u32 h) override { + D3D12_RECT rect{}; + rect.left = x; + rect.top = y; + rect.right = x + static_cast(w); + rect.bottom = y + static_cast(h); + + m_ctx.cmdList->RSSetScissorRects(1, &rect); + } + + void setBlendConstant(f32 r, f32 g, f32 b, f32 a) override { + f32 color[4] = { r, g, b, a }; + m_ctx.cmdList->OMSetBlendFactor(color); + } + + void setStencilReference(u32 reference) override { + m_ctx.cmdList->OMSetStencilRef(reference); + } + + // ---- Draw Commands ---- + + void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) override { + m_ctx.cmdList->DrawInstanced(vertexCount, instanceCount, firstVertex, firstInstance); + } + + void drawIndexed(u32 indexCount, u32 instanceCount, u32 firstIndex, i32 baseVertex, u32 firstInstance) override { + m_ctx.cmdList->DrawIndexedInstanced(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + } + + void drawIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf) return; + + auto* sig = m_ctx.drawSig; + if (!sig) return; + + u32 actualStride = (stride > 0) ? stride : 16; // sizeof(D3D12_DRAW_ARGUMENTS) + for (u32 i = 0; i < drawCount; ++i) { + m_ctx.cmdList->ExecuteIndirect(sig, 1, dxBuf->handle(), + offset + static_cast(i) * actualStride, nullptr, 0); + } + } + + void drawIndexedIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf) return; + + auto* sig = m_ctx.drawIndexedSig; + if (!sig) return; + + u32 actualStride = (stride > 0) ? stride : 20; // sizeof(D3D12_DRAW_INDEXED_ARGUMENTS) + for (u32 i = 0; i < drawCount; ++i) { + m_ctx.cmdList->ExecuteIndirect(sig, 1, dxBuf->handle(), + offset + static_cast(i) * actualStride, nullptr, 0); + } + } + + // ---- Queries ---- + + void writeTimestamp(QuerySet* querySet, u32 index) override { + auto* qs = static_cast(querySet); + if (qs) m_ctx.cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_TIMESTAMP, index); + } + + void beginOcclusionQuery(QuerySet* querySet, u32 index) override { + auto* qs = static_cast(querySet); + if (qs) m_ctx.cmdList->BeginQuery(qs->handle(), D3D12_QUERY_TYPE_OCCLUSION, index); + } + + void endOcclusionQuery(QuerySet* querySet, u32 index) override { + auto* qs = static_cast(querySet); + if (qs) m_ctx.cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_OCCLUSION, index); + } + + // ---- MeshShaderPassExt ---- + + void setMeshPipeline(MeshPipeline* pipeline) override { + auto* dxPipeline = static_cast(pipeline); + if (!dxPipeline) return; + m_currentMeshPipeline = dxPipeline; + m_currentPipeline = nullptr; // clear regular pipeline + + auto* cmdList = m_ctx.cmdList; + cmdList->SetPipelineState(dxPipeline->handle()); + cmdList->SetGraphicsRootSignature(dxPipeline->pipelineLayout()->handle()); + } + + void drawMeshTasks(u32 groupCountX, u32 groupCountY, u32 groupCountZ) override { + // Need ID3D12GraphicsCommandList6 for DispatchMesh. + ID3D12GraphicsCommandList6* cmdList6 = nullptr; + HRESULT hr = m_ctx.cmdList->QueryInterface(IID_PPV_ARGS(&cmdList6)); + if (SUCCEEDED(hr) && cmdList6) { + cmdList6->DispatchMesh(groupCountX, groupCountY, groupCountZ); + cmdList6->Release(); + } + } + + void drawMeshTasksIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + auto* dxBuf = static_cast(buffer); + if (!dxBuf) return; + + auto* sig = m_ctx.dispatchMeshSig; + if (!sig) return; + + u32 actualStride = (stride > 0) ? stride : 12; // sizeof(D3D12_DISPATCH_MESH_ARGUMENTS): 3 x u32 + for (u32 i = 0; i < drawCount; ++i) { + m_ctx.cmdList->ExecuteIndirect(sig, 1, dxBuf->handle(), + offset + static_cast(i) * actualStride, nullptr, 0); + } + } + + void drawMeshTasksIndirectCount(Buffer* buffer, u64 offset, + Buffer* countBuffer, u64 countOffset, + u32 maxDrawCount, u32 /*stride*/) override { + auto* dxBuf = static_cast(buffer); + auto* dxCountBuf = static_cast(countBuffer); + if (!dxBuf || !dxCountBuf) return; + + auto* sig = m_ctx.dispatchMeshSig; + if (!sig) return; + + m_ctx.cmdList->ExecuteIndirect(sig, maxDrawCount, dxBuf->handle(), + offset, dxCountBuf->handle(), countOffset); + } + + // ---- End ---- + + void executeBundles(std::span bundles) override { + for (usize i = 0; i < bundles.size(); ++i) { + if (auto* b = static_cast(bundles[i])) { + // DX12 requires the parent command list to have the same root signature + // and PSO set before ExecuteBundle. + if (b->rootSig()) m_ctx.cmdList->SetGraphicsRootSignature(b->rootSig()); + if (b->pso()) m_ctx.cmdList->SetPipelineState(b->pso()); + m_ctx.cmdList->ExecuteBundle(b->handle()); + } + } + } + + void end() override { + // Timestamp at pass end. + if (m_desc.timestampQuerySet) { + auto* qs = static_cast(m_desc.timestampQuerySet); + if (qs) + m_ctx.cmdList->EndQuery(qs->handle(), D3D12_QUERY_TYPE_TIMESTAMP, m_desc.endTimestampIndex); + } + + // MSAA resolve: resolve multisampled color attachments to their resolve targets. + const auto& colorAtts = m_desc.colorAttachments; + for (usize i = 0; i < colorAtts.size(); ++i) { + const auto& ca = colorAtts[i]; + if (!ca.resolveTarget) continue; + + auto* srcView = static_cast(ca.view); + auto* dstView = static_cast(ca.resolveTarget); + if (!srcView || !dstView) continue; + + auto* srcTex = srcView->dxTexture(); + auto* dstTex = dstView->dxTexture(); + + TextureFormat format = srcView->format(); + if (format == TextureFormat::Undefined) + format = srcView->dxTexture()->desc.format; + DXGI_FORMAT dxgiFormat = toDxgiFormat(format); + + // Transition src to resolve source, dst to resolve dest. + D3D12_RESOURCE_BARRIER barriers[2]{}; + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = srcTex->handle(); + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_RESOLVE_SOURCE; + barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = dstTex->handle(); + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RESOLVE_DEST; + barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + + m_ctx.cmdList->ResourceBarrier(2, barriers); + + m_ctx.cmdList->ResolveSubresource(dstTex->handle(), 0, srcTex->handle(), 0, dxgiFormat); + + // Transition back. + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_RESOLVE_SOURCE; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RESOLVE_DEST; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + + m_ctx.cmdList->ResourceBarrier(2, barriers); + } + + m_currentPipeline = nullptr; + m_currentMeshPipeline = nullptr; + } + + // Accessors for bundle recording: the last pipeline/root-sig set on this encoder. + [[nodiscard]] ID3D12PipelineState* currentPso() const { return m_currentPipeline ? m_currentPipeline->handle() : nullptr; } + [[nodiscard]] ID3D12RootSignature* currentRootSig() const { + auto* l = m_currentPipeline ? m_currentPipeline->pipelineLayout() : nullptr; + return l ? l->handle() : nullptr; + } + +private: + DxPipelineLayoutImpl* getCurrentLayout() { + if (m_currentPipeline) + return m_currentPipeline->pipelineLayout(); + if (m_currentMeshPipeline) + return m_currentMeshPipeline->pipelineLayout(); + return nullptr; + } + + DxRenderPassContext m_ctx; + RenderPassDesc m_desc{}; + DxRenderPipelineImpl* m_currentPipeline = nullptr; + DxMeshPipelineImpl* m_currentMeshPipeline = nullptr; + + // Cached vertex buffer views for re-application on pipeline change. + D3D12_VERTEX_BUFFER_VIEW m_cachedVbs[8]{}; + u32 m_cachedVbCount = 0; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPipeline.cppm new file mode 100644 index 00000000..92dc0cb2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRenderPipeline.cppm @@ -0,0 +1,167 @@ +/// DX12 implementation of RenderPipeline. +/// Wraps a D3D12 graphics pipeline state object. + +module; + +#include "DxIncludes.h" +#include +#include + +#include +#include + +export module rhi.dx12:render_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :pipeline_layout; +import :shader_module; +import :pipeline_cache; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxRenderPipelineImpl : public RenderPipeline { +public: + Status init(ID3D12Device* device, const RenderPipelineDesc& d) { + m_layout = static_cast(d.layout); + if (!m_layout) return ErrorCode::Unknown; + + D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = m_layout->handle(); + + // Vertex shader. + auto* vsMod = static_cast(d.vertex.shader.module); + if (!vsMod) return ErrorCode::Unknown; + auto vsCode = vsMod->bytecode(); + pso.VS = { vsCode.data(), vsCode.size() }; + + // Fragment shader. + if (d.fragment.has_value()) { + auto* psMod = static_cast(d.fragment->shader.module); + if (psMod) { auto ps = psMod->bytecode(); pso.PS = { ps.data(), ps.size() }; } + } + + // Input layout. + std::vector elems; + auto bufs = d.vertex.buffers; + m_vtxBufCount = static_cast(std::min(bufs.size(), usize(8))); + for (usize i = 0; i < bufs.size(); ++i) { + const auto& buf = bufs[i]; + if (i < 8) m_vtxStrides[i] = buf.stride; + auto attrs = buf.attributes; + for (usize j = 0; j < attrs.size(); ++j) { + const auto& a = attrs[j]; + D3D12_INPUT_ELEMENT_DESC e{}; + e.SemanticName = "TEXCOORD"; + e.SemanticIndex = a.shaderLocation; + e.Format = toDxgiVertexFormat(a.format); + e.InputSlot = static_cast(i); + e.AlignedByteOffset = a.offset; + e.InputSlotClass = (buf.stepMode == VertexStepMode::Instance) + ? D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA + : D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; + e.InstanceDataStepRate = (buf.stepMode == VertexStepMode::Instance) ? 1 : 0; + elems.push_back(e); + } + } + pso.InputLayout = { elems.data(), static_cast(elems.size()) }; + + // Topology. + pso.PrimitiveTopologyType = toPrimitiveTopologyType(d.primitive.topology); + m_topology = toPrimitiveTopology(d.primitive.topology); + + // Rasterizer. + pso.RasterizerState.FillMode = toFillMode(d.primitive.fillMode); + pso.RasterizerState.CullMode = toCullMode(d.primitive.cullMode); + pso.RasterizerState.FrontCounterClockwise = (d.primitive.frontFace == FrontFace::CCW) ? TRUE : FALSE; + pso.RasterizerState.DepthClipEnable = d.primitive.depthClipEnabled ? TRUE : FALSE; + pso.RasterizerState.MultisampleEnable = (d.multisample.count > 1) ? TRUE : FALSE; + + if (d.depthStencil.has_value()) { + pso.RasterizerState.DepthBias = d.depthStencil->depthBias; + pso.RasterizerState.DepthBiasClamp = d.depthStencil->depthBiasClamp; + pso.RasterizerState.SlopeScaledDepthBias = d.depthStencil->depthBiasSlopeScale; + } + + // Blend. + std::span targets = d.fragment.has_value() ? d.fragment->targets : std::span(); + pso.BlendState.AlphaToCoverageEnable = d.multisample.alphaToCoverageEnabled ? TRUE : FALSE; + pso.BlendState.IndependentBlendEnable = (targets.size() > 1) ? TRUE : FALSE; + for (usize i = 0; i < targets.size() && i < 8; ++i) { + const auto& t = targets[i]; + auto& rt = pso.BlendState.RenderTarget[i]; + rt.RenderTargetWriteMask = static_cast(t.writeMask); + if (t.blend.has_value()) { + rt.BlendEnable = TRUE; + rt.SrcBlend = toBlendFactor(t.blend->color.srcFactor); + rt.DestBlend = toBlendFactor(t.blend->color.dstFactor); + rt.BlendOp = toBlendOp(t.blend->color.operation); + rt.SrcBlendAlpha = toBlendFactor(t.blend->alpha.srcFactor); + rt.DestBlendAlpha= toBlendFactor(t.blend->alpha.dstFactor); + rt.BlendOpAlpha = toBlendOp(t.blend->alpha.operation); + } + } + + // Depth/stencil. + if (d.depthStencil.has_value()) { + const auto& ds = *d.depthStencil; + pso.DepthStencilState.DepthEnable = ds.depthTestEnabled ? TRUE : FALSE; + pso.DepthStencilState.DepthWriteMask = ds.depthWriteEnabled ? D3D12_DEPTH_WRITE_MASK_ALL : D3D12_DEPTH_WRITE_MASK_ZERO; + pso.DepthStencilState.DepthFunc = toComparisonFunc(ds.depthCompare); + pso.DepthStencilState.StencilEnable = ds.stencilEnabled ? TRUE : FALSE; + pso.DepthStencilState.StencilReadMask = ds.stencilReadMask; + pso.DepthStencilState.StencilWriteMask= ds.stencilWriteMask; + + auto& ff = pso.DepthStencilState.FrontFace; + ff.StencilFailOp = toStencilOp(ds.stencilFront.failOp); + ff.StencilDepthFailOp = toStencilOp(ds.stencilFront.depthFailOp); + ff.StencilPassOp = toStencilOp(ds.stencilFront.passOp); + ff.StencilFunc = toComparisonFunc(ds.stencilFront.compare); + + auto& bf = pso.DepthStencilState.BackFace; + bf.StencilFailOp = toStencilOp(ds.stencilBack.failOp); + bf.StencilDepthFailOp = toStencilOp(ds.stencilBack.depthFailOp); + bf.StencilPassOp = toStencilOp(ds.stencilBack.passOp); + bf.StencilFunc = toComparisonFunc(ds.stencilBack.compare); + + pso.DSVFormat = toDxgiFormat(ds.format); + } + + // Render targets. + pso.NumRenderTargets = static_cast(std::min(targets.size(), usize(8))); + for (usize i = 0; i < targets.size() && i < 8; ++i) + pso.RTVFormats[i] = toDxgiFormat(targets[i].format); + + // Multisample. + pso.SampleDesc.Count = std::max(d.multisample.count, 1u); + pso.SampleMask = (d.multisample.mask != 0) ? d.multisample.mask : ~0u; + + // Create PSO. + HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&m_pipelineState)); + if (FAILED(hr)) { + logErrorf("DxRenderPipeline: CreateGraphicsPipelineState failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + return ErrorCode::Ok; + } + + void cleanup() { m_pipelineState.Reset(); } + + [[nodiscard]] ID3D12PipelineState* handle() const { return m_pipelineState.Get(); } + [[nodiscard]] D3D_PRIMITIVE_TOPOLOGY topology() const { return m_topology; } + [[nodiscard]] DxPipelineLayoutImpl* pipelineLayout() const { return m_layout; } + [[nodiscard]] u32 getVertexStride(u32 slot) const { return (slot < m_vtxBufCount) ? m_vtxStrides[slot] : 0; } + +private: + ComPtr m_pipelineState; + DxPipelineLayoutImpl* m_layout = nullptr; + D3D_PRIMITIVE_TOPOLOGY m_topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; + u32 m_vtxStrides[8]{}; + u32 m_vtxBufCount = 0; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRhi.test.cpp b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRhi.test.cpp new file mode 100644 index 00000000..5f2e0e45 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxRhi.test.cpp @@ -0,0 +1,40 @@ +#include + +import core; +import rhi; +import rhi.dx12; + +using namespace draco; +using namespace draco::rhi; + +// Windows-only. Requires a working DXGI/D3D12 stack (a hardware adapter or the +// WARP software adapter). Where no device is available the backend still +// initializes; adapter-dependent checks are guarded. + +TEST_CASE("rhi.dx12: backend initializes and enumerates adapters") +{ + Backend* backend = nullptr; + dx12::DxBackendDesc desc{}; + REQUIRE(dx12::createDxBackend(desc, backend).isOk()); + REQUIRE(backend != nullptr); + CHECK(backend->isInitialized); + + auto adapters = backend->enumerateAdapters(); + INFO("adapter count: ", adapters.size()); + + for (Adapter* a : adapters) { + const AdapterInfo info = a->info(); + CHECK(!info.name.empty()); + } + + if (!adapters.empty()) { + // Adapters are ordered best-first; a device should be creatable from [0]. + Device* device = nullptr; + CHECK(adapters[0]->createDevice(DeviceDesc{}, device).isOk()); + CHECK(device != nullptr); + // Tear the device down before the backend releases the factory/adapters. + if (device) device->destroy(); + } + + backend->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSampler.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSampler.cppm new file mode 100644 index 00000000..448fc6d0 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSampler.cppm @@ -0,0 +1,65 @@ +/// DX12 implementation of Sampler. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:sampler; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :descriptor_heap; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxSamplerImpl : public Sampler { +public: + Status init(ID3D12Device* device, const SamplerDesc& d, DxDescriptorHeapAllocator* samplerHeap) { + m_samplerHeap = samplerHeap; + + bool isComparison = d.compare.has_value(); + + D3D12_SAMPLER_DESC sd{}; + if (isComparison) + sd.Filter = toFilter(d.minFilter, d.magFilter, d.mipmapFilter, true); + else if (d.maxAnisotropy > 1) + sd.Filter = D3D12_FILTER_ANISOTROPIC; + else + sd.Filter = toFilter(d.minFilter, d.magFilter, d.mipmapFilter, false); + + sd.AddressU = toAddressMode(d.addressU); + sd.AddressV = toAddressMode(d.addressV); + sd.AddressW = toAddressMode(d.addressW); + sd.MipLODBias = d.mipLodBias; + sd.MaxAnisotropy = static_cast(d.maxAnisotropy); + sd.ComparisonFunc = isComparison ? toComparisonFunc(*d.compare) : D3D12_COMPARISON_FUNC_NEVER; + sd.MinLOD = d.minLod; + sd.MaxLOD = d.maxLod; + + switch (d.borderColor) { + case SamplerBorderColor::TransparentBlack: sd.BorderColor[0]=0; sd.BorderColor[1]=0; sd.BorderColor[2]=0; sd.BorderColor[3]=0; break; + case SamplerBorderColor::OpaqueBlack: sd.BorderColor[0]=0; sd.BorderColor[1]=0; sd.BorderColor[2]=0; sd.BorderColor[3]=1; break; + case SamplerBorderColor::OpaqueWhite: sd.BorderColor[0]=1; sd.BorderColor[1]=1; sd.BorderColor[2]=1; sd.BorderColor[3]=1; break; + } + + m_handle = samplerHeap->allocate(); + device->CreateSampler(&sd, m_handle); + return ErrorCode::Ok; + } + + void cleanup() { + if (m_samplerHeap) m_samplerHeap->free(m_handle); + } + + [[nodiscard]] D3D12_CPU_DESCRIPTOR_HANDLE handle() const { return m_handle; } + +private: + D3D12_CPU_DESCRIPTOR_HANDLE m_handle{}; + DxDescriptorHeapAllocator* m_samplerHeap = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxShaderModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxShaderModule.cppm new file mode 100644 index 00000000..d2d47d00 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxShaderModule.cppm @@ -0,0 +1,36 @@ +/// DX12 implementation of ShaderModule. Stores DXIL bytecode. + +module; + +#include +#include + +#include + +export module rhi.dx12:shader_module; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxShaderModuleImpl : public ShaderModule { +public: + Status init(const ShaderModuleDesc& d) { + m_bytecode.resize(d.code.size()); + std::memcpy(m_bytecode.data(), d.code.data(), d.code.size()); + return ErrorCode::Ok; + } + + void cleanup() { m_bytecode.clear(); } + + [[nodiscard]] std::span bytecode() const { return { m_bytecode.data(), m_bytecode.size() }; } + +private: + std::vector m_bytecode; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSurface.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSurface.cppm new file mode 100644 index 00000000..bcd5c9c5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSurface.cppm @@ -0,0 +1,27 @@ +/// DX12 implementation of Surface. Simply stores the HWND. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:surface; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxSurfaceImpl : public Surface { +public: + explicit DxSurfaceImpl(HWND hwnd) : m_hwnd(hwnd) {} + + [[nodiscard]] HWND handle() const { return m_hwnd; } + +private: + HWND m_hwnd = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSwapChain.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSwapChain.cppm new file mode 100644 index 00000000..2dca4e64 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxSwapChain.cppm @@ -0,0 +1,161 @@ +/// DX12 implementation of SwapChain. +/// Wraps IDXGISwapChain3 for presentation. + +module; + +#include "DxIncludes.h" +#include + +export module rhi.dx12:swap_chain; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :surface; +import :texture; +import :texture_view; +import :descriptor_heap; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxDeviceImpl; // forward +class DxQueueImpl; // forward + +class DxSwapChainImpl : public SwapChain { +public: + Status init(ID3D12Device* device, IDXGIFactory4* factory, ID3D12CommandQueue* gfxQueue, + DxSurfaceImpl* surface, const SwapChainDesc& d, + DxDescriptorHeapAllocator* srvHeap, DxDescriptorHeapAllocator* rtvHeap, + DxDescriptorHeapAllocator* dsvHeap) { + m_d3dDevice = device; + m_format = d.format; + m_width = d.width; + m_height = d.height; + m_bufferCount = d.bufferCount; + m_presentMode = d.presentMode; + m_srvHeap = srvHeap; + m_rtvHeap = rtvHeap; + m_dsvHeap = dsvHeap; + + DXGI_FORMAT swapFmt = stripSrgb(toDxgiFormat(d.format)); + + DXGI_SWAP_CHAIN_DESC1 sd{}; + sd.Width = d.width; + sd.Height = d.height; + sd.Format = swapFmt; + sd.SampleDesc = { 1, 0 }; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.BufferCount = d.bufferCount; + sd.Scaling = DXGI_SCALING_STRETCH; + sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + sd.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + if (d.presentMode == PresentMode::Immediate) + sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; + + ComPtr sc1; + HRESULT hr = factory->CreateSwapChainForHwnd( + gfxQueue, surface->handle(), &sd, nullptr, nullptr, &sc1); + if (FAILED(hr)) { + logErrorf("DxSwapChain: CreateSwapChainForHwnd failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + factory->MakeWindowAssociation(surface->handle(), DXGI_MWA_NO_ALT_ENTER); + + hr = sc1.As(&m_swapChain); + if (FAILED(hr)) return ErrorCode::Unknown; + + if (acquireBackBuffers() != ErrorCode::Ok) return ErrorCode::Unknown; + m_currentIndex = m_swapChain->GetCurrentBackBufferIndex(); + return ErrorCode::Ok; + } + + // ---- SwapChain interface ---- + TextureFormat format() const override { return m_format; } + u32 width() const override { return m_width; } + u32 height() const override { return m_height; } + u32 bufferCount() const override { return m_bufferCount; } + u32 currentImageIndex() const override { return m_currentIndex; } + Texture* currentTexture() override { return (m_currentIndex < m_textures.size()) ? m_textures[m_currentIndex] : nullptr; } + TextureView* currentTextureView()override { return (m_currentIndex < m_views.size()) ? m_views[m_currentIndex] : nullptr; } + + Status acquireNextImage() override { + m_currentIndex = m_swapChain->GetCurrentBackBufferIndex(); + return ErrorCode::Ok; + } + + Status present(Queue* /*queue*/) override { + UINT syncInterval = 1, flags = 0; + switch (m_presentMode) { + case PresentMode::Immediate: syncInterval = 0; flags = DXGI_PRESENT_ALLOW_TEARING; break; + case PresentMode::Mailbox: syncInterval = 0; break; + case PresentMode::Fifo: syncInterval = 1; break; + case PresentMode::FifoRelaxed: syncInterval = 1; break; + } + return SUCCEEDED(m_swapChain->Present(syncInterval, flags)) ? ErrorCode::Ok : ErrorCode::Unknown; + } + + Status resize(u32 w, u32 h) override { + if (w == 0 || h == 0) return ErrorCode::Ok; + m_width = w; m_height = h; + releaseBackBuffers(); + HRESULT hr = m_swapChain->ResizeBuffers(m_bufferCount, w, h, + stripSrgb(toDxgiFormat(m_format)), 0); + if (FAILED(hr)) return ErrorCode::Unknown; + if (acquireBackBuffers() != ErrorCode::Ok) return ErrorCode::Unknown; + m_currentIndex = m_swapChain->GetCurrentBackBufferIndex(); + return ErrorCode::Ok; + } + + void cleanup() { + releaseBackBuffers(); + m_swapChain.Reset(); + } + +private: + Status acquireBackBuffers() { + for (u32 i = 0; i < m_bufferCount; ++i) { + ID3D12Resource* resource = nullptr; + if (FAILED(m_swapChain->GetBuffer(i, IID_PPV_ARGS(&resource)))) return ErrorCode::Unknown; + + auto* tex = new DxTextureImpl(); + TextureDesc td{}; td.dimension = TextureDimension::Texture2D; td.format = m_format; + td.width = m_width; td.height = m_height; td.arrayLayerCount = 1; td.mipLevelCount = 1; + td.sampleCount = 1; td.usage = TextureUsage::RenderTarget; + tex->initFromExisting(resource, td); + resource->Release(); // initFromExisting AddRef'd + m_textures.push_back(tex); + + auto* view = new DxTextureViewImpl(); + TextureViewDesc vd{}; vd.format = m_format; vd.dimension = TextureViewDimension::Texture2D; + vd.mipLevelCount = 1; vd.arrayLayerCount = 1; + view->init(m_d3dDevice, tex, vd, m_srvHeap, m_rtvHeap, m_dsvHeap); + m_views.push_back(view); + } + return ErrorCode::Ok; + } + + void releaseBackBuffers() { + for (auto* v : m_views) { v->cleanup(); delete v; } + m_views.clear(); + for (auto* t : m_textures) { t->cleanup(); delete t; } + m_textures.clear(); + } + + ComPtr m_swapChain; + ID3D12Device* m_d3dDevice = nullptr; + TextureFormat m_format = TextureFormat::RGBA8UnormSrgb; + u32 m_width = 0, m_height = 0, m_bufferCount = 2; + u32 m_currentIndex = 0; + PresentMode m_presentMode = PresentMode::Fifo; + std::vector m_textures; + std::vector m_views; + DxDescriptorHeapAllocator* m_srvHeap = nullptr; + DxDescriptorHeapAllocator* m_rtvHeap = nullptr; + DxDescriptorHeapAllocator* m_dsvHeap = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTexture.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTexture.cppm new file mode 100644 index 00000000..9220ae1b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTexture.cppm @@ -0,0 +1,136 @@ +/// DX12 implementation of Texture. + +module; + +#include "DxIncludes.h" +#include + +#include +#include + +export module rhi.dx12:texture; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxTextureImpl : public Texture { +public: + Status init(ID3D12Device* device, const TextureDesc& d) { + desc = d; + + DXGI_FORMAT format = isDepthFormat(d.format) + ? toTypelessDepthFormat(d.format) + : toDxgiFormat(d.format); + + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = toResourceDimension(d.dimension); + rd.Width = static_cast(d.width); + rd.Height = d.height; + rd.DepthOrArraySize = static_cast((d.dimension == TextureDimension::Texture3D) ? d.depth : d.arrayLayerCount); + rd.MipLevels = static_cast(d.mipLevelCount); + rd.Format = format; + rd.SampleDesc = { d.sampleCount, 0 }; + rd.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + rd.Flags = toTextureResourceFlags(d.usage); + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = D3D12_HEAP_TYPE_DEFAULT; + + m_state = D3D12_RESOURCE_STATE_COMMON; + + D3D12_CLEAR_VALUE clearVal{}; + D3D12_CLEAR_VALUE* pClearVal = nullptr; + + if (static_cast(d.usage & TextureUsage::DepthStencil)) { + clearVal.Format = toDxgiFormat(d.format); + clearVal.DepthStencil = { 1.0f, 0 }; + pClearVal = &clearVal; + m_state = D3D12_RESOURCE_STATE_DEPTH_WRITE; + } else if (static_cast(d.usage & TextureUsage::RenderTarget)) { + clearVal.Format = format; + clearVal.Color[0] = 0; clearVal.Color[1] = 0; clearVal.Color[2] = 0; clearVal.Color[3] = 1; + pClearVal = &clearVal; + m_state = D3D12_RESOURCE_STATE_RENDER_TARGET; + } + + HRESULT hr = device->CreateCommittedResource( + &heapProps, D3D12_HEAP_FLAG_NONE, + &rd, m_state, pClearVal, + IID_PPV_ARGS(&m_resource)); + if (FAILED(hr)) { + logErrorf("DxTexture: CreateCommittedResource failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + m_ownsResource = true; + return ErrorCode::Ok; + } + + /// Initialize from an existing resource (e.g. swap chain buffer). Does not own. + void initFromExisting(ID3D12Resource* resource, const TextureDesc& d) { + m_resource.Attach(resource); + m_resource->AddRef(); // ComPtr will Release - balance it + desc = d; + m_ownsResource = false; + m_state = D3D12_RESOURCE_STATE_PRESENT; + } + + void cleanup() { + m_subresourceStates.clear(); + m_resource.Reset(); + } + + // ---- Internal ---- + [[nodiscard]] ID3D12Resource* handle() const { return m_resource.Get(); } + + [[nodiscard]] D3D12_RESOURCE_STATES currentState() const { return m_state; } + void setState(D3D12_RESOURCE_STATES s) { m_state = s; m_subresourceStates.clear(); } + + [[nodiscard]] D3D12_RESOURCE_STATES getSubresourceState(u32 mip, u32 layer) const { + if (m_subresourceStates.empty()) return m_state; + u32 idx = mip + layer * desc.mipLevelCount; + return (idx < m_subresourceStates.size()) ? m_subresourceStates[idx] : m_state; + } + + void setSubresourceState(u32 baseMip, u32 mipCount, u32 baseLayer, u32 layerCount, D3D12_RESOURCE_STATES s) { + u32 totalMips = desc.mipLevelCount; + u32 totalLayers = std::max((desc.dimension == TextureDimension::Texture3D) ? desc.depth : desc.arrayLayerCount, 1u); + u32 mipEnd = (mipCount == ~0u) ? totalMips : std::min(baseMip + mipCount, totalMips); + u32 layerEnd = (layerCount == ~0u) ? totalLayers : std::min(baseLayer + layerCount, totalLayers); + + // All subresources? Collapse to uniform. + if (baseMip == 0 && mipEnd >= totalMips && baseLayer == 0 && layerEnd >= totalLayers) { + m_state = s; + m_subresourceStates.clear(); + return; + } + // Promote to per-subresource. + if (m_subresourceStates.empty()) { + if (s == m_state) return; + m_subresourceStates.resize(totalMips * totalLayers, m_state); + } + for (u32 l = baseLayer; l < layerEnd; ++l) + for (u32 m = baseMip; m < mipEnd; ++m) + m_subresourceStates[m + l * totalMips] = s; + + // Try to collapse. + auto first = m_subresourceStates[0]; + for (usize i = 1; i < m_subresourceStates.size(); ++i) + if (m_subresourceStates[i] != first) return; + m_state = first; + m_subresourceStates.clear(); + } + +private: + ComPtr m_resource; + D3D12_RESOURCE_STATES m_state = D3D12_RESOURCE_STATE_COMMON; + std::vector m_subresourceStates; + bool m_ownsResource = true; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTextureView.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTextureView.cppm new file mode 100644 index 00000000..5d506e0d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTextureView.cppm @@ -0,0 +1,289 @@ +/// DX12 implementation of TextureView. +/// Lazily creates SRV/RTV/DSV/UAV CPU descriptor handles on demand. + +module; + +#include "DxIncludes.h" + +export module rhi.dx12:texture_view; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :texture; +import :descriptor_heap; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxTextureViewImpl : public TextureView { +public: + Status init(ID3D12Device* device, DxTextureImpl* tex, const TextureViewDesc& d, + DxDescriptorHeapAllocator* srvHeap, DxDescriptorHeapAllocator* rtvHeap, + DxDescriptorHeapAllocator* dsvHeap) { + m_device = device; + m_texture = tex; + m_viewDesc= d; + m_srvHeap = srvHeap; + m_rtvHeap = rtvHeap; + m_dsvHeap = dsvHeap; + return ErrorCode::Ok; + } + + // ---- Lazy SRV ---- + D3D12_CPU_DESCRIPTOR_HANDLE getSrv() { + if (m_hasSrv) return m_srv; + + auto fmt = (m_viewDesc.format == TextureFormat::Undefined) ? m_texture->desc.format : m_viewDesc.format; + DXGI_FORMAT srvFmt = isDepthFormat(fmt) ? toDepthSrvFormat(fmt) : toDxgiFormat(fmt); + + D3D12_SHADER_RESOURCE_VIEW_DESC sd{}; + sd.Format = srvFmt; + sd.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + + u32 mips = m_viewDesc.mipLevelCount; + if (mips == 0) mips = m_texture->desc.mipLevelCount - m_viewDesc.baseMipLevel; + + switch (m_viewDesc.dimension) { + case TextureViewDimension::Texture1D: + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D; + sd.Texture1D.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.Texture1D.MipLevels = mips; + break; + case TextureViewDimension::Texture1DArray: + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1DARRAY; + sd.Texture1DArray.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.Texture1DArray.MipLevels = mips; + sd.Texture1DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + sd.Texture1DArray.ArraySize = m_viewDesc.arrayLayerCount; + break; + case TextureViewDimension::Texture2D: + if (m_texture->desc.sampleCount > 1) { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS; + } else { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.Texture2D.MipLevels = mips; + } + break; + case TextureViewDimension::Texture2DArray: + if (m_texture->desc.sampleCount > 1) { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY; + sd.Texture2DMSArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + sd.Texture2DMSArray.ArraySize = m_viewDesc.arrayLayerCount; + } else { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + sd.Texture2DArray.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.Texture2DArray.MipLevels = mips; + sd.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + sd.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + } + break; + case TextureViewDimension::TextureCube: + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; + sd.TextureCube.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.TextureCube.MipLevels = mips; + break; + case TextureViewDimension::TextureCubeArray: + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBEARRAY; + sd.TextureCubeArray.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.TextureCubeArray.MipLevels = mips; + sd.TextureCubeArray.First2DArrayFace = m_viewDesc.baseArrayLayer; + sd.TextureCubeArray.NumCubes = m_viewDesc.arrayLayerCount / 6; + break; + case TextureViewDimension::Texture3D: + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; + sd.Texture3D.MostDetailedMip = m_viewDesc.baseMipLevel; + sd.Texture3D.MipLevels = mips; + break; + } + + m_srv = m_srvHeap->allocate(); + m_device->CreateShaderResourceView(m_texture->handle(), &sd, m_srv); + m_hasSrv = true; + return m_srv; + } + + // ---- Lazy RTV ---- + D3D12_CPU_DESCRIPTOR_HANDLE getRtv() { + if (m_hasRtv) return m_rtv; + + auto fmt = (m_viewDesc.format == TextureFormat::Undefined) ? m_texture->desc.format : m_viewDesc.format; + bool isArray = m_texture->desc.arrayLayerCount > 1; + + D3D12_RENDER_TARGET_VIEW_DESC rd{}; + rd.Format = toDxgiFormat(fmt); + + if (m_viewDesc.dimension == TextureViewDimension::Texture2D) { + if (isArray) { + if (m_texture->desc.sampleCount > 1) { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY; + rd.Texture2DMSArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + rd.Texture2DMSArray.ArraySize = m_viewDesc.arrayLayerCount; + } else { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DARRAY; + rd.Texture2DArray.MipSlice = m_viewDesc.baseMipLevel; + rd.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + rd.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + } + } else if (m_texture->desc.sampleCount > 1) { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DMS; + } else { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; + rd.Texture2D.MipSlice = m_viewDesc.baseMipLevel; + } + } else if (m_viewDesc.dimension == TextureViewDimension::Texture3D) { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE3D; + rd.Texture3D.MipSlice = m_viewDesc.baseMipLevel; + rd.Texture3D.WSize = m_viewDesc.arrayLayerCount; + } else { + // 2DArray, Cube, CubeArray + if (m_texture->desc.sampleCount > 1) { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY; + rd.Texture2DMSArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + rd.Texture2DMSArray.ArraySize = m_viewDesc.arrayLayerCount; + } else { + rd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DARRAY; + rd.Texture2DArray.MipSlice = m_viewDesc.baseMipLevel; + rd.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + rd.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + } + } + + m_rtv = m_rtvHeap->allocate(); + m_device->CreateRenderTargetView(m_texture->handle(), &rd, m_rtv); + m_hasRtv = true; + return m_rtv; + } + + // ---- Lazy DSV ---- + D3D12_CPU_DESCRIPTOR_HANDLE getDsv() { + if (m_hasDsv) return m_dsv; + + auto fmt = (m_viewDesc.format == TextureFormat::Undefined) ? m_texture->desc.format : m_viewDesc.format; + bool isArray = m_texture->desc.arrayLayerCount > 1; + + D3D12_DEPTH_STENCIL_VIEW_DESC dd{}; + dd.Format = toDxgiFormat(fmt); + + if (m_viewDesc.dimension == TextureViewDimension::Texture2D) { + if (isArray) { + if (m_texture->desc.sampleCount > 1) { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY; + dd.Texture2DMSArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + dd.Texture2DMSArray.ArraySize = m_viewDesc.arrayLayerCount; + } else { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DARRAY; + dd.Texture2DArray.MipSlice = m_viewDesc.baseMipLevel; + dd.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + dd.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + } + } else if (m_texture->desc.sampleCount > 1) { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DMS; + } else { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; + dd.Texture2D.MipSlice = m_viewDesc.baseMipLevel; + } + } else { + if (m_texture->desc.sampleCount > 1) { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY; + dd.Texture2DMSArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + dd.Texture2DMSArray.ArraySize = m_viewDesc.arrayLayerCount; + } else { + dd.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DARRAY; + dd.Texture2DArray.MipSlice = m_viewDesc.baseMipLevel; + dd.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + dd.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + } + } + + m_dsv = m_dsvHeap->allocate(); + m_device->CreateDepthStencilView(m_texture->handle(), &dd, m_dsv); + m_hasDsv = true; + return m_dsv; + } + + // ---- Lazy UAV ---- + D3D12_CPU_DESCRIPTOR_HANDLE getUav() { + if (m_hasUav) return m_uav; + + auto fmt = (m_viewDesc.format == TextureFormat::Undefined) ? m_texture->desc.format : m_viewDesc.format; + + D3D12_UNORDERED_ACCESS_VIEW_DESC ud{}; + ud.Format = toDxgiFormat(fmt); + + switch (m_viewDesc.dimension) { + case TextureViewDimension::Texture1D: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1D; + ud.Texture1D.MipSlice = m_viewDesc.baseMipLevel; + break; + case TextureViewDimension::Texture1DArray: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE1DARRAY; + ud.Texture1DArray.MipSlice = m_viewDesc.baseMipLevel; + ud.Texture1DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + ud.Texture1DArray.ArraySize = m_viewDesc.arrayLayerCount; + break; + case TextureViewDimension::Texture2D: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + ud.Texture2D.MipSlice = m_viewDesc.baseMipLevel; + break; + case TextureViewDimension::Texture2DArray: + case TextureViewDimension::TextureCube: + case TextureViewDimension::TextureCubeArray: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2DARRAY; + ud.Texture2DArray.MipSlice = m_viewDesc.baseMipLevel; + ud.Texture2DArray.FirstArraySlice = m_viewDesc.baseArrayLayer; + ud.Texture2DArray.ArraySize = m_viewDesc.arrayLayerCount; + break; + case TextureViewDimension::Texture3D: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE3D; + ud.Texture3D.MipSlice = m_viewDesc.baseMipLevel; + ud.Texture3D.FirstWSlice = m_viewDesc.baseArrayLayer; + ud.Texture3D.WSize = m_viewDesc.arrayLayerCount; + break; + default: + ud.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + ud.Texture2D.MipSlice = m_viewDesc.baseMipLevel; + break; + } + + m_uav = m_srvHeap->allocate(); + m_device->CreateUnorderedAccessView(m_texture->handle(), nullptr, &ud, m_uav); + m_hasUav = true; + return m_uav; + } + + void cleanup() { + if (m_hasSrv) { m_srvHeap->free(m_srv); m_hasSrv = false; } + if (m_hasRtv) { m_rtvHeap->free(m_rtv); m_hasRtv = false; } + if (m_hasDsv) { m_dsvHeap->free(m_dsv); m_hasDsv = false; } + if (m_hasUav) { m_srvHeap->free(m_uav); m_hasUav = false; } + } + + // ---- Internal ---- + [[nodiscard]] DxTextureImpl* dxTexture() const { return m_texture; } + [[nodiscard]] TextureViewDesc viewDesc() const { return m_viewDesc; } + [[nodiscard]] TextureFormat format() const { + return (m_viewDesc.format == TextureFormat::Undefined) ? m_texture->desc.format : m_viewDesc.format; + } + // Base-mip extent (a view onto mip N is half-sized per level). DX12 has no Vulkan-style renderArea + // validation so a stale base size here wouldn't fault, but callers (e.g. the render graph's + // viewport default) expect the view's true dimensions - keep it correct + consistent with Vulkan. + [[nodiscard]] u32 width() const { u32 w = m_texture->desc.width >> m_viewDesc.baseMipLevel; return w ? w : 1u; } + [[nodiscard]] u32 height() const { u32 h = m_texture->desc.height >> m_viewDesc.baseMipLevel; return h ? h : 1u; } + +private: + ID3D12Device* m_device = nullptr; + DxTextureImpl* m_texture = nullptr; + TextureViewDesc m_viewDesc{}; + DxDescriptorHeapAllocator* m_srvHeap = nullptr; + DxDescriptorHeapAllocator* m_rtvHeap = nullptr; + DxDescriptorHeapAllocator* m_dsvHeap = nullptr; + + D3D12_CPU_DESCRIPTOR_HANDLE m_srv{}, m_rtv{}, m_dsv{}, m_uav{}; + bool m_hasSrv = false, m_hasRtv = false, m_hasDsv = false, m_hasUav = false; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTransferBatch.cppm b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTransferBatch.cppm new file mode 100644 index 00000000..53ef4ec6 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/DX12/DxTransferBatch.cppm @@ -0,0 +1,278 @@ +/// DX12 implementation of TransferBatch. + +module; + +#include "DxIncludes.h" +#include + +#include +#include + +export module rhi.dx12:transfer_batch; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :texture; +import :fence; + +using namespace draco; + +export namespace draco::rhi::dx12 { + +class DxQueueImpl; // forward + +class DxTransferBatchImpl : public TransferBatch { +public: + Status init(ID3D12Device* device, ID3D12CommandQueue* queue, QueueType queueType) { + m_device = device; + m_queue = queue; + + HRESULT hr = device->CreateCommandAllocator( + toCommandListType(queueType), + IID_PPV_ARGS(&m_allocator)); + if (FAILED(hr)) { + logErrorf("DxTransferBatch: CreateCommandAllocator failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + hr = device->CreateCommandList(0, + toCommandListType(queueType), + m_allocator.Get(), nullptr, + IID_PPV_ARGS(&m_cmdList)); + if (FAILED(hr)) { + logErrorf("DxTransferBatch: CreateCommandList failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + // Command list starts open; close it until we need it. + m_cmdList->Close(); + + // Create fence for synchronous submit. + hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)); + if (FAILED(hr)) { + logErrorf("DxTransferBatch: CreateFence failed (0x%08X)", static_cast(hr)); + return ErrorCode::Unknown; + } + + m_fenceEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + m_fenceValue = 0; + + return ErrorCode::Ok; + } + + // ---- TransferBatch interface ---- + + void writeBuffer(Buffer* dst, u64 dstOffset, std::span data) override { + auto* dxDst = static_cast(dst); + if (!dxDst || data.size() == 0) return; + + ensureRecording(); + + // Create upload-heap staging buffer. + u64 stagingSize = static_cast(data.size()); + ComPtr staging; + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; + + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + rd.Width = stagingSize; + rd.Height = 1; + rd.DepthOrArraySize = 1; + rd.MipLevels = 1; + rd.Format = DXGI_FORMAT_UNKNOWN; + rd.SampleDesc = { 1, 0 }; + rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + rd.Flags = D3D12_RESOURCE_FLAG_NONE; + + HRESULT hr = m_device->CreateCommittedResource( + &heapProps, D3D12_HEAP_FLAG_NONE, + &rd, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, + IID_PPV_ARGS(&staging)); + if (FAILED(hr)) return; + + // Map and copy data. + void* mapped = nullptr; + staging->Map(0, nullptr, &mapped); + std::memcpy(mapped, data.data(), data.size()); + staging->Unmap(0, nullptr); + + m_stagingBuffers.push_back(std::move(staging)); + + // Record copy command. + m_cmdList->CopyBufferRegion(dxDst->handle(), dstOffset, + m_stagingBuffers.back().Get(), 0, stagingSize); + } + + void writeTexture(Texture* dst, std::span data, + const TextureDataLayout& layout, Extent3D extent, + u32 mipLevel, u32 arrayLayer) override { + auto* dxTex = static_cast(dst); + if (!dxTex || data.size() == 0) return; + + ensureRecording(); + + // Calculate aligned row pitch (D3D12 requires 256-byte row alignment). + u32 alignedRowPitch = (layout.bytesPerRow + 255) & ~u32(255); + u32 rowsPerImage = (layout.rowsPerImage > 0) ? layout.rowsPerImage : extent.height; + u64 stagingSize = static_cast(alignedRowPitch) * rowsPerImage * extent.depth; + + ComPtr staging; + + D3D12_HEAP_PROPERTIES heapProps{}; + heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; + + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + rd.Width = stagingSize; + rd.Height = 1; + rd.DepthOrArraySize = 1; + rd.MipLevels = 1; + rd.Format = DXGI_FORMAT_UNKNOWN; + rd.SampleDesc = { 1, 0 }; + rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + rd.Flags = D3D12_RESOURCE_FLAG_NONE; + + HRESULT hr = m_device->CreateCommittedResource( + &heapProps, D3D12_HEAP_FLAG_NONE, + &rd, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, + IID_PPV_ARGS(&staging)); + if (FAILED(hr)) return; + + // Map and copy data row by row (handles pitch alignment). + void* mapped = nullptr; + staging->Map(0, nullptr, &mapped); + + const u8* srcPtr = data.data() + layout.offset; + u8* dstPtr = static_cast(mapped); + for (u32 z = 0; z < extent.depth; ++z) { + for (u32 row = 0; row < rowsPerImage; ++row) { + std::memcpy( + dstPtr + (z * rowsPerImage + row) * alignedRowPitch, + srcPtr + (z * rowsPerImage + row) * layout.bytesPerRow, + layout.bytesPerRow); + } + } + + staging->Unmap(0, nullptr); + m_stagingBuffers.push_back(std::move(staging)); + + // Transition texture to copy dest. + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = dxTex->handle(); + barrier.Transition.StateBefore = dxTex->currentState(); + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + if (dxTex->currentState() != D3D12_RESOURCE_STATE_COPY_DEST) + m_cmdList->ResourceBarrier(1, &barrier); + + u32 subresource = mipLevel + arrayLayer * dxTex->desc.mipLevelCount; + + D3D12_TEXTURE_COPY_LOCATION srcLoc{}; + srcLoc.pResource = m_stagingBuffers.back().Get(); + srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + srcLoc.PlacedFootprint.Offset = 0; + srcLoc.PlacedFootprint.Footprint.Format = toDxgiFormat(dxTex->desc.format); + srcLoc.PlacedFootprint.Footprint.Width = extent.width; + srcLoc.PlacedFootprint.Footprint.Height = extent.height; + srcLoc.PlacedFootprint.Footprint.Depth = extent.depth; + srcLoc.PlacedFootprint.Footprint.RowPitch = alignedRowPitch; + + D3D12_TEXTURE_COPY_LOCATION dstLoc{}; + dstLoc.pResource = dxTex->handle(); + dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dstLoc.SubresourceIndex = subresource; + + m_cmdList->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, nullptr); + + // Transition back to common. + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COMMON; + m_cmdList->ResourceBarrier(1, &barrier); + dxTex->setState(D3D12_RESOURCE_STATE_COMMON); + } + + Status submit() override { + if (!m_isRecording) return ErrorCode::Ok; + + m_cmdList->Close(); + m_isRecording = false; + + ID3D12CommandList* lists[] = { m_cmdList.Get() }; + m_queue->ExecuteCommandLists(1, lists); + + // Wait for completion. + ++m_fenceValue; + m_queue->Signal(m_fence.Get(), m_fenceValue); + if (m_fence->GetCompletedValue() < m_fenceValue) { + m_fence->SetEventOnCompletion(m_fenceValue, m_fenceEvent); + WaitForSingleObject(m_fenceEvent, INFINITE); + } + + releaseStagingBuffers(); + return ErrorCode::Ok; + } + + Status submitAsync(Fence* fence, u64 signalValue) override { + if (!m_isRecording) return ErrorCode::Ok; + + m_cmdList->Close(); + m_isRecording = false; + + ID3D12CommandList* lists[] = { m_cmdList.Get() }; + m_queue->ExecuteCommandLists(1, lists); + + if (auto* dxFence = static_cast(fence)) + m_queue->Signal(dxFence->handle(), signalValue); + + // Note: staging buffers can't be released until GPU is done. + // Caller must wait on the fence before calling reset(). + return ErrorCode::Ok; + } + + void reset() override { + releaseStagingBuffers(); + } + + void destroy() override { + releaseStagingBuffers(); + + if (m_fenceEvent) { CloseHandle(m_fenceEvent); m_fenceEvent = nullptr; } + m_fence.Reset(); + m_cmdList.Reset(); + m_allocator.Reset(); + } + +private: + void ensureRecording() { + if (!m_isRecording) { + m_allocator->Reset(); + m_cmdList->Reset(m_allocator.Get(), nullptr); + m_isRecording = true; + } + } + + void releaseStagingBuffers() { + m_stagingBuffers.clear(); + } + + ID3D12Device* m_device = nullptr; + ID3D12CommandQueue* m_queue = nullptr; + ComPtr m_allocator; + ComPtr m_cmdList; + bool m_isRecording = false; + + // ComPtr's operator overloads are incompatible with Array's placement-new. + std::vector> m_stagingBuffers; + + ComPtr m_fence; + u64 m_fenceValue = 0; + HANDLE m_fenceEvent = nullptr; +}; + +} // namespace draco::rhi::dx12 diff --git a/Engine/cpp/Runtime/Rendering/RHI/Descriptors.cppm b/Engine/cpp/Runtime/Rendering/RHI/Descriptors.cppm new file mode 100644 index 00000000..82606f7c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Descriptors.cppm @@ -0,0 +1,421 @@ +/// Descriptor structs for creating RHI resources, pipelines, and render passes. + +module; + +#include +#include +#include +#include + +export module rhi:descriptors; + +import core.stdtypes; +import :enums; +import :texture_format; +import :types; +import :forward; + +using namespace draco; + +export namespace draco::rhi { + +// ---- Resources ---- + +struct BufferDesc { + u64 size = 0; + BufferUsage usage = BufferUsage::None; + MemoryLocation memory = MemoryLocation::GpuOnly; + std::u8string_view label; +}; + +struct TextureDesc { + TextureDimension dimension = TextureDimension::Texture2D; + TextureFormat format = TextureFormat::Undefined; + u32 width = 1, height = 1, depth = 1; + u32 arrayLayerCount = 1; + u32 mipLevelCount = 1; + u32 sampleCount = 1; + TextureUsage usage = TextureUsage::None; + std::u8string_view label; + + /// Convenience factory for a 2D render target. + static TextureDesc renderTarget(TextureFormat fmt, u32 w, u32 h, u32 samples = 1, std::u8string_view lbl = {}) { + TextureDesc d{}; + d.format = fmt; d.width = w; d.height = h; d.sampleCount = samples; + d.usage = TextureUsage::RenderTarget | TextureUsage::Sampled; + d.label = lbl; + return d; + } + + /// Convenience factory for a depth buffer. + static TextureDesc depthBuffer(TextureFormat fmt, u32 w, u32 h, u32 samples = 1, std::u8string_view lbl = {}) { + TextureDesc d{}; + d.format = fmt; d.width = w; d.height = h; d.sampleCount = samples; + d.usage = TextureUsage::DepthStencil; + d.label = lbl; + return d; + } +}; + +struct TextureViewDesc { + TextureFormat format = TextureFormat::Undefined; + TextureViewDimension dimension = TextureViewDimension::Texture2D; + u32 baseMipLevel = 0; + u32 mipLevelCount = 1; + u32 baseArrayLayer = 0; + u32 arrayLayerCount = 1; + TextureAspect aspect = TextureAspect::All; + std::u8string_view label; +}; + +struct SamplerDesc { + FilterMode minFilter = FilterMode::Linear; + FilterMode magFilter = FilterMode::Linear; + MipmapFilterMode mipmapFilter = MipmapFilterMode::Linear; + AddressMode addressU = AddressMode::Repeat; + AddressMode addressV = AddressMode::Repeat; + AddressMode addressW = AddressMode::Repeat; + f32 mipLodBias = 0.0f; + f32 minLod = 0.0f; + f32 maxLod = 1000.0f; + u16 maxAnisotropy = 1; + std::optional compare; + SamplerBorderColor borderColor = SamplerBorderColor::TransparentBlack; + std::u8string_view label; +}; + +struct ShaderModuleDesc { + std::span code; ///< SPIR-V or DXIL bytecode. + std::u8string_view label; +}; + +struct QuerySetDesc { + QueryType type = QueryType::Timestamp; + u32 count = 0; + std::u8string_view label; +}; + +struct SwapChainDesc { + u32 width = 0; + u32 height = 0; + TextureFormat format = TextureFormat::BGRA8UnormSrgb; + PresentMode presentMode = PresentMode::Fifo; + u32 bufferCount = 2; + std::u8string_view label; +}; + +// ---- Binding ---- + +/// Describes one entry in a bind group layout. +struct BindGroupLayoutEntry { + u32 binding = 0; + ShaderStage visibility = ShaderStage::None; + BindingType type = BindingType::UniformBuffer; + TextureViewDimension textureDimension = TextureViewDimension::Texture2D; + bool textureMultisampled = false; + TextureFormat storageTextureFormat = TextureFormat::Undefined; + bool hasDynamicOffset = false; + u32 storageBufferStride = 0; + u32 count = 1; + std::u8string_view label; + + /// Factory: uniform buffer binding. + static BindGroupLayoutEntry uniformBuffer(u32 binding, ShaderStage vis) { + BindGroupLayoutEntry e{}; e.binding = binding; e.visibility = vis; + e.type = BindingType::UniformBuffer; return e; + } + + /// Factory: sampled texture binding. + static BindGroupLayoutEntry sampledTexture(u32 binding, ShaderStage vis, + TextureViewDimension dim = TextureViewDimension::Texture2D) { + BindGroupLayoutEntry e{}; e.binding = binding; e.visibility = vis; + e.type = BindingType::SampledTexture; e.textureDimension = dim; return e; + } + + /// Factory: sampler binding. + static BindGroupLayoutEntry sampler(u32 binding, ShaderStage vis) { + BindGroupLayoutEntry e{}; e.binding = binding; e.visibility = vis; + e.type = BindingType::Sampler; return e; + } + + /// Factory: storage buffer (read-write) binding. + static BindGroupLayoutEntry storageBuffer(u32 binding, ShaderStage vis, bool readOnly = false) { + BindGroupLayoutEntry e{}; e.binding = binding; e.visibility = vis; + e.type = readOnly ? BindingType::StorageBufferReadOnly : BindingType::StorageBufferReadWrite; + return e; + } +}; + +struct BindGroupLayoutDesc { + std::span entries; + std::u8string_view label; +}; + +/// Describes one resource binding within a bind group. +struct BindGroupEntry { + Buffer* buffer = nullptr; + u64 bufferOffset = 0; + u64 bufferSize = 0; + TextureView* textureView = nullptr; + Sampler* sampler = nullptr; + AccelStruct* accelStruct = nullptr; + + static BindGroupEntry bufferEntry(Buffer* buf, u64 offset, u64 size) { + BindGroupEntry e{}; e.buffer = buf; e.bufferOffset = offset; e.bufferSize = size; return e; + } + static BindGroupEntry textureEntry(TextureView* view) { + BindGroupEntry e{}; e.textureView = view; return e; + } + static BindGroupEntry samplerEntry(Sampler* s) { + BindGroupEntry e{}; e.sampler = s; return e; + } + static BindGroupEntry accelStructEntry(AccelStruct* as) { + BindGroupEntry e{}; e.accelStruct = as; return e; + } +}; + +struct BindGroupDesc { + BindGroupLayout* layout = nullptr; + std::span entries; + std::u8string_view label; +}; + +/// For updating individual entries in a bindless bind group. +struct BindlessUpdateEntry { + u32 layoutIndex = 0; + u32 arrayIndex = 0; + Buffer* buffer = nullptr; + u64 bufferOffset = 0; + u64 bufferSize = 0; + TextureView* textureView = nullptr; + Sampler* sampler = nullptr; +}; + +// ---- Pipelines ---- + +struct PipelineLayoutDesc { + std::span bindGroupLayouts; + std::span pushConstantRanges; + std::u8string_view label; +}; + +struct PipelineCacheDesc { + std::span initialData; + std::u8string_view label; +}; + +struct VertexAttribute { + VertexFormat format = VertexFormat::Float32; + u32 offset = 0; + u32 shaderLocation = 0; +}; + +struct VertexBufferLayout { + u32 stride = 0; + VertexStepMode stepMode = VertexStepMode::Vertex; + std::span attributes; +}; + +struct BlendComponent { + BlendFactor srcFactor = BlendFactor::One; + BlendFactor dstFactor = BlendFactor::Zero; + BlendOperation operation = BlendOperation::Add; +}; + +struct BlendState { + BlendComponent color; + BlendComponent alpha; + + // Standard alpha blending: srcAlpha*src + (1-srcAlpha)*dst. + static constexpr BlendState alphaBlend() { + return { { BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha, BlendOperation::Add }, + { BlendFactor::One, BlendFactor::OneMinusSrcAlpha, BlendOperation::Add } }; + } + // Premultiplied alpha: src + (1-srcAlpha)*dst. + static constexpr BlendState premultipliedAlpha() { + return { { BlendFactor::One, BlendFactor::OneMinusSrcAlpha, BlendOperation::Add }, + { BlendFactor::One, BlendFactor::OneMinusSrcAlpha, BlendOperation::Add } }; + } + // Additive: src + dst - accumulate, e.g. the bloom upsample chain. + static constexpr BlendState additive() { + return { { BlendFactor::One, BlendFactor::One, BlendOperation::Add }, + { BlendFactor::One, BlendFactor::One, BlendOperation::Add } }; + } + // Multiply: src * dst. + static constexpr BlendState multiply() { + return { { BlendFactor::Dst, BlendFactor::Zero, BlendOperation::Add }, + { BlendFactor::DstAlpha, BlendFactor::Zero, BlendOperation::Add } }; + } +}; + +struct ColorTargetState { + TextureFormat format = TextureFormat::Undefined; + std::optional blend; + ColorWriteMask writeMask = ColorWriteMask::All; +}; + +struct StencilFaceState { + CompareFunction compare = CompareFunction::Always; + StencilOperation failOp = StencilOperation::Keep; + StencilOperation depthFailOp = StencilOperation::Keep; + StencilOperation passOp = StencilOperation::Keep; +}; + +struct DepthStencilState { + TextureFormat format = TextureFormat::Undefined; + bool depthTestEnabled = true; + bool depthWriteEnabled = true; + CompareFunction depthCompare = CompareFunction::Less; + bool stencilEnabled = false; + u8 stencilReadMask = 0xFF; + u8 stencilWriteMask = 0xFF; + StencilFaceState stencilFront; + StencilFaceState stencilBack; + i32 depthBias = 0; + f32 depthBiasSlopeScale = 0.0f; + f32 depthBiasClamp = 0.0f; +}; + +struct PrimitiveState { + PrimitiveTopology topology = PrimitiveTopology::TriangleList; + FrontFace frontFace = FrontFace::CCW; + CullMode cullMode = CullMode::None; + FillMode fillMode = FillMode::Solid; + bool depthClipEnabled = true; +}; + +struct MultisampleState { + u32 count = 1; + u32 mask = 0xFFFFFFFF; + bool alphaToCoverageEnabled = false; +}; + +/// Programmable shader stage (vertex, fragment, compute, mesh, task, etc.). +struct ProgrammableStage { + ShaderModule* module = nullptr; + std::u8string_view entryPoint = u8"main"; + ShaderStage stage = ShaderStage::None; +}; + +struct VertexState { + ProgrammableStage shader; + std::span buffers; +}; + +struct FragmentState { + ProgrammableStage shader; + std::span targets; +}; + +/// Descriptor for creating a graphics (render) pipeline. +struct RenderPipelineDesc { + PipelineLayout* layout = nullptr; + VertexState vertex; + std::optional fragment; + PrimitiveState primitive; + std::optional depthStencil; + MultisampleState multisample; + PipelineCache* cache = nullptr; + std::u8string_view label; +}; + +/// Descriptor for creating a compute pipeline. +struct ComputePipelineDesc { + PipelineLayout* layout = nullptr; + ProgrammableStage compute; + PipelineCache* cache = nullptr; + std::u8string_view label; +}; + +// ---- Render pass ---- + +/// Color attachment for a render pass. +struct ColorAttachment { + TextureView* view = nullptr; + TextureView* resolveTarget = nullptr; + LoadOp loadOp = LoadOp::Clear; + StoreOp storeOp = StoreOp::Store; + ClearColor clearValue = ClearColor::black(); +}; + +/// Depth/stencil attachment for a render pass. +struct DepthStencilAttachment { + TextureView* view = nullptr; + LoadOp depthLoadOp = LoadOp::Clear; + StoreOp depthStoreOp = StoreOp::Store; + f32 depthClearValue = 1.0f; + bool depthReadOnly = false; + LoadOp stencilLoadOp = LoadOp::Clear; + StoreOp stencilStoreOp = StoreOp::Store; + u32 stencilClearValue = 0; + bool stencilReadOnly = false; +}; + +/// Color attachment list (up to maxColorAttachments). +using ColorAttachmentList = std::vector; + +/// Descriptor for beginning a render pass. +struct RenderPassDesc { + ColorAttachmentList colorAttachments; + std::optional depthStencilAttachment; + QuerySet* timestampQuerySet = nullptr; + u32 beginTimestampIndex = 0; + u32 endTimestampIndex = 0; + // How draws are supplied. Default Inline; set SecondaryCommandBuffers to execute render + // bundles into this pass (Vulkan needs to know at begin time; other backends ignore it). + RenderPassContents contents = RenderPassContents::Inline; + std::u8string_view label; +}; + +// Describes a render bundle's target signature so it can be validated against - and replayed +// into - compatible render passes: the attachment formats + sample count it records for. The +// render-area extent lets the Vulkan backend record a full-target viewport/scissor into the +// secondary command buffer (bundles carry no pass-level dynamic state; other backends inherit +// it from the pass and ignore the extent). +struct RenderBundleDesc { + TextureFormat colorFormats[maxColorAttachments] = { TextureFormat::Undefined }; + u32 colorFormatCount = 0; + TextureFormat depthStencilFormat = TextureFormat::Undefined; + u32 sampleCount = 1; + // The bundle's viewport/scissor (a sub-rect of the target for split-screen). x/y default to 0; + // width/height are the viewport extent. A Vulkan secondary / DX12 bundle records this up front + // since it can't inherit dynamic viewport state from the parent. + i32 viewportX = 0; + i32 viewportY = 0; + u32 width = 0; + u32 height = 0; + std::u8string_view label; +}; + +// ---- Barriers ---- + +struct BufferBarrier { + Buffer* buffer = nullptr; + ResourceState oldState = ResourceState::Undefined; + ResourceState newState = ResourceState::Undefined; + u64 offset = 0; + u64 size = ~0ull; +}; + +struct TextureBarrier { + Texture* texture = nullptr; + ResourceState oldState = ResourceState::Undefined; + ResourceState newState = ResourceState::Undefined; + u32 baseMipLevel = 0; + u32 mipLevelCount = ~0u; + u32 baseArrayLayer = 0; + u32 arrayLayerCount= ~0u; +}; + +struct MemoryBarrier { + ResourceState oldState = ResourceState::Undefined; + ResourceState newState = ResourceState::Undefined; +}; + +struct BarrierGroup { + std::span bufferBarriers; + std::span textureBarriers; + std::span memoryBarriers; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Device.cppm b/Engine/cpp/Runtime/Rendering/RHI/Device.cppm new file mode 100644 index 00000000..0922d02d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Device.cppm @@ -0,0 +1,200 @@ +/// Abstract Backend, Adapter, and Device interfaces. +/// +/// Backend is the entry point - it enumerates GPU adapters and creates +/// presentation surfaces. Adapter represents a physical GPU. Device is +/// the central factory for all GPU resources. +/// +/// Mesh shader and ray tracing creation methods are on Device directly +/// (virtual no-ops returning ErrorCode::NotSupported when not supported). +/// Callers check device->features.meshShaders / .rayTracing before use. + +module; + +#include +#include + +export module rhi:device; + +import core.stdtypes; +import core.status; +import :enums; +import :texture_format; +import :types; +import :descriptors; +import :ext_descriptors; +import :resources; +import :commands; +import :queue; +import :swapchain; + +using namespace draco; + +export namespace draco::rhi { + +// ---- Backend ---- + +/// RHI backend entry point (Vulkan, DX12, etc.). +class Backend { +public: + virtual ~Backend() = default; + + bool isInitialized = false; + + /// Returns all available GPU adapters in preference order, most + /// preferred first (discrete > integrated > unknown > CPU). Backends + /// guarantee this ordering via sortAdaptersByPreference(), so callers + /// that just want "the best available GPU" can take element [0]. + [[nodiscard]] virtual std::span enumerateAdapters() = 0; + + /// Creates a presentation surface from a native window handle. + /// On Win32: windowHandle = HWND, displayHandle = nullptr. + /// On X11: windowHandle = XID (as void*), displayHandle = Display*. + /// On Wayland: windowHandle = wl_surface*, displayHandle = wl_display*. + virtual Status createSurface(void* windowHandle, void* displayHandle, Surface*& out) = 0; + + Status createSurface(void* windowHandle, Surface*& out) { + return createSurface(windowHandle, nullptr, out); + } + + /// Destroy the backend and all objects it owns. + virtual void destroy() = 0; +}; + +// ---- Adapter ---- + +/// Represents a physical GPU. Query capabilities and create a logical device. +class Adapter { +public: + virtual ~Adapter() = default; + + /// Populate adapter info (name, vendor, features, limits). + virtual void getInfo(AdapterInfo& out) = 0; + + /// Convenience: returns a copy of the adapter info. + [[nodiscard]] AdapterInfo info() { AdapterInfo i; getInfo(i); return i; } + + /// Create a logical device from this adapter. + virtual Status createDevice(const DeviceDesc& desc, Device*& out) = 0; +}; + +/// Selection preference for an adapter type - lower is more preferred. +/// Defines the single source of truth for "best GPU first" ordering. +[[nodiscard]] inline int adapterPreferenceRank(AdapterType type) { + switch (type) { + case AdapterType::DiscreteGpu: return 0; + case AdapterType::IntegratedGpu: return 1; + case AdapterType::Unknown: return 2; + case AdapterType::Cpu: return 3; + } + return 4; +} + +/// Reorder adapters so the most preferred GPU is first (see +/// adapterPreferenceRank). Backends call this after enumeration so that +/// enumerateAdapters()[0] is the recommended default. The sort is stable, +/// preserving the driver's native order among adapters of equal type. +inline void sortAdaptersByPreference(std::vector& adapters) { + // Stable insertion sort by preference rank (adapter counts are tiny). + for (usize i = 1; i < adapters.size(); ++i) { + Adapter* key = adapters[i]; + const int keyRank = adapterPreferenceRank(key->info().type); + usize j = i; + while (j > 0 && adapterPreferenceRank(adapters[j - 1]->info().type) > keyRank) { + adapters[j] = adapters[j - 1]; + --j; + } + adapters[j] = key; + } +} + +// ---- Device ---- + +/// Central factory for GPU resources, pipelines, and command infrastructure. +class Device { +public: + virtual ~Device() = default; + + DeviceType type = DeviceType::Null; + DeviceFeatures features{}; + + // ---- Queries ---- + [[nodiscard]] virtual Queue* getQueue(QueueType type, u32 index = 0) = 0; + [[nodiscard]] virtual u32 getQueueCount(QueueType type) = 0; + /// Query hardware format support for a given texture format. + [[nodiscard]] virtual FormatSupport getFormatSupport(TextureFormat format) = 0; + + // ---- Resource creation ---- + virtual Status createBuffer(const BufferDesc& desc, Buffer*& out) = 0; + virtual Status createTexture(const TextureDesc& desc, Texture*& out) = 0; + virtual Status createTextureView(Texture* texture, const TextureViewDesc& desc, TextureView*& out) = 0; + virtual Status createSampler(const SamplerDesc& desc, Sampler*& out) = 0; + virtual Status createShaderModule(const ShaderModuleDesc& desc, ShaderModule*& out) = 0; + virtual Status createBindGroupLayout(const BindGroupLayoutDesc& desc, BindGroupLayout*& out) = 0; + virtual Status createBindGroup(const BindGroupDesc& desc, BindGroup*& out) = 0; + virtual Status createPipelineLayout(const PipelineLayoutDesc& desc, PipelineLayout*& out) = 0; + virtual Status createPipelineCache(const PipelineCacheDesc& desc, PipelineCache*& out) = 0; + virtual Status createRenderPipeline(const RenderPipelineDesc& desc, RenderPipeline*& out) = 0; + virtual Status createComputePipeline(const ComputePipelineDesc& desc, ComputePipeline*& out) = 0; + virtual Status createCommandPool(QueueType queueType, CommandPool*& out) = 0; + virtual Status createFence(u64 initialValue, Fence*& out) = 0; + virtual Status createQuerySet(const QuerySetDesc& desc, QuerySet*& out) = 0; + virtual Status createSwapChain(Surface* surface, const SwapChainDesc& desc, SwapChain*& out) = 0; + + // ---- Resource destruction ---- + virtual void destroyBuffer(Buffer*& buf) = 0; + virtual void destroyTexture(Texture*& tex) = 0; + virtual void destroyTextureView(TextureView*& view) = 0; + virtual void destroySampler(Sampler*& sampler) = 0; + virtual void destroyShaderModule(ShaderModule*& shaderModule) = 0; + virtual void destroyBindGroupLayout(BindGroupLayout*& layout) = 0; + virtual void destroyBindGroup(BindGroup*& group) = 0; + virtual void destroyPipelineLayout(PipelineLayout*& layout) = 0; + virtual void destroyPipelineCache(PipelineCache*& cache) = 0; + virtual void destroyRenderPipeline(RenderPipeline*& pipeline) = 0; + virtual void destroyComputePipeline(ComputePipeline*& pipeline) = 0; + virtual void destroyCommandPool(CommandPool*& pool) = 0; + virtual void destroyFence(Fence*& fence) = 0; + virtual void destroyQuerySet(QuerySet*& querySet) = 0; + virtual void destroySwapChain(SwapChain*& swapChain) = 0; + virtual void destroySurface(Surface*& surface) = 0; + + // ---- Mesh shader extension (folded into Device) ---- + /// Create a mesh shader pipeline. Returns Unsupported if mesh shaders + /// are not enabled on this device. + virtual Status createMeshPipeline(const MeshPipelineDesc& desc, MeshPipeline*& out) { + (void)desc; out = nullptr; return ErrorCode::NotSupported; + } + virtual void destroyMeshPipeline(MeshPipeline*& pipeline) { (void)pipeline; } + + // ---- Ray tracing extension (folded into Device) ---- + /// Shader binding table handle properties. Populated by the backend + /// during device creation when ray tracing is enabled. + u32 shaderGroupHandleSize = 0; + u32 shaderGroupHandleAlignment = 0; + u32 shaderGroupBaseAlignment = 0; + + /// Create an acceleration structure. Returns Unsupported if ray tracing + /// is not enabled on this device. + virtual Status createAccelStruct(const AccelStructDesc& desc, AccelStruct*& out) { + (void)desc; out = nullptr; return ErrorCode::NotSupported; + } + virtual void destroyAccelStruct(AccelStruct*& accelStruct) { (void)accelStruct; } + + virtual Status createRayTracingPipeline(const RayTracingPipelineDesc& desc, RayTracingPipeline*& out) { + (void)desc; out = nullptr; return ErrorCode::NotSupported; + } + virtual void destroyRayTracingPipeline(RayTracingPipeline*& pipeline) { (void)pipeline; } + + /// Retrieve shader group handles for building shader binding tables. + virtual Status getShaderGroupHandles(RayTracingPipeline* pipeline, u32 firstGroup, + u32 groupCount, std::span outData) { + (void)pipeline; (void)firstGroup; (void)groupCount; (void)outData; + return ErrorCode::NotSupported; + } + + // ---- Lifecycle ---- + virtual void waitIdle() = 0; + virtual void destroy() = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Enums.cppm b/Engine/cpp/Runtime/Rendering/RHI/Enums.cppm new file mode 100644 index 00000000..bed22d4e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Enums.cppm @@ -0,0 +1,193 @@ +// All RHI enumerations and flag operators. + +export module rhi:enums; + +import core.stdtypes; + +using namespace draco; + +export namespace draco::rhi { + +enum class DeviceType : u32 { Vulkan, DX12, Null }; +enum class QueueType : u32 { Graphics, Compute, Transfer }; + +enum class BufferUsage : u32 { + None = 0, + CopySrc = 1 << 0, + CopyDst = 1 << 1, + Vertex = 1 << 2, + Index = 1 << 3, + Uniform = 1 << 4, + Storage = 1 << 5, + StorageRead = 1 << 6, + Indirect = 1 << 7, + AccelStructInput = 1 << 8, + ShaderBindingTable = 1 << 9, + AccelStructScratch = 1 << 10, +}; +inline constexpr BufferUsage operator|(BufferUsage a, BufferUsage b) { return static_cast(static_cast(a) | static_cast(b)); } +inline constexpr BufferUsage operator&(BufferUsage a, BufferUsage b) { return static_cast(static_cast(a) & static_cast(b)); } +inline constexpr bool hasFlag(BufferUsage v, BufferUsage f) { return (v & f) == f; } + +enum class TextureUsage : u32 { + None = 0, + CopySrc = 1 << 0, + CopyDst = 1 << 1, + Sampled = 1 << 2, + Storage = 1 << 3, + RenderTarget = 1 << 4, + DepthStencil = 1 << 5, + InputAttachment = 1 << 6, +}; +inline constexpr TextureUsage operator|(TextureUsage a, TextureUsage b) { return static_cast(static_cast(a) | static_cast(b)); } +inline constexpr TextureUsage operator&(TextureUsage a, TextureUsage b) { return static_cast(static_cast(a) & static_cast(b)); } + +enum class MemoryLocation : u32 { GpuOnly, CpuToGpu, GpuToCpu, Auto }; + +enum class ResourceState : u32 { + Undefined = 0, + VertexBuffer = 1 << 0, + IndexBuffer = 1 << 1, + UniformBuffer = 1 << 2, + ShaderRead = 1 << 3, + ShaderWrite = 1 << 4, + RenderTarget = 1 << 5, + DepthStencilWrite= 1 << 6, + DepthStencilRead = 1 << 7, + IndirectArgument = 1 << 8, + CopySrc = 1 << 9, + CopyDst = 1 << 10, + Present = 1 << 11, + InputAttachment = 1 << 12, + General = 1 << 13, + AccelStructRead = 1 << 14, + AccelStructWrite = 1 << 15, +}; +inline constexpr ResourceState operator|(ResourceState a, ResourceState b) { return static_cast(static_cast(a) | static_cast(b)); } +inline constexpr ResourceState operator&(ResourceState a, ResourceState b) { return static_cast(static_cast(a) & static_cast(b)); } + +enum class TextureDimension : u32 { Texture1D, Texture2D, Texture3D }; +enum class TextureViewDimension : u32 { Texture1D, Texture1DArray, Texture2D, Texture2DArray, TextureCube, TextureCubeArray, Texture3D }; + +enum class TextureAspect : u32 { All = 0, DepthOnly = 1, StencilOnly = 2 }; + +enum class ShaderStage : u32 { + None = 0, + Vertex = 1 << 0, + Fragment = 1 << 1, + Compute = 1 << 2, + Mesh = 1 << 3, + Task = 1 << 4, + RayGen = 1 << 5, + ClosestHit = 1 << 6, + Miss = 1 << 7, + AnyHit = 1 << 8, + Intersection = 1 << 9, + Callable = 1 << 10, + AllGraphics = Vertex | Fragment, + All = 0x7FF, +}; +inline constexpr ShaderStage operator|(ShaderStage a, ShaderStage b) { return static_cast(static_cast(a) | static_cast(b)); } +inline constexpr ShaderStage operator&(ShaderStage a, ShaderStage b) { return static_cast(static_cast(a) & static_cast(b)); } + +enum class BindingType : u32 { + UniformBuffer, StorageBufferReadOnly, StorageBufferReadWrite, + SampledTexture, StorageTextureReadOnly, StorageTextureReadWrite, + Sampler, ComparisonSampler, + BindlessTextures, BindlessSamplers, BindlessStorageBuffers, BindlessStorageTextures, + AccelerationStructure, +}; + +enum class FilterMode : u32 { Nearest, Linear }; +enum class MipmapFilterMode: u32 { Nearest, Linear }; +enum class AddressMode : u32 { Repeat, MirrorRepeat, ClampToEdge, ClampToBorder }; +enum class SamplerBorderColor : u32 { TransparentBlack, OpaqueBlack, OpaqueWhite }; + +enum class PrimitiveTopology : u32 { PointList, LineList, LineStrip, TriangleList, TriangleStrip }; +enum class FrontFace : u32 { CCW, CW }; +enum class CullMode : u32 { None, Front, Back }; +enum class FillMode : u32 { Solid, Wireframe }; + +enum class CompareFunction : u32 { Never, Less, Equal, LessEqual, Greater, NotEqual, GreaterEqual, Always }; +enum class StencilOperation: u32 { Keep, Zero, Replace, IncrementClamp, DecrementClamp, Invert, IncrementWrap, DecrementWrap }; + +enum class BlendFactor : u32 { + Zero, One, Src, OneMinusSrc, SrcAlpha, OneMinusSrcAlpha, + Dst, OneMinusDst, DstAlpha, OneMinusDstAlpha, + SrcAlphaSaturated, Constant, OneMinusConstant, +}; +enum class BlendOperation : u32 { Add, Subtract, ReverseSubtract, Min, Max }; + +enum class LoadOp : u32 { Load, Clear, DontCare }; +enum class StoreOp : u32 { Store, DontCare }; + +// How a render pass's draw commands are supplied. `Inline` records draws directly into the +// pass (the default). `SecondaryCommandBuffers` means the pass body is supplied by executed +// render bundles only (no inline draws) - Vulkan begins the rendering scope with the secondary- +// command-buffer contents flag; DX12 / WebGPU ignore it (they allow bundles in any pass). +enum class RenderPassContents : u32 { Inline, SecondaryCommandBuffers }; + +enum class IndexFormat : u32 { UInt16, UInt32 }; +enum class VertexStepMode: u32 { Vertex, Instance }; + +enum class VertexFormat : u32 { + Uint8x2, Uint8x4, Sint8x2, Sint8x4, + Unorm8x2, Unorm8x4, Snorm8x2, Snorm8x4, + Uint16x2, Uint16x4, Sint16x2, Sint16x4, + Unorm16x2, Unorm16x4, Snorm16x2, Snorm16x4, + Float16x2, Float16x4, + Float32, Float32x2, Float32x3, Float32x4, + Uint32, Uint32x2, Uint32x3, Uint32x4, + Sint32, Sint32x2, Sint32x3, Sint32x4, +}; + +enum class ColorWriteMask : u8 { + None = 0, + Red = 1 << 0, + Green = 1 << 1, + Blue = 1 << 2, + Alpha = 1 << 3, + All = Red | Green | Blue | Alpha, +}; +inline constexpr ColorWriteMask operator|(ColorWriteMask a, ColorWriteMask b) { return static_cast(static_cast(a) | static_cast(b)); } + +enum class FormatSupport : u32 { + Unsupported = 0, + Texture = 1 << 0, + StorageTexture = 1 << 1, + ColorAttachment = 1 << 2, + DepthStencil = 1 << 3, + Buffer = 1 << 4, + StorageBuffer = 1 << 5, + VertexBuffer = 1 << 6, + BlendableColor = 1 << 7, + LinearFilter = 1 << 8, +}; +inline constexpr FormatSupport operator|(FormatSupport a, FormatSupport b) { return static_cast(static_cast(a) | static_cast(b)); } +inline constexpr FormatSupport operator&(FormatSupport a, FormatSupport b) { return static_cast(static_cast(a) & static_cast(b)); } + +enum class PresentMode : u32 { Immediate, Mailbox, Fifo, FifoRelaxed }; +enum class AdapterType : u32 { DiscreteGpu, IntegratedGpu, Cpu, Unknown }; +enum class QueryType : u32 { Timestamp, Occlusion, PipelineStatistics }; + +// Ray tracing enums. +enum class AccelStructType : u32 { TopLevel, BottomLevel }; +enum class GeometryType : u32 { Triangles, AABBs }; + +enum class GeometryFlags : u32 { + None = 0, + Opaque = 1 << 0, + NoDuplicateAnyHitInvocation = 1 << 1, +}; +inline constexpr GeometryFlags operator|(GeometryFlags a, GeometryFlags b) { return static_cast(static_cast(a) | static_cast(b)); } + +enum class AccelStructBuildFlags : u32 { + None = 0, + AllowUpdate = 1 << 0, + AllowCompaction = 1 << 1, + PreferFastTrace = 1 << 2, + PreferFastBuild = 1 << 3, +}; +inline constexpr AccelStructBuildFlags operator|(AccelStructBuildFlags a, AccelStructBuildFlags b) { return static_cast(static_cast(a) | static_cast(b)); } + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/ExtDescriptors.cppm b/Engine/cpp/Runtime/Rendering/RHI/ExtDescriptors.cppm new file mode 100644 index 00000000..7f7f35fd --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/ExtDescriptors.cppm @@ -0,0 +1,95 @@ +/// Descriptor structs for mesh shader and ray tracing extensions. + +module; + +#include +#include +#include + +export module rhi:ext_descriptors; + +import core.stdtypes; +import :enums; +import :texture_format; +import :types; +import :forward; +import :descriptors; + +using namespace draco; + +export namespace draco::rhi { + +// ---- Mesh shader pipeline ---- + +/// Descriptor for creating a mesh shader pipeline. +struct MeshPipelineDesc { + PipelineLayout* layout = nullptr; + std::optional task; ///< Optional task (amplification) shader. + ProgrammableStage mesh; ///< Required mesh shader. + std::optional fragment; + std::span colorTargets; + PrimitiveState primitive; + std::optional depthStencil; + MultisampleState multisample; + PipelineCache* cache = nullptr; + std::u8string_view label; +}; + +// ---- Ray tracing ---- + +/// Descriptor for creating an acceleration structure. +struct AccelStructDesc { + AccelStructType type = AccelStructType::BottomLevel; + AccelStructBuildFlags flags = AccelStructBuildFlags::PreferFastTrace; + std::u8string_view label; +}; + +/// Triangle geometry for BLAS construction. +struct AccelStructGeometryTriangles { + Buffer* vertexBuffer = nullptr; + u64 vertexOffset = 0; + u32 vertexCount = 0; + u32 vertexStride = 0; + VertexFormat vertexFormat = VertexFormat::Float32x3; + Buffer* indexBuffer = nullptr; + u64 indexOffset = 0; + u32 indexCount = 0; + IndexFormat indexFormat = IndexFormat::UInt32; + Buffer* transformBuffer = nullptr; + u64 transformOffset = 0; + GeometryFlags flags = GeometryFlags::Opaque; +}; + +/// AABB geometry for procedural BLAS construction. +struct AccelStructGeometryAABBs { + Buffer* aabbBuffer = nullptr; + u64 offset = 0; + u32 count = 0; + u32 stride = 24; ///< sizeof(VkAabbPositionsKHR) + GeometryFlags flags = GeometryFlags::Opaque; +}; + +/// Shader group definition for a ray tracing pipeline. +struct RayTracingShaderGroup { + enum class Type : u32 { General, TrianglesHitGroup, ProceduralHitGroup }; + + Type type = Type::General; + u32 generalShaderIndex = ~0u; + u32 closestHitShaderIndex = ~0u; + u32 anyHitShaderIndex = ~0u; + u32 intersectionShaderIndex = ~0u; +}; + +/// Descriptor for creating a ray tracing pipeline. +struct RayTracingPipelineDesc { + PipelineLayout* layout = nullptr; + std::span stages; + std::span groups; + u32 maxRecursionDepth = 1; + u32 maxPayloadSize = 0; + u32 maxAttributeSize = 0; + PipelineCache* cache = nullptr; + std::u8string_view label; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Extensions.cppm b/Engine/cpp/Runtime/Rendering/RHI/Extensions.cppm new file mode 100644 index 00000000..85b57a4f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Extensions.cppm @@ -0,0 +1,76 @@ +/// Extension base classes for multiple inheritance. +/// +/// VK backend encoders inherit these alongside their core base classes: +/// class VkRenderPassEncoder : public RenderPassEncoder, public MeshShaderPassExt { ... }; +/// class VkCommandEncoder : public CommandEncoder, public RayTracingEncoderExt { ... }; +/// +/// Callers probe for support via the asMeshShaderExt() / asRayTracingExt() +/// cross-query methods (which return `this` on supporting encoders, else null). + +module; + +#include + +export module rhi:extensions; + +import core.stdtypes; +import :enums; +import :types; +import :descriptors; +import :ext_descriptors; +import :resources; + +using namespace draco; + +export namespace draco::rhi { + +/// Mesh shader render pass extension. Implemented by backend +/// RenderPassEncoder subclasses that support mesh shaders. +class MeshShaderPassExt { +public: + virtual ~MeshShaderPassExt() = default; + + /// Bind a mesh shader pipeline. + virtual void setMeshPipeline(MeshPipeline* pipeline) = 0; + /// Dispatch mesh shader work groups. + virtual void drawMeshTasks(u32 groupCountX, u32 groupCountY = 1, u32 groupCountZ = 1) = 0; + /// Dispatch mesh shader work groups via an indirect buffer. + virtual void drawMeshTasksIndirect(Buffer* buffer, u64 offset, u32 drawCount = 1, u32 stride = 0) = 0; + /// Dispatch mesh shader work groups with an indirect count buffer. + virtual void drawMeshTasksIndirectCount(Buffer* buffer, u64 offset, + Buffer* countBuffer, u64 countOffset, + u32 maxDrawCount, u32 stride) = 0; +}; + +/// Ray tracing command encoder extension. Implemented by backend +/// CommandEncoder subclasses that support ray tracing. +class RayTracingEncoderExt { +public: + virtual ~RayTracingEncoderExt() = default; + + /// Build a bottom-level acceleration structure from triangle and/or AABB geometry. + virtual void buildBottomLevelAccelStruct(AccelStruct* dst, Buffer* scratchBuffer, u64 scratchOffset, + std::span triangles, + std::span aabbs) = 0; + + /// Build a top-level acceleration structure from an instance buffer. + virtual void buildTopLevelAccelStruct(AccelStruct* dst, Buffer* scratchBuffer, u64 scratchOffset, + Buffer* instanceBuffer, u64 instanceOffset, u32 instanceCount) = 0; + + /// Bind a ray tracing pipeline. + virtual void setRayTracingPipeline(RayTracingPipeline* pipeline) = 0; + + /// Bind a resource group for the ray tracing pipeline. + virtual void setBindGroup(u32 index, BindGroup* group, std::span dynamicOffsets = {}) = 0; + + /// Upload push constants for the ray tracing pipeline. + virtual void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) = 0; + + /// Dispatch rays using shader binding tables. + virtual void traceRays(Buffer* raygenSBT, u64 raygenOffset, u64 raygenStride, + Buffer* missSBT, u64 missOffset, u64 missStride, + Buffer* hitSBT, u64 hitOffset, u64 hitStride, + u32 width, u32 height, u32 depth = 1) = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Forward.cppm b/Engine/cpp/Runtime/Rendering/RHI/Forward.cppm new file mode 100644 index 00000000..b99c76c2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Forward.cppm @@ -0,0 +1,43 @@ +/// Forward declarations for all RHI types. + +export module rhi:forward; + +export namespace draco::rhi { + +class Backend; +class Adapter; +class Device; +class Queue; +class Surface; +class SwapChain; +class Buffer; +class Texture; +class TextureView; +class Sampler; +class ShaderModule; +class Fence; +class QuerySet; +class BindGroupLayout; +class BindGroup; +class PipelineLayout; +class PipelineCache; +class RenderPipeline; +class ComputePipeline; +class MeshPipeline; +class AccelStruct; +class RayTracingPipeline; +class CommandBuffer; +class CommandPool; +class CommandEncoder; +class RenderCommandEncoder; +class RenderPassEncoder; +class RenderBundleEncoder; +class RenderBundle; +class ComputePassEncoder; +class TransferBatch; + +// Extension base classes (for multiple inheritance). +class MeshShaderPassExt; +class RayTracingEncoderExt; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Log.cppm b/Engine/cpp/Runtime/Rendering/RHI/Log.cppm new file mode 100644 index 00000000..a2a069a0 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Log.cppm @@ -0,0 +1,41 @@ +// RHI logging shim. The ported backends use printf-style char* logging +// (logError / logWarning / logErrorf / logWarningf); this routes those to +// stdout/stderr. A thin layer so the large backend bodies port unchanged. + +module; +#include +#include + +export module rhi:log; + +export namespace draco::rhi +{ + inline void logWrite(bool error, const char* utf8) + { + std::FILE* out = error ? stderr : stdout; + std::fputs(utf8, out); + std::fputc('\n', out); + } + + inline void logError(const char* message) { logWrite(true, message); } + inline void logWarning(const char* message) { logWrite(false, message); } + inline void logInfo(const char* message) { logWrite(false, message); } + + inline void logErrorf(const char* fmt, ...) + { + char buf[1024]; + va_list ap; va_start(ap, fmt); + std::vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + logWrite(true, buf); + } + + inline void logWarningf(const char* fmt, ...) + { + char buf[1024]; + va_list ap; va_start(ap, fmt); + std::vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + logWrite(false, buf); + } +} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Null/NullModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/Null/NullModule.cppm new file mode 100644 index 00000000..bc8fa838 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Null/NullModule.cppm @@ -0,0 +1,334 @@ +/// Null RHI backend - stub implementations for all interfaces. +/// Useful for headless testing, CI, or when no GPU is available. + +module; + +#include +#include +#include + +export module rhi.null; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::null { + +// ---- Stub resource classes ---- + +class NullBuffer : public Buffer { +public: + void* map() override { return m_mapped; } + void unmap() override {} + void allocate(u64 size) { m_data.resize(static_cast(size)); m_mapped = m_data.data(); } +private: + std::vector m_data; + void* m_mapped = nullptr; +}; + +class NullTexture : public Texture {}; +class NullTextureView : public TextureView {}; +class NullSampler : public Sampler {}; +class NullShaderModule : public ShaderModule {}; +class NullSurface : public Surface {}; +class NullCommandBuffer : public CommandBuffer {}; + +class NullFence : public Fence { +public: + u64 completedValue() override { return m_value; } + bool wait(u64 value, u64) override { m_value = value; return true; } + void signal(u64 v) { m_value = v; } +private: + u64 m_value = 0; +}; + +class NullQuerySet : public QuerySet {}; + +class NullBindGroupLayout : public BindGroupLayout { +public: + std::span entries() const override { return {}; } +}; + +class NullBindGroup : public BindGroup { +public: + BindGroupLayout* layout() override { return nullptr; } + void updateBindless(std::span) override {} +}; + +class NullPipelineLayout : public PipelineLayout {}; + +class NullPipelineCache : public PipelineCache { +public: + u32 getDataSize() override { return 0; } + Status getData(std::span) override { return ErrorCode::Ok; } +}; + +class NullRenderPipeline : public RenderPipeline {}; +class NullComputePipeline : public ComputePipeline {}; +class NullMeshPipeline : public MeshPipeline {}; + +class NullAccelStruct : public AccelStruct { +public: + AccelStructType type() const override { return AccelStructType::BottomLevel; } + u64 deviceAddress() const override { return 0; } +}; + +class NullRayTracingPipeline : public RayTracingPipeline {}; + +// ---- Stub encoders ---- + +class NullRenderPassEncoder : public RenderPassEncoder, public MeshShaderPassExt { +public: + MeshShaderPassExt* asMeshShaderExt() noexcept override { return this; } + void setPipeline(RenderPipeline*) override {} + void setBindGroup(u32, BindGroup*, std::span) override {} + void setPushConstants(ShaderStage, u32, u32, const void*) override {} + void setVertexBuffer(u32, Buffer*, u64) override {} + void setIndexBuffer(Buffer*, IndexFormat, u64) override {} + void setViewport(f32, f32, f32, f32, f32, f32) override {} + void setScissor(i32, i32, u32, u32) override {} + void setBlendConstant(f32, f32, f32, f32) override {} + void setStencilReference(u32) override {} + void draw(u32, u32, u32, u32) override {} + void drawIndexed(u32, u32, u32, i32, u32) override {} + void drawIndirect(Buffer*, u64, u32, u32) override {} + void drawIndexedIndirect(Buffer*, u64, u32, u32) override {} + void executeBundles(std::span) override {} + void writeTimestamp(QuerySet*, u32) override {} + void beginOcclusionQuery(QuerySet*, u32) override {} + void endOcclusionQuery(QuerySet*, u32) override {} + void end() override {} + void setMeshPipeline(MeshPipeline*) override {} + void drawMeshTasks(u32, u32, u32) override {} + void drawMeshTasksIndirect(Buffer*, u64, u32, u32) override {} + void drawMeshTasksIndirectCount(Buffer*, u64, Buffer*, u64, u32, u32) override {} +}; + +class NullRenderBundle : public RenderBundle {}; + +class NullRenderBundleEncoder : public RenderBundleEncoder { +public: + void setPipeline(RenderPipeline*) override {} + void setBindGroup(u32, BindGroup*, std::span) override {} + void setPushConstants(ShaderStage, u32, u32, const void*) override {} + void setVertexBuffer(u32, Buffer*, u64) override {} + void setIndexBuffer(Buffer*, IndexFormat, u64) override {} + void draw(u32, u32, u32, u32) override {} + void drawIndexed(u32, u32, u32, i32, u32) override {} + void drawIndirect(Buffer*, u64, u32, u32) override {} + void drawIndexedIndirect(Buffer*, u64, u32, u32) override {} + RenderBundle* finish() override { return &bundle; } + NullRenderBundle bundle; +}; + +class NullComputePassEncoder : public ComputePassEncoder { +public: + void setPipeline(ComputePipeline*) override {} + void setBindGroup(u32, BindGroup*, std::span) override {} + void setPushConstants(ShaderStage, u32, u32, const void*) override {} + void dispatch(u32, u32, u32) override {} + void dispatchIndirect(Buffer*, u64) override {} + void computeBarrier() override {} + void writeTimestamp(QuerySet*, u32) override {} + void end() override {} +}; + +class NullCommandEncoder : public CommandEncoder, public RayTracingEncoderExt { +public: + RayTracingEncoderExt* asRayTracingExt() noexcept override { return this; } + NullRenderPassEncoder rpe; + NullComputePassEncoder cpe; + NullRenderBundleEncoder rbe; + NullCommandBuffer cb; + + RenderPassEncoder* beginRenderPass(const RenderPassDesc&) override { return &rpe; } + ComputePassEncoder* beginComputePass(std::u8string_view) override { return &cpe; } + RenderBundleEncoder* createRenderBundleEncoder(const RenderBundleDesc&) override { return &rbe; } + void barrier(const BarrierGroup&) override {} + void copyBufferToBuffer(Buffer*, u64, Buffer*, u64, u64) override {} + void copyBufferToTexture(Buffer*, Texture*, const BufferTextureCopyRegion&) override {} + void copyTextureToBuffer(Texture*, Buffer*, const BufferTextureCopyRegion&) override {} + void copyTextureToTexture(Texture*, Texture*, const TextureCopyRegion&) override {} + void blit(Texture*, Texture*) override {} + void generateMipmaps(Texture*) override {} + void resolveTexture(Texture*, Texture*) override {} + void resetQuerySet(QuerySet*, u32, u32) override {} + void writeTimestamp(QuerySet*, u32) override {} + void resolveQuerySet(QuerySet*, u32, u32, Buffer*, u64) override {} + void beginDebugLabel(std::u8string_view, f32, f32, f32, f32) override {} + void endDebugLabel() override {} + void insertDebugLabel(std::u8string_view, f32, f32, f32, f32) override {} + CommandBuffer* finish() override { return &cb; } + + // RayTracingEncoderExt + void buildBottomLevelAccelStruct(AccelStruct*, Buffer*, u64, std::span, std::span) override {} + void buildTopLevelAccelStruct(AccelStruct*, Buffer*, u64, Buffer*, u64, u32) override {} + void setRayTracingPipeline(RayTracingPipeline*) override {} + void setBindGroup(u32, BindGroup*, std::span) override {} + void setPushConstants(ShaderStage, u32, u32, const void*) override {} + void traceRays(Buffer*, u64, u64, Buffer*, u64, u64, Buffer*, u64, u64, u32, u32, u32) override {} +}; + +class NullCommandPool : public CommandPool { +public: + NullCommandEncoder enc; + Status createEncoder(CommandEncoder*& out) override { out = &enc; return ErrorCode::Ok; } + void destroyEncoder(CommandEncoder*&) override {} + void reset() override {} +}; + +class NullTransferBatch : public TransferBatch { +public: + void writeBuffer(Buffer*, u64, std::span) override {} + void writeTexture(Texture*, std::span, const TextureDataLayout&, Extent3D, u32, u32) override {} + Status submit() override { return ErrorCode::Ok; } + Status submitAsync(Fence*, u64) override { return ErrorCode::Ok; } + void reset() override {} + void destroy() override {} +}; + +class NullSwapChain : public SwapChain { +public: + NullTexture tex; + NullTextureView view; + TextureFormat m_format = TextureFormat::BGRA8UnormSrgb; + u32 m_width = 0; + u32 m_height = 0; + u32 m_count = 2; + u32 m_imgIdx = 0; + + TextureFormat format() const override { return m_format; } + u32 width() const override { return m_width; } + u32 height() const override { return m_height; } + u32 bufferCount() const override { return m_count; } + u32 currentImageIndex() const override { return m_imgIdx; } + Status acquireNextImage() override { m_imgIdx = (m_imgIdx + 1) % m_count; return ErrorCode::Ok; } + Texture* currentTexture() override { return &tex; } + TextureView* currentTextureView() override { return &view; } + Status present(Queue*) override { return ErrorCode::Ok; } + Status resize(u32 w, u32 h) override { m_width = w; m_height = h; return ErrorCode::Ok; } +}; + +class NullQueue : public Queue { +public: + NullTransferBatch tb; + void submit(std::span) override {} + void submit(std::span, Fence* f, u64 v) override { if (auto* nf = static_cast(f)) nf->signal(v); } + void submit(std::span, std::span, std::span, Fence* f, u64 v) override { if (auto* nf = static_cast(f)) nf->signal(v); } + void waitIdle() override {} + Status createTransferBatch(TransferBatch*& out) override { out = &tb; return ErrorCode::Ok; } + void destroyTransferBatch(TransferBatch*&) override {} + f32 timestampPeriod() const override { return 1.0f; } +}; + +// ---- Null Device ---- + +class NullDevice : public Device { +public: + NullQueue gfxQueue, compQueue, xferQueue; + + NullDevice() { + type = DeviceType::Null; + gfxQueue.queueType = QueueType::Graphics; + compQueue.queueType = QueueType::Compute; + xferQueue.queueType = QueueType::Transfer; + } + + Queue* getQueue(QueueType t, u32) override { + switch (t) { + case QueueType::Graphics: return &gfxQueue; + case QueueType::Compute: return &compQueue; + case QueueType::Transfer: return &xferQueue; + } return nullptr; + } + u32 getQueueCount(QueueType) override { return 1; } + FormatSupport getFormatSupport(TextureFormat) override { return FormatSupport::Texture | FormatSupport::ColorAttachment | FormatSupport::DepthStencil; } + + Status createBuffer(const BufferDesc& d, Buffer*& out) override { + auto* b = new NullBuffer(); b->desc = d; b->allocate(d.size); out = b; return ErrorCode::Ok; + } + Status createTexture(const TextureDesc& d, Texture*& out) override { auto* t = new NullTexture(); t->desc = d; out = t; return ErrorCode::Ok; } + Status createTextureView(Texture* tex, const TextureViewDesc& d, TextureView*& out) override { auto* v = new NullTextureView(); v->desc = d; v->texture = tex; out = v; return ErrorCode::Ok; } + Status createSampler(const SamplerDesc& d, Sampler*& out) override { auto* s = new NullSampler(); s->desc = d; out = s; return ErrorCode::Ok; } + Status createShaderModule(const ShaderModuleDesc&, ShaderModule*& out) override { out = new NullShaderModule(); return ErrorCode::Ok; } + Status createBindGroupLayout(const BindGroupLayoutDesc&, BindGroupLayout*& out) override { out = new NullBindGroupLayout(); return ErrorCode::Ok; } + Status createBindGroup(const BindGroupDesc&, BindGroup*& out) override { out = new NullBindGroup(); return ErrorCode::Ok; } + Status createPipelineLayout(const PipelineLayoutDesc&, PipelineLayout*& out) override { out = new NullPipelineLayout(); return ErrorCode::Ok; } + Status createPipelineCache(const PipelineCacheDesc&, PipelineCache*& out) override { out = new NullPipelineCache(); return ErrorCode::Ok; } + Status createRenderPipeline(const RenderPipelineDesc&, RenderPipeline*& out) override { out = new NullRenderPipeline(); return ErrorCode::Ok; } + Status createComputePipeline(const ComputePipelineDesc&, ComputePipeline*& out) override { out = new NullComputePipeline(); return ErrorCode::Ok; } + Status createCommandPool(QueueType, CommandPool*& out) override { out = new NullCommandPool(); return ErrorCode::Ok; } + Status createFence(u64, Fence*& out) override { out = new NullFence(); return ErrorCode::Ok; } + Status createQuerySet(const QuerySetDesc& d, QuerySet*& out) override { auto* q = new NullQuerySet(); q->type = d.type; q->count = d.count; out = q; return ErrorCode::Ok; } + Status createSwapChain(Surface*, const SwapChainDesc& d, SwapChain*& out) override { + auto* sc = new NullSwapChain(); sc->m_format = d.format; sc->m_width = d.width; sc->m_height = d.height; sc->m_count = d.bufferCount; out = sc; return ErrorCode::Ok; + } + + void destroyBuffer(Buffer*& x) override { delete x; x = nullptr; } + void destroyTexture(Texture*& x) override { delete x; x = nullptr; } + void destroyTextureView(TextureView*& x) override { delete x; x = nullptr; } + void destroySampler(Sampler*& x) override { delete x; x = nullptr; } + void destroyShaderModule(ShaderModule*& x)override { delete x; x = nullptr; } + void destroyBindGroupLayout(BindGroupLayout*& x) override { delete x; x = nullptr; } + void destroyBindGroup(BindGroup*& x) override { delete x; x = nullptr; } + void destroyPipelineLayout(PipelineLayout*& x) override { delete x; x = nullptr; } + void destroyPipelineCache(PipelineCache*& x) override { delete x; x = nullptr; } + void destroyRenderPipeline(RenderPipeline*& x) override { delete x; x = nullptr; } + void destroyComputePipeline(ComputePipeline*& x) override { delete x; x = nullptr; } + void destroyCommandPool(CommandPool*& x) override { delete x; x = nullptr; } + void destroyFence(Fence*& x) override { delete x; x = nullptr; } + void destroyQuerySet(QuerySet*& x) override { delete x; x = nullptr; } + void destroySwapChain(SwapChain*& x) override { delete x; x = nullptr; } + void destroySurface(Surface*& x) override { delete x; x = nullptr; } + + void waitIdle() override {} + void destroy() override { delete this; } +}; + +// ---- Null Adapter ---- + +class NullAdapter : public Adapter { +public: + void getInfo(AdapterInfo& out) override { + out.name = u8"Null Device"; + out.vendorId = 0; + out.deviceId = 0; + out.type = AdapterType::Cpu; + } + Status createDevice(const DeviceDesc&, Device*& out) override { + out = new NullDevice(); + return ErrorCode::Ok; + } +}; + +// ---- Null Backend ---- + +class NullBackend : public Backend { +public: + NullAdapter adapter; + Adapter* adapterPtr = &adapter; + + std::span enumerateAdapters() override { + return std::span(&adapterPtr, 1); + } + + Status createSurface(void*, void*, Surface*& out) override { + out = new NullSurface(); + return ErrorCode::Ok; + } + + void destroy() override { delete this; } +}; + +/// Creates a null backend for headless / GPU-less testing. +Status createNullBackend(Backend*& out) { + auto* b = new NullBackend(); + b->isInitialized = true; + out = b; + return ErrorCode::Ok; +} + +} // namespace draco::rhi::null diff --git a/Engine/cpp/Runtime/Rendering/RHI/Null/NullRhi.test.cpp b/Engine/cpp/Runtime/Rendering/RHI/Null/NullRhi.test.cpp new file mode 100644 index 00000000..85cd0f28 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Null/NullRhi.test.cpp @@ -0,0 +1,55 @@ +#include + +import core; +import rhi; +import rhi.null; + +using namespace draco; +using namespace draco::rhi; + +TEST_CASE("rhi.null: backend enumerates an adapter and creates a device") +{ + Backend* backend = nullptr; + REQUIRE(null::createNullBackend(backend).isOk()); + REQUIRE(backend != nullptr); + CHECK(backend->isInitialized); + + auto adapters = backend->enumerateAdapters(); + REQUIRE(adapters.size() == 1u); + + const AdapterInfo info = adapters[0]->info(); + CHECK(info.name == u8"Null Device"); + CHECK(info.type == AdapterType::Cpu); + + Device* device = nullptr; + REQUIRE(adapters[0]->createDevice(DeviceDesc{}, device).isOk()); + REQUIRE(device != nullptr); + + backend->destroy(); +} + +TEST_CASE("rhi.null: device creates resources and a mappable buffer") +{ + Backend* backend = nullptr; + REQUIRE(null::createNullBackend(backend).isOk()); + Device* device = nullptr; + REQUIRE(backend->enumerateAdapters()[0]->createDevice(DeviceDesc{}, device).isOk()); + + BufferDesc bufferDesc{}; + bufferDesc.size = 256; + Buffer* buffer = nullptr; + REQUIRE(device->createBuffer(bufferDesc, buffer).isOk()); + REQUIRE(buffer != nullptr); + CHECK(buffer->map() != nullptr); // null backend backs it with host memory + buffer->unmap(); + + Texture* texture = nullptr; + CHECK(device->createTexture(TextureDesc{}, texture).isOk()); + CHECK(texture != nullptr); + + // Unsupported extensions degrade, they don't crash. + MeshPipeline* mesh = nullptr; + CHECK(device->createMeshPipeline(MeshPipelineDesc{}, mesh).code() == ErrorCode::NotSupported); + + backend->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Pipelines.cpp b/Engine/cpp/Runtime/Rendering/RHI/Pipelines.cpp deleted file mode 100644 index ef711c23..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Pipelines.cpp +++ /dev/null @@ -1,70 +0,0 @@ -module; - -#include -#include -#include "macros.h" - -module rendering.rhi; - -import core.stdtypes; -import core.math.constants; - -namespace draco::rendering::rhi -{ - PipelineHandle createPipeline(const PipelineDesc& desc) - { - RHI_ASSERT(desc.vs != InvalidShader, "Pipeline missing vertex shader"); - RHI_ASSERT(desc.fs != InvalidShader, "Pipeline missing fragment shader"); - - bgfx::ProgramHandle prog = bgfx::createProgram(resolve(desc.vs), resolve(desc.fs), true); - - u64 state = mapState(desc.state, desc.blend, desc.depth, desc.cull, desc.depthWrite); - - return g_pipelines.create({ prog, state }); - } - - LayoutHandle createVertexLayout(const VertexLayoutDesc& desc) - { - bgfx::VertexLayout layout; - layout.begin(); - - for (const auto& e : desc.elements) - { - layout.add(map_attrib(e.attrib), e.count, map_attrib_type(e.type), e.normalized); - } - - layout.end(); - - return g_layouts.create({ layout }); - } - - ShaderHandle createShader(const void* data, u32 size) - { - RHI_ASSERT(data && size > 0, "Invalid shader data"); - - bgfx::ShaderHandle sh = bgfx::createShader(bgfx::copy(data, size)); - - return g_shaders.create(sh); - } - - bgfx::ShaderHandle resolve(ShaderHandle h) - { - auto* sh = g_shaders.get(h); - return sh ? *sh : bgfx::ShaderHandle{ bgfx::kInvalidHandle }; - } - - // For debugging/tooling - bgfx::ShaderHandle* getShaderNative(ShaderHandle h) - { - return getChecked(g_shaders, h, "Shader"); - } - - void destroyShader(ShaderHandle h) - { - if (auto* sh = getChecked(g_shaders, h, "Shader")) - { - destroyLater(*sh); - g_shaders.destroy(h); - } - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Queue.cppm b/Engine/cpp/Runtime/Rendering/RHI/Queue.cppm new file mode 100644 index 00000000..7993da1e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Queue.cppm @@ -0,0 +1,52 @@ +/// Abstract GPU command queue. Handles command submission, fence +/// synchronization, and transfer batch creation. + +module; + +#include + +export module rhi:queue; + +import core.stdtypes; +import core.status; +import :enums; +import :resources; + +using namespace draco; + +export namespace draco::rhi { + +class TransferBatch; + +class Queue { +public: + virtual ~Queue() = default; + + /// The type of work this queue supports. + QueueType queueType = QueueType::Graphics; + + /// Submit command buffers for execution. + virtual void submit(std::span commandBuffers) = 0; + + /// Submit with fence signaling. + virtual void submit(std::span commandBuffers, + Fence* signalFence, u64 signalValue) = 0; + + /// Submit with full synchronization: wait on fences, then signal. + virtual void submit(std::span commandBuffers, + std::span waitFences, std::span waitValues, + Fence* signalFence, u64 signalValue) = 0; + + /// Block until all submitted work on this queue completes. + virtual void waitIdle() = 0; + + /// Create a transfer batch for batching CPU->GPU uploads. + virtual Status createTransferBatch(TransferBatch*& out) = 0; + /// Destroy a transfer batch. + virtual void destroyTransferBatch(TransferBatch*& batch) = 0; + + /// Timestamp period in nanoseconds per tick. + [[nodiscard]] virtual f32 timestampPeriod() const = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/RHI.cppm b/Engine/cpp/Runtime/Rendering/RHI/RHI.cppm deleted file mode 100644 index 491e8c95..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/RHI.cppm +++ /dev/null @@ -1,340 +0,0 @@ -module; - -#include -#include -#include -#include -#include - -#include - -#include "macros.h" - -export module rendering.rhi; - -import core.stdtypes; -import core.math.constants; -import core.memory; -import rendering.rhi.vertex; - -export namespace draco::rendering::rhi -{ - // Which native windowing system the handles passed to init() belong to. RHI - // only needs this to tell bgfx when a surface is Wayland (a wl_surface* must - // be flagged as such); everything else uses bgfx's default handling. The - // caller maps its own window abstraction onto this. - enum class NativeWindowType - { - Default, - Win32, - X11, - Wayland, - Cocoa, - }; - - struct BufferTag {}; - struct PipelineTag {}; - struct ShaderTag {}; - struct UniformTag {}; - struct TextureTag {}; - struct FramebufferTag {}; - struct LayoutTag {}; - - using BufferHandle = core::memory::Handle; - using PipelineHandle = core::memory::Handle; - using ShaderHandle = core::memory::Handle; - using UniformHandle = core::memory::Handle; - using TextureHandle = core::memory::Handle; - using FramebufferHandle = core::memory::Handle; - using LayoutHandle = core::memory::Handle; - - using ViewID = u16; // bgfx native - using SamplerHandle = u64; // bgfx sampler flags - - inline constexpr BufferHandle InvalidBuffer{}; - inline constexpr PipelineHandle InvalidPipeline{}; - inline constexpr ShaderHandle InvalidShader{}; - inline constexpr UniformHandle InvalidUniform{}; - inline constexpr TextureHandle InvalidTexture{}; - inline constexpr FramebufferHandle InvalidFramebuffer{}; - inline constexpr LayoutHandle InvalidLayout{}; - inline constexpr SamplerHandle InvalidSampler = 0; - inline constexpr ViewID InvalidView = math::UINT16_MAX_VAL; - - enum class PipelineState : u64 { - Default = 0, - WriteRGB = 1ULL << 0, - WriteAlpha = 1ULL << 1, - MSAA = 1ULL << 2, - PrimitiveTriStrip = 1ULL << 3, - }; - - enum class ClearFlags : u32 { - Color = BGFX_CLEAR_COLOR, - Depth = BGFX_CLEAR_DEPTH, - Stencil = BGFX_CLEAR_STENCIL - }; - - struct ViewDesc { - FramebufferHandle fb = InvalidFramebuffer; - u16 x = 0, y = 0, w = 0, h = 0; - u32 clearFlags = 0; - u32 clearColor = 0; - }; - - enum class UniformType - { - Sampler, - Vec4, - Mat3, - Mat4, - }; - - struct UniformBind { - UniformHandle handle; - const void* data; - u16 num; - }; - - enum class TextureFormat { - RGBA8, - BGRA8, - D16, - D24, - D24S8, - D32 - }; - - enum class BlendMode { - None, - Alpha, - Additive, - Multiply - }; - - enum class DepthTest { - None, - Less, - Equal, - Always - }; - - enum class CullMode { - None, - CW, - CCW - }; - - struct Buffer - { - bgfx::VertexBufferHandle vbh = BGFX_INVALID_HANDLE; - bgfx::DynamicVertexBufferHandle dvbh = BGFX_INVALID_HANDLE; - bgfx::IndexBufferHandle ibh = BGFX_INVALID_HANDLE; - bgfx::DynamicIndexBufferHandle dibh; - bool isDynamic = false; - bool isIndex = false; - }; - - struct FramebufferResource { - bgfx::FrameBufferHandle fbh; - TextureHandle texture; - }; - - struct VertexLayoutResource - { - bgfx::VertexLayout layout; - }; - - struct ScissorRect { - u16 x, y, w, h; - bool enabled = true; - }; - - struct DeletionReq { - u64 frame; - std::function cleanup; - }; - - struct PipelineDesc - { - ShaderHandle vs; - ShaderHandle fs; - PipelineState state = PipelineState::Default; - - BlendMode blend = BlendMode::None; - DepthTest depth = DepthTest::Less; - CullMode cull = CullMode::CCW; - - bool depthWrite = true; - }; - - struct RenderPacket - { - u64 sortKey = 0; - - BufferHandle vertexBuffer = InvalidBuffer; - BufferHandle indexBuffer = InvalidBuffer; - PipelineHandle pipeline = InvalidPipeline; - - u32 vertexCount = math::UINT32_MAX_VAL; - u32 indexCount = math::UINT32_MAX_VAL; - - UniformHandle samplerUniform = InvalidUniform; - SamplerHandle samplerFlags = InvalidSampler; - TextureHandle textureHandle = InvalidTexture; - - f32 color[4] = {1,1,1,1}; - - std::vector uniforms; - u8 textureUnit = 0; - - f32 model[16] = { - 1,0,0,0, - 0,1,0,0, - 0,0,1,0, - 0,0,0,1 - }; - - u32 drawTags = 0; - }; - - struct Pipeline { - bgfx::ProgramHandle program; - u64 state; - }; - - bool init(void* display_type, void* window_handle, NativeWindowType window_type, u16 width, u16 height); - void resize(u16 width, u16 height); - void shutdown(); - - PipelineHandle createPipeline(const PipelineDesc&); - - BufferHandle createVertexBuffer(const void* data, u32 size, LayoutHandle layout_h); - BufferHandle createIndexBuffer(const void* data, u32 size); - void destroyBuffer(BufferHandle handle); - - UniformHandle createUniform(const char* name, UniformType type, u16 num = 1); - void destroyUniform(UniformHandle handle); - void setUniform(UniformHandle handle, const void* value, u16 num = 1); - - TextureHandle createTexture(const void* data, u32 width, u32 height, u32 flags = 0); - void destroyTexture(TextureHandle handle); - - FramebufferHandle createFramebuffer(u32 width, u32 height, TextureFormat format); - void destroyFramebuffer(FramebufferHandle handle); - TextureHandle getFramebufferTexture(FramebufferHandle handle); - - BufferHandle createDynamicVertexBuffer(u32 size, LayoutHandle layout); - void updateDynamicVertexBuffer(BufferHandle handle, u32 start_vertex, const void* data, u32 size); - - BufferHandle createDynamicIndexBuffer(u32 size, u16 flags = BGFX_BUFFER_NONE); - void updateDynamicIndexBuffer(BufferHandle handle, u32 start_index, const void* data, u32 size); - - LayoutHandle createVertexLayout(const VertexLayoutDesc& desc); - - SamplerHandle createSampler(bool linear, bool clamp); - - // Expects bgfx compiled shader binary (shaderc output) - ShaderHandle createShader(const void* data, u32 size); - bgfx::ShaderHandle resolve(ShaderHandle h); - // For debugging/tooling - bgfx::ShaderHandle* getShaderNative(ShaderHandle h); - void destroyShader(ShaderHandle h); - - void perspective(f32* out, f32 fov, f32 aspect, f32 nearp, f32 farp); - void lookAt(f32* out, const f32* eye, const f32* at, const f32* up); - - // Note: Internal use only, use apply_view() instead - void setViewRect(ViewID view, u16 x, u16 y, u16 w, u16 h); - void setViewFramebuffer(ViewID view, FramebufferHandle handle); - - void setViewProjection(ViewID view, const f32* view_mtx, const f32* proj_mtx); - void setScissor(const ScissorRect& r); - void setStencil(u32 f_stencil, u32 b_stencil); - - void applyView(ViewID view, const ViewDesc& desc); - - void identityMatrix(f32* _mtx); - - u64 mapState(PipelineState s, BlendMode, DepthTest, CullMode, bool depth_write); - bgfx::UniformType::Enum map_uniform_type(UniformType t); - bgfx::Attrib::Enum map_attrib(Attrib a); - bgfx::AttribType::Enum map_attrib_type(AttribType t); - - void submit(const RenderPacket&, ViewID); - - void beginFrame(); - void endFrame(); - - template - void destroyResource(T handle); - - void processDeletions(); - - inline u64 makeSortKey(u8 layer, u8 pass, u16 pipeline, u16 texture, u16 depth = 0) - { - return (u64(layer) << 56) | (u64(pass) << 48) | (u64(pipeline) << 32) | (u64(texture) << 16) | u64(depth); - } - - constexpr PipelineState operator|(PipelineState a, PipelineState b) { - return static_cast(static_cast(a) | static_cast(b)); - } - - constexpr PipelineState operator&(PipelineState a, PipelineState b) { - return static_cast(static_cast(a) & static_cast(b)); - } -} - -// These are the things that we don't export but are visible to all implementation files -namespace draco::rendering::rhi -{ - using namespace draco::core::memory; - - extern HandleRegistry g_buffers; - extern HandleRegistry g_pipelines; - extern HandleRegistry g_uniforms; - extern HandleRegistry g_textures; - extern HandleRegistry g_framebuffers; - extern HandleRegistry g_shaders; - extern HandleRegistry g_layouts; - - // Deferred destruction queue (GPU-safe deletion) - extern std::vector g_deletion_queue; - - extern u16 g_width; - extern u16 g_height; - - // Ensures a handle is valid before use - // TODO: Replace with something better - template - auto* getChecked(Registry& reg, HandleT h, const char* name) - { - if (!reg.valid(h)) - { - RHI_WARN(false, "{} handle invalid or stale!", name); - return (decltype(reg.get(h)))nullptr; - } - - return reg.get(h); - } -} - -// Re-export the namespace since the things it uses aren't declared before -// This is a bit hacky but it works -// The problem is that if we move the unexported namespace above the other exported namespace, stuff isn't declared yet & it gives errors -// The same goes for the exported namespace -// In a nutshell, they both rely on each other which is why we use this hack (AR-DEV-1) -export namespace draco::rendering::rhi -{ - void queueDestruction(std::function cb); - - // Explicit overloads for each bgfx resource type - void destroyLater(bgfx::ShaderHandle handle); - void destroyLater(bgfx::UniformHandle handle); - void destroyLater(bgfx::VertexBufferHandle handle); - void destroyLater(bgfx::IndexBufferHandle handle); - void destroyLater(bgfx::DynamicVertexBufferHandle handle); - void destroyLater(bgfx::DynamicIndexBufferHandle handle); - void destroyLater(bgfx::TextureHandle handle); - void destroyLater(bgfx::FrameBufferHandle handle); -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Resources.cppm b/Engine/cpp/Runtime/Rendering/RHI/Resources.cppm new file mode 100644 index 00000000..4e143822 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Resources.cppm @@ -0,0 +1,174 @@ +/// Abstract resource classes. Each represents a GPU-allocated object +/// owned and destroyed by the Device. + +module; + +#include + +export module rhi:resources; + +import core.stdtypes; +import core.status; +import :enums; +import :texture_format; +import :types; +import :descriptors; +import :ext_descriptors; + +using namespace draco; + +export namespace draco::rhi { + +/// GPU buffer (vertex, index, uniform, storage, etc.). +class Buffer { +public: + virtual ~Buffer() = default; + + BufferDesc desc{}; + + [[nodiscard]] u64 getSize() const { return desc.size; } + [[nodiscard]] BufferUsage usage() const { return desc.usage; } + + /// Map the buffer for CPU access. Returns nullptr if not mappable. + [[nodiscard]] virtual void* map() = 0; + virtual void unmap() = 0; +}; + +/// GPU texture (1D/2D/3D, with mip levels and array layers). +class Texture { +public: + virtual ~Texture() = default; + + TextureDesc desc{}; + ResourceState initialState = ResourceState::Undefined; +}; + +/// A view into a subset of a texture's mip levels and array layers. +class TextureView { +public: + virtual ~TextureView() = default; + + TextureViewDesc desc{}; + Texture* texture = nullptr; +}; + +/// Texture sampler (filtering, addressing, comparison). +class Sampler { +public: + virtual ~Sampler() = default; + SamplerDesc desc{}; +}; + +/// Compiled shader module (SPIR-V or DXIL bytecode). +class ShaderModule { +public: + virtual ~ShaderModule() = default; +}; + +/// Presentation surface created from a native window handle. +class Surface { +public: + virtual ~Surface() = default; +}; + +/// Immutable recorded command buffer, ready for queue submission. +class CommandBuffer { +public: + virtual ~CommandBuffer() = default; +}; + +/// Timeline fence for CPU/GPU synchronization. +class Fence { +public: + virtual ~Fence() = default; + + /// Returns the most recently completed (signaled) value. + [[nodiscard]] virtual u64 completedValue() = 0; + + /// Blocks the CPU until the fence reaches `value`, or until timeout. + /// Returns true on success, false on timeout. + virtual bool wait(u64 value, u64 timeoutNs = ~0ull) = 0; +}; + +/// GPU query set (timestamp, occlusion, pipeline statistics). +class QuerySet { +public: + virtual ~QuerySet() = default; + + QueryType type = QueryType::Timestamp; + u32 count = 0; +}; + +/// Defines the layout of resource bindings for a bind group. +class BindGroupLayout { +public: + virtual ~BindGroupLayout() = default; + + [[nodiscard]] virtual std::span entries() const = 0; +}; + +/// A set of resource bindings that can be bound to a pipeline. +class BindGroup { +public: + virtual ~BindGroup() = default; + + [[nodiscard]] virtual BindGroupLayout* layout() = 0; + + /// Update individual entries in a bindless bind group. + virtual void updateBindless(std::span entries) = 0; +}; + +/// Describes the resource binding layout for a pipeline (bind group +/// layouts + push constant ranges). +class PipelineLayout { +public: + virtual ~PipelineLayout() = default; +}; + +/// Caches compiled pipeline state for faster subsequent creation. +class PipelineCache { +public: + virtual ~PipelineCache() = default; + + [[nodiscard]] virtual u32 getDataSize() = 0; + virtual Status getData(std::span outData) = 0; +}; + +/// Compiled graphics (rasterization) pipeline. +class RenderPipeline { +public: + virtual ~RenderPipeline() = default; + PipelineLayout* layout = nullptr; +}; + +/// Compiled compute pipeline. +class ComputePipeline { +public: + virtual ~ComputePipeline() = default; + PipelineLayout* layout = nullptr; +}; + +/// Compiled mesh shader pipeline. +class MeshPipeline { +public: + virtual ~MeshPipeline() = default; + PipelineLayout* layout = nullptr; +}; + +/// Bottom-level or top-level acceleration structure for ray tracing. +class AccelStruct { +public: + virtual ~AccelStruct() = default; + + [[nodiscard]] virtual AccelStructType type() const = 0; + [[nodiscard]] virtual u64 deviceAddress() const = 0; +}; + +/// Compiled ray tracing pipeline. +class RayTracingPipeline { +public: + virtual ~RayTracingPipeline() = default; + PipelineLayout* layout = nullptr; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/RhiModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/RhiModule.cppm new file mode 100644 index 00000000..b3a39714 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/RhiModule.cppm @@ -0,0 +1,15 @@ +export module rhi; + +export import :log; +export import :enums; +export import :texture_format; +export import :types; +export import :forward; +export import :descriptors; +export import :ext_descriptors; +export import :resources; +export import :commands; +export import :extensions; +export import :queue; +export import :swapchain; +export import :device; diff --git a/Engine/cpp/Runtime/Rendering/RHI/SwapChain.cppm b/Engine/cpp/Runtime/Rendering/RHI/SwapChain.cppm new file mode 100644 index 00000000..c6205c94 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/SwapChain.cppm @@ -0,0 +1,46 @@ +/// Abstract swap chain for double/triple-buffered presentation. + +export module rhi:swapchain; + +import core.stdtypes; +import core.status; +import :enums; +import :texture_format; +import :resources; +import :queue; + +using namespace draco; + +export namespace draco::rhi { + +class SwapChain { +public: + virtual ~SwapChain() = default; + + /// Format of the swap chain back buffers. + [[nodiscard]] virtual TextureFormat format() const = 0; + /// Width in pixels. + [[nodiscard]] virtual u32 width() const = 0; + /// Height in pixels. + [[nodiscard]] virtual u32 height() const = 0; + /// Number of back buffers. + [[nodiscard]] virtual u32 bufferCount() const = 0; + /// Index of the currently acquired back buffer. + [[nodiscard]] virtual u32 currentImageIndex() const = 0; + + /// Acquire the next back buffer for rendering. Must be called + /// before accessing currentTexture / currentTextureView. + virtual Status acquireNextImage() = 0; + + /// The texture of the currently acquired back buffer. + [[nodiscard]] virtual Texture* currentTexture() = 0; + /// A view of the currently acquired back buffer. + [[nodiscard]] virtual TextureView* currentTextureView() = 0; + + /// Present the current back buffer to the screen. + virtual Status present(Queue* queue) = 0; + /// Resize the swap chain (e.g. after a window resize). + virtual Status resize(u32 width, u32 height) = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Texture.cpp b/Engine/cpp/Runtime/Rendering/RHI/Texture.cpp deleted file mode 100644 index 4a9193ce..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Texture.cpp +++ /dev/null @@ -1,126 +0,0 @@ -module; - -#include -#include "macros.h" - -module rendering.rhi; - -import core.stdtypes; -import core.math.constants; - -namespace draco::rendering::rhi -{ - UniformHandle createUniform(const char* name, UniformType type, u16 num) - { - RHI_ASSERT(name != nullptr, "Uniform name is null"); - - auto u = bgfx::createUniform(name, map_uniform_type(type), num); - return g_uniforms.create(u); - } - - void setUniform(UniformHandle h, const void* data, u16 num) - { - auto* u = getChecked(g_uniforms, h, "Uniform"); - if (!u) return; - - RHI_ASSERT(data != nullptr, "Uniform data is null"); - - bgfx::setUniform(*u, data, num); - } - - void destroyUniform(UniformHandle h) - { - auto* u = getChecked(g_uniforms, h, "Uniform"); - if (!u) return; - - destroyLater(*u); - g_uniforms.destroy(h); - } - - TextureHandle createTexture(const void* data, u32 w, u32 h, u32 flags) - { - RHI_ASSERT(data != nullptr, "Texture data is null"); - RHI_ASSERT(w > 0 && h > 0, "Invalid texture dimensions"); - - auto tex = bgfx::createTexture2D( - w, h, false, 1, - bgfx::TextureFormat::RGBA8, - flags == 0 ? (BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP) : flags, - bgfx::copy(data, w * h * 4) - ); - - return g_textures.create(tex); - } - - void destroyTexture(TextureHandle h) - { - auto* tex = getChecked(g_textures, h, "Texture"); - if (!tex) return; - - destroyLater(*tex); - g_textures.destroy(h); - } - - FramebufferHandle createFramebuffer(u32 width, u32 height, TextureFormat format) - { - // We set render target flags so it can be attached to a framebuffer object - u64 flags = BGFX_TEXTURE_RT | BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; - - bgfx::TextureFormat::Enum bgfx_format = bgfx::TextureFormat::RGBA8; - - bgfx::TextureHandle th = bgfx::createTexture2D(static_cast(width), static_cast(height), false, 1, bgfx_format, flags); - - RHI_ASSERT(bgfx::isValid(th), "Failed to allocate backing texture for Framebuffer"); - - TextureHandle color_tex_h = g_textures.create(th); - - bgfx::FrameBufferHandle fbh = bgfx::createFrameBuffer(1, &th, false); - - if (!bgfx::isValid(fbh)) - { - RHI_WARN(false, "Failed to construct native bgfx Framebuffer target!"); - // Roll back the allocated texture if the framebuffer generation bricks - destroyTexture(color_tex_h); - return InvalidFramebuffer; - } - - FramebufferResource res{}; - res.fbh = fbh; - res.texture = color_tex_h; - - return g_framebuffers.create(res); - } - - void destroyFramebuffer(FramebufferHandle handle) - { - if (auto* fb = getChecked(g_framebuffers, handle, "Framebuffer")) - { - // Safely queue the native hardware framebuffer destruction 2 frames out - destroyLater(fb->fbh); - - // Clean up the associated internal texture resource using existing pipelines - if (fb->texture != InvalidTexture) - { - if (auto* th = g_textures.get(fb->texture)) - { - destroyLater(*th); - } - g_textures.destroy(fb->texture); - } - - // Evict our registry tracking slot - g_framebuffers.destroy(handle); - } - } - - TextureHandle getFramebufferTexture(FramebufferHandle handle) - { - auto* fb = getChecked(g_framebuffers, handle, "Framebuffer"); - if (!fb) - { - return InvalidTexture; - } - - return fb->texture; - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/TextureFormat.cppm b/Engine/cpp/Runtime/Rendering/RHI/TextureFormat.cppm new file mode 100644 index 00000000..aa7c78f5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/TextureFormat.cppm @@ -0,0 +1,157 @@ +/// Texture format enum and query helpers. + +export module rhi:texture_format; + +import core.stdtypes; + +using namespace draco; + +export namespace draco::rhi { + +/// Pixel formats for textures and render targets. Naming follows +/// WebGPU / Vulkan conventions: component layout + bit-depth + type. +enum class TextureFormat : u32 { + Undefined = 0, + + // 8-bit per channel. + R8Unorm, R8Snorm, R8Uint, R8Sint, + + // 16-bit per channel. + R16Uint, R16Sint, R16Float, + RG8Unorm, RG8Snorm, RG8Uint, RG8Sint, + + // 32-bit per channel. + R32Uint, R32Sint, R32Float, + RG16Uint, RG16Sint, RG16Float, + RGBA8Unorm, RGBA8UnormSrgb, RGBA8Snorm, RGBA8Uint, RGBA8Sint, + BGRA8Unorm, BGRA8UnormSrgb, + RGB10A2Unorm, RGB10A2Uint, + RG11B10Float, RGB9E5Float, + + // 64-bit per channel. + RG32Uint, RG32Sint, RG32Float, + RGBA16Uint, RGBA16Sint, RGBA16Float, RGBA16Unorm, RGBA16Snorm, + + // 128-bit per channel. + RGBA32Uint, RGBA32Sint, RGBA32Float, + + // Depth / stencil. + Depth16Unorm, Depth24Plus, Depth24PlusStencil8, + Depth32Float, Depth32FloatStencil8, Stencil8, + + // BC compressed. + BC1RGBAUnorm, BC1RGBAUnormSrgb, + BC2RGBAUnorm, BC2RGBAUnormSrgb, + BC3RGBAUnorm, BC3RGBAUnormSrgb, + BC4RUnorm, BC4RSnorm, + BC5RGUnorm, BC5RGSnorm, + BC6HRGBUfloat, BC6HRGBFloat, + BC7RGBAUnorm, BC7RGBAUnormSrgb, + + // ASTC compressed. + ASTC4x4Unorm, ASTC4x4UnormSrgb, + ASTC5x5Unorm, ASTC5x5UnormSrgb, + ASTC6x6Unorm, ASTC6x6UnormSrgb, + ASTC8x8Unorm, ASTC8x8UnormSrgb, +}; + +/// True for Depth16Unorm, Depth24Plus, Depth32Float and their stencil variants. +[[nodiscard]] constexpr bool isDepthFormat(TextureFormat f) { + return f >= TextureFormat::Depth16Unorm && f <= TextureFormat::Depth32FloatStencil8; +} + +/// True for any depth or stencil format (includes Stencil8). +[[nodiscard]] constexpr bool isDepthStencil(TextureFormat f) { + return f >= TextureFormat::Depth16Unorm && f <= TextureFormat::Stencil8; +} + +/// True if the format has a depth component. +[[nodiscard]] constexpr bool hasDepth(TextureFormat f) { + switch (f) { + case TextureFormat::Depth16Unorm: + case TextureFormat::Depth24Plus: + case TextureFormat::Depth24PlusStencil8: + case TextureFormat::Depth32Float: + case TextureFormat::Depth32FloatStencil8: + return true; + default: return false; + } +} + +/// True if the format has a stencil component. +[[nodiscard]] constexpr bool hasStencil(TextureFormat f) { + switch (f) { + case TextureFormat::Depth24PlusStencil8: + case TextureFormat::Depth32FloatStencil8: + case TextureFormat::Stencil8: + return true; + default: return false; + } +} + +/// True for BC or ASTC compressed formats. +[[nodiscard]] constexpr bool isCompressed(TextureFormat f) { + return f >= TextureFormat::BC1RGBAUnorm && f <= TextureFormat::ASTC8x8UnormSrgb; +} + +/// True for sRGB variants. +[[nodiscard]] constexpr bool isSrgb(TextureFormat f) { + switch (f) { + case TextureFormat::RGBA8UnormSrgb: + case TextureFormat::BGRA8UnormSrgb: + case TextureFormat::BC1RGBAUnormSrgb: + case TextureFormat::BC2RGBAUnormSrgb: + case TextureFormat::BC3RGBAUnormSrgb: + case TextureFormat::BC7RGBAUnormSrgb: + case TextureFormat::ASTC4x4UnormSrgb: + case TextureFormat::ASTC5x5UnormSrgb: + case TextureFormat::ASTC6x6UnormSrgb: + case TextureFormat::ASTC8x8UnormSrgb: + return true; + default: return false; + } +} + +/// Returns bytes per pixel for uncompressed formats; 0 for compressed or unknown. +[[nodiscard]] constexpr u32 bytesPerPixel(TextureFormat f) { + switch (f) { + case TextureFormat::R8Unorm: + case TextureFormat::Stencil8: + return 1; + case TextureFormat::R16Uint: + case TextureFormat::R16Sint: + case TextureFormat::R16Float: + case TextureFormat::RG8Unorm: + case TextureFormat::Depth16Unorm: + return 2; + case TextureFormat::RGBA8Unorm: + case TextureFormat::RGBA8UnormSrgb: + case TextureFormat::BGRA8Unorm: + case TextureFormat::BGRA8UnormSrgb: + case TextureFormat::RG16Float: + case TextureFormat::R32Float: + case TextureFormat::R32Uint: + case TextureFormat::R32Sint: + case TextureFormat::RGB10A2Unorm: + case TextureFormat::RG11B10Float: + case TextureFormat::Depth24Plus: + case TextureFormat::Depth24PlusStencil8: + case TextureFormat::Depth32Float: + return 4; + case TextureFormat::Depth32FloatStencil8: + return 8; + case TextureFormat::RG32Float: + case TextureFormat::RG32Uint: + case TextureFormat::RGBA16Float: + case TextureFormat::RGBA16Uint: + case TextureFormat::RGBA16Sint: + return 8; + case TextureFormat::RGBA32Float: + case TextureFormat::RGBA32Uint: + case TextureFormat::RGBA32Sint: + return 16; + default: return 0; // Compressed or unknown. + } +} + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/Types.cppm b/Engine/cpp/Runtime/Rendering/RHI/Types.cppm new file mode 100644 index 00000000..9ed60c8d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Types.cppm @@ -0,0 +1,142 @@ +/// Core RHI types: geometry primitives, adapter info, device features, +/// device descriptor, copy regions, and push constant ranges. + +module; + +#include +#include + +export module rhi:types; + +import core.stdtypes; +import :enums; +import :texture_format; + +using namespace draco; + +export namespace draco::rhi { + +/// Maximum number of simultaneous color attachments. +constexpr i32 maxColorAttachments = 8; + +// ---- Geometry primitives ---- + +struct Extent3D { + u32 width = 0; + u32 height = 1; + u32 depth = 1; +}; + +struct Origin3D { + u32 x = 0; + u32 y = 0; + u32 z = 0; +}; + +/// RGBA clear color in linear space. +struct ClearColor { + f32 r = 0.0f, g = 0.0f, b = 0.0f, a = 1.0f; + + static constexpr ClearColor black() { return { 0, 0, 0, 1 }; } + static constexpr ClearColor white() { return { 1, 1, 1, 1 }; } + static constexpr ClearColor cornflowerBlue() { return { 0.392f, 0.584f, 0.929f, 1.0f }; } +}; + +// ---- Adapter / Device info ---- + +/// Information about a physical GPU adapter. +struct AdapterInfo { + std::u8string name; + u32 vendorId = 0; + u32 deviceId = 0; + AdapterType type = AdapterType::Unknown; + + /// Features and limits the adapter supports. Populate via the backend's + /// adapter enumeration; the caller inspects these before creating a device. + struct Features { + // Feature flags. + bool bindlessDescriptors = false; + bool timestampQueries = false; + bool pipelineStatisticsQueries= false; + bool multiDrawIndirect = false; + bool depthClamp = false; + bool fillModeWireframe = false; + bool textureCompressionBC = false; + bool textureCompressionASTC = false; + bool independentBlend = false; + bool multiViewport = false; + bool meshShaders = false; + bool rayTracing = false; + + // Limits. + u32 maxBindGroups = 4; + u32 maxBindingsPerGroup = 16; + u32 maxPushConstantSize = 128; + u32 maxTextureDimension2D = 8192; + u32 maxTextureArrayLayers = 256; + u32 maxComputeWorkgroupSizeX = 256; + u32 maxComputeWorkgroupSizeY = 256; + u32 maxComputeWorkgroupSizeZ = 64; + u32 maxComputeWorkgroupsPerDimension = 65535; + u32 minUniformBufferOffsetAlignment = 256; + u32 minStorageBufferOffsetAlignment = 256; + u32 timestampPeriodNs = 1; + u64 maxBufferSize = 256ull * 1024 * 1024; + + // Mesh shader limits. + u32 maxMeshOutputVertices = 0; + u32 maxMeshOutputPrimitives = 0; + u32 maxMeshWorkgroupSize = 0; + u32 maxTaskWorkgroupSize = 0; + } supportedFeatures; +}; + +/// Features and limits requested when creating a device. +using DeviceFeatures = AdapterInfo::Features; + +/// Descriptor for logical device creation. +struct DeviceDesc { + DeviceFeatures requiredFeatures{}; + u32 graphicsQueueCount = 1; + u32 computeQueueCount = 0; + u32 transferQueueCount = 0; + std::u8string_view label; +}; + +// ---- Copy regions ---- + +/// Layout of a buffer holding texture data (for CPU->GPU uploads). +struct TextureDataLayout { + u64 offset = 0; + u32 bytesPerRow = 0; + u32 rowsPerImage = 0; +}; + +/// Region for texture-to-texture copies. +struct TextureCopyRegion { + u32 srcMipLevel = 0; + u32 srcArrayLayer = 0; + u32 dstMipLevel = 0; + u32 dstArrayLayer = 0; + Extent3D extent; +}; + +/// Region for buffer<->texture copies. +struct BufferTextureCopyRegion { + u64 bufferOffset = 0; + u32 bytesPerRow = 0; + u32 rowsPerImage = 0; + u32 textureMipLevel = 0; + u32 textureArrayLayer = 0; + Origin3D textureOrigin; + Extent3D textureExtent; +}; + +/// Push constant range within a pipeline layout. +struct PushConstantRange { + ShaderStage stages = ShaderStage::None; + u32 offset = 0; + u32 size = 0; +}; + +} // namespace draco::rhi diff --git a/Engine/cpp/Runtime/Rendering/RHI/UniformRegistry.cppm b/Engine/cpp/Runtime/Rendering/RHI/UniformRegistry.cppm deleted file mode 100644 index 2bf48381..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/UniformRegistry.cppm +++ /dev/null @@ -1,48 +0,0 @@ -module; - -#include -#include -#include -#include - -export module rendering.rhi.uniform_registry; - -import rendering.rhi; - -export namespace draco::rendering::rhi -{ - inline std::unordered_map g_uniform_map; - - inline uint32_t hashUniform(const std::string& name) - { - return static_cast(std::hash{}(name)); - } - - inline void registerUniform(uint32_t hash, UniformHandle h) - { - g_uniform_map[hash] = h; - } - - inline void unregisterUniform(uint32_t hash, UniformHandle h) - { - auto it = g_uniform_map.find(hash); - - if (it != g_uniform_map.end() && it->second == h) - g_uniform_map.erase(it); - } - - inline void clearUniformRegistry() - { - g_uniform_map.clear(); - } - - inline UniformHandle getUniform(uint32_t hash) - { - auto it = g_uniform_map.find(hash); - - if (it == g_uniform_map.end()) - return InvalidUniform; - - return it->second; - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedAdapter.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedAdapter.cppm new file mode 100644 index 00000000..d841926f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedAdapter.cppm @@ -0,0 +1,28 @@ +/// Validation wrapper for Adapter. + +export module rhi.validation:validated_adapter; + +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedDevice; + +class ValidatedAdapter : public Adapter { +public: + explicit ValidatedAdapter(Adapter* inner) : m_inner(inner) {} + + void getInfo(AdapterInfo& out) override { m_inner->getInfo(out); } + + Status createDevice(const DeviceDesc& desc, Device*& out) override; + + Adapter* inner() const { return m_inner; } + +private: + Adapter* m_inner; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedBackend.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedBackend.cppm new file mode 100644 index 00000000..d22c56cc --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedBackend.cppm @@ -0,0 +1,76 @@ +/// Validation wrapper for Backend. + +module; + +#include +#include + +export module rhi.validation:validated_backend; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_adapter; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedBackend : public Backend { +public: + explicit ValidatedBackend(Backend* inner) : m_inner(inner) { + isInitialized = inner->isInitialized; + } + + std::span enumerateAdapters() override { + if (!m_inner->isInitialized) { + logError("[Validation] enumerateAdapters: backend not initialized"); + return {}; + } + + if (m_adapterWrappers.empty()) { + auto innerAdapters = m_inner->enumerateAdapters(); + m_adapterWrappers.reserve(innerAdapters.size()); + m_adapterPtrs.reserve(innerAdapters.size()); + for (usize i = 0; i < innerAdapters.size(); ++i) { + auto* w = createValidatedAdapter(innerAdapters[i]); + m_adapterWrappers.push_back(w); + m_adapterPtrs.push_back(w); + } + } + return std::span(m_adapterPtrs.data(), m_adapterPtrs.size()); + } + + Status createSurface(void* windowHandle, void* displayHandle, Surface*& out) override { + if (!windowHandle) { + logError("[Validation] createSurface: windowHandle is null"); + out = nullptr; + return ErrorCode::InvalidArgument; + } + return m_inner->createSurface(windowHandle, displayHandle, out); + } + + void destroy() override { + for (auto* w : m_adapterWrappers) delete w; + m_adapterWrappers.clear(); + m_adapterPtrs.clear(); + m_inner->destroy(); + delete this; + } + + Backend* inner() const { return m_inner; } + +private: + static ValidatedAdapter* createValidatedAdapter(Adapter* inner); + + Backend* m_inner; + std::vector m_adapterWrappers; + std::vector m_adapterPtrs; +}; + +Backend* createValidatedBackend(Backend* inner) { + if (!inner) { logError("[Validation] createValidatedBackend: inner is null"); return nullptr; } + return new ValidatedBackend(inner); +} + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandEncoder.cppm new file mode 100644 index 00000000..6267eaaf --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandEncoder.cppm @@ -0,0 +1,271 @@ +/// Validation wrapper for CommandEncoder + RayTracingEncoderExt. + +module; + +#include +#include +#include + +export module rhi.validation:validated_command_encoder; + +import core.stdtypes; +import rhi; +import :validated_render_pass_encoder; +import :validated_compute_pass_encoder; +import :validated_render_bundle_encoder; + +using namespace draco; + +export namespace draco::rhi::validation { + +enum class EncoderState { Recording, InRenderPass, InComputePass, Finished }; + +class ValidatedCommandEncoder : public CommandEncoder, public RayTracingEncoderExt { +public: + RayTracingEncoderExt* asRayTracingExt() noexcept override { return this; } + explicit ValidatedCommandEncoder(CommandEncoder* inner) + : m_inner(inner) {} + + ~ValidatedCommandEncoder() override { + // Bundle encoders (and the bundles they own) live until the command encoder is + // destroyed - by then its submission has completed, so the inner bundles are done. + for (auto* e : m_bundleEncoders) delete e; + } + + // Called by sub-encoders when their end() fires. + void onPassEnded() { m_state = EncoderState::Recording; } + + // ---- CommandEncoder ---- + + RenderPassEncoder* beginRenderPass(const RenderPassDesc& desc) override { + if (!checkState("beginRenderPass", EncoderState::Recording)) return &m_rpe; + if (desc.colorAttachments.empty() && + (!desc.depthStencilAttachment.has_value() || !desc.depthStencilAttachment->view)) + logWarning("[Validation] beginRenderPass: no color or depth attachment"); + for (usize i = 0; i < desc.colorAttachments.size(); ++i) + if (!desc.colorAttachments[i].view) + logErrorf("[Validation] beginRenderPass: color attachment %d view is null", static_cast(i)); + + m_state = EncoderState::InRenderPass; + auto* innerRpe = m_inner->beginRenderPass(desc); + m_rpe.begin(innerRpe, this); + return &m_rpe; + } + + ComputePassEncoder* beginComputePass(std::u8string_view label) override { + if (!checkState("beginComputePass", EncoderState::Recording)) return &m_cpe; + m_state = EncoderState::InComputePass; + auto* innerCpe = m_inner->beginComputePass(label); + m_cpe.begin(innerCpe, this); + return &m_cpe; + } + + RenderBundleEncoder* createRenderBundleEncoder(const RenderBundleDesc& desc) override { + if (!checkState("createRenderBundleEncoder", EncoderState::Recording)) return nullptr; + auto* inner = m_inner->createRenderBundleEncoder(desc); + if (!inner) return nullptr; // backend does not support bundles + auto* wrapped = new ValidatedRenderBundleEncoder(inner); + m_bundleEncoders.push_back(wrapped); // owned: freed in this encoder's destructor + return wrapped; + } + + void barrier(const BarrierGroup& group) override { + if (!checkState("barrier", EncoderState::Recording)) return; + m_inner->barrier(group); + } + + void copyBufferToBuffer(Buffer* src, u64 srcOff, Buffer* dst, u64 dstOff, u64 size) override { + if (!checkState("copyBufferToBuffer", EncoderState::Recording)) return; + if (!src) { logError("[Validation] copyBufferToBuffer: src is null"); return; } + if (!dst) { logError("[Validation] copyBufferToBuffer: dst is null"); return; } + if (size == 0) logWarning("[Validation] copyBufferToBuffer: size is 0"); + m_inner->copyBufferToBuffer(src, srcOff, dst, dstOff, size); + } + + void copyBufferToTexture(Buffer* src, Texture* dst, const BufferTextureCopyRegion& r) override { + if (!checkState("copyBufferToTexture", EncoderState::Recording)) return; + if (!src) { logError("[Validation] copyBufferToTexture: src is null"); return; } + if (!dst) { logError("[Validation] copyBufferToTexture: dst is null"); return; } + m_inner->copyBufferToTexture(src, dst, r); + } + + void copyTextureToBuffer(Texture* src, Buffer* dst, const BufferTextureCopyRegion& r) override { + if (!checkState("copyTextureToBuffer", EncoderState::Recording)) return; + if (!src) { logError("[Validation] copyTextureToBuffer: src is null"); return; } + if (!dst) { logError("[Validation] copyTextureToBuffer: dst is null"); return; } + m_inner->copyTextureToBuffer(src, dst, r); + } + + void copyTextureToTexture(Texture* src, Texture* dst, const TextureCopyRegion& r) override { + if (!checkState("copyTextureToTexture", EncoderState::Recording)) return; + if (!src) { logError("[Validation] copyTextureToTexture: src is null"); return; } + if (!dst) { logError("[Validation] copyTextureToTexture: dst is null"); return; } + m_inner->copyTextureToTexture(src, dst, r); + } + + void blit(Texture* src, Texture* dst) override { + if (!checkState("blit", EncoderState::Recording)) return; + if (!src || !dst) { logError("[Validation] blit: src or dst is null"); return; } + m_inner->blit(src, dst); + } + + void generateMipmaps(Texture* tex) override { + if (!checkState("generateMipmaps", EncoderState::Recording)) return; + if (!tex) { logError("[Validation] generateMipmaps: texture is null"); return; } + m_inner->generateMipmaps(tex); + } + + void resolveTexture(Texture* src, Texture* dst) override { + if (!checkState("resolveTexture", EncoderState::Recording)) return; + if (!src || !dst) { logError("[Validation] resolveTexture: src or dst is null"); return; } + m_inner->resolveTexture(src, dst); + } + + void resetQuerySet(QuerySet* qs, u32 first, u32 count) override { + if (!checkState("resetQuerySet", EncoderState::Recording)) return; + if (!qs) { logError("[Validation] resetQuerySet: querySet is null"); return; } + m_inner->resetQuerySet(qs, first, count); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + if (!checkState("writeTimestamp", EncoderState::Recording)) return; + if (!qs) { logError("[Validation] writeTimestamp: querySet is null"); return; } + m_inner->writeTimestamp(qs, index); + } + + void resolveQuerySet(QuerySet* qs, u32 first, u32 count, Buffer* dst, u64 dstOff) override { + if (!checkState("resolveQuerySet", EncoderState::Recording)) return; + if (!qs) { logError("[Validation] resolveQuerySet: querySet is null"); return; } + if (!dst) { logError("[Validation] resolveQuerySet: dst is null"); return; } + m_inner->resolveQuerySet(qs, first, count, dst, dstOff); + } + + void beginDebugLabel(std::u8string_view label, f32 r, f32 g, f32 b, f32 a) override { + if (m_state == EncoderState::Finished) { logError("[Validation] beginDebugLabel: encoder finished"); return; } + m_debugLabelDepth++; + m_inner->beginDebugLabel(label, r, g, b, a); + } + + void endDebugLabel() override { + if (m_state == EncoderState::Finished) { logError("[Validation] endDebugLabel: encoder finished"); return; } + if (m_debugLabelDepth <= 0) { logError("[Validation] endDebugLabel: no matching begin"); return; } + m_debugLabelDepth--; + m_inner->endDebugLabel(); + } + + void insertDebugLabel(std::u8string_view label, f32 r, f32 g, f32 b, f32 a) override { + if (m_state == EncoderState::Finished) return; + m_inner->insertDebugLabel(label, r, g, b, a); + } + + CommandBuffer* finish() override { + if (m_state == EncoderState::Finished) { logError("[Validation] finish: encoder already finished"); return nullptr; } + if (m_state == EncoderState::InRenderPass) logError("[Validation] finish: render pass still open"); + if (m_state == EncoderState::InComputePass) logError("[Validation] finish: compute pass still open"); + if (m_debugLabelDepth > 0) logWarningf("[Validation] finish: %d debug label(s) not closed", m_debugLabelDepth); + m_state = EncoderState::Finished; + return m_inner->finish(); + } + + // ---- RayTracingEncoderExt ---- + + void buildBottomLevelAccelStruct(AccelStruct* dst, Buffer* scratch, u64 scratchOff, + std::span tris, + std::span aabbs) override { + if (!checkState("buildBLAS", EncoderState::Recording)) return; + if (!dst) { logError("[Validation] buildBLAS: dst is null"); return; } + if (!scratch) { logError("[Validation] buildBLAS: scratch is null"); return; } + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->buildBottomLevelAccelStruct(dst, scratch, scratchOff, tris, aabbs); + else logError("[Validation] buildBLAS: inner encoder does not support ray tracing"); + } + + void buildTopLevelAccelStruct(AccelStruct* dst, Buffer* scratch, u64 scratchOff, + Buffer* instanceBuf, u64 instanceOff, u32 instanceCount) override { + if (!checkState("buildTLAS", EncoderState::Recording)) return; + if (!dst || !scratch || !instanceBuf) { logError("[Validation] buildTLAS: null argument"); return; } + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->buildTopLevelAccelStruct(dst, scratch, scratchOff, instanceBuf, instanceOff, instanceCount); + else logError("[Validation] buildTLAS: inner encoder does not support ray tracing"); + } + + void setRayTracingPipeline(RayTracingPipeline* pipeline) override { + if (!checkState("setRayTracingPipeline", EncoderState::Recording)) return; + if (!pipeline) { logError("[Validation] setRayTracingPipeline: pipeline is null"); return; } + m_rtPipelineBound = true; + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->setRayTracingPipeline(pipeline); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + if (!checkState("RT setBindGroup", EncoderState::Recording)) return; + if (!group) { logError("[Validation] RT setBindGroup: group is null"); return; } + if (!m_rtPipelineBound) logWarning("[Validation] RT setBindGroup: no RT pipeline bound"); + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->setBindGroup(index, group, dynOffsets); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + if (!checkState("RT setPushConstants", EncoderState::Recording)) return; + if (!m_rtPipelineBound) logWarning("[Validation] RT setPushConstants: no RT pipeline bound"); + if (!data && size > 0) { logError("[Validation] RT setPushConstants: data is null"); return; } + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->setPushConstants(stages, offset, size, data); + } + + void traceRays(Buffer* raygenSBT, u64 raygenOff, u64 raygenStride, + Buffer* missSBT, u64 missOff, u64 missStride, + Buffer* hitSBT, u64 hitOff, u64 hitStride, + u32 width, u32 height, u32 depth) override { + if (!checkState("traceRays", EncoderState::Recording)) return; + if (!m_rtPipelineBound) { logError("[Validation] traceRays: no RT pipeline bound"); return; } + if (!raygenSBT) { logError("[Validation] traceRays: raygenSBT is null"); return; } + auto* rt = m_inner->asRayTracingExt(); + if (rt) rt->traceRays(raygenSBT, raygenOff, raygenStride, missSBT, missOff, missStride, + hitSBT, hitOff, hitStride, width, height, depth); + } + + CommandEncoder* inner() const { return m_inner; } + +private: + bool checkState(const char* method, EncoderState expected) { + if (m_state == EncoderState::Finished) { + logErrorf("[Validation] %s: encoder already finished", method); + return false; + } + if (m_state != expected) { + logErrorf("[Validation] %s: wrong state (expected Recording, got %s)", method, + m_state == EncoderState::InRenderPass ? "InRenderPass" : + m_state == EncoderState::InComputePass ? "InComputePass" : "?"); + return false; + } + return true; + } + + CommandEncoder* m_inner; + EncoderState m_state = EncoderState::Recording; + i32 m_debugLabelDepth = 0; + bool m_rtPipelineBound = false; + + ValidatedRenderPassEncoder m_rpe; + ValidatedComputePassEncoder m_cpe; + std::vector m_bundleEncoders; // owned wrappers (freed in dtor) +}; + +// ---- Deferred end() implementations ---- + +void ValidatedRenderPassEncoder::end() { + if (m_ended) { logError("[Validation] RenderPassEncoder::end: already ended"); return; } + m_ended = true; + m_inner->end(); + if (m_parent) m_parent->onPassEnded(); +} + +void ValidatedComputePassEncoder::end() { + if (m_ended) { logError("[Validation] ComputePassEncoder::end: already ended"); return; } + m_ended = true; + m_inner->end(); + if (m_parent) m_parent->onPassEnded(); +} + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandPool.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandPool.cppm new file mode 100644 index 00000000..192c1384 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedCommandPool.cppm @@ -0,0 +1,47 @@ +/// Validation wrapper for CommandPool. + +export module rhi.validation:validated_command_pool; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_command_encoder; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedCommandPool : public CommandPool { +public: + explicit ValidatedCommandPool(CommandPool* inner) : m_inner(inner) {} + + Status createEncoder(CommandEncoder*& out) override { + CommandEncoder* innerEnc = nullptr; + Status r = m_inner->createEncoder(innerEnc); + if (r != ErrorCode::Ok || !innerEnc) { out = nullptr; return r; } + out = new ValidatedCommandEncoder(innerEnc); + return ErrorCode::Ok; + } + + void destroyEncoder(CommandEncoder*& encoder) override { + if (!encoder) return; + auto* ve = static_cast(encoder); + if (ve) { + CommandEncoder* innerEnc = ve->inner(); + m_inner->destroyEncoder(innerEnc); + delete ve; + } else { + m_inner->destroyEncoder(encoder); + } + encoder = nullptr; + } + + void reset() override { m_inner->reset(); } + + CommandPool* inner() const { return m_inner; } + +private: + CommandPool* m_inner; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedComputePassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedComputePassEncoder.cppm new file mode 100644 index 00000000..2991dcd7 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedComputePassEncoder.cppm @@ -0,0 +1,82 @@ +/// Validation wrapper for ComputePassEncoder. + +module; + +#include + +export module rhi.validation:validated_compute_pass_encoder; + +import core.stdtypes; +import rhi; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedCommandEncoder; // forward + +class ValidatedComputePassEncoder : public ComputePassEncoder { +public: + void begin(ComputePassEncoder* inner, ValidatedCommandEncoder* parent) { + m_inner = inner; m_parent = parent; + m_pipelineBound = false; m_ended = false; + } + + void setPipeline(ComputePipeline* pipeline) override { + if (m_ended) { logError("[Validation] compute setPipeline: pass ended"); return; } + if (!pipeline) { logError("[Validation] compute setPipeline: pipeline is null"); return; } + m_pipelineBound = true; + m_inner->setPipeline(pipeline); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + if (m_ended) return; + if (!group) { logError("[Validation] compute setBindGroup: group is null"); return; } + if (!m_pipelineBound) logWarning("[Validation] compute setBindGroup: no pipeline bound"); + m_inner->setBindGroup(index, group, dynOffsets); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + if (m_ended) return; + if (!m_pipelineBound) logWarning("[Validation] compute setPushConstants: no pipeline bound"); + if (!data && size > 0) { logError("[Validation] compute setPushConstants: data is null"); return; } + if (offset % 4 != 0) logError("[Validation] compute setPushConstants: offset not 4-byte aligned"); + if (size % 4 != 0) logError("[Validation] compute setPushConstants: size not 4-byte aligned"); + m_inner->setPushConstants(stages, offset, size, data); + } + + void dispatch(u32 x, u32 y, u32 z) override { + if (m_ended) { logError("[Validation] dispatch: pass ended"); return; } + if (!m_pipelineBound) { logError("[Validation] dispatch: no pipeline bound"); return; } + if (x == 0 || y == 0 || z == 0) logWarning("[Validation] dispatch: zero dimension"); + m_inner->dispatch(x, y, z); + } + + void dispatchIndirect(Buffer* buffer, u64 offset) override { + if (m_ended) { logError("[Validation] dispatchIndirect: pass ended"); return; } + if (!m_pipelineBound) { logError("[Validation] dispatchIndirect: no pipeline bound"); return; } + if (!buffer) { logError("[Validation] dispatchIndirect: buffer is null"); return; } + m_inner->dispatchIndirect(buffer, offset); + } + + void computeBarrier() override { + if (m_ended) return; + m_inner->computeBarrier(); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + if (m_ended) return; + if (!qs) { logError("[Validation] compute writeTimestamp: querySet is null"); return; } + m_inner->writeTimestamp(qs, index); + } + + void end() override; + +private: + ComputePassEncoder* m_inner = nullptr; + ValidatedCommandEncoder* m_parent = nullptr; + bool m_pipelineBound = false; + bool m_ended = false; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedDevice.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedDevice.cppm new file mode 100644 index 00000000..1f25f90a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedDevice.cppm @@ -0,0 +1,260 @@ +/// Validation wrapper for Device. Tracks all live resources for leak +/// detection, validates create/destroy parameters. + +module; + +#include +#include + +export module rhi.validation:validated_device; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_fence; +import :validated_swap_chain; +import :validated_command_pool; +import :validated_queue; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedDevice : public Device { +public: + explicit ValidatedDevice(Device* inner) : m_inner(inner) { + type = inner->type; + features = inner->features; + shaderGroupHandleSize = inner->shaderGroupHandleSize; + shaderGroupHandleAlignment = inner->shaderGroupHandleAlignment; + shaderGroupBaseAlignment = inner->shaderGroupBaseAlignment; + } + + // ---- Queues ---- + Queue* getQueue(QueueType t, u32 index) override { + // Wrap on first access. + Queue* raw = m_inner->getQueue(t, index); + if (!raw) return nullptr; + for (auto& w : m_queueWrappers) if (w.raw == raw) return w.validated; + auto* vq = new ValidatedQueue(raw); + m_queueWrappers.push_back({ raw, vq }); + return vq; + } + u32 getQueueCount(QueueType t) override { return m_inner->getQueueCount(t); } + FormatSupport getFormatSupport(TextureFormat f) override { return m_inner->getFormatSupport(f); } + + // ---- Create methods (with validation + tracking) ---- +#define V_CREATE(Type, method, desc_t) \ + Status method(const desc_t& d, Type*& out) override { \ + if (m_destroyed) { logError("[Validation] " #method ": device destroyed"); out = nullptr; return ErrorCode::Unknown; } \ + Status r = m_inner->method(d, out); \ + if (r == ErrorCode::Ok && out) m_live##Type##s.push_back(out); \ + return r; \ + } + + V_CREATE(Buffer, createBuffer, BufferDesc) + V_CREATE(Texture, createTexture, TextureDesc) + V_CREATE(Sampler, createSampler, SamplerDesc) + V_CREATE(ShaderModule, createShaderModule, ShaderModuleDesc) + V_CREATE(BindGroupLayout, createBindGroupLayout, BindGroupLayoutDesc) + V_CREATE(BindGroup, createBindGroup, BindGroupDesc) + V_CREATE(PipelineLayout, createPipelineLayout, PipelineLayoutDesc) + V_CREATE(PipelineCache, createPipelineCache, PipelineCacheDesc) + V_CREATE(RenderPipeline, createRenderPipeline, RenderPipelineDesc) + V_CREATE(ComputePipeline, createComputePipeline, ComputePipelineDesc) + V_CREATE(QuerySet, createQuerySet, QuerySetDesc) +#undef V_CREATE + + Status createTextureView(Texture* tex, const TextureViewDesc& d, TextureView*& out) override { + if (m_destroyed) { logError("[Validation] createTextureView: device destroyed"); out = nullptr; return ErrorCode::Unknown; } + if (!tex) { logError("[Validation] createTextureView: texture is null"); out = nullptr; return ErrorCode::Unknown; } + Status r = m_inner->createTextureView(tex, d, out); + if (r == ErrorCode::Ok && out) m_liveTextureViews.push_back(out); + return r; + } + + Status createCommandPool(QueueType qt, CommandPool*& out) override { + if (m_destroyed) { logError("[Validation] createCommandPool: device destroyed"); out = nullptr; return ErrorCode::Unknown; } + CommandPool* innerPool = nullptr; + Status r = m_inner->createCommandPool(qt, innerPool); + if (r != ErrorCode::Ok || !innerPool) { out = nullptr; return r; } + out = new ValidatedCommandPool(innerPool); + m_liveCommandPools.push_back(out); + return ErrorCode::Ok; + } + + Status createFence(u64 initialValue, Fence*& out) override { + if (m_destroyed) { logError("[Validation] createFence: device destroyed"); out = nullptr; return ErrorCode::Unknown; } + Fence* innerFence = nullptr; + Status r = m_inner->createFence(initialValue, innerFence); + if (r != ErrorCode::Ok || !innerFence) { out = nullptr; return r; } + out = new ValidatedFence(innerFence); + m_liveFences.push_back(out); + return ErrorCode::Ok; + } + + Status createSwapChain(Surface* surface, const SwapChainDesc& d, SwapChain*& out) override { + if (m_destroyed) { logError("[Validation] createSwapChain: device destroyed"); out = nullptr; return ErrorCode::Unknown; } + SwapChain* innerSc = nullptr; + Status r = m_inner->createSwapChain(surface, d, innerSc); + if (r != ErrorCode::Ok || !innerSc) { out = nullptr; return r; } + out = new ValidatedSwapChain(innerSc); + m_liveSwapChains.push_back(out); + return ErrorCode::Ok; + } + + // ---- Mesh/RT (forwarded, validated for destroyed state) ---- + Status createMeshPipeline(const MeshPipelineDesc& d, MeshPipeline*& out) override { + if (m_destroyed) { out = nullptr; return ErrorCode::Unknown; } + Status r = m_inner->createMeshPipeline(d, out); + if (r == ErrorCode::Ok && out) m_liveMeshPipelines.push_back(out); + return r; + } + void destroyMeshPipeline(MeshPipeline*& p) override { removeAndDestroy(m_liveMeshPipelines, p, [&](auto*& x){ m_inner->destroyMeshPipeline(x); }); } + + Status createAccelStruct(const AccelStructDesc& d, AccelStruct*& out) override { + if (m_destroyed) { out = nullptr; return ErrorCode::Unknown; } + Status r = m_inner->createAccelStruct(d, out); + if (r == ErrorCode::Ok && out) m_liveAccelStructs.push_back(out); + return r; + } + void destroyAccelStruct(AccelStruct*& a) override { removeAndDestroy(m_liveAccelStructs, a, [&](auto*& x){ m_inner->destroyAccelStruct(x); }); } + + Status createRayTracingPipeline(const RayTracingPipelineDesc& d, RayTracingPipeline*& out) override { + if (m_destroyed) { out = nullptr; return ErrorCode::Unknown; } + Status r = m_inner->createRayTracingPipeline(d, out); + if (r == ErrorCode::Ok && out) m_liveRtPipelines.push_back(out); + return r; + } + void destroyRayTracingPipeline(RayTracingPipeline*& p) override { removeAndDestroy(m_liveRtPipelines, p, [&](auto*& x){ m_inner->destroyRayTracingPipeline(x); }); } + + Status getShaderGroupHandles(RayTracingPipeline* p, u32 first, u32 count, std::span out) override { + return m_inner->getShaderGroupHandles(p, first, count, out); + } + + // ---- Destroy methods (with tracking removal) ---- +#define V_DESTROY(Type, method, list) \ + void method(Type*& x) override { removeAndDestroy(list, x, [&](auto*& p){ m_inner->method(p); }); } + + V_DESTROY(Buffer, destroyBuffer, m_liveBuffers) + V_DESTROY(Texture, destroyTexture, m_liveTextures) + V_DESTROY(TextureView, destroyTextureView, m_liveTextureViews) + V_DESTROY(Sampler, destroySampler, m_liveSamplers) + V_DESTROY(ShaderModule, destroyShaderModule, m_liveShaderModules) + V_DESTROY(BindGroupLayout, destroyBindGroupLayout, m_liveBindGroupLayouts) + V_DESTROY(BindGroup, destroyBindGroup, m_liveBindGroups) + V_DESTROY(PipelineLayout, destroyPipelineLayout, m_livePipelineLayouts) + V_DESTROY(PipelineCache, destroyPipelineCache, m_livePipelineCaches) + V_DESTROY(RenderPipeline, destroyRenderPipeline, m_liveRenderPipelines) + V_DESTROY(ComputePipeline, destroyComputePipeline, m_liveComputePipelines) + V_DESTROY(QuerySet, destroyQuerySet, m_liveQuerySets) +#undef V_DESTROY + + void destroyCommandPool(CommandPool*& pool) override { + if (!pool) return; + removeFromList(m_liveCommandPools, pool); + auto* vp = static_cast(pool); + if (vp) { CommandPool* innerPool = vp->inner(); m_inner->destroyCommandPool(innerPool); delete vp; } + else m_inner->destroyCommandPool(pool); + pool = nullptr; + } + + void destroyFence(Fence*& fence) override { + if (!fence) return; + removeFromList(m_liveFences, fence); + auto* vf = static_cast(fence); + if (vf) { Fence* innerFence = vf->inner(); m_inner->destroyFence(innerFence); delete vf; } + else m_inner->destroyFence(fence); + fence = nullptr; + } + + void destroySwapChain(SwapChain*& sc) override { + if (!sc) return; + removeFromList(m_liveSwapChains, sc); + auto* vs = static_cast(sc); + if (vs) { SwapChain* innerSc = vs->inner(); m_inner->destroySwapChain(innerSc); delete vs; } + else m_inner->destroySwapChain(sc); + sc = nullptr; + } + + void destroySurface(Surface*& s) override { m_inner->destroySurface(s); } + + void waitIdle() override { m_inner->waitIdle(); } + + void destroy() override { + if (m_destroyed) { logError("[Validation] Device::destroy: already destroyed"); return; } + m_destroyed = true; + reportLeaks(); + for (auto& w : m_queueWrappers) delete w.validated; + m_queueWrappers.clear(); + m_inner->destroy(); + delete this; + } + +private: + template + void removeFromList(std::vector& list, T* item) { + for (usize i = 0; i < list.size(); ++i) { + if (list[i] == item) { list.erase(list.begin() + i); return; } + } + } + + template + void removeAndDestroy(std::vector& list, T*& item, Fn destroyFn) { + if (!item) return; + removeFromList(list, item); + destroyFn(item); + item = nullptr; + } + + void reportLeaks() { + auto report = [](const char* name, usize count) { + if (count > 0) logWarningf("[Validation] Device destroyed with %zu live %s(s)", count, name); + }; + report("Buffer", m_liveBuffers.size()); + report("Texture", m_liveTextures.size()); + report("TextureView", m_liveTextureViews.size()); + report("Sampler", m_liveSamplers.size()); + report("ShaderModule", m_liveShaderModules.size()); + report("BindGroupLayout", m_liveBindGroupLayouts.size()); + report("BindGroup", m_liveBindGroups.size()); + report("PipelineLayout", m_livePipelineLayouts.size()); + report("PipelineCache", m_livePipelineCaches.size()); + report("RenderPipeline", m_liveRenderPipelines.size()); + report("ComputePipeline", m_liveComputePipelines.size()); + report("MeshPipeline", m_liveMeshPipelines.size()); + report("AccelStruct", m_liveAccelStructs.size()); + report("RayTracingPipeline", m_liveRtPipelines.size()); + report("CommandPool", m_liveCommandPools.size()); + report("Fence", m_liveFences.size()); + report("SwapChain", m_liveSwapChains.size()); + report("QuerySet", m_liveQuerySets.size()); + } + + Device* m_inner; + bool m_destroyed = false; + + struct QueueWrap { Queue* raw; ValidatedQueue* validated; }; + std::vector m_queueWrappers; + + std::vector m_liveBuffers; + std::vector m_liveTextures; + std::vector m_liveTextureViews; + std::vector m_liveSamplers; + std::vector m_liveShaderModules; + std::vector m_liveBindGroupLayouts; + std::vector m_liveBindGroups; + std::vector m_livePipelineLayouts; + std::vector m_livePipelineCaches; + std::vector m_liveRenderPipelines; + std::vector m_liveComputePipelines; + std::vector m_liveMeshPipelines; + std::vector m_liveAccelStructs; + std::vectorm_liveRtPipelines; + std::vector m_liveCommandPools; + std::vector m_liveFences; + std::vector m_liveSwapChains; + std::vector m_liveQuerySets; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedFence.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedFence.cppm new file mode 100644 index 00000000..08cdd789 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedFence.cppm @@ -0,0 +1,41 @@ +/// Validation wrapper for Fence. + +export module rhi.validation:validated_fence; + +import core.stdtypes; +import rhi; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedFence : public Fence { +public: + explicit ValidatedFence(Fence* inner) : m_inner(inner) {} + + u64 completedValue() override { return m_inner->completedValue(); } + + bool wait(u64 value, u64 timeoutNs) override { + if (value > m_lastSignaled && m_lastSignaled > 0) { + logWarningf("[Validation] Fence::wait: waiting for value %llu but highest signaled is %llu", + static_cast(value), static_cast(m_lastSignaled)); + } + return m_inner->wait(value, timeoutNs); + } + + void trackSignal(u64 value) { + if (value <= m_lastSignaled && m_lastSignaled > 0) { + logWarningf("[Validation] Fence signal value %llu is not monotonically increasing (last=%llu)", + static_cast(value), static_cast(m_lastSignaled)); + } + m_lastSignaled = value; + } + + Fence* inner() const { return m_inner; } + +private: + Fence* m_inner; + u64 m_lastSignaled = 0; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedQueue.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedQueue.cppm new file mode 100644 index 00000000..d8cd71fe --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedQueue.cppm @@ -0,0 +1,86 @@ +/// Validation wrapper for Queue. + +module; + +#include +#include + +export module rhi.validation:validated_queue; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_fence; +import :validated_transfer_batch; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedQueue : public Queue { +public: + explicit ValidatedQueue(Queue* inner) : m_inner(inner) { queueType = inner->queueType; } + + void submit(std::span cmdBufs) override { + for (usize i = 0; i < cmdBufs.size(); ++i) + if (!cmdBufs[i]) logErrorf("[Validation] Queue::submit: commandBuffer[%zu] is null", i); + m_inner->submit(cmdBufs); + } + + void submit(std::span cmdBufs, Fence* signalFence, u64 signalValue) override { + if (!signalFence) { logError("[Validation] Queue::submit: signalFence is null"); return; } + auto* vf = static_cast(signalFence); + Fence* innerFence = vf ? vf->inner() : signalFence; + if (vf) vf->trackSignal(signalValue); + m_inner->submit(cmdBufs, innerFence, signalValue); + } + + void submit(std::span cmdBufs, + std::span waitFences, std::span waitValues, + Fence* signalFence, u64 signalValue) override { + if (waitFences.size() != waitValues.size()) + logError("[Validation] Queue::submit: waitFences and waitValues count mismatch"); + // Unwrap validated fences for both wait and signal. + std::vector innerWait(waitFences.size()); + for (usize i = 0; i < waitFences.size(); ++i) { + auto* vw = static_cast(waitFences[i]); + innerWait[i] = vw ? vw->inner() : waitFences[i]; + } + auto* vf = static_cast(signalFence); + Fence* innerSignal = vf ? vf->inner() : signalFence; + if (vf) vf->trackSignal(signalValue); + m_inner->submit(cmdBufs, std::span(innerWait.data(), innerWait.size()), waitValues, innerSignal, signalValue); + } + + void waitIdle() override { m_inner->waitIdle(); } + + Status createTransferBatch(TransferBatch*& out) override { + TransferBatch* innerBatch = nullptr; + Status r = m_inner->createTransferBatch(innerBatch); + if (r != ErrorCode::Ok || !innerBatch) { out = nullptr; return r; } + out = new ValidatedTransferBatch(innerBatch); + return ErrorCode::Ok; + } + + void destroyTransferBatch(TransferBatch*& batch) override { + if (!batch) return; + auto* vt = static_cast(batch); + if (vt) { + TransferBatch* innerBatch = vt->inner(); + m_inner->destroyTransferBatch(innerBatch); + delete vt; + } else { + m_inner->destroyTransferBatch(batch); + } + batch = nullptr; + } + + f32 timestampPeriod() const override { return m_inner->timestampPeriod(); } + + Queue* inner() const { return m_inner; } + +private: + Queue* m_inner; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderBundleEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderBundleEncoder.cppm new file mode 100644 index 00000000..e5116f7b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderBundleEncoder.cppm @@ -0,0 +1,112 @@ +/// Validation wrappers for RenderBundleEncoder + RenderBundle. +/// Mirrors ValidatedRenderPassEncoder's draw-recording checks for the bundle subset. + +module; + +#include + +export module rhi.validation:validated_render_bundle_encoder; + +import core.stdtypes; +import rhi; + +using namespace draco; + +export namespace draco::rhi::validation { + +// Wraps an inner bundle so the validation layer can unwrap it at executeBundles time. +class ValidatedRenderBundle : public RenderBundle { +public: + explicit ValidatedRenderBundle(RenderBundle* inner) : m_inner(inner) {} + [[nodiscard]] RenderBundle* inner() const noexcept { return m_inner; } +private: + RenderBundle* m_inner; +}; + +// Validates the draw-recording subset (pipeline bound before draws, non-null resources) and +// forwards to the inner bundle encoder. Owns the ValidatedRenderBundle it produces at finish. +class ValidatedRenderBundleEncoder : public RenderBundleEncoder { +public: + explicit ValidatedRenderBundleEncoder(RenderBundleEncoder* inner) : m_inner(inner) {} + ~ValidatedRenderBundleEncoder() override { delete m_bundle; } + + void setPipeline(RenderPipeline* pipeline) override { + if (m_finished) { logError("[Validation] bundle setPipeline: bundle already finished"); return; } + if (!pipeline) { logError("[Validation] bundle setPipeline: pipeline is null"); return; } + m_pipelineBound = true; + m_inner->setPipeline(pipeline); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + if (m_finished) { logError("[Validation] bundle setBindGroup: bundle already finished"); return; } + if (!group) { logError("[Validation] bundle setBindGroup: group is null"); return; } + m_inner->setBindGroup(index, group, dynOffsets); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + if (m_finished) { logError("[Validation] bundle setPushConstants: bundle already finished"); return; } + if (!data && size > 0) { logError("[Validation] bundle setPushConstants: data is null but size > 0"); return; } + if (offset % 4 != 0) logError("[Validation] bundle setPushConstants: offset must be 4-byte aligned"); + if (size % 4 != 0) logError("[Validation] bundle setPushConstants: size must be 4-byte aligned"); + m_inner->setPushConstants(stages, offset, size, data); + } + + void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset) override { + if (m_finished) { logError("[Validation] bundle setVertexBuffer: bundle already finished"); return; } + if (!buffer) { logError("[Validation] bundle setVertexBuffer: buffer is null"); return; } + m_inner->setVertexBuffer(slot, buffer, offset); + } + + void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset) override { + if (m_finished) { logError("[Validation] bundle setIndexBuffer: bundle already finished"); return; } + if (!buffer) { logError("[Validation] bundle setIndexBuffer: buffer is null"); return; } + m_inner->setIndexBuffer(buffer, format, offset); + } + + void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) override { + if (!checkDrawReady("bundle draw")) return; + if (vertexCount == 0) logWarning("[Validation] bundle draw: vertexCount is 0"); + m_inner->draw(vertexCount, instanceCount, firstVertex, firstInstance); + } + + void drawIndexed(u32 indexCount, u32 instanceCount, u32 firstIndex, i32 baseVertex, u32 firstInstance) override { + if (!checkDrawReady("bundle drawIndexed")) return; + if (indexCount == 0) logWarning("[Validation] bundle drawIndexed: indexCount is 0"); + m_inner->drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + } + + void drawIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + if (!checkDrawReady("bundle drawIndirect")) return; + if (!buffer) { logError("[Validation] bundle drawIndirect: buffer is null"); return; } + m_inner->drawIndirect(buffer, offset, drawCount, stride); + } + + void drawIndexedIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + if (!checkDrawReady("bundle drawIndexedIndirect")) return; + if (!buffer) { logError("[Validation] bundle drawIndexedIndirect: buffer is null"); return; } + m_inner->drawIndexedIndirect(buffer, offset, drawCount, stride); + } + + RenderBundle* finish() override { + if (m_finished) { logError("[Validation] bundle finish: already finished"); return m_bundle; } + m_finished = true; + RenderBundle* innerBundle = m_inner->finish(); + if (innerBundle == nullptr) { logError("[Validation] bundle finish: inner returned null"); return nullptr; } + m_bundle = new ValidatedRenderBundle(innerBundle); // freed by this encoder's destructor + return m_bundle; + } + +private: + bool checkDrawReady(const char* method) { + if (m_finished) { logErrorf("[Validation] %s: bundle already finished", method); return false; } + if (!m_pipelineBound) { logErrorf("[Validation] %s: no pipeline bound", method); return false; } + return true; + } + + RenderBundleEncoder* m_inner; + ValidatedRenderBundle* m_bundle = nullptr; + bool m_pipelineBound = false; + bool m_finished = false; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderPassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderPassEncoder.cppm new file mode 100644 index 00000000..cbf5e304 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedRenderPassEncoder.cppm @@ -0,0 +1,194 @@ +/// Validation wrapper for RenderPassEncoder + MeshShaderPassExt. + +module; + +#include +#include + +export module rhi.validation:validated_render_pass_encoder; + +import core.stdtypes; +import rhi; +import :validated_render_bundle_encoder; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedCommandEncoder; // forward + +class ValidatedRenderPassEncoder : public RenderPassEncoder, public MeshShaderPassExt { +public: + MeshShaderPassExt* asMeshShaderExt() noexcept override { return this; } + void begin(RenderPassEncoder* inner, ValidatedCommandEncoder* parent) { + m_inner = inner; m_parent = parent; + m_pipelineBound = false; m_viewportSet = false; m_scissorSet = false; m_ended = false; + m_meshPipelineBound = false; + } + + // ---- RenderPassEncoder ---- + + void setPipeline(RenderPipeline* pipeline) override { + if (m_ended) { logError("[Validation] setPipeline: render pass ended"); return; } + if (!pipeline) { logError("[Validation] setPipeline: pipeline is null"); return; } + m_pipelineBound = true; m_meshPipelineBound = false; + m_inner->setPipeline(pipeline); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + if (m_ended) { logError("[Validation] setBindGroup: render pass ended"); return; } + if (!group) { logError("[Validation] setBindGroup: group is null"); return; } + m_inner->setBindGroup(index, group, dynOffsets); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + if (m_ended) { logError("[Validation] setPushConstants: render pass ended"); return; } + if (!m_pipelineBound && !m_meshPipelineBound) logWarning("[Validation] setPushConstants: no pipeline bound"); + if (!data && size > 0) { logError("[Validation] setPushConstants: data is null but size > 0"); return; } + if (size == 0) { logWarning("[Validation] setPushConstants: size is 0"); return; } + if (offset % 4 != 0) logError("[Validation] setPushConstants: offset must be 4-byte aligned"); + if (size % 4 != 0) logError("[Validation] setPushConstants: size must be 4-byte aligned"); + m_inner->setPushConstants(stages, offset, size, data); + } + + void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset) override { + if (m_ended) { logError("[Validation] setVertexBuffer: render pass ended"); return; } + if (!buffer) { logError("[Validation] setVertexBuffer: buffer is null"); return; } + m_inner->setVertexBuffer(slot, buffer, offset); + } + + void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset) override { + if (m_ended) { logError("[Validation] setIndexBuffer: render pass ended"); return; } + if (!buffer) { logError("[Validation] setIndexBuffer: buffer is null"); return; } + m_inner->setIndexBuffer(buffer, format, offset); + } + + void setViewport(f32 x, f32 y, f32 w, f32 h, f32 minD, f32 maxD) override { + if (m_ended) { logError("[Validation] setViewport: render pass ended"); return; } + m_viewportSet = true; + m_inner->setViewport(x, y, w, h, minD, maxD); + } + + void setScissor(i32 x, i32 y, u32 w, u32 h) override { + if (m_ended) { logError("[Validation] setScissor: render pass ended"); return; } + m_scissorSet = true; + m_inner->setScissor(x, y, w, h); + } + + void setBlendConstant(f32 r, f32 g, f32 b, f32 a) override { + if (m_ended) return; + m_inner->setBlendConstant(r, g, b, a); + } + + void setStencilReference(u32 ref) override { + if (m_ended) return; + m_inner->setStencilReference(ref); + } + + void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) override { + if (!checkDrawReady("draw")) return; + if (vertexCount == 0) logWarning("[Validation] draw: vertexCount is 0"); + m_inner->draw(vertexCount, instanceCount, firstVertex, firstInstance); + } + + void drawIndexed(u32 indexCount, u32 instanceCount, u32 firstIndex, i32 baseVertex, u32 firstInstance) override { + if (!checkDrawReady("drawIndexed")) return; + if (indexCount == 0) logWarning("[Validation] drawIndexed: indexCount is 0"); + m_inner->drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + } + + void drawIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + if (!checkDrawReady("drawIndirect")) return; + if (!buffer) { logError("[Validation] drawIndirect: buffer is null"); return; } + m_inner->drawIndirect(buffer, offset, drawCount, stride); + } + + void drawIndexedIndirect(Buffer* buffer, u64 offset, u32 drawCount, u32 stride) override { + if (!checkDrawReady("drawIndexedIndirect")) return; + if (!buffer) { logError("[Validation] drawIndexedIndirect: buffer is null"); return; } + m_inner->drawIndexedIndirect(buffer, offset, drawCount, stride); + } + + void executeBundles(std::span bundles) override { + if (m_ended) { logError("[Validation] executeBundles: render pass ended"); return; } + if (!m_viewportSet) logWarning("[Validation] executeBundles: viewport not set - bundles inherit viewport from the parent pass"); + if (!m_scissorSet) logWarning("[Validation] executeBundles: scissor not set - bundles inherit scissor from the parent pass"); + // Unwrap each ValidatedRenderBundle to its inner bundle before forwarding. + std::vector inner(bundles.size()); + for (usize i = 0; i < bundles.size(); ++i) { + auto* vb = static_cast(bundles[i]); + if (!vb) { logErrorf("[Validation] executeBundles: bundle %d is null", static_cast(i)); return; } + inner[i] = vb->inner(); + } + m_inner->executeBundles(std::span{ inner.data(), inner.size() }); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + if (m_ended) return; + if (!qs) { logError("[Validation] writeTimestamp: querySet is null"); return; } + m_inner->writeTimestamp(qs, index); + } + + void beginOcclusionQuery(QuerySet* qs, u32 index) override { + if (m_ended) return; + if (!qs) { logError("[Validation] beginOcclusionQuery: querySet is null"); return; } + m_inner->beginOcclusionQuery(qs, index); + } + + void endOcclusionQuery(QuerySet* qs, u32 index) override { + if (m_ended) return; + m_inner->endOcclusionQuery(qs, index); + } + + void end() override; + + // ---- MeshShaderPassExt ---- + + void setMeshPipeline(MeshPipeline* pipeline) override { + if (m_ended) { logError("[Validation] setMeshPipeline: render pass ended"); return; } + if (!pipeline) { logError("[Validation] setMeshPipeline: pipeline is null"); return; } + m_meshPipelineBound = true; m_pipelineBound = false; + auto* mp = m_inner->asMeshShaderExt(); + if (mp) mp->setMeshPipeline(pipeline); + else logError("[Validation] setMeshPipeline: inner encoder does not support mesh shaders"); + } + + void drawMeshTasks(u32 gx, u32 gy, u32 gz) override { + if (!checkDrawReady("drawMeshTasks")) return; + auto* mp = m_inner->asMeshShaderExt(); + if (mp) mp->drawMeshTasks(gx, gy, gz); + } + + void drawMeshTasksIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override { + if (!checkDrawReady("drawMeshTasksIndirect")) return; + if (!buf) { logError("[Validation] drawMeshTasksIndirect: buffer is null"); return; } + auto* mp = m_inner->asMeshShaderExt(); + if (mp) mp->drawMeshTasksIndirect(buf, offset, drawCount, stride); + } + + void drawMeshTasksIndirectCount(Buffer* buf, u64 offset, Buffer* countBuf, u64 countOffset, u32 maxDrawCount, u32 stride) override { + if (!checkDrawReady("drawMeshTasksIndirectCount")) return; + if (!buf || !countBuf) { logError("[Validation] drawMeshTasksIndirectCount: buffer is null"); return; } + auto* mp = m_inner->asMeshShaderExt(); + if (mp) mp->drawMeshTasksIndirectCount(buf, offset, countBuf, countOffset, maxDrawCount, stride); + } + +private: + bool checkDrawReady(const char* method) { + if (m_ended) { logErrorf("[Validation] %s: render pass ended", method); return false; } + if (!m_pipelineBound && !m_meshPipelineBound) { logErrorf("[Validation] %s: no pipeline bound", method); return false; } + if (!m_viewportSet) { logErrorf("[Validation] %s: viewport not set", method); return false; } + if (!m_scissorSet) { logErrorf("[Validation] %s: scissor not set", method); return false; } + return true; + } + + RenderPassEncoder* m_inner = nullptr; + ValidatedCommandEncoder* m_parent = nullptr; + bool m_pipelineBound = false; + bool m_meshPipelineBound = false; + bool m_viewportSet = false; + bool m_scissorSet = false; + bool m_ended = false; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedSwapChain.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedSwapChain.cppm new file mode 100644 index 00000000..9f56245d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedSwapChain.cppm @@ -0,0 +1,54 @@ +/// Validation wrapper for SwapChain. + +export module rhi.validation:validated_swap_chain; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_queue; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedSwapChain : public SwapChain { +public: + explicit ValidatedSwapChain(SwapChain* inner) : m_inner(inner) {} + + TextureFormat format() const override { return m_inner->format(); } + u32 width() const override { return m_inner->width(); } + u32 height() const override { return m_inner->height(); } + u32 bufferCount() const override { return m_inner->bufferCount(); } + u32 currentImageIndex() const override { return m_inner->currentImageIndex(); } + Texture* currentTexture() override { return m_inner->currentTexture(); } + TextureView* currentTextureView() override { return m_inner->currentTextureView(); } + + Status acquireNextImage() override { + if (m_imageAcquired) logWarning("[Validation] SwapChain::acquireNextImage: image already acquired"); + Status r = m_inner->acquireNextImage(); + if (r == ErrorCode::Ok) m_imageAcquired = true; + return r; + } + + Status present(Queue* queue) override { + if (!m_imageAcquired) logWarning("[Validation] SwapChain::present: no image acquired"); + m_imageAcquired = false; + // Unwrap validated queue so the inner swap chain gets the raw queue. + auto* vq = static_cast(queue); + return m_inner->present(vq ? vq->inner() : queue); + } + + Status resize(u32 w, u32 h) override { + if (m_imageAcquired) logError("[Validation] SwapChain::resize: cannot resize while image is acquired"); + if (w == 0 || h == 0) logError("[Validation] SwapChain::resize: dimensions are zero"); + return m_inner->resize(w, h); + } + + SwapChain* inner() const { return m_inner; } + +private: + SwapChain* m_inner; + bool m_imageAcquired = false; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedTransferBatch.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedTransferBatch.cppm new file mode 100644 index 00000000..56b39827 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedTransferBatch.cppm @@ -0,0 +1,74 @@ +/// Validation wrapper for TransferBatch. + +module; + +#include + +export module rhi.validation:validated_transfer_batch; + +import core.stdtypes; +import core.status; +import rhi; +import :validated_fence; + +using namespace draco; + +export namespace draco::rhi::validation { + +class ValidatedTransferBatch : public TransferBatch { +public: + explicit ValidatedTransferBatch(TransferBatch* inner) : m_inner(inner) {} + + void writeBuffer(Buffer* dst, u64 dstOffset, std::span data) override { + if (m_destroyed) { logError("[Validation] TransferBatch::writeBuffer: batch already destroyed"); return; } + if (!dst) { logError("[Validation] TransferBatch::writeBuffer: dst is null"); return; } + if (data.size() == 0) { logWarning("[Validation] TransferBatch::writeBuffer: data is empty"); return; } + m_pendingWrites++; + m_inner->writeBuffer(dst, dstOffset, data); + } + + void writeTexture(Texture* dst, std::span data, const TextureDataLayout& layout, + Extent3D extent, u32 mipLevel, u32 arrayLayer) override { + if (m_destroyed) { logError("[Validation] TransferBatch::writeTexture: batch already destroyed"); return; } + if (!dst) { logError("[Validation] TransferBatch::writeTexture: dst is null"); return; } + if (data.size() == 0) { logWarning("[Validation] TransferBatch::writeTexture: data is empty"); return; } + if (extent.width == 0 || extent.height == 0) { logError("[Validation] TransferBatch::writeTexture: extent is zero"); return; } + m_pendingWrites++; + m_inner->writeTexture(dst, data, layout, extent, mipLevel, arrayLayer); + } + + Status submit() override { + if (m_destroyed) { logError("[Validation] TransferBatch::submit: batch already destroyed"); return ErrorCode::Unknown; } + if (m_pendingWrites == 0) logWarning("[Validation] TransferBatch::submit: no pending writes"); + m_pendingWrites = 0; + return m_inner->submit(); + } + + Status submitAsync(Fence* fence, u64 signalValue) override { + if (m_destroyed) { logError("[Validation] TransferBatch::submitAsync: batch already destroyed"); return ErrorCode::Unknown; } + if (!fence) { logError("[Validation] TransferBatch::submitAsync: fence is null"); return ErrorCode::Unknown; } + if (m_pendingWrites == 0) logWarning("[Validation] TransferBatch::submitAsync: no pending writes"); + m_pendingWrites = 0; + auto* vf = static_cast(fence); + Fence* innerFence = vf ? vf->inner() : fence; + if (vf) vf->trackSignal(signalValue); + return m_inner->submitAsync(innerFence, signalValue); + } + + void reset() override { m_pendingWrites = 0; m_inner->reset(); } + + void destroy() override { + if (m_destroyed) { logWarning("[Validation] TransferBatch::destroy: already destroyed"); return; } + m_destroyed = true; + m_inner->destroy(); + } + + TransferBatch* inner() const { return m_inner; } + +private: + TransferBatch* m_inner; + bool m_destroyed = false; + i32 m_pendingWrites = 0; +}; + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedWiring.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedWiring.cppm new file mode 100644 index 00000000..33377069 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidatedWiring.cppm @@ -0,0 +1,28 @@ +/// Deferred implementations that break circular dependencies between +/// ValidatedBackend, ValidatedAdapter, and ValidatedDevice. + +export module rhi.validation:wiring; + +import core.status; +import rhi; +import :validated_backend; +import :validated_adapter; +import :validated_device; + +using namespace draco; + +namespace draco::rhi::validation { + +ValidatedAdapter* ValidatedBackend::createValidatedAdapter(Adapter* inner) { + return new ValidatedAdapter(inner); +} + +Status ValidatedAdapter::createDevice(const DeviceDesc& desc, Device*& out) { + Device* innerDevice = nullptr; + Status r = m_inner->createDevice(desc, innerDevice); + if (r != ErrorCode::Ok || !innerDevice) { out = nullptr; return r; } + out = new ValidatedDevice(innerDevice); + return ErrorCode::Ok; +} + +} // namespace draco::rhi::validation diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationModule.cppm new file mode 100644 index 00000000..274250e5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationModule.cppm @@ -0,0 +1,17 @@ +/// Validation layer module surface. +/// Provides createValidatedBackend() as the entry point. + +export module rhi.validation; + +export import :validated_backend; +export import :validated_adapter; +export import :validated_device; +export import :validated_queue; +export import :validated_command_pool; +export import :validated_command_encoder; +export import :validated_render_pass_encoder; +export import :validated_render_bundle_encoder; +export import :validated_compute_pass_encoder; +export import :validated_swap_chain; +export import :validated_fence; +export import :validated_transfer_batch; diff --git a/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationRhi.test.cpp b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationRhi.test.cpp new file mode 100644 index 00000000..7c2027da --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Validation/ValidationRhi.test.cpp @@ -0,0 +1,45 @@ +#include + +import core; +import rhi; +import rhi.null; +import rhi.validation; + +using namespace draco; +using namespace draco::rhi; + +TEST_CASE("rhi.validation: wraps a backend and forwards valid calls") +{ + Backend* inner = nullptr; + REQUIRE(null::createNullBackend(inner).isOk()); + + validation::ValidatedBackend vb(inner); + auto adapters = vb.enumerateAdapters(); + REQUIRE(adapters.size() >= 1u); + + Device* device = nullptr; + REQUIRE(adapters[0]->createDevice(DeviceDesc{}, device).isOk()); + REQUIRE(device != nullptr); + + BufferDesc bufferDesc{}; + bufferDesc.size = 128; + Buffer* buffer = nullptr; + CHECK(device->createBuffer(bufferDesc, buffer).isOk()); + CHECK(buffer != nullptr); +} + +TEST_CASE("rhi.validation: catches invalid usage (null texture)") +{ + Backend* inner = nullptr; + REQUIRE(null::createNullBackend(inner).isOk()); + + validation::ValidatedBackend vb(inner); + Device* device = nullptr; + REQUIRE(vb.enumerateAdapters()[0]->createDevice(DeviceDesc{}, device).isOk()); + + // The validation layer rejects a null texture (and logs a diagnostic) instead + // of forwarding it to the backend. + TextureView* view = nullptr; + CHECK_FALSE(device->createTextureView(nullptr, TextureViewDesc{}, view).isOk()); + CHECK(view == nullptr); +} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vertex.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vertex.cppm deleted file mode 100644 index fa2356e5..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/Vertex.cppm +++ /dev/null @@ -1,51 +0,0 @@ -module; - -#include -#include - -export module rendering.rhi.vertex; -import core.stdtypes; - -export namespace draco::rendering::rhi { - enum class Attrib { - Position, - Color0, - TexCoord0, - Normal, - Tangent - }; - - enum class AttribType { - Float, - Uint8 - }; - - struct VertexElement { - Attrib attrib; - u16 count; - AttribType type; - bool normalized = false; - }; - - struct VertexLayoutDesc { - std::vector elements; - }; - - struct alignas(u32) TexturedVertex { - float x, y, z; - float u, v; - u32 color; - }; - static_assert(sizeof(TexturedVertex) == 24); - - // Helper to get the standard layout for the current vertex struct - inline VertexLayoutDesc getTexturedVertexLayout() { - return { - .elements = { - { Attrib::Position, 3, AttribType::Float }, - { Attrib::TexCoord0, 2, AttribType::Float }, - { Attrib::Color0, 4, AttribType::Uint8, true } - } - }; - } -} diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAccelStruct.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAccelStruct.cppm new file mode 100644 index 00000000..9c4c8457 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAccelStruct.cppm @@ -0,0 +1,100 @@ +/// Vulkan implementation of AccelStruct (acceleration structure). + +module; + +#include "VkIncludes.h" + +export module rhi.vk:accel_struct; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkAccelStructImpl : public AccelStruct { +public: + Status init(VkDevice device, VkAdapterImpl* adapter, const AccelStructDesc& desc, u64 size) { + m_type = desc.type; + m_device = device; + + // Create buffer for the acceleration structure. + VkBufferCreateInfo bufCi{}; + bufCi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCi.size = size; + bufCi.usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + if (vkCreateBuffer(device, &bufCi, nullptr, &m_buffer) != VK_SUCCESS) return ErrorCode::Unknown; + + VkMemoryRequirements memReqs{}; + vkGetBufferMemoryRequirements(device, m_buffer, &memReqs); + + i32 memType = adapter->findMemoryType(static_cast(memReqs.memoryTypeBits), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (memType < 0) { vkDestroyBuffer(device, m_buffer, nullptr); m_buffer = VK_NULL_HANDLE; return ErrorCode::Unknown; } + + VkMemoryAllocateFlagsInfo allocFlags{}; + allocFlags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO; + allocFlags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT; + + VkMemoryAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.pNext = &allocFlags; + ai.allocationSize = memReqs.size; + ai.memoryTypeIndex = static_cast(memType); + if (vkAllocateMemory(device, &ai, nullptr, &m_memory) != VK_SUCCESS) { + vkDestroyBuffer(device, m_buffer, nullptr); m_buffer = VK_NULL_HANDLE; return ErrorCode::Unknown; + } + vkBindBufferMemory(device, m_buffer, m_memory, 0); + + // Create acceleration structure. + VkAccelerationStructureCreateInfoKHR asCi{}; + asCi.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR; + asCi.buffer = m_buffer; + asCi.size = size; + asCi.type = desc.type == AccelStructType::TopLevel + ? VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR + : VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; + + auto pfnCreate = reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCreateAccelerationStructureKHR")); + if (!pfnCreate || pfnCreate(device, &asCi, nullptr, &m_accel) != VK_SUCCESS) return ErrorCode::Unknown; + + // Get device address. + VkAccelerationStructureDeviceAddressInfoKHR addrInfo{}; + addrInfo.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR; + addrInfo.accelerationStructure = m_accel; + auto pfnAddr = reinterpret_cast( + vkGetDeviceProcAddr(device, "vkGetAccelerationStructureDeviceAddressKHR")); + if (pfnAddr) m_deviceAddress = pfnAddr(device, &addrInfo); + + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_accel != VK_NULL_HANDLE) { + auto pfn = reinterpret_cast( + vkGetDeviceProcAddr(device, "vkDestroyAccelerationStructureKHR")); + if (pfn) pfn(device, m_accel, nullptr); + m_accel = VK_NULL_HANDLE; + } + if (m_memory != VK_NULL_HANDLE) { vkFreeMemory(device, m_memory, nullptr); m_memory = VK_NULL_HANDLE; } + if (m_buffer != VK_NULL_HANDLE) { vkDestroyBuffer(device, m_buffer, nullptr); m_buffer = VK_NULL_HANDLE; } + } + + AccelStructType type() const override { return m_type; } + u64 deviceAddress() const override { return m_deviceAddress; } + + [[nodiscard]] VkAccelerationStructureKHR handle() const { return m_accel; } + +private: + VkAccelerationStructureKHR m_accel = VK_NULL_HANDLE; + VkBuffer m_buffer = VK_NULL_HANDLE; + VkDeviceMemory m_memory = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + AccelStructType m_type = AccelStructType::BottomLevel; + u64 m_deviceAddress = 0; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAdapter.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAdapter.cppm new file mode 100644 index 00000000..c4913a50 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkAdapter.cppm @@ -0,0 +1,224 @@ +/// Vulkan implementation of Adapter. +/// Queries physical device properties, features, and queue families. +/// Provides feature detection and queue family selection. + +module; + +#include "VkIncludes.h" +#include +#include +#include + +#include + +export module rhi.vk:adapter; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDeviceImpl; // forward + +class VkAdapterImpl : public Adapter { +public: + VkAdapterImpl(VkPhysicalDevice physicalDevice, VkInstance instance) + : m_physicalDevice(physicalDevice), m_instance(instance) + { + vkGetPhysicalDeviceProperties(m_physicalDevice, &m_properties); + vkGetPhysicalDeviceFeatures(m_physicalDevice, &m_features10); + vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &m_memoryProperties); + + u32 qfCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(m_physicalDevice, &qfCount, nullptr); + m_queueFamilies.resize(qfCount); + vkGetPhysicalDeviceQueueFamilyProperties(m_physicalDevice, &qfCount, m_queueFamilies.data()); + + queryExtensionSupport(); + } + + // ---- Adapter interface ---- + + void getInfo(AdapterInfo& out) override { + out.name = std::u8string(std::u8string_view(reinterpret_cast(m_properties.deviceName))); + out.vendorId = m_properties.vendorID; + out.deviceId = m_properties.deviceID; + + switch (m_properties.deviceType) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: out.type = AdapterType::DiscreteGpu; break; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: out.type = AdapterType::IntegratedGpu; break; + case VK_PHYSICAL_DEVICE_TYPE_CPU: out.type = AdapterType::Cpu; break; + default: out.type = AdapterType::Unknown; break; + } + + out.supportedFeatures = buildFeatures(); + } + + Status createDevice(const DeviceDesc& desc, Device*& out) override; + + // ---- Feature building ---- + + DeviceFeatures buildFeatures() const { + DeviceFeatures f{}; + + f.bindlessDescriptors = m_supportsDescriptorIndexing; + f.timestampQueries = m_properties.limits.timestampComputeAndGraphics; + f.multiDrawIndirect = m_features10.multiDrawIndirect; + f.depthClamp = m_features10.depthClamp; + f.fillModeWireframe = m_features10.fillModeNonSolid; + f.textureCompressionBC = m_features10.textureCompressionBC; + f.textureCompressionASTC = m_features10.textureCompressionASTC_LDR; + f.independentBlend = m_features10.independentBlend; + f.multiViewport = m_features10.multiViewport; + f.meshShaders = m_supportsMeshShader; + f.rayTracing = m_supportsRayTracing; + f.pipelineStatisticsQueries = m_features10.pipelineStatisticsQuery; + + // Mesh shader limits. + if (m_supportsMeshShader) { + VkPhysicalDeviceMeshShaderPropertiesEXT meshProps{}; + meshProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 p2{}; + p2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + p2.pNext = &meshProps; + vkGetPhysicalDeviceProperties2(m_physicalDevice, &p2); + f.maxMeshOutputVertices = meshProps.maxMeshOutputVertices; + f.maxMeshOutputPrimitives = meshProps.maxMeshOutputPrimitives; + f.maxMeshWorkgroupSize = meshProps.maxMeshWorkGroupInvocations; + f.maxTaskWorkgroupSize = meshProps.maxTaskWorkGroupInvocations; + } + + // Limits. + f.maxBindGroups = m_properties.limits.maxBoundDescriptorSets; + f.maxBindingsPerGroup = m_properties.limits.maxDescriptorSetUniformBuffers; + f.maxPushConstantSize = m_properties.limits.maxPushConstantsSize; + f.maxTextureDimension2D = m_properties.limits.maxImageDimension2D; + f.maxTextureArrayLayers = m_properties.limits.maxImageArrayLayers; + f.maxComputeWorkgroupSizeX = m_properties.limits.maxComputeWorkGroupSize[0]; + f.maxComputeWorkgroupSizeY = m_properties.limits.maxComputeWorkGroupSize[1]; + f.maxComputeWorkgroupSizeZ = m_properties.limits.maxComputeWorkGroupSize[2]; + f.maxComputeWorkgroupsPerDimension = m_properties.limits.maxComputeWorkGroupCount[0]; + f.maxBufferSize = static_cast(m_properties.limits.maxStorageBufferRange); + f.minUniformBufferOffsetAlignment = static_cast(m_properties.limits.minUniformBufferOffsetAlignment); + f.minStorageBufferOffsetAlignment = static_cast(m_properties.limits.minStorageBufferOffsetAlignment); + f.timestampPeriodNs = static_cast(m_properties.limits.timestampPeriod); + + return f; + } + + // ---- Queue family selection ---- + + /// Finds the best queue family index for the given type. + /// Prefers dedicated families for Compute and Transfer. + i32 findQueueFamily(QueueType type) const { + const auto count = static_cast(m_queueFamilies.size()); + switch (type) { + case QueueType::Graphics: + for (i32 i = 0; i < count; ++i) + if (m_queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) return i; + break; + case QueueType::Compute: + // Prefer dedicated (no graphics). + for (i32 i = 0; i < count; ++i) { + auto f = m_queueFamilies[i].queueFlags; + if ((f & VK_QUEUE_COMPUTE_BIT) && !(f & VK_QUEUE_GRAPHICS_BIT)) return i; + } + for (i32 i = 0; i < count; ++i) + if (m_queueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT) return i; + break; + case QueueType::Transfer: + // Prefer dedicated (no graphics or compute). + for (i32 i = 0; i < count; ++i) { + auto f = m_queueFamilies[i].queueFlags; + if ((f & VK_QUEUE_TRANSFER_BIT) && !(f & VK_QUEUE_GRAPHICS_BIT) && !(f & VK_QUEUE_COMPUTE_BIT)) + return i; + } + for (i32 i = 0; i < count; ++i) + if (m_queueFamilies[i].queueFlags & VK_QUEUE_TRANSFER_BIT) return i; + break; + } + return -1; + } + + /// Finds a memory type index matching the filter and property requirements. + i32 findMemoryType(u32 typeFilter, VkMemoryPropertyFlags properties) const { + for (u32 i = 0; i < m_memoryProperties.memoryTypeCount; ++i) { + if ((typeFilter & (1 << i)) && + (m_memoryProperties.memoryTypes[i].propertyFlags & properties) == properties) + return static_cast(i); + } + return -1; + } + + /// Maps a MemoryLocation to VkMemoryPropertyFlags. + static VkMemoryPropertyFlags getMemoryFlags(MemoryLocation loc) { + switch (loc) { + case MemoryLocation::GpuOnly: return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + case MemoryLocation::CpuToGpu: return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + case MemoryLocation::GpuToCpu: return VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + case MemoryLocation::Auto: return VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + } + return 0; + } + + // ---- Internal accessors ---- + [[nodiscard]] VkPhysicalDevice physicalDevice() const { return m_physicalDevice; } + [[nodiscard]] const VkPhysicalDeviceProperties& properties() const { return m_properties; } + [[nodiscard]] const VkPhysicalDeviceFeatures& features10() const { return m_features10; } + [[nodiscard]] const VkPhysicalDeviceMemoryProperties& memoryProperties() const { return m_memoryProperties; } + [[nodiscard]] const std::vector& queueFamilies() const { return m_queueFamilies; } + + [[nodiscard]] bool supportsDescriptorIndexing() const { return m_supportsDescriptorIndexing; } + [[nodiscard]] bool supportsMeshShader() const { return m_supportsMeshShader; } + [[nodiscard]] bool supportsRayTracing() const { return m_supportsRayTracing; } + +private: + void queryExtensionSupport() { + u32 extCount = 0; + vkEnumerateDeviceExtensionProperties(m_physicalDevice, nullptr, &extCount, nullptr); + std::vector exts(extCount); + vkEnumerateDeviceExtensionProperties(m_physicalDevice, nullptr, &extCount, exts.data()); + + for (const auto& ext : exts) { + const char* name = ext.extensionName; + if (std::strcmp(name, "VK_KHR_dynamic_rendering") == 0) m_supportsDynamicRendering = true; + if (std::strcmp(name, "VK_KHR_timeline_semaphore") == 0) m_supportsTimelineSemaphore = true; + if (std::strcmp(name, "VK_KHR_synchronization2") == 0) m_supportsSynchronization2 = true; + if (std::strcmp(name, "VK_EXT_descriptor_indexing") == 0) m_supportsDescriptorIndexing = true; + if (std::strcmp(name, "VK_EXT_mesh_shader") == 0) m_supportsMeshShader = true; + if (std::strcmp(name, "VK_KHR_ray_tracing_pipeline") == 0) m_supportsRayTracing = true; + } + + // Vulkan 1.3+: these are core. + u32 major = VK_API_VERSION_MAJOR(m_properties.apiVersion); + u32 minor = VK_API_VERSION_MINOR(m_properties.apiVersion); + if (major > 1 || (major == 1 && minor >= 3)) { + m_supportsDynamicRendering = true; + m_supportsTimelineSemaphore = true; + m_supportsSynchronization2 = true; + m_supportsDescriptorIndexing = true; + } else if (major == 1 && minor >= 2) { + m_supportsTimelineSemaphore = true; + m_supportsDescriptorIndexing = true; + } + } + + VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; + VkInstance m_instance = VK_NULL_HANDLE; + VkPhysicalDeviceProperties m_properties{}; + VkPhysicalDeviceFeatures m_features10{}; + VkPhysicalDeviceMemoryProperties m_memoryProperties{}; + std::vector m_queueFamilies; + + bool m_supportsDynamicRendering = false; + bool m_supportsTimelineSemaphore = false; + bool m_supportsSynchronization2 = false; + bool m_supportsDescriptorIndexing = false; + bool m_supportsMeshShader = false; + bool m_supportsRayTracing = false; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBackend.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBackend.cppm new file mode 100644 index 00000000..de0fde99 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBackend.cppm @@ -0,0 +1,351 @@ +/// Vulkan implementation of Backend. +/// Creates VkInstance, enumerates physical devices, creates surfaces. + +module; + +#include "VkIncludes.h" +#include +#include + +#include +#include +#include +#include + +export module rhi.vk:backend; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; +import :surface; + +using namespace draco; + +// Linux surface types - forward-declared to avoid header pollution. In the module +// purview (not the GMF): GCC requires the GMF to contain only #includes. +#if defined(__linux__) +extern "C" { typedef struct _XDisplay Display; } +typedef unsigned long XID; + +struct VkXlibSurfaceCreateInfoKHR { + VkStructureType sType; + const void* pNext; + VkFlags flags; + Display* dpy; + XID window; +}; +#define VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR static_cast(1000004000) +#define VK_KHR_XLIB_SURFACE_EXTENSION_NAME "VK_KHR_xlib_surface" +using PFN_vkCreateXlibSurfaceKHR = VkResult(VKAPI_PTR*)( + VkInstance, const VkXlibSurfaceCreateInfoKHR*, const VkAllocationCallbacks*, VkSurfaceKHR*); + +extern "C" { struct wl_display; struct wl_surface; } + +struct VkWaylandSurfaceCreateInfoKHR { + VkStructureType sType; + const void* pNext; + VkFlags flags; + wl_display* display; + wl_surface* surface; +}; +#define VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR static_cast(1000006000) +#define VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME "VK_KHR_wayland_surface" +using PFN_vkCreateWaylandSurfaceKHR = VkResult(VKAPI_PTR*)( + VkInstance, const VkWaylandSurfaceCreateInfoKHR*, const VkAllocationCallbacks*, VkSurfaceKHR*); +#endif // __linux__ + +export namespace draco::rhi::vk { + +/// Configuration for VK backend creation. +struct VkBackendDesc { + bool enableValidation = false; +}; + +/// Vulkan implementation of Backend. +class VkBackendImpl : public Backend { +public: + ~VkBackendImpl() override { destroyImpl(); } + + // ---- Backend interface ---- + + std::span enumerateAdapters() override { + return std::span(m_adapterPtrs.data(), m_adapterPtrs.size()); + } + + Status createSurface(void* windowHandle, void* +#if defined(__linux__) + displayHandle +#else + /*displayHandle*/ +#endif + , Surface*& out) override { + out = nullptr; + if (!windowHandle) { + logError("VkBackend: window handle is null"); + return ErrorCode::InvalidArgument; + } + + VkSurfaceKHR vkSurface = VK_NULL_HANDLE; + +#ifdef _WIN32 + VkWin32SurfaceCreateInfoKHR ci{}; + ci.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; + ci.hinstance = ::GetModuleHandleW(nullptr); + ci.hwnd = reinterpret_cast(windowHandle); + + VkResult vr = vkCreateWin32SurfaceKHR(m_instance, &ci, nullptr, &vkSurface); + if (vr != VK_SUCCESS) { + logErrorf("VkBackend: vkCreateWin32SurfaceKHR failed (%d)", static_cast(vr)); + return ErrorCode::Unknown; + } +#elif defined(__linux__) + // Detect surface type from the window handles SDL3 actually provides. + // SDL3 may choose Wayland even when DISPLAY is set (e.g. on Arch). + // displayHandle == X11 Display* for X11, or wl_display* for Wayland. + // We try Wayland first (if displayHandle looks like wl_display and we have the ext), + // then fall back to X11. + VkResult vr = VK_ERROR_INITIALIZATION_FAILED; + bool triedWayland = false, triedX11 = false; + + // If we have Wayland ext and the display handle could be wl_display, + // try Wayland. The SDL3 window's displayHandle returns wl_display* + // when SDL chose Wayland, or X11 Display* when SDL chose X11. + // We can't easily distinguish the pointer types, so we try Wayland first + // if the extension is available and WAYLAND_DISPLAY is set, then X11. + if (m_hasWayland && std::getenv("WAYLAND_DISPLAY")) { + auto fn = reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkCreateWaylandSurfaceKHR")); + if (fn) { + VkWaylandSurfaceCreateInfoKHR ci{}; + ci.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR; + ci.display = reinterpret_cast(displayHandle); + ci.surface = reinterpret_cast(windowHandle); + vr = fn(m_instance, &ci, nullptr, &vkSurface); + triedWayland = true; + } + } + + // Fall back to X11 if Wayland failed or wasn't tried. + if (vr != VK_SUCCESS && m_hasXlib) { + auto fn = reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkCreateXlibSurfaceKHR")); + if (fn) { + VkXlibSurfaceCreateInfoKHR ci{}; + ci.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; + ci.dpy = reinterpret_cast(displayHandle); + ci.window = static_cast(reinterpret_cast(windowHandle)); + vr = fn(m_instance, &ci, nullptr, &vkSurface); + triedX11 = true; + } + } + + if (vr != VK_SUCCESS) { + logErrorf("VkBackend: surface creation failed (tried wayland=%d, x11=%d)", triedWayland, triedX11); + return ErrorCode::Unknown; + } + if (vr != VK_SUCCESS) { + logErrorf("VkBackend: surface creation failed (%d)", static_cast(vr)); + return ErrorCode::Unknown; + } +#else + (void)displayHandle; + logError("VkBackend: surface creation not supported on this platform"); + return ErrorCode::NotSupported; +#endif + + out = new VkSurfaceImpl(vkSurface, m_instance); + return ErrorCode::Ok; + } + + void destroy() override { + destroyImpl(); + delete this; + } + + // ---- Internal ---- + + [[nodiscard]] VkInstance instance() const { return m_instance; } + [[nodiscard]] bool validationEnabled() const { return m_validationEnabled; } + +private: + friend Status createBackend(const VkBackendDesc& desc, Backend*& out); + + Status init(bool enableValidation) { + m_validationEnabled = enableValidation; + // Announce validation state so a perf run can confirm the layers are OFF (they add real overhead). + std::fprintf(stderr, "[Vulkan] validation layers: %s\n", enableValidation ? "ENABLED" : "DISABLED"); + + // ---- Application info ---- + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Draconic"; + appInfo.applicationVersion = VK_MAKE_API_VERSION(0, 1, 0, 0); + appInfo.pEngineName = "Draconic"; + appInfo.engineVersion = VK_MAKE_API_VERSION(0, 1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_3; + + // ---- Extensions ---- + std::vector extensions; + extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); + +#ifdef _WIN32 + extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME); +#elif defined(__linux__) + { + u32 availCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &availCount, nullptr); + std::vector avail(availCount); + vkEnumerateInstanceExtensionProperties(nullptr, &availCount, avail.data()); + auto hasExt = [&](const char* name) { + for (const auto& e : avail) + if (std::strcmp(e.extensionName, name) == 0) return true; + return false; + }; + + bool hasXlib = hasExt(VK_KHR_XLIB_SURFACE_EXTENSION_NAME); + bool hasWayland = hasExt(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME); + + // Enable both surface extensions if available. The actual surface + // type is detected at createSurface time from the window handles, + // because SDL3 may choose Wayland even when DISPLAY is set. + if (hasXlib) extensions.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME); + if (hasWayland) extensions.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME); + m_hasXlib = hasXlib; m_hasWayland = hasWayland; + if (!hasXlib && !hasWayland) { + logError("VkBackend: no surface extension available (need xlib or wayland)"); + return ErrorCode::Unknown; + } + } +#endif + + if (enableValidation) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + + // ---- Layers ---- + std::vector layers; + if (enableValidation) layers.push_back("VK_LAYER_KHRONOS_validation"); + + // ---- Create instance ---- + VkInstanceCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + ci.pApplicationInfo = &appInfo; + ci.enabledExtensionCount = static_cast(extensions.size()); + ci.ppEnabledExtensionNames = extensions.data(); + ci.enabledLayerCount = static_cast(layers.size()); + ci.ppEnabledLayerNames = layers.data(); + + VkResult vr = vkCreateInstance(&ci, nullptr, &m_instance); + if (vr != VK_SUCCESS) { + logErrorf("VkBackend: vkCreateInstance failed (%d)", static_cast(vr)); + return ErrorCode::Unknown; + } + + if (enableValidation) setupDebugMessenger(); + + enumeratePhysicalDevices(); + + isInitialized = true; + return ErrorCode::Ok; + } + + void setupDebugMessenger() { + VkDebugUtilsMessengerCreateInfoEXT ci{}; + ci.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + ci.messageSeverity = + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + ci.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; + ci.pfnUserCallback = &debugCallback; + + auto pfn = reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkCreateDebugUtilsMessengerEXT")); + if (pfn) pfn(m_instance, &ci, nullptr, &m_debugMessenger); + } + + static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT /*types*/, + const VkDebugUtilsMessengerCallbackDataEXT* data, + void* /*userData*/) + { + // Print straight to stderr (not only LogErrorf, whose sink may be swallowed) so validation + // is actually visible in dev builds - the point of running with it on. + if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + std::fprintf(stderr, "[Vulkan ERROR] %s\n", data->pMessage); + logErrorf("[Vulkan ERROR] %s", data->pMessage); + } else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + std::fprintf(stderr, "[Vulkan WARN] %s\n", data->pMessage); + logWarningf("[Vulkan WARN] %s", data->pMessage); + } + return VK_FALSE; + } + + void enumeratePhysicalDevices() { + u32 count = 0; + vkEnumeratePhysicalDevices(m_instance, &count, nullptr); + if (count == 0) return; + + std::vector devices(count); + vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); + + m_adapters.reserve(count); + m_adapterPtrs.reserve(count); + for (VkPhysicalDevice pd : devices) { + auto* a = new VkAdapterImpl(pd, m_instance); + m_adapters.push_back(a); + m_adapterPtrs.push_back(a); + } + + // Expose adapters best-GPU-first; callers take [0]. See Backend::enumerateAdapters. + sortAdaptersByPreference(m_adapterPtrs); + } + + void destroyImpl() { + for (auto* a : m_adapters) delete a; + m_adapters.clear(); + m_adapterPtrs.clear(); + + if (m_debugMessenger != VK_NULL_HANDLE) { + auto pfn = reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkDestroyDebugUtilsMessengerEXT")); + if (pfn) pfn(m_instance, m_debugMessenger, nullptr); + m_debugMessenger = VK_NULL_HANDLE; + } + + if (m_instance != VK_NULL_HANDLE) { + vkDestroyInstance(m_instance, nullptr); + m_instance = VK_NULL_HANDLE; + } + + isInitialized = false; + } + + VkInstance m_instance = VK_NULL_HANDLE; + VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE; + bool m_validationEnabled = false; + std::vector m_adapters; + std::vector m_adapterPtrs; + +#if defined(__linux__) + bool m_hasXlib = false; + bool m_hasWayland = false; +#endif +}; + +/// Creates a Vulkan backend. Caller owns the returned pointer - dispose via destroy(). +[[nodiscard]] Status createBackend(const VkBackendDesc& desc, Backend*& out) { + out = nullptr; + auto* b = new VkBackendImpl(); + Status r = b->init(desc.enableValidation); + if (r != ErrorCode::Ok) { + delete b; + return r; + } + out = b; + return ErrorCode::Ok; +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBarrierHelper.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBarrierHelper.cppm new file mode 100644 index 00000000..c4c2351a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBarrierHelper.cppm @@ -0,0 +1,69 @@ +/// Converts ResourceState to Vulkan synchronization2 stage/access/layout. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:barrier_helper; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +struct StageAccess { + u64 stageMask = 0; + u64 accessMask = 0; +}; + +inline StageAccess getStageAccess(ResourceState state) { + StageAccess sa{}; + auto s = static_cast(state); + auto has = [&](ResourceState f) { return (s & static_cast(f)) != 0; }; + + if (has(ResourceState::VertexBuffer)) { sa.stageMask |= VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT; sa.accessMask |= VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT; } + if (has(ResourceState::IndexBuffer)) { sa.stageMask |= VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT; sa.accessMask |= VK_ACCESS_2_INDEX_READ_BIT; } + if (has(ResourceState::UniformBuffer)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; sa.accessMask |= VK_ACCESS_2_UNIFORM_READ_BIT; } + if (has(ResourceState::ShaderRead)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; sa.accessMask |= VK_ACCESS_2_SHADER_READ_BIT; } + if (has(ResourceState::ShaderWrite)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; sa.accessMask |= VK_ACCESS_2_SHADER_WRITE_BIT; } + if (has(ResourceState::RenderTarget)) { sa.stageMask |= VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; sa.accessMask |= VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT; } + if (has(ResourceState::DepthStencilWrite)){ sa.stageMask |= VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; sa.accessMask |= VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT; } + if (has(ResourceState::DepthStencilRead)) { sa.stageMask |= VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; sa.accessMask |= VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT; } + if (has(ResourceState::IndirectArgument)) { sa.stageMask |= VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT; sa.accessMask |= VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT; } + if (has(ResourceState::CopySrc)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; sa.accessMask |= VK_ACCESS_2_TRANSFER_READ_BIT; } + if (has(ResourceState::CopyDst)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; sa.accessMask |= VK_ACCESS_2_TRANSFER_WRITE_BIT; } + if (has(ResourceState::Present)) { sa.stageMask |= VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT; } + if (has(ResourceState::AccelStructRead)) { sa.stageMask |= VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR; sa.accessMask |= VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR; } + if (has(ResourceState::AccelStructWrite)) { sa.stageMask |= VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; sa.accessMask |= VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; } + if (has(ResourceState::General)) { sa.stageMask |= VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; sa.accessMask |= VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT; } + + if (sa.stageMask == 0) sa.stageMask = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT; + return sa; +} + +inline VkImageLayout getImageLayout(ResourceState state, TextureFormat format = TextureFormat::Undefined) { + auto s = static_cast(state); + auto has = [&](ResourceState f) { return (s & static_cast(f)) != 0; }; + + if (has(ResourceState::Present)) return VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + if (has(ResourceState::RenderTarget)) return VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + if (has(ResourceState::DepthStencilWrite))return VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + if (has(ResourceState::DepthStencilRead)) return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + if (has(ResourceState::ShaderRead)) { + // Depth/stencil textures must use DEPTH_STENCIL_READ_ONLY when sampled, + // not SHADER_READ_ONLY - the latter is only for color textures. + if (isDepthFormat(format)) + return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + if (has(ResourceState::ShaderWrite)) return VK_IMAGE_LAYOUT_GENERAL; + if (has(ResourceState::CopySrc)) return VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + if (has(ResourceState::CopyDst)) return VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + if (has(ResourceState::General)) return VK_IMAGE_LAYOUT_GENERAL; + return VK_IMAGE_LAYOUT_UNDEFINED; +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroup.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroup.cppm new file mode 100644 index 00000000..b6fc8bf2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroup.cppm @@ -0,0 +1,210 @@ +/// Vulkan implementation of BindGroup. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:bind_group; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :binding_shifts; +import :bind_group_layout; +import :buffer; +import :texture; +import :texture_view; +import :sampler; +import :accel_struct; +import :descriptor_pool_manager; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkBindGroupImpl : public BindGroup { +public: + Status init(VkDevice device, VkDescriptorPoolManager* poolMgr, const BindGroupDesc& desc, + const BindingShifts& shifts = {}) { + m_device = device; + m_shifts = shifts; + m_layout = static_cast(desc.layout); + if (!m_layout) return ErrorCode::Unknown; + + VkDescriptorPool pool; + if (poolMgr->allocate(m_layout->handle(), pool, m_layout->hasBindless(), m_layout->bindlessCount()) != ErrorCode::Ok) + return ErrorCode::Unknown; + m_pool = pool; + m_set = poolMgr->lastAllocatedSet(); + + writeDescriptors(device, desc); + return ErrorCode::Ok; + } + + void cleanup(VkDevice device, VkDescriptorPoolManager* poolMgr) { + if (m_set != VK_NULL_HANDLE && m_pool != VK_NULL_HANDLE) { + poolMgr->free(m_pool, m_set); + m_set = VK_NULL_HANDLE; + m_pool = VK_NULL_HANDLE; + } + (void)device; + } + + BindGroupLayout* layout() override { return m_layout; } + + void updateBindless(std::span entries) override { + if (entries.size() == 0) return; + + std::vector writes; + std::vector bufInfos; + std::vector imgInfos; + bufInfos.reserve(entries.size()); + imgInfos.reserve(entries.size()); + + auto layoutEntries = m_layout->entries(); + + for (usize i = 0; i < entries.size(); ++i) { + const auto& e = entries[i]; + if (e.layoutIndex >= static_cast(layoutEntries.size())) continue; + const auto& le = layoutEntries[e.layoutIndex]; + + VkWriteDescriptorSet w{}; + w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w.dstSet = m_set; + w.dstBinding = m_shifts.apply(le.type, le.binding); + w.dstArrayElement = e.arrayIndex; + w.descriptorCount = 1; + w.descriptorType = toVkDescriptorType(le); + + switch (le.type) { + case BindingType::BindlessStorageBuffers: + if (auto* vkBuf = static_cast(e.buffer)) { + VkDescriptorBufferInfo bi{}; bi.buffer = vkBuf->handle(); bi.offset = e.bufferOffset; + bi.range = e.bufferSize > 0 ? e.bufferSize : VK_WHOLE_SIZE; + bufInfos.push_back(bi); w.pBufferInfo = &bufInfos.back(); + } else continue; + break; + case BindingType::BindlessTextures: case BindingType::BindlessStorageTextures: + if (auto* vkView = static_cast(e.textureView)) { + VkDescriptorImageInfo ii{}; ii.imageView = vkView->handle(); + ii.imageLayout = le.type == BindingType::BindlessTextures + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; + imgInfos.push_back(ii); w.pImageInfo = &imgInfos.back(); + } else continue; + break; + case BindingType::BindlessSamplers: + if (auto* vkSamp = static_cast(e.sampler)) { + VkDescriptorImageInfo ii{}; ii.sampler = vkSamp->handle(); + imgInfos.push_back(ii); w.pImageInfo = &imgInfos.back(); + } else continue; + break; + default: continue; + } + writes.push_back(w); + } + + if (!writes.empty()) + vkUpdateDescriptorSets(m_device, static_cast(writes.size()), writes.data(), 0, nullptr); + } + + [[nodiscard]] VkDescriptorSet handle() const { return m_set; } + +private: + void writeDescriptors(VkDevice device, const BindGroupDesc& desc) { + if (desc.entries.size() == 0) return; + + auto layoutEntries = m_layout->entries(); + + std::vector writes; + std::vector bufInfos; + std::vector imgInfos; + std::vector asWriteInfos; + std::vector asHandles; + bufInfos.reserve(desc.entries.size()); + imgInfos.reserve(desc.entries.size()); + asWriteInfos.reserve(desc.entries.size()); + asHandles.reserve(desc.entries.size()); + + usize entryIdx = 0; + for (usize i = 0; i < layoutEntries.size(); ++i) { + const auto& le = layoutEntries[i]; + // Skip bindless entries. + switch (le.type) { + case BindingType::BindlessTextures: case BindingType::BindlessSamplers: + case BindingType::BindlessStorageBuffers: case BindingType::BindlessStorageTextures: + continue; + default: break; + } + if (entryIdx >= desc.entries.size()) break; + const auto& e = desc.entries[entryIdx++]; + + VkWriteDescriptorSet w{}; + w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w.dstSet = m_set; + w.dstBinding = m_shifts.apply(le.type, le.binding); + w.dstArrayElement = 0; + w.descriptorCount = 1; + w.descriptorType = toVkDescriptorType(le); + + switch (le.type) { + case BindingType::UniformBuffer: case BindingType::StorageBufferReadOnly: case BindingType::StorageBufferReadWrite: + if (auto* vkBuf = static_cast(e.buffer)) { + VkDescriptorBufferInfo bi{}; bi.buffer = vkBuf->handle(); bi.offset = e.bufferOffset; + bi.range = e.bufferSize > 0 ? e.bufferSize : VK_WHOLE_SIZE; + bufInfos.push_back(bi); w.pBufferInfo = &bufInfos.back(); + } else continue; + break; + case BindingType::SampledTexture: case BindingType::StorageTextureReadOnly: case BindingType::StorageTextureReadWrite: + if (auto* vkView = static_cast(e.textureView)) { + VkDescriptorImageInfo ii{}; ii.imageView = vkView->handle(); + if (le.type == BindingType::SampledTexture) { + // Depth/stencil textures are always sampled in DEPTH_STENCIL_READ_ONLY layout. + auto* vkTex = vkView->texture; + if (vkTex && isDepthFormat(vkTex->desc.format)) + ii.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + else + ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } else { + ii.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + } + imgInfos.push_back(ii); w.pImageInfo = &imgInfos.back(); + } else continue; + break; + case BindingType::Sampler: case BindingType::ComparisonSampler: + if (auto* vkSamp = static_cast(e.sampler)) { + VkDescriptorImageInfo ii{}; ii.sampler = vkSamp->handle(); + imgInfos.push_back(ii); w.pImageInfo = &imgInfos.back(); + } else continue; + break; + case BindingType::AccelerationStructure: + if (auto* vkAs = static_cast(e.accelStruct)) { + asHandles.push_back(vkAs->handle()); + VkWriteDescriptorSetAccelerationStructureKHR asInfo{}; + asInfo.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_ACCELERATION_STRUCTURE_KHR; + asInfo.accelerationStructureCount = 1; + asInfo.pAccelerationStructures = &asHandles.back(); + asWriteInfos.push_back(asInfo); + w.pNext = &asWriteInfos.back(); + } else continue; + break; + default: continue; + } + writes.push_back(w); + } + + if (!writes.empty()) + vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); + } + + VkDevice m_device = VK_NULL_HANDLE; + VkDescriptorSet m_set = VK_NULL_HANDLE; + VkDescriptorPool m_pool = VK_NULL_HANDLE; + VkBindGroupLayoutImpl* m_layout = nullptr; + BindingShifts m_shifts{}; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroupLayout.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroupLayout.cppm new file mode 100644 index 00000000..05fdc082 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindGroupLayout.cppm @@ -0,0 +1,110 @@ +/// Vulkan implementation of BindGroupLayout. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:bind_group_layout; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :binding_shifts; + +using namespace draco; + +export namespace draco::rhi::vk { + +inline VkDescriptorType toVkDescriptorType(const BindGroupLayoutEntry& e) { + switch (e.type) { + case BindingType::UniformBuffer: + return e.hasDynamicOffset ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + case BindingType::StorageBufferReadOnly: + case BindingType::StorageBufferReadWrite: + return e.hasDynamicOffset ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + case BindingType::SampledTexture: return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + case BindingType::StorageTextureReadOnly: + case BindingType::StorageTextureReadWrite: return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + case BindingType::Sampler: + case BindingType::ComparisonSampler: return VK_DESCRIPTOR_TYPE_SAMPLER; + case BindingType::BindlessTextures: return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + case BindingType::BindlessSamplers: return VK_DESCRIPTOR_TYPE_SAMPLER; + case BindingType::BindlessStorageBuffers: return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + case BindingType::BindlessStorageTextures: return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + case BindingType::AccelerationStructure: return VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR; + } + return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +} + +class VkBindGroupLayoutImpl : public BindGroupLayout { +public: + Status init(VkDevice device, const BindGroupLayoutDesc& desc, const BindingShifts& shifts = {}) { + m_entries.clear(); + for (usize i = 0; i < desc.entries.size(); ++i) { m_entries.push_back(desc.entries[i]); } + + std::vector bindings(desc.entries.size()); + std::vector flags(desc.entries.size()); + + for (usize i = 0; i < desc.entries.size(); ++i) { + const auto& e = desc.entries[i]; + auto& b = bindings[i]; + b = {}; + b.binding = shifts.apply(e.type, e.binding); + b.descriptorType = toVkDescriptorType(e); + b.descriptorCount = e.count; + b.stageFlags = toVkShaderStageFlags(e.visibility); + + flags[i] = 0; + if (e.count == ~0u) { + b.descriptorCount = 1024 * 16; + m_hasBindless = true; + m_bindlessCount = b.descriptorCount; + flags[i] = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + } + } + + VkDescriptorSetLayoutBindingFlagsCreateInfo flagsInfo{}; + flagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO; + flagsInfo.bindingCount = static_cast(desc.entries.size()); + flagsInfo.pBindingFlags = flags.data(); + + VkDescriptorSetLayoutCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + ci.bindingCount = static_cast(desc.entries.size()); + ci.pBindings = bindings.data(); + if (m_hasBindless) { + ci.flags |= VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + ci.pNext = &flagsInfo; + } + + if (vkCreateDescriptorSetLayout(device, &ci, nullptr, &m_layout) != VK_SUCCESS) + return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_layout != VK_NULL_HANDLE) { vkDestroyDescriptorSetLayout(device, m_layout, nullptr); m_layout = VK_NULL_HANDLE; } + } + + std::span entries() const override { + return std::span(m_entries.data(), m_entries.size()); + } + + [[nodiscard]] VkDescriptorSetLayout handle() const { return m_layout; } + [[nodiscard]] bool hasBindless() const { return m_hasBindless; } + [[nodiscard]] u32 bindlessCount() const { return m_bindlessCount; } + +private: + VkDescriptorSetLayout m_layout = VK_NULL_HANDLE; + std::vector m_entries; + bool m_hasBindless = false; + u32 m_bindlessCount = 0; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindingShifts.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindingShifts.cppm new file mode 100644 index 00000000..660b443b --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBindingShifts.cppm @@ -0,0 +1,54 @@ +/// HLSL register binding shift configuration. + +export module rhi.vk:binding_shifts; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +/// Maps HLSL register spaces to Vulkan descriptor bindings. +/// DXC's -fvk-*-shift flags use these values when compiling HLSL to SPIR-V. +struct BindingShifts { + u32 cbvShift = 0; ///< Constant buffer (b) register shift. + u32 srvShift = 0; ///< Shader resource view (t) register shift. + u32 uavShift = 0; ///< Unordered access view (u) register shift. + u32 samplerShift = 0; ///< Sampler (s) register shift. + + /// Standard layout: CBV=0, SRV=1000, UAV=2000, Sampler=3000. + static constexpr BindingShifts standard() { + return { 0, 1000, 2000, 3000 }; + } + + /// Applies the appropriate shift for a binding type. + u32 apply(BindingType type, u32 binding) const { + switch (type) { + case BindingType::UniformBuffer: + return binding + cbvShift; + case BindingType::SampledTexture: + case BindingType::BindlessTextures: + case BindingType::AccelerationStructure: + // A read-only StructuredBuffer is an SRV in HLSL (a `t` register), so it takes the SRV + // shift - unlike RWStructuredBuffer (a `u` register / UAV). DXC shifts the SPIR-V + // binding accordingly, so the layout must use the same class to match. + case BindingType::StorageBufferReadOnly: + return binding + srvShift; + case BindingType::StorageTextureReadOnly: + case BindingType::StorageTextureReadWrite: + case BindingType::BindlessStorageTextures: + case BindingType::StorageBufferReadWrite: + case BindingType::BindlessStorageBuffers: + return binding + uavShift; + case BindingType::Sampler: + case BindingType::ComparisonSampler: + case BindingType::BindlessSamplers: + return binding + samplerShift; + } + return binding; + } +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBuffer.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBuffer.cppm new file mode 100644 index 00000000..b77a0cf2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkBuffer.cppm @@ -0,0 +1,100 @@ +/// Vulkan implementation of Buffer. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:buffer; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDeviceImpl; // forward + +class VkBufferImpl : public Buffer { +public: + Status init(VkDevice device, VkAdapterImpl* adapter, const BufferDesc& d) { + desc = d; + + VkBufferCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + ci.size = d.size; + ci.usage = toVkBufferUsage(d.usage); + ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &ci, nullptr, &m_buffer) != VK_SUCCESS) return ErrorCode::Unknown; + + VkMemoryRequirements memReqs{}; + vkGetBufferMemoryRequirements(device, m_buffer, &memReqs); + + auto memFlags = VkAdapterImpl::getMemoryFlags(d.memory); + i32 memType = adapter->findMemoryType(static_cast(memReqs.memoryTypeBits), memFlags); + + if (memType < 0 && d.memory == MemoryLocation::Auto) + memType = adapter->findMemoryType(static_cast(memReqs.memoryTypeBits), + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + if (memType < 0) { + vkDestroyBuffer(device, m_buffer, nullptr); + m_buffer = VK_NULL_HANDLE; + return ErrorCode::Unknown; + } + + bool needsDeviceAddress = hasFlag(d.usage, BufferUsage::AccelStructInput) + || hasFlag(d.usage, BufferUsage::ShaderBindingTable) + || hasFlag(d.usage, BufferUsage::AccelStructScratch); + + VkMemoryAllocateFlagsInfo allocFlags{}; + allocFlags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO; + if (needsDeviceAddress) + allocFlags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT; + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + if (needsDeviceAddress) allocInfo.pNext = &allocFlags; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = static_cast(memType); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &m_memory) != VK_SUCCESS) { + vkDestroyBuffer(device, m_buffer, nullptr); + m_buffer = VK_NULL_HANDLE; + return ErrorCode::Unknown; + } + + vkBindBufferMemory(device, m_buffer, m_memory, 0); + + // Persistently map host-visible buffers. + if (d.memory == MemoryLocation::CpuToGpu || d.memory == MemoryLocation::GpuToCpu) + vkMapMemory(device, m_memory, 0, d.size, 0, &m_mappedPtr); + + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_mappedPtr) { vkUnmapMemory(device, m_memory); m_mappedPtr = nullptr; } + if (m_memory != VK_NULL_HANDLE) { vkFreeMemory(device, m_memory, nullptr); m_memory = VK_NULL_HANDLE; } + if (m_buffer != VK_NULL_HANDLE) { vkDestroyBuffer(device, m_buffer, nullptr); m_buffer = VK_NULL_HANDLE; } + } + + // ---- Buffer interface ---- + void* map() override { return m_mappedPtr; } + void unmap() override { /* persistently mapped */ } + + // ---- Internal ---- + [[nodiscard]] VkBuffer handle() const { return m_buffer; } + [[nodiscard]] VkDeviceMemory memory() const { return m_memory; } + +private: + VkBuffer m_buffer = VK_NULL_HANDLE; + VkDeviceMemory m_memory = VK_NULL_HANDLE; + void* m_mappedPtr = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandBuffer.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandBuffer.cppm new file mode 100644 index 00000000..f60b08bc --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandBuffer.cppm @@ -0,0 +1,27 @@ +/// Vulkan implementation of CommandBuffer. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:command_buffer; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkCommandBufferImpl : public CommandBuffer { +public: + explicit VkCommandBufferImpl(VkCommandBuffer cmdBuf) : m_cmdBuf(cmdBuf) {} + + [[nodiscard]] VkCommandBuffer handle() const { return m_cmdBuf; } + +private: + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandEncoder.cppm new file mode 100644 index 00000000..027d6860 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandEncoder.cppm @@ -0,0 +1,688 @@ +/// Vulkan implementation of CommandEncoder + RayTracingEncoderExt. + +module; + +#include "VkIncludes.h" +#include +#include +#include +#include + +#include + +export module rhi.vk:command_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :barrier_helper; +import :buffer; +import :texture; +import :texture_view; +import :bind_group; +import :pipeline_layout; +import :query_set; +import :command_buffer; +import :command_pool; +import :render_pass_encoder; +import :render_bundle_encoder; +import :compute_pass_encoder; +import :accel_struct; +import :ray_tracing_pipeline; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkCommandEncoderImpl : public CommandEncoder, public RayTracingEncoderExt { +public: + RayTracingEncoderExt* asRayTracingExt() noexcept override { return this; } + VkCommandEncoderImpl(VkCommandBuffer cmdBuf, VkDevice device, VkCommandPoolImpl* pool) + : m_cmdBuf(cmdBuf), m_device(device), m_pool(pool), + m_rpe(cmdBuf, device), m_cpe(cmdBuf) {} + + // ---- CommandEncoder ---- + + RenderPassEncoder* beginRenderPass(const RenderPassDesc& desc) override { + const auto& colorAtts = desc.colorAttachments; + std::vector vkColor(colorAtts.size()); + + for (usize i = 0; i < colorAtts.size(); ++i) { + const auto& att = colorAtts[i]; + auto* view = static_cast(att.view); + VkRenderingAttachmentInfo info{}; + info.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + info.imageView = view ? view->handle() : VK_NULL_HANDLE; + info.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + // Update texture layout tracking - dynamic rendering implicitly + // transitions attachments to the specified layout. + if (view) { + if (auto* vkTex = static_cast(view->texture)) + vkTex->setSubresourceLayout(view->desc.baseMipLevel, 1, view->desc.baseArrayLayer, 1, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + } + info.loadOp = toVkLoadOp(att.loadOp); + info.storeOp = toVkStoreOp(att.storeOp); + info.clearValue.color.float32[0] = att.clearValue.r; + info.clearValue.color.float32[1] = att.clearValue.g; + info.clearValue.color.float32[2] = att.clearValue.b; + info.clearValue.color.float32[3] = att.clearValue.a; + if (auto* rv = static_cast(att.resolveTarget)) { + info.resolveMode = VK_RESOLVE_MODE_AVERAGE_BIT; + info.resolveImageView = rv->handle(); + info.resolveImageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + vkColor[i] = info; + } + + VkRect2D renderArea{}; + if (colorAtts.size() > 0) { + if (auto* v = static_cast(colorAtts[0].view)) + renderArea.extent = { v->width(), v->height() }; + } else if (desc.depthStencilAttachment.has_value()) { + if (auto* v = static_cast(desc.depthStencilAttachment->view)) + renderArea.extent = { v->width(), v->height() }; + } + + VkRenderingInfo ri{}; + ri.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + ri.renderArea = renderArea; + ri.layerCount = 1; + ri.colorAttachmentCount = static_cast(vkColor.size()); + ri.pColorAttachments = vkColor.data(); + + VkRenderingAttachmentInfo depthAtt{}, stencilAtt{}; + if (desc.depthStencilAttachment.has_value()) { + const auto& ds = *desc.depthStencilAttachment; + if (auto* dv = static_cast(ds.view)) { + depthAtt.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + depthAtt.imageView = dv->handle(); + depthAtt.imageLayout = ds.depthReadOnly + ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + // Update texture layout tracking. + if (auto* vkTex = static_cast(dv->texture)) + vkTex->setSubresourceLayout(dv->desc.baseMipLevel, 1, dv->desc.baseArrayLayer, 1, depthAtt.imageLayout); + depthAtt.loadOp = toVkLoadOp(ds.depthLoadOp); + depthAtt.storeOp = toVkStoreOp(ds.depthStoreOp); + depthAtt.clearValue.depthStencil = { ds.depthClearValue, ds.stencilClearValue }; + ri.pDepthAttachment = &depthAtt; + if (hasStencil(dv->format())) { + stencilAtt = depthAtt; + stencilAtt.loadOp = toVkLoadOp(ds.stencilLoadOp); + stencilAtt.storeOp = toVkStoreOp(ds.stencilStoreOp); + ri.pStencilAttachment = &stencilAtt; + } + } + } + + if (desc.contents == RenderPassContents::SecondaryCommandBuffers) + ri.flags |= VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT; + + vkCmdBeginRendering(m_cmdBuf, &ri); + return &m_rpe; + } + + ComputePassEncoder* beginComputePass(std::u8string_view) override { return &m_cpe; } + + RenderBundleEncoder* createRenderBundleEncoder(const RenderBundleDesc& desc) override { + VkCommandBuffer sec = m_pool->acquireSecondary(); + if (sec == VK_NULL_HANDLE) return nullptr; + + VkFormat colorFmts[maxColorAttachments] = {}; + for (u32 i = 0; i < desc.colorFormatCount && i < maxColorAttachments; ++i) + colorFmts[i] = toVkFormat(desc.colorFormats[i]); + const bool hasDepth = desc.depthStencilFormat != TextureFormat::Undefined; + + // Dynamic-rendering inheritance: the attachment signature this bundle is compatible with. + VkCommandBufferInheritanceRenderingInfo inh{}; + inh.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO; + inh.colorAttachmentCount = desc.colorFormatCount; + inh.pColorAttachmentFormats = colorFmts; + inh.depthAttachmentFormat = hasDepth ? toVkFormat(desc.depthStencilFormat) : VK_FORMAT_UNDEFINED; + inh.stencilAttachmentFormat = (hasDepth && hasStencil(desc.depthStencilFormat)) + ? toVkFormat(desc.depthStencilFormat) : VK_FORMAT_UNDEFINED; + inh.rasterizationSamples = static_cast(desc.sampleCount ? desc.sampleCount : 1u); + + VkCommandBufferInheritanceInfo ii{}; + ii.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; + ii.pNext = &inh; + + VkCommandBufferBeginInfo bi{}; + bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT | VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + bi.pInheritanceInfo = ⅈ + vkBeginCommandBuffer(sec, &bi); + + // Bundles carry no pass-level dynamic state + Vulkan secondaries don't inherit it, so record + // the bundle's viewport + scissor up front (Y-flipped, like the pass encoder). The viewport + // is the desc's sub-rect (split-screen), not necessarily the full target. + if (desc.width > 0 && desc.height > 0) { + VkViewport vp{}; vp.x = static_cast(desc.viewportX); + vp.y = static_cast(desc.viewportY) + static_cast(desc.height); + vp.width = static_cast(desc.width); vp.height = -static_cast(desc.height); + vp.minDepth = 0.0f; vp.maxDepth = 1.0f; + vkCmdSetViewport(sec, 0, 1, &vp); + VkRect2D scs{}; scs.offset = { desc.viewportX, desc.viewportY }; scs.extent = { desc.width, desc.height }; + vkCmdSetScissor(sec, 0, 1, &scs); + } + + auto* enc = new VkRenderBundleEncoderImpl(sec); + m_pool->trackBundleEncoder(enc); + return enc; + } + + void barrier(const BarrierGroup& group) override { + std::vector memBs(group.memoryBarriers.size()); + std::vector bufBs(group.bufferBarriers.size()); + std::vector imgBs(group.textureBarriers.size()); + + for (usize i = 0; i < group.memoryBarriers.size(); ++i) { + auto src = getStageAccess(group.memoryBarriers[i].oldState); + auto dst = getStageAccess(group.memoryBarriers[i].newState); + memBs[i] = {}; memBs[i].sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + memBs[i].srcStageMask = src.stageMask; memBs[i].srcAccessMask = src.accessMask; + memBs[i].dstStageMask = dst.stageMask; memBs[i].dstAccessMask = dst.accessMask; + } + + for (usize i = 0; i < group.bufferBarriers.size(); ++i) { + const auto& bb = group.bufferBarriers[i]; + auto src = getStageAccess(bb.oldState); auto dst = getStageAccess(bb.newState); + bufBs[i] = {}; bufBs[i].sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; + bufBs[i].srcStageMask = src.stageMask; bufBs[i].srcAccessMask = src.accessMask; + bufBs[i].dstStageMask = dst.stageMask; bufBs[i].dstAccessMask = dst.accessMask; + bufBs[i].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufBs[i].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + if (auto* vb = static_cast(bb.buffer)) bufBs[i].buffer = vb->handle(); + bufBs[i].offset = bb.offset; + bufBs[i].size = (bb.size == ~0ull) ? VK_WHOLE_SIZE : bb.size; + } + + for (usize i = 0; i < group.textureBarriers.size(); ++i) { + const auto& tb = group.textureBarriers[i]; + auto src = getStageAccess(tb.oldState); auto dst = getStageAccess(tb.newState); + + auto* vkTex = static_cast(tb.texture); + TextureFormat format = vkTex ? vkTex->desc.format : TextureFormat::Undefined; + + auto newLayout = getImageLayout(tb.newState, format); + + // Resolve old layout from per-subresource tracking. + // Tracking is kept in sync via beginRenderPass layout updates. + VkImageLayout oldLayout; + if (vkTex) { + bool isWholeResource = (tb.mipLevelCount == ~0u) && (tb.arrayLayerCount == ~0u); + if (isWholeResource) + oldLayout = vkTex->currentLayout; + else + oldLayout = vkTex->getSubresourceLayout(tb.baseMipLevel, tb.baseArrayLayer); + // Update tracking. + vkTex->setSubresourceLayout(tb.baseMipLevel, tb.mipLevelCount, + tb.baseArrayLayer, tb.arrayLayerCount, newLayout); + } else { + oldLayout = getImageLayout(tb.oldState, format); + } + + imgBs[i] = {}; imgBs[i].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED) { + imgBs[i].srcStageMask = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT; imgBs[i].srcAccessMask = 0; + } else { + imgBs[i].srcStageMask = src.stageMask; imgBs[i].srcAccessMask = src.accessMask; + } + imgBs[i].dstStageMask = dst.stageMask; imgBs[i].dstAccessMask = dst.accessMask; + imgBs[i].oldLayout = oldLayout; imgBs[i].newLayout = newLayout; + imgBs[i].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imgBs[i].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + if (vkTex) imgBs[i].image = vkTex->handle(); + TextureFormat fmt = vkTex ? vkTex->desc.format : TextureFormat::Undefined; + imgBs[i].subresourceRange.aspectMask = getAspectMask(fmt); + imgBs[i].subresourceRange.baseMipLevel = tb.baseMipLevel; + imgBs[i].subresourceRange.levelCount = tb.mipLevelCount == ~0u ? VK_REMAINING_MIP_LEVELS : tb.mipLevelCount; + imgBs[i].subresourceRange.baseArrayLayer = tb.baseArrayLayer; + imgBs[i].subresourceRange.layerCount = tb.arrayLayerCount== ~0u ? VK_REMAINING_ARRAY_LAYERS : tb.arrayLayerCount; + } + + VkDependencyInfo di{}; di.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + di.memoryBarrierCount = static_cast(memBs.size()); di.pMemoryBarriers = memBs.data(); + di.bufferMemoryBarrierCount = static_cast(bufBs.size()); di.pBufferMemoryBarriers = bufBs.data(); + di.imageMemoryBarrierCount = static_cast(imgBs.size()); di.pImageMemoryBarriers = imgBs.data(); + vkCmdPipelineBarrier2(m_cmdBuf, &di); + } + + void copyBufferToBuffer(Buffer* src, u64 srcOff, Buffer* dst, u64 dstOff, u64 size) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + VkBufferCopy r{}; r.srcOffset = srcOff; r.dstOffset = dstOff; r.size = size; + vkCmdCopyBuffer(m_cmdBuf, s->handle(), d->handle(), 1, &r); + } + + void copyBufferToTexture(Buffer* src, Texture* dst, const BufferTextureCopyRegion& region) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + VkBufferImageCopy c{}; + c.bufferOffset = region.bufferOffset; + u32 bpp2 = bytesPerPixel(d->desc.format); + c.bufferRowLength = (bpp2 > 0 && region.bytesPerRow > 0) ? region.bytesPerRow / bpp2 : 0; + c.bufferImageHeight = region.rowsPerImage; + c.imageSubresource.aspectMask = getAspectMask(d->desc.format); + c.imageSubresource.mipLevel = region.textureMipLevel; + c.imageSubresource.baseArrayLayer = region.textureArrayLayer; + c.imageSubresource.layerCount = 1; + c.imageOffset = { static_cast(region.textureOrigin.x), static_cast(region.textureOrigin.y), static_cast(region.textureOrigin.z) }; + c.imageExtent = { region.textureExtent.width, region.textureExtent.height, region.textureExtent.depth }; + vkCmdCopyBufferToImage(m_cmdBuf, s->handle(), d->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &c); + } + + void copyTextureToBuffer(Texture* src, Buffer* dst, const BufferTextureCopyRegion& region) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + VkBufferImageCopy c{}; + c.bufferOffset = region.bufferOffset; + u32 bpp = bytesPerPixel(s->desc.format); + c.bufferRowLength = (bpp > 0 && region.bytesPerRow > 0) ? region.bytesPerRow / bpp : 0; + c.bufferImageHeight = region.rowsPerImage; + c.imageSubresource.aspectMask = getAspectMask(s->desc.format); + c.imageSubresource.mipLevel = region.textureMipLevel; + c.imageSubresource.baseArrayLayer = region.textureArrayLayer; + c.imageSubresource.layerCount = 1; + c.imageOffset = { static_cast(region.textureOrigin.x), static_cast(region.textureOrigin.y), static_cast(region.textureOrigin.z) }; + c.imageExtent = { region.textureExtent.width, region.textureExtent.height, region.textureExtent.depth }; + vkCmdCopyImageToBuffer(m_cmdBuf, s->handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, d->handle(), 1, &c); + } + + void copyTextureToTexture(Texture* src, Texture* dst, const TextureCopyRegion& region) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + VkImageCopy c{}; + c.srcSubresource = { getAspectMask(s->desc.format), region.srcMipLevel, region.srcArrayLayer, 1 }; + c.dstSubresource = { getAspectMask(d->desc.format), region.dstMipLevel, region.dstArrayLayer, 1 }; + c.extent = { region.extent.width, region.extent.height, region.extent.depth }; + vkCmdCopyImage(m_cmdBuf, s->handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + d->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &c); + } + + void blit(Texture* src, Texture* dst) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + VkImageBlit b{}; + b.srcSubresource = { getAspectMask(s->desc.format), 0, 0, 1 }; + b.srcOffsets[1] = { static_cast(s->desc.width), static_cast(s->desc.height), 1 }; + b.dstSubresource = { getAspectMask(d->desc.format), 0, 0, 1 }; + b.dstOffsets[1] = { static_cast(d->desc.width), static_cast(d->desc.height), 1 }; + vkCmdBlitImage(m_cmdBuf, s->handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + d->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &b, VK_FILTER_LINEAR); + } + + void generateMipmaps(Texture* texture) override { + auto* vkTex = static_cast(texture); + if (!vkTex || vkTex->desc.mipLevelCount <= 1) return; + // Simplified: caller transitions all mips to TRANSFER_DST before calling. + i32 mipW = static_cast(vkTex->desc.width); + i32 mipH = static_cast(vkTex->desc.height); + auto aspect = getAspectMask(vkTex->desc.format); + u32 layers = vkTex->desc.arrayLayerCount; + + for (u32 i = 1; i < vkTex->desc.mipLevelCount; ++i) { + // Transition mip i-1 to TRANSFER_SRC for blit source. + // For mip 0 (i==1), use UNDEFINED as old layout because the caller's + // actual layout is unknown (TransferBatch leaves SHADER_READ_ONLY, etc.). + VkImageMemoryBarrier2 srcBarrier{}; srcBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + srcBarrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; + srcBarrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + srcBarrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; + srcBarrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; + srcBarrier.oldLayout = (i == 1) ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + srcBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + srcBarrier.image = vkTex->handle(); + srcBarrier.subresourceRange = { aspect, i - 1, 1, 0, layers }; + + // Transition mip i to TRANSFER_DST for blit destination. + VkImageMemoryBarrier2 dstBarrier{}; dstBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + dstBarrier.srcStageMask = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT; + dstBarrier.srcAccessMask = 0; + dstBarrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; + dstBarrier.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + dstBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + dstBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + dstBarrier.image = vkTex->handle(); + dstBarrier.subresourceRange = { aspect, i, 1, 0, layers }; + + VkImageMemoryBarrier2 barriers[2] = { srcBarrier, dstBarrier }; + VkDependencyInfo dep{}; dep.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dep.imageMemoryBarrierCount = 2; dep.pImageMemoryBarriers = barriers; + vkCmdPipelineBarrier2(m_cmdBuf, &dep); + + i32 nw = std::max(1, mipW / 2), nh = std::max(1, mipH / 2); + VkImageBlit bl{}; + bl.srcSubresource = { aspect, i - 1, 0, layers }; bl.srcOffsets[1] = { mipW, mipH, 1 }; + bl.dstSubresource = { aspect, i, 0, layers }; bl.dstOffsets[1] = { nw, nh, 1 }; + vkCmdBlitImage(m_cmdBuf, vkTex->handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkTex->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bl, VK_FILTER_LINEAR); + mipW = nw; mipH = nh; + } + // Transition last mip to SRC. + VkImageMemoryBarrier2 lb{}; lb.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + lb.srcStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; lb.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + lb.dstStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; lb.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; + lb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; lb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + lb.image = vkTex->handle(); + lb.subresourceRange = { aspect, vkTex->desc.mipLevelCount - 1, 1, 0, layers }; + VkDependencyInfo ldep{}; ldep.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + ldep.imageMemoryBarrierCount = 1; ldep.pImageMemoryBarriers = &lb; + vkCmdPipelineBarrier2(m_cmdBuf, &ldep); + + // Update tracked layout - all mips now in TRANSFER_SRC. + vkTex->currentLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + } + + void resolveTexture(Texture* src, Texture* dst) override { + auto* s = static_cast(src); auto* d = static_cast(dst); + if (!s || !d) return; + auto aspect = getAspectMask(s->desc.format); + VkImageResolve r{}; r.srcSubresource = { aspect, 0, 0, 1 }; r.dstSubresource = { aspect, 0, 0, 1 }; + r.extent = { s->desc.width, s->desc.height, 1 }; + vkCmdResolveImage(m_cmdBuf, s->handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + d->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r); + } + + void resetQuerySet(QuerySet* qs, u32 first, u32 count) override { + if (auto* q = static_cast(qs)) vkCmdResetQueryPool(m_cmdBuf, q->handle(), first, count); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + if (auto* q = static_cast(qs)) + vkCmdWriteTimestamp(m_cmdBuf, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, q->handle(), index); + } + + void resolveQuerySet(QuerySet* qs, u32 first, u32 count, Buffer* dst, u64 dstOffset) override { + auto* q = static_cast(qs); auto* b = static_cast(dst); + if (q && b) vkCmdCopyQueryPoolResults(m_cmdBuf, q->handle(), first, count, b->handle(), dstOffset, 8, + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + } + + void beginDebugLabel(std::u8string_view label, f32 r, f32 g, f32 b, f32 a) override { + char buf[256]{}; auto len = std::min(label.size(), static_cast(255)); + std::memcpy(buf, label.data(), len); + VkDebugUtilsLabelEXT li{}; li.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + li.pLabelName = buf; li.color[0] = r; li.color[1] = g; li.color[2] = b; li.color[3] = a; + auto pfn = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkCmdBeginDebugUtilsLabelEXT")); + if (pfn) pfn(m_cmdBuf, &li); + } + + void endDebugLabel() override { + auto pfn = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkCmdEndDebugUtilsLabelEXT")); + if (pfn) pfn(m_cmdBuf); + } + + void insertDebugLabel(std::u8string_view label, f32 r, f32 g, f32 b, f32 a) override { + char buf[256]{}; auto len = std::min(label.size(), static_cast(255)); + std::memcpy(buf, label.data(), len); + VkDebugUtilsLabelEXT li{}; li.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + li.pLabelName = buf; li.color[0] = r; li.color[1] = g; li.color[2] = b; li.color[3] = a; + auto pfn = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkCmdInsertDebugUtilsLabelEXT")); + if (pfn) pfn(m_cmdBuf, &li); + } + + CommandBuffer* finish() override { + vkEndCommandBuffer(m_cmdBuf); + auto* cb = new VkCommandBufferImpl(m_cmdBuf); + m_pool->trackCommandBuffer(cb); + return cb; + } + + // ---- RayTracingEncoderExt ---- + + void buildBottomLevelAccelStruct(AccelStruct* dst, Buffer* scratch, u64 scratchOffset, + std::span tris, std::span aabbs) override; + void buildTopLevelAccelStruct(AccelStruct* dst, Buffer* scratch, u64 scratchOffset, + Buffer* instanceBuf, u64 instanceOffset, u32 instanceCount) override; + void setRayTracingPipeline(RayTracingPipeline* pipeline) override; + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override; + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override; + void traceRays(Buffer* raygenSBT, u64 raygenOff, u64 raygenStride, + Buffer* missSBT, u64 missOff, u64 missStride, + Buffer* hitSBT, u64 hitOff, u64 hitStride, + u32 width, u32 height, u32 depth) override; + +private: + u64 getBufferDeviceAddress(VkBufferImpl* buf) { + VkBufferDeviceAddressInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO; + info.buffer = buf->handle(); + if (!m_pfnGetBufAddr) m_pfnGetBufAddr = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkGetBufferDeviceAddress")); + return m_pfnGetBufAddr ? m_pfnGetBufAddr(m_device, &info) : 0; + } + + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + VkCommandPoolImpl* m_pool = nullptr; + VkRenderPassEncoderImpl m_rpe; + VkComputePassEncoderImpl m_cpe; + + // RT state. + VkRayTracingPipelineImpl* m_currentRtPipeline = nullptr; + PFN_vkGetBufferDeviceAddress m_pfnGetBufAddr = nullptr; + PFN_vkCmdBuildAccelerationStructuresKHR m_pfnBuild = nullptr; + PFN_vkCmdTraceRaysKHR m_pfnTrace = nullptr; +}; + +// ---- Deferred RT method implementations ---- + +inline void VkCommandEncoderImpl::setRayTracingPipeline(RayTracingPipeline* pipeline) { + m_currentRtPipeline = static_cast(pipeline); + if (m_currentRtPipeline) + vkCmdBindPipeline(m_cmdBuf, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, m_currentRtPipeline->handle()); +} + +inline void VkCommandEncoderImpl::setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) { + auto* bg = static_cast(group); + if (!bg || !m_currentRtPipeline || !m_currentRtPipeline->vkLayout()) return; + VkDescriptorSet set = bg->handle(); + vkCmdBindDescriptorSets(m_cmdBuf, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, + m_currentRtPipeline->vkLayout()->handle(), index, 1, &set, + static_cast(dynOffsets.size()), dynOffsets.data()); +} + +inline void VkCommandEncoderImpl::setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) { + if (!m_currentRtPipeline || !m_currentRtPipeline->vkLayout()) return; + vkCmdPushConstants(m_cmdBuf, m_currentRtPipeline->vkLayout()->handle(), + toVkShaderStageFlags(stages), offset, size, data); +} + +inline void VkCommandEncoderImpl::traceRays( + Buffer* raygenSBT, u64 raygenOff, u64 raygenStride, + Buffer* missSBT, u64 missOff, u64 missStride, + Buffer* hitSBT, u64 hitOff, u64 hitStride, u32 w, u32 h, u32 d) { + auto* rg = static_cast(raygenSBT); + if (!rg) return; + VkStridedDeviceAddressRegionKHR rgn{}, msn{}, htn{}, cal{}; + rgn.deviceAddress = getBufferDeviceAddress(rg) + raygenOff; rgn.stride = raygenStride; rgn.size = raygenStride; + if (auto* ms = static_cast(missSBT)) { + msn.deviceAddress = getBufferDeviceAddress(ms) + missOff; msn.stride = missStride; msn.size = missStride; + } + if (auto* ht = static_cast(hitSBT)) { + htn.deviceAddress = getBufferDeviceAddress(ht) + hitOff; htn.stride = hitStride; htn.size = hitStride; + } + if (!m_pfnTrace) m_pfnTrace = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkCmdTraceRaysKHR")); + if (m_pfnTrace) m_pfnTrace(m_cmdBuf, &rgn, &msn, &htn, &cal, w, h, d); +} + +inline void VkCommandEncoderImpl::buildBottomLevelAccelStruct( + AccelStruct* dst, Buffer* scratch, u64 scratchOffset, + std::span tris, std::span aabbs) { + auto* as = static_cast(dst); + auto* sc = static_cast(scratch); + if (!as || !sc) return; + + usize total = tris.size() + aabbs.size(); + std::vector geoms(total); + std::vector ranges(total); + usize idx = 0; + + for (usize i = 0; i < tris.size(); ++i) { + const auto& t = tris[i]; + auto* vb = static_cast(t.vertexBuffer); if (!vb) continue; + VkAccelerationStructureGeometryTrianglesDataKHR td{}; + td.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR; + td.vertexFormat = toVkVertexFormat(t.vertexFormat); + td.vertexData.deviceAddress = getBufferDeviceAddress(vb) + t.vertexOffset; + td.vertexStride = t.vertexStride; td.maxVertex = t.vertexCount - 1; + if (auto* ib = static_cast(t.indexBuffer)) { + td.indexType = toVkIndexType(t.indexFormat); + td.indexData.deviceAddress = getBufferDeviceAddress(ib) + t.indexOffset; + } else td.indexType = VK_INDEX_TYPE_NONE_KHR; + if (auto* tb = static_cast(t.transformBuffer)) + td.transformData.deviceAddress = getBufferDeviceAddress(tb) + t.transformOffset; + geoms[idx] = {}; geoms[idx].sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR; + geoms[idx].geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR; geoms[idx].geometry.triangles = td; + { VkGeometryFlagsKHR gf = 0; + if (static_cast(t.flags) & static_cast(GeometryFlags::Opaque)) gf |= VK_GEOMETRY_OPAQUE_BIT_KHR; + if (static_cast(t.flags) & static_cast(GeometryFlags::NoDuplicateAnyHitInvocation)) gf |= VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR; + geoms[idx].flags = gf; } + ranges[idx] = {}; ranges[idx].primitiveCount = t.indexBuffer ? t.indexCount / 3 : t.vertexCount / 3; + ++idx; + } + for (usize i = 0; i < aabbs.size(); ++i) { + const auto& a = aabbs[i]; + auto* ab = static_cast(a.aabbBuffer); if (!ab) continue; + VkAccelerationStructureGeometryAabbsDataKHR ad{}; + ad.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR; + ad.data.deviceAddress = getBufferDeviceAddress(ab) + a.offset; ad.stride = a.stride; + geoms[idx] = {}; geoms[idx].sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR; + geoms[idx].geometryType = VK_GEOMETRY_TYPE_AABBS_KHR; geoms[idx].geometry.aabbs = ad; + { VkGeometryFlagsKHR gf = 0; + if (static_cast(a.flags) & static_cast(GeometryFlags::Opaque)) gf |= VK_GEOMETRY_OPAQUE_BIT_KHR; + if (static_cast(a.flags) & static_cast(GeometryFlags::NoDuplicateAnyHitInvocation)) gf |= VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR; + geoms[idx].flags = gf; } + ranges[idx] = {}; ranges[idx].primitiveCount = a.count; + ++idx; + } + + VkAccelerationStructureBuildGeometryInfoKHR bi{}; + bi.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR; + bi.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; + bi.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR; + bi.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; + bi.dstAccelerationStructure = as->handle(); + bi.geometryCount = static_cast(idx); bi.pGeometries = geoms.data(); + bi.scratchData.deviceAddress = getBufferDeviceAddress(sc) + scratchOffset; + + if (!m_pfnBuild) m_pfnBuild = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdBuildAccelerationStructuresKHR")); + if (!m_pfnBuild) return; + const VkAccelerationStructureBuildRangeInfoKHR* pRange = ranges.data(); + m_pfnBuild(m_cmdBuf, 1, &bi, &pRange); +} + +inline void VkCommandEncoderImpl::buildTopLevelAccelStruct( + AccelStruct* dst, Buffer* scratch, u64 scratchOffset, + Buffer* instanceBuf, u64 instanceOffset, u32 instanceCount) { + auto* as = static_cast(dst); + auto* sc = static_cast(scratch); + auto* ib = static_cast(instanceBuf); + if (!as || !sc || !ib) return; + + VkAccelerationStructureGeometryInstancesDataKHR id{}; + id.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR; + id.data.deviceAddress = getBufferDeviceAddress(ib) + instanceOffset; + VkAccelerationStructureGeometryKHR geom{}; geom.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR; + geom.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR; geom.geometry.instances = id; + + VkAccelerationStructureBuildGeometryInfoKHR bi{}; + bi.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR; + bi.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR; + bi.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR; + bi.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR; + bi.dstAccelerationStructure = as->handle(); + bi.geometryCount = 1; bi.pGeometries = &geom; + bi.scratchData.deviceAddress = getBufferDeviceAddress(sc) + scratchOffset; + + VkAccelerationStructureBuildRangeInfoKHR range{}; range.primitiveCount = instanceCount; + if (!m_pfnBuild) m_pfnBuild = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdBuildAccelerationStructuresKHR")); + if (!m_pfnBuild) return; + const VkAccelerationStructureBuildRangeInfoKHR* pRange = ⦥ + m_pfnBuild(m_cmdBuf, 1, &bi, &pRange); +} + +// ---- Deferred mesh shader method implementations on RenderPassEncoder ---- + +VkPipelineLayoutImpl* VkRenderPassEncoderImpl::getCurrentLayout() { + if (m_currentPipeline) return m_currentPipeline->vkLayout(); + if (m_currentMeshPipeline) return m_currentMeshPipeline->vkLayout(); + return nullptr; +} + +void VkRenderPassEncoderImpl::setMeshPipeline(MeshPipeline* pipeline) { + m_currentMeshPipeline = static_cast(pipeline); + m_currentPipeline = nullptr; + if (m_currentMeshPipeline) + vkCmdBindPipeline(m_cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_currentMeshPipeline->handle()); +} + +void VkRenderPassEncoderImpl::drawMeshTasks(u32 gx, u32 gy, u32 gz) { + if (!m_pfnDrawMesh) m_pfnDrawMesh = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdDrawMeshTasksEXT")); + if (m_pfnDrawMesh) m_pfnDrawMesh(m_cmdBuf, gx, gy, gz); +} + +void VkRenderPassEncoderImpl::drawMeshTasksIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) { + auto* vkBuf = static_cast(buf); if (!vkBuf) return; + if (!m_pfnDrawMeshIndirect) m_pfnDrawMeshIndirect = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdDrawMeshTasksIndirectEXT")); + if (m_pfnDrawMeshIndirect) m_pfnDrawMeshIndirect(m_cmdBuf, vkBuf->handle(), offset, drawCount, stride > 0 ? stride : 12); +} + +void VkRenderPassEncoderImpl::drawMeshTasksIndirectCount( + Buffer* buf, u64 offset, Buffer* countBuf, u64 countOffset, u32 maxDrawCount, u32 stride) { + auto* vkBuf = static_cast(buf); + auto* vkCb = static_cast(countBuf); + if (!vkBuf || !vkCb) return; + if (!m_pfnDrawMeshIndCount) m_pfnDrawMeshIndCount = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdDrawMeshTasksIndirectCountEXT")); + if (m_pfnDrawMeshIndCount) m_pfnDrawMeshIndCount(m_cmdBuf, vkBuf->handle(), offset, + vkCb->handle(), countOffset, maxDrawCount, stride > 0 ? stride : 12); +} + +// ---- CommandPool deferred implementations ---- + +Status VkCommandPoolImpl::createEncoder(CommandEncoder*& out) { + out = nullptr; + VkCommandBuffer cmdBuf = VK_NULL_HANDLE; + + if (!m_freeHandles.empty()) { + cmdBuf = m_freeHandles.back(); m_freeHandles.pop_back(); + } else { + VkCommandBufferAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ai.commandPool = m_pool; + ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ai.commandBufferCount = 1; + if (vkAllocateCommandBuffers(m_device, &ai, &cmdBuf) != VK_SUCCESS) return ErrorCode::Unknown; + } + + VkCommandBufferBeginInfo bi{}; + bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmdBuf, &bi); + + out = new VkCommandEncoderImpl(cmdBuf, m_device, this); + return ErrorCode::Ok; +} + +void VkCommandPoolImpl::destroyEncoder(CommandEncoder*& encoder) { + if (encoder) { delete encoder; encoder = nullptr; } +} + +void VkCommandPoolImpl::reset() { + for (auto* cb : m_trackedBuffers) { m_freeHandles.push_back(cb->handle()); delete cb; } + m_trackedBuffers.clear(); + for (auto* e : m_trackedBundleEncoders) delete e; // each frees its produced bundle + m_trackedBundleEncoders.clear(); + for (auto h : m_liveSecondaries) m_freeSecondaries.push_back(h); // recycle (pool reset below) + m_liveSecondaries.clear(); + vkResetCommandPool(m_device, m_pool, 0); +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandPool.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandPool.cppm new file mode 100644 index 00000000..29f77b76 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkCommandPool.cppm @@ -0,0 +1,100 @@ +/// Vulkan implementation of CommandPool. + +module; + +#include "VkIncludes.h" +#include + + +export module rhi.vk:command_pool; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; +import :command_buffer; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDeviceImpl; // forward +class VkCommandEncoderImpl; // forward + +class VkCommandPoolImpl : public CommandPool { +public: + Status init(VkDevice device, VkAdapterImpl* adapter, QueueType queueType) { + m_device = device; + + i32 familyIndex = adapter->findQueueFamily(queueType); + if (familyIndex < 0) return ErrorCode::Unknown; + m_familyIndex = static_cast(familyIndex); + + VkCommandPoolCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + ci.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + ci.queueFamilyIndex = m_familyIndex; + + if (vkCreateCommandPool(device, &ci, nullptr, &m_pool) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + // ---- CommandPool interface ---- + Status createEncoder(CommandEncoder*& out) override; + void destroyEncoder(CommandEncoder*& encoder) override; + void reset() override; + + void cleanup() { + for (auto* cb : m_trackedBuffers) delete cb; + m_trackedBuffers.clear(); + m_freeHandles.clear(); + for (auto* e : m_trackedBundleEncoders) delete e; // each frees its produced bundle + m_trackedBundleEncoders.clear(); + m_liveSecondaries.clear(); + m_freeSecondaries.clear(); + + if (m_pool != VK_NULL_HANDLE) { vkDestroyCommandPool(m_device, m_pool, nullptr); m_pool = VK_NULL_HANDLE; } + } + + // Called by encoder's finish() to register the command buffer. + void trackCommandBuffer(VkCommandBufferImpl* cb) { m_trackedBuffers.push_back(cb); } + + // ---- render bundles (secondary command buffers) ---- + + // Allocate (or recycle) a SECONDARY command buffer for a render bundle encoder. + [[nodiscard]] VkCommandBuffer acquireSecondary() { + VkCommandBuffer cb = VK_NULL_HANDLE; + if (!m_freeSecondaries.empty()) { cb = m_freeSecondaries.back(); m_freeSecondaries.pop_back(); } + else { + VkCommandBufferAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ai.commandPool = m_pool; + ai.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; + ai.commandBufferCount = 1; + if (vkAllocateCommandBuffers(m_device, &ai, &cb) != VK_SUCCESS) return VK_NULL_HANDLE; + } + m_liveSecondaries.push_back(cb); // recycled on Reset + return cb; + } + + // Track a bundle-encoder wrapper so it (and the bundle it owns) is freed on Reset. + void trackBundleEncoder(RenderBundleEncoder* e) { m_trackedBundleEncoders.push_back(e); } + + [[nodiscard]] VkCommandPool handle() const { return m_pool; } + [[nodiscard]] VkDevice vkDevice() const { return m_device; } + + // Stored so the encoder can access it. + VkDeviceImpl* ownerDevice = nullptr; + +private: + VkDevice m_device = VK_NULL_HANDLE; + VkCommandPool m_pool = VK_NULL_HANDLE; + u32 m_familyIndex = 0; + std::vector m_freeHandles; + std::vector m_trackedBuffers; + std::vector m_freeSecondaries; // recyclable secondary handles + std::vector m_liveSecondaries; // handed out this cycle + std::vector m_trackedBundleEncoders; // wrappers freed on reset +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePassEncoder.cppm new file mode 100644 index 00000000..dee634dc --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePassEncoder.cppm @@ -0,0 +1,80 @@ +/// Vulkan implementation of ComputePassEncoder. + +module; + +#include "VkIncludes.h" +#include + +export module rhi.vk:compute_pass_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :bind_group; +import :compute_pipeline; +import :pipeline_layout; +import :query_set; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkComputePassEncoderImpl : public ComputePassEncoder { +public: + VkComputePassEncoderImpl(VkCommandBuffer cmdBuf) : m_cmdBuf(cmdBuf) {} + + void setPipeline(ComputePipeline* pipeline) override { + m_current = static_cast(pipeline); + if (m_current) vkCmdBindPipeline(m_cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_current->handle()); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + auto* bg = static_cast(group); + if (!bg || !m_current || !m_current->vkLayout()) return; + VkDescriptorSet set = bg->handle(); + vkCmdBindDescriptorSets(m_cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, + m_current->vkLayout()->handle(), index, 1, &set, + static_cast(dynOffsets.size()), dynOffsets.data()); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + if (!m_current || !m_current->vkLayout()) return; + vkCmdPushConstants(m_cmdBuf, m_current->vkLayout()->handle(), + toVkShaderStageFlags(stages), offset, size, data); + } + + void dispatch(u32 x, u32 y, u32 z) override { vkCmdDispatch(m_cmdBuf, x, y, z); } + + void dispatchIndirect(Buffer* buffer, u64 offset) override { + auto* vkBuf = static_cast(buffer); + if (vkBuf) vkCmdDispatchIndirect(m_cmdBuf, vkBuf->handle(), offset); + } + + void computeBarrier() override { + VkMemoryBarrier2 mb{}; + mb.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + mb.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + mb.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; + mb.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + mb.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT; + VkDependencyInfo di{}; + di.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + di.memoryBarrierCount = 1; di.pMemoryBarriers = &mb; + vkCmdPipelineBarrier2(m_cmdBuf, &di); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + auto* q = static_cast(qs); + if (q) vkCmdWriteTimestamp(m_cmdBuf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, q->handle(), index); + } + + void end() override { m_current = nullptr; } + +private: + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; + VkComputePipelineImpl* m_current = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePipeline.cppm new file mode 100644 index 00000000..659f499d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkComputePipeline.cppm @@ -0,0 +1,66 @@ +/// Vulkan implementation of ComputePipeline. + +module; + +#include "VkIncludes.h" +#include + + +export module rhi.vk:compute_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :shader_module; +import :pipeline_layout; +import :pipeline_cache; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkComputePipelineImpl : public ComputePipeline { +public: + Status init(VkDevice device, const ComputePipelineDesc& desc) { + auto* vkLayout = static_cast(desc.layout); + if (!vkLayout) return ErrorCode::Unknown; + layout = desc.layout; + m_layout = vkLayout; + + auto* vkMod = static_cast(desc.compute.module); + if (!vkMod) return ErrorCode::Unknown; + + std::u8string entry = std::u8string(desc.compute.entryPoint); + + VkPipelineShaderStageCreateInfo stage{}; + stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + stage.module = vkMod->handle(); + stage.pName = reinterpret_cast(entry.c_str()); + + VkComputePipelineCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + ci.stage = stage; + ci.layout = vkLayout->handle(); + + VkPipelineCache cacheHandle = VK_NULL_HANDLE; + if (desc.cache) cacheHandle = static_cast(desc.cache)->handle(); + + if (vkCreateComputePipelines(device, cacheHandle, 1, &ci, nullptr, &m_pipeline) != VK_SUCCESS) + return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, m_pipeline, nullptr); m_pipeline = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkPipeline handle() const { return m_pipeline; } + [[nodiscard]] VkPipelineLayoutImpl* vkLayout() const { return m_layout; } + +private: + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkConversions.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkConversions.cppm new file mode 100644 index 00000000..a287d29c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkConversions.cppm @@ -0,0 +1,341 @@ +/// Conversion utilities between draco::rhi enums and Vulkan enums. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:conversions; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +/// Depth format support flags - set once at VkDevice init via setDepthFormatSupport(). +/// Defaults to supported; probing overrides if hardware lacks D24. +namespace detail { + inline bool g_depth24S8Supported = true; + inline bool g_depth24Supported = true; +} + +/// Called by VkDevice::init after probing physical device format properties. +inline void setDepthFormatSupport(bool depth24S8, bool depth24) { + detail::g_depth24S8Supported = depth24S8; + detail::g_depth24Supported = depth24; +} + +inline VkFormat toVkFormat(TextureFormat f) { + switch (f) { + case TextureFormat::Undefined: return VK_FORMAT_UNDEFINED; + case TextureFormat::R8Unorm: return VK_FORMAT_R8_UNORM; + case TextureFormat::R8Snorm: return VK_FORMAT_R8_SNORM; + case TextureFormat::R8Uint: return VK_FORMAT_R8_UINT; + case TextureFormat::R8Sint: return VK_FORMAT_R8_SINT; + case TextureFormat::R16Uint: return VK_FORMAT_R16_UINT; + case TextureFormat::R16Sint: return VK_FORMAT_R16_SINT; + case TextureFormat::R16Float: return VK_FORMAT_R16_SFLOAT; + case TextureFormat::RG8Unorm: return VK_FORMAT_R8G8_UNORM; + case TextureFormat::RG8Snorm: return VK_FORMAT_R8G8_SNORM; + case TextureFormat::RG8Uint: return VK_FORMAT_R8G8_UINT; + case TextureFormat::RG8Sint: return VK_FORMAT_R8G8_SINT; + case TextureFormat::R32Uint: return VK_FORMAT_R32_UINT; + case TextureFormat::R32Sint: return VK_FORMAT_R32_SINT; + case TextureFormat::R32Float: return VK_FORMAT_R32_SFLOAT; + case TextureFormat::RG16Uint: return VK_FORMAT_R16G16_UINT; + case TextureFormat::RG16Sint: return VK_FORMAT_R16G16_SINT; + case TextureFormat::RG16Float: return VK_FORMAT_R16G16_SFLOAT; + case TextureFormat::RGBA8Unorm: return VK_FORMAT_R8G8B8A8_UNORM; + case TextureFormat::RGBA8UnormSrgb: return VK_FORMAT_R8G8B8A8_SRGB; + case TextureFormat::RGBA8Snorm: return VK_FORMAT_R8G8B8A8_SNORM; + case TextureFormat::RGBA8Uint: return VK_FORMAT_R8G8B8A8_UINT; + case TextureFormat::RGBA8Sint: return VK_FORMAT_R8G8B8A8_SINT; + case TextureFormat::BGRA8Unorm: return VK_FORMAT_B8G8R8A8_UNORM; + case TextureFormat::BGRA8UnormSrgb: return VK_FORMAT_B8G8R8A8_SRGB; + case TextureFormat::RGB10A2Unorm: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case TextureFormat::RGB10A2Uint: return VK_FORMAT_A2B10G10R10_UINT_PACK32; + case TextureFormat::RG11B10Float: return VK_FORMAT_B10G11R11_UFLOAT_PACK32; + case TextureFormat::RGB9E5Float: return VK_FORMAT_E5B9G9R9_UFLOAT_PACK32; + case TextureFormat::RG32Uint: return VK_FORMAT_R32G32_UINT; + case TextureFormat::RG32Sint: return VK_FORMAT_R32G32_SINT; + case TextureFormat::RG32Float: return VK_FORMAT_R32G32_SFLOAT; + case TextureFormat::RGBA16Uint: return VK_FORMAT_R16G16B16A16_UINT; + case TextureFormat::RGBA16Sint: return VK_FORMAT_R16G16B16A16_SINT; + case TextureFormat::RGBA16Float: return VK_FORMAT_R16G16B16A16_SFLOAT; + case TextureFormat::RGBA16Unorm: return VK_FORMAT_R16G16B16A16_UNORM; + case TextureFormat::RGBA16Snorm: return VK_FORMAT_R16G16B16A16_SNORM; + case TextureFormat::RGBA32Uint: return VK_FORMAT_R32G32B32A32_UINT; + case TextureFormat::RGBA32Sint: return VK_FORMAT_R32G32B32A32_SINT; + case TextureFormat::RGBA32Float: return VK_FORMAT_R32G32B32A32_SFLOAT; + case TextureFormat::Depth16Unorm: return VK_FORMAT_D16_UNORM; + case TextureFormat::Depth24Plus: return detail::g_depth24Supported ? VK_FORMAT_X8_D24_UNORM_PACK32 : VK_FORMAT_D32_SFLOAT; + case TextureFormat::Depth24PlusStencil8:return detail::g_depth24S8Supported ? VK_FORMAT_D24_UNORM_S8_UINT : VK_FORMAT_D32_SFLOAT_S8_UINT; + case TextureFormat::Depth32Float: return VK_FORMAT_D32_SFLOAT; + case TextureFormat::Depth32FloatStencil8:return VK_FORMAT_D32_SFLOAT_S8_UINT; + case TextureFormat::Stencil8: return VK_FORMAT_S8_UINT; + case TextureFormat::BC1RGBAUnorm: return VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + case TextureFormat::BC1RGBAUnormSrgb: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case TextureFormat::BC2RGBAUnorm: return VK_FORMAT_BC2_UNORM_BLOCK; + case TextureFormat::BC2RGBAUnormSrgb: return VK_FORMAT_BC2_SRGB_BLOCK; + case TextureFormat::BC3RGBAUnorm: return VK_FORMAT_BC3_UNORM_BLOCK; + case TextureFormat::BC3RGBAUnormSrgb: return VK_FORMAT_BC3_SRGB_BLOCK; + case TextureFormat::BC4RUnorm: return VK_FORMAT_BC4_UNORM_BLOCK; + case TextureFormat::BC4RSnorm: return VK_FORMAT_BC4_SNORM_BLOCK; + case TextureFormat::BC5RGUnorm: return VK_FORMAT_BC5_UNORM_BLOCK; + case TextureFormat::BC5RGSnorm: return VK_FORMAT_BC5_SNORM_BLOCK; + case TextureFormat::BC6HRGBUfloat: return VK_FORMAT_BC6H_UFLOAT_BLOCK; + case TextureFormat::BC6HRGBFloat: return VK_FORMAT_BC6H_SFLOAT_BLOCK; + case TextureFormat::BC7RGBAUnorm: return VK_FORMAT_BC7_UNORM_BLOCK; + case TextureFormat::BC7RGBAUnormSrgb: return VK_FORMAT_BC7_SRGB_BLOCK; + case TextureFormat::ASTC4x4Unorm: return VK_FORMAT_ASTC_4x4_UNORM_BLOCK; + case TextureFormat::ASTC4x4UnormSrgb: return VK_FORMAT_ASTC_4x4_SRGB_BLOCK; + case TextureFormat::ASTC5x5Unorm: return VK_FORMAT_ASTC_5x5_UNORM_BLOCK; + case TextureFormat::ASTC5x5UnormSrgb: return VK_FORMAT_ASTC_5x5_SRGB_BLOCK; + case TextureFormat::ASTC6x6Unorm: return VK_FORMAT_ASTC_6x6_UNORM_BLOCK; + case TextureFormat::ASTC6x6UnormSrgb: return VK_FORMAT_ASTC_6x6_SRGB_BLOCK; + case TextureFormat::ASTC8x8Unorm: return VK_FORMAT_ASTC_8x8_UNORM_BLOCK; + case TextureFormat::ASTC8x8UnormSrgb: return VK_FORMAT_ASTC_8x8_SRGB_BLOCK; + default: return VK_FORMAT_UNDEFINED; + } +} + +inline VkFormat toVkVertexFormat(VertexFormat f) { + switch (f) { + case VertexFormat::Uint8x2: return VK_FORMAT_R8G8_UINT; + case VertexFormat::Uint8x4: return VK_FORMAT_R8G8B8A8_UINT; + case VertexFormat::Sint8x2: return VK_FORMAT_R8G8_SINT; + case VertexFormat::Sint8x4: return VK_FORMAT_R8G8B8A8_SINT; + case VertexFormat::Unorm8x2: return VK_FORMAT_R8G8_UNORM; + case VertexFormat::Unorm8x4: return VK_FORMAT_R8G8B8A8_UNORM; + case VertexFormat::Snorm8x2: return VK_FORMAT_R8G8_SNORM; + case VertexFormat::Snorm8x4: return VK_FORMAT_R8G8B8A8_SNORM; + case VertexFormat::Uint16x2: return VK_FORMAT_R16G16_UINT; + case VertexFormat::Uint16x4: return VK_FORMAT_R16G16B16A16_UINT; + case VertexFormat::Sint16x2: return VK_FORMAT_R16G16_SINT; + case VertexFormat::Sint16x4: return VK_FORMAT_R16G16B16A16_SINT; + case VertexFormat::Unorm16x2: return VK_FORMAT_R16G16_UNORM; + case VertexFormat::Unorm16x4: return VK_FORMAT_R16G16B16A16_UNORM; + case VertexFormat::Snorm16x2: return VK_FORMAT_R16G16_SNORM; + case VertexFormat::Snorm16x4: return VK_FORMAT_R16G16B16A16_SNORM; + case VertexFormat::Float16x2: return VK_FORMAT_R16G16_SFLOAT; + case VertexFormat::Float16x4: return VK_FORMAT_R16G16B16A16_SFLOAT; + case VertexFormat::Float32: return VK_FORMAT_R32_SFLOAT; + case VertexFormat::Float32x2: return VK_FORMAT_R32G32_SFLOAT; + case VertexFormat::Float32x3: return VK_FORMAT_R32G32B32_SFLOAT; + case VertexFormat::Float32x4: return VK_FORMAT_R32G32B32A32_SFLOAT; + case VertexFormat::Uint32: return VK_FORMAT_R32_UINT; + case VertexFormat::Uint32x2: return VK_FORMAT_R32G32_UINT; + case VertexFormat::Uint32x3: return VK_FORMAT_R32G32B32_UINT; + case VertexFormat::Uint32x4: return VK_FORMAT_R32G32B32A32_UINT; + case VertexFormat::Sint32: return VK_FORMAT_R32_SINT; + case VertexFormat::Sint32x2: return VK_FORMAT_R32G32_SINT; + case VertexFormat::Sint32x3: return VK_FORMAT_R32G32B32_SINT; + case VertexFormat::Sint32x4: return VK_FORMAT_R32G32B32A32_SINT; + default: return VK_FORMAT_UNDEFINED; + } +} + +inline VkBufferUsageFlags toVkBufferUsage(BufferUsage u) { + VkBufferUsageFlags f = 0; + if (hasFlag(u, BufferUsage::CopySrc)) f |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + if (hasFlag(u, BufferUsage::CopyDst)) f |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if (hasFlag(u, BufferUsage::Vertex)) f |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + if (hasFlag(u, BufferUsage::Index)) f |= VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + if (hasFlag(u, BufferUsage::Uniform)) f |= VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + if (hasFlag(u, BufferUsage::Storage)) f |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + if (hasFlag(u, BufferUsage::StorageRead))f|= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + if (hasFlag(u, BufferUsage::Indirect)) f |= VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; + if (hasFlag(u, BufferUsage::AccelStructInput)) + f |= VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + if (hasFlag(u, BufferUsage::ShaderBindingTable)) + f |= VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + if (hasFlag(u, BufferUsage::AccelStructScratch)) + f |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + return f; +} + +inline VkImageUsageFlags toVkImageUsage(TextureUsage u) { + VkImageUsageFlags f = 0; + if ((static_cast(u) & static_cast(TextureUsage::CopySrc)) != 0) f |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + if ((static_cast(u) & static_cast(TextureUsage::CopyDst)) != 0) f |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if ((static_cast(u) & static_cast(TextureUsage::Sampled)) != 0) f |= VK_IMAGE_USAGE_SAMPLED_BIT; + if ((static_cast(u) & static_cast(TextureUsage::Storage)) != 0) f |= VK_IMAGE_USAGE_STORAGE_BIT; + if ((static_cast(u) & static_cast(TextureUsage::RenderTarget)) != 0) f |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + if ((static_cast(u) & static_cast(TextureUsage::DepthStencil)) != 0) f |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + if ((static_cast(u) & static_cast(TextureUsage::InputAttachment))!= 0) f |= VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT; + return f; +} + +inline VkImageType toVkImageType(TextureDimension d) { + switch (d) { + case TextureDimension::Texture1D: return VK_IMAGE_TYPE_1D; + case TextureDimension::Texture2D: return VK_IMAGE_TYPE_2D; + case TextureDimension::Texture3D: return VK_IMAGE_TYPE_3D; + } return VK_IMAGE_TYPE_2D; +} + +inline VkImageViewType toVkImageViewType(TextureViewDimension d) { + switch (d) { + case TextureViewDimension::Texture1D: return VK_IMAGE_VIEW_TYPE_1D; + case TextureViewDimension::Texture1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case TextureViewDimension::Texture2D: return VK_IMAGE_VIEW_TYPE_2D; + case TextureViewDimension::Texture2DArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + case TextureViewDimension::TextureCube: return VK_IMAGE_VIEW_TYPE_CUBE; + case TextureViewDimension::TextureCubeArray: return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + case TextureViewDimension::Texture3D: return VK_IMAGE_VIEW_TYPE_3D; + } return VK_IMAGE_VIEW_TYPE_2D; +} + +inline VkFilter toVkFilter(FilterMode m) { return m == FilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } +inline VkSamplerMipmapMode toVkMipmapMode(MipmapFilterMode m) { return m == MipmapFilterMode::Nearest ? VK_SAMPLER_MIPMAP_MODE_NEAREST : VK_SAMPLER_MIPMAP_MODE_LINEAR; } + +inline VkSamplerAddressMode toVkAddressMode(AddressMode m) { + switch (m) { + case AddressMode::Repeat: return VK_SAMPLER_ADDRESS_MODE_REPEAT; + case AddressMode::MirrorRepeat: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + case AddressMode::ClampToEdge: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + case AddressMode::ClampToBorder:return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + } return VK_SAMPLER_ADDRESS_MODE_REPEAT; +} + +inline VkBorderColor toVkBorderColor(SamplerBorderColor c) { + switch (c) { + case SamplerBorderColor::TransparentBlack: return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + case SamplerBorderColor::OpaqueBlack: return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; + case SamplerBorderColor::OpaqueWhite: return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + } return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; +} + +inline VkCompareOp toVkCompareOp(CompareFunction f) { + switch (f) { + case CompareFunction::Never: return VK_COMPARE_OP_NEVER; + case CompareFunction::Less: return VK_COMPARE_OP_LESS; + case CompareFunction::Equal: return VK_COMPARE_OP_EQUAL; + case CompareFunction::LessEqual: return VK_COMPARE_OP_LESS_OR_EQUAL; + case CompareFunction::Greater: return VK_COMPARE_OP_GREATER; + case CompareFunction::NotEqual: return VK_COMPARE_OP_NOT_EQUAL; + case CompareFunction::GreaterEqual: return VK_COMPARE_OP_GREATER_OR_EQUAL; + case CompareFunction::Always: return VK_COMPARE_OP_ALWAYS; + } return VK_COMPARE_OP_NEVER; +} + +inline VkPrimitiveTopology toVkTopology(PrimitiveTopology t) { + switch (t) { + case PrimitiveTopology::PointList: return VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + case PrimitiveTopology::LineList: return VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + case PrimitiveTopology::LineStrip: return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + case PrimitiveTopology::TriangleList: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + case PrimitiveTopology::TriangleStrip: return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + } return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +} + +inline VkFrontFace toVkFrontFace(FrontFace f) { return f == FrontFace::CCW ? VK_FRONT_FACE_COUNTER_CLOCKWISE : VK_FRONT_FACE_CLOCKWISE; } +inline VkCullModeFlags toVkCullMode(CullMode m) { switch (m) { case CullMode::None: return VK_CULL_MODE_NONE; case CullMode::Front: return VK_CULL_MODE_FRONT_BIT; case CullMode::Back: return VK_CULL_MODE_BACK_BIT; } return VK_CULL_MODE_NONE; } +inline VkPolygonMode toVkPolygonMode(FillMode m) { return m == FillMode::Solid ? VK_POLYGON_MODE_FILL : VK_POLYGON_MODE_LINE; } + +inline VkBlendFactor toVkBlendFactor(BlendFactor f) { + switch (f) { + case BlendFactor::Zero: return VK_BLEND_FACTOR_ZERO; + case BlendFactor::One: return VK_BLEND_FACTOR_ONE; + case BlendFactor::Src: return VK_BLEND_FACTOR_SRC_COLOR; + case BlendFactor::OneMinusSrc: return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR; + case BlendFactor::SrcAlpha: return VK_BLEND_FACTOR_SRC_ALPHA; + case BlendFactor::OneMinusSrcAlpha: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + case BlendFactor::Dst: return VK_BLEND_FACTOR_DST_COLOR; + case BlendFactor::OneMinusDst: return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR; + case BlendFactor::DstAlpha: return VK_BLEND_FACTOR_DST_ALPHA; + case BlendFactor::OneMinusDstAlpha: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA; + case BlendFactor::SrcAlphaSaturated:return VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; + case BlendFactor::Constant: return VK_BLEND_FACTOR_CONSTANT_COLOR; + case BlendFactor::OneMinusConstant: return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR; + } return VK_BLEND_FACTOR_ZERO; +} + +inline VkBlendOp toVkBlendOp(BlendOperation o) { + switch (o) { + case BlendOperation::Add: return VK_BLEND_OP_ADD; + case BlendOperation::Subtract: return VK_BLEND_OP_SUBTRACT; + case BlendOperation::ReverseSubtract: return VK_BLEND_OP_REVERSE_SUBTRACT; + case BlendOperation::Min: return VK_BLEND_OP_MIN; + case BlendOperation::Max: return VK_BLEND_OP_MAX; + } return VK_BLEND_OP_ADD; +} + +inline VkStencilOp toVkStencilOp(StencilOperation o) { + switch (o) { + case StencilOperation::Keep: return VK_STENCIL_OP_KEEP; + case StencilOperation::Zero: return VK_STENCIL_OP_ZERO; + case StencilOperation::Replace: return VK_STENCIL_OP_REPLACE; + case StencilOperation::IncrementClamp: return VK_STENCIL_OP_INCREMENT_AND_CLAMP; + case StencilOperation::DecrementClamp: return VK_STENCIL_OP_DECREMENT_AND_CLAMP; + case StencilOperation::Invert: return VK_STENCIL_OP_INVERT; + case StencilOperation::IncrementWrap: return VK_STENCIL_OP_INCREMENT_AND_WRAP; + case StencilOperation::DecrementWrap: return VK_STENCIL_OP_DECREMENT_AND_WRAP; + } return VK_STENCIL_OP_KEEP; +} + +inline VkAttachmentLoadOp toVkLoadOp(LoadOp o) { switch (o) { case LoadOp::Load: return VK_ATTACHMENT_LOAD_OP_LOAD; case LoadOp::Clear: return VK_ATTACHMENT_LOAD_OP_CLEAR; case LoadOp::DontCare: return VK_ATTACHMENT_LOAD_OP_DONT_CARE; } return VK_ATTACHMENT_LOAD_OP_CLEAR; } +inline VkAttachmentStoreOp toVkStoreOp(StoreOp o) { return o == StoreOp::Store ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; } +inline VkIndexType toVkIndexType(IndexFormat f) { return f == IndexFormat::UInt16 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32; } + +inline VkPresentModeKHR toVkPresentMode(PresentMode m) { + switch (m) { + case PresentMode::Immediate: return VK_PRESENT_MODE_IMMEDIATE_KHR; + case PresentMode::Mailbox: return VK_PRESENT_MODE_MAILBOX_KHR; + case PresentMode::Fifo: return VK_PRESENT_MODE_FIFO_KHR; + case PresentMode::FifoRelaxed: return VK_PRESENT_MODE_FIFO_RELAXED_KHR; + } return VK_PRESENT_MODE_FIFO_KHR; +} + +inline VkImageAspectFlags getAspectMask(TextureFormat f) { + if (isDepthStencil(f)) { + VkImageAspectFlags a = 0; + if (hasDepth(f)) a |= VK_IMAGE_ASPECT_DEPTH_BIT; + if (hasStencil(f)) a |= VK_IMAGE_ASPECT_STENCIL_BIT; + return a; + } + return VK_IMAGE_ASPECT_COLOR_BIT; +} + +inline VkSampleCountFlagBits toVkSampleCount(u32 c) { + switch (c) { + case 1: return VK_SAMPLE_COUNT_1_BIT; + case 2: return VK_SAMPLE_COUNT_2_BIT; + case 4: return VK_SAMPLE_COUNT_4_BIT; + case 8: return VK_SAMPLE_COUNT_8_BIT; + case 16: return VK_SAMPLE_COUNT_16_BIT; + case 32: return VK_SAMPLE_COUNT_32_BIT; + case 64: return VK_SAMPLE_COUNT_64_BIT; + default: return VK_SAMPLE_COUNT_1_BIT; + } +} + +inline VkColorComponentFlags toVkColorWriteMask(ColorWriteMask m) { + VkColorComponentFlags f = 0; + if (static_cast(m) & static_cast(ColorWriteMask::Red)) f |= VK_COLOR_COMPONENT_R_BIT; + if (static_cast(m) & static_cast(ColorWriteMask::Green)) f |= VK_COLOR_COMPONENT_G_BIT; + if (static_cast(m) & static_cast(ColorWriteMask::Blue)) f |= VK_COLOR_COMPONENT_B_BIT; + if (static_cast(m) & static_cast(ColorWriteMask::Alpha)) f |= VK_COLOR_COMPONENT_A_BIT; + return f; +} + +inline VkShaderStageFlags toVkShaderStageFlags(ShaderStage s) { + VkShaderStageFlags f = 0; + auto v = static_cast(s); + if (v & static_cast(ShaderStage::Vertex)) f |= VK_SHADER_STAGE_VERTEX_BIT; + if (v & static_cast(ShaderStage::Fragment)) f |= VK_SHADER_STAGE_FRAGMENT_BIT; + if (v & static_cast(ShaderStage::Compute)) f |= VK_SHADER_STAGE_COMPUTE_BIT; + if (v & static_cast(ShaderStage::Mesh)) f |= VK_SHADER_STAGE_MESH_BIT_EXT; + if (v & static_cast(ShaderStage::Task)) f |= VK_SHADER_STAGE_TASK_BIT_EXT; + if (v & static_cast(ShaderStage::RayGen)) f |= VK_SHADER_STAGE_RAYGEN_BIT_KHR; + if (v & static_cast(ShaderStage::ClosestHit)) f |= VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR; + if (v & static_cast(ShaderStage::Miss)) f |= VK_SHADER_STAGE_MISS_BIT_KHR; + if (v & static_cast(ShaderStage::AnyHit)) f |= VK_SHADER_STAGE_ANY_HIT_BIT_KHR; + if (v & static_cast(ShaderStage::Intersection)) f |= VK_SHADER_STAGE_INTERSECTION_BIT_KHR; + if (v & static_cast(ShaderStage::Callable)) f |= VK_SHADER_STAGE_CALLABLE_BIT_KHR; + return f; +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDescriptorPoolManager.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDescriptorPoolManager.cppm new file mode 100644 index 00000000..4856943e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDescriptorPoolManager.cppm @@ -0,0 +1,121 @@ +/// Manages VkDescriptorPool allocation with auto-grow. + +module; + +#include "VkIncludes.h" +#include + + +export module rhi.vk:descriptor_pool_manager; + +import core.stdtypes; +import core.status; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDescriptorPoolManager { +public: + VkDescriptorPoolManager(VkDevice device, u32 maxSetsPerPool = 256, bool accelStructEnabled = false) + : m_device(device), m_maxSetsPerPool(maxSetsPerPool), m_accelStructEnabled(accelStructEnabled) {} + + Status allocate(VkDescriptorSetLayout layout, VkDescriptorPool& outPool, + bool updateAfterBind = false, u32 variableCount = 0) { + outPool = VK_NULL_HANDLE; + + VkDescriptorSetVariableDescriptorCountAllocateInfo varCountInfo{}; + varCountInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO; + if (variableCount > 0) { + varCountInfo.descriptorSetCount = 1; + varCountInfo.pDescriptorCounts = &variableCount; + } + + // Try existing pools. + for (auto pool : m_pools) { + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ai.descriptorPool = pool; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &layout; + if (variableCount > 0) ai.pNext = &varCountInfo; + + if (vkAllocateDescriptorSets(m_device, &ai, &set) == VK_SUCCESS) { + outPool = pool; + m_lastAllocatedSet = set; + return ErrorCode::Ok; + } + } + + // Create new pool. + if (createPool(updateAfterBind) != ErrorCode::Ok) return ErrorCode::Unknown; + + auto pool = m_pools.back(); + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ai.descriptorPool = pool; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &layout; + if (variableCount > 0) ai.pNext = &varCountInfo; + + if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS) return ErrorCode::Unknown; + + outPool = pool; + m_lastAllocatedSet = set; + return ErrorCode::Ok; + } + + [[nodiscard]] VkDescriptorSet lastAllocatedSet() const { return m_lastAllocatedSet; } + + void free(VkDescriptorPool pool, VkDescriptorSet set) { + vkFreeDescriptorSets(m_device, pool, 1, &set); + } + + void destroy() { + for (auto pool : m_pools) vkDestroyDescriptorPool(m_device, pool, nullptr); + m_pools.clear(); + } + +private: + Status createPool(bool updateAfterBind) { + u32 mult = updateAfterBind ? 64 : 1; + VkDescriptorPoolSize sizes[] = { + { VK_DESCRIPTOR_TYPE_SAMPLER, m_maxSetsPerPool * mult }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, m_maxSetsPerPool * 4 * mult }, + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, m_maxSetsPerPool * mult }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, m_maxSetsPerPool * 2 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, m_maxSetsPerPool * 2 * mult }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, m_maxSetsPerPool }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, m_maxSetsPerPool }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, m_maxSetsPerPool * 4 * mult }, + { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, m_maxSetsPerPool }, + { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, m_maxSetsPerPool }, + { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, m_maxSetsPerPool }, + { VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, m_maxSetsPerPool }, + }; + u32 sizeCount = m_accelStructEnabled ? 12 : 11; + + VkDescriptorPoolCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + if (updateAfterBind) ci.flags |= VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + ci.maxSets = m_maxSetsPerPool; + ci.poolSizeCount = sizeCount; + ci.pPoolSizes = sizes; + + VkDescriptorPool pool = VK_NULL_HANDLE; + if (vkCreateDescriptorPool(m_device, &ci, nullptr, &pool) != VK_SUCCESS) return ErrorCode::Unknown; + m_pools.push_back(pool); + return ErrorCode::Ok; + } + + VkDevice m_device; + std::vector m_pools; + u32 m_maxSetsPerPool; + bool m_accelStructEnabled; + VkDescriptorSet m_lastAllocatedSet = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDevice.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDevice.cppm new file mode 100644 index 00000000..5c024ffa --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkDevice.cppm @@ -0,0 +1,553 @@ +/// Vulkan implementation of Device. + +module; + +#include "VkIncludes.h" +#include +#include +#include +#include + +#include +#include + +export module rhi.vk:device; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; +import :surface; +import :buffer; +import :texture; +import :texture_view; +import :sampler; +import :shader_module; +import :fence; +import :query_set; +import :binding_shifts; +import :bind_group_layout; +import :bind_group; +import :pipeline_layout; +import :pipeline_cache; +import :render_pipeline; +import :compute_pipeline; +import :mesh_pipeline; +import :accel_struct; +import :ray_tracing_pipeline; +import :command_pool; +import :command_buffer; +import :queue; +import :swap_chain; +import :descriptor_pool_manager; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDeviceImpl : public Device { +public: + Status init(VkAdapterImpl* adapter, const DeviceDesc& desc) { + m_adapter = adapter; + features = adapter->buildFeatures(); + + bool bindlessEnabled = desc.requiredFeatures.bindlessDescriptors && adapter->supportsDescriptorIndexing(); + bool meshEnabled = desc.requiredFeatures.meshShaders && adapter->supportsMeshShader(); + bool rtEnabled = desc.requiredFeatures.rayTracing && adapter->supportsRayTracing(); + m_meshEnabled = meshEnabled; + m_rtEnabled = rtEnabled; + m_bindlessEnabled = bindlessEnabled; + // Validation flag: set externally by Adapter.createDevice after init. + + // Find queue families. + i32 gfxFamily = adapter->findQueueFamily(QueueType::Graphics); + i32 compFamily = adapter->findQueueFamily(QueueType::Compute); + i32 xferFamily = adapter->findQueueFamily(QueueType::Transfer); + if (gfxFamily < 0) return ErrorCode::Unknown; + + // Build queue create infos. + struct FamilyRequest { u32 family; u32 count; }; + std::vector familyReqs; + auto addFamily = [&](i32 f, u32 requested) { + if (f < 0 || requested == 0) return; + u32 avail = adapter->queueFamilies()[f].queueCount; + for (auto& fr : familyReqs) { if (fr.family == static_cast(f)) { fr.count = std::min(fr.count + requested, avail); return; } } + familyReqs.push_back({ static_cast(f), std::min(requested, avail) }); + }; + addFamily(gfxFamily, desc.graphicsQueueCount); + addFamily(compFamily, desc.computeQueueCount); + addFamily(xferFamily, desc.transferQueueCount); + + std::vector queueCis; + std::vector> priorities; + for (auto& fr : familyReqs) { + priorities.emplace_back(fr.count, 1.0f); + VkDeviceQueueCreateInfo qci{}; qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + qci.queueFamilyIndex = fr.family; qci.queueCount = fr.count; + qci.pQueuePriorities = priorities.back().data(); + queueCis.push_back(qci); + } + + // Extensions. + std::vector exts; + exts.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + if (meshEnabled) exts.push_back("VK_EXT_mesh_shader"); + if (rtEnabled) { + exts.push_back("VK_KHR_ray_tracing_pipeline"); + exts.push_back("VK_KHR_acceleration_structure"); + exts.push_back("VK_KHR_deferred_host_operations"); + exts.push_back("VK_KHR_ray_query"); + } + + // Feature chain. + VkPhysicalDeviceVulkan13Features features13{}; features13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + features13.dynamicRendering = VK_TRUE; features13.synchronization2 = VK_TRUE; + features13.shaderDemoteToHelperInvocation = VK_TRUE; + + VkPhysicalDeviceVulkan12Features features12{}; features12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + features12.pNext = &features13; + features12.timelineSemaphore = VK_TRUE; + if (bindlessEnabled) { + features12.descriptorIndexing = VK_TRUE; + features12.descriptorBindingPartiallyBound = VK_TRUE; + features12.descriptorBindingVariableDescriptorCount = VK_TRUE; + features12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE; + features12.descriptorBindingStorageBufferUpdateAfterBind = VK_TRUE; + features12.runtimeDescriptorArray = VK_TRUE; + features12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + features12.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE; + } + if (rtEnabled) features12.bufferDeviceAddress = VK_TRUE; + + void* featureChainTail = &features12; + + VkPhysicalDeviceMeshShaderFeaturesEXT meshFeatures{}; + meshFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT; + if (meshEnabled) { meshFeatures.taskShader = VK_TRUE; meshFeatures.meshShader = VK_TRUE; + meshFeatures.pNext = featureChainTail; featureChainTail = &meshFeatures; } + + VkPhysicalDeviceRayTracingPipelineFeaturesKHR rtPipeFeatures{}; + rtPipeFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR; + VkPhysicalDeviceAccelerationStructureFeaturesKHR asFeatures{}; + asFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR; + VkPhysicalDeviceRayQueryFeaturesKHR rqFeatures{}; + rqFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR; + if (rtEnabled) { + rtPipeFeatures.rayTracingPipeline = VK_TRUE; + asFeatures.accelerationStructure = VK_TRUE; + rqFeatures.rayQuery = VK_TRUE; + rtPipeFeatures.pNext = featureChainTail; featureChainTail = &rtPipeFeatures; + asFeatures.pNext = featureChainTail; featureChainTail = &asFeatures; + rqFeatures.pNext = featureChainTail; featureChainTail = &rqFeatures; + } + + VkPhysicalDeviceFeatures2 features2{}; features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features2.pNext = featureChainTail; + features2.features = adapter->features10(); + + VkDeviceCreateInfo dci{}; dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + dci.pNext = &features2; + dci.queueCreateInfoCount = static_cast(queueCis.size()); + dci.pQueueCreateInfos = queueCis.data(); + dci.enabledExtensionCount = static_cast(exts.size()); + dci.ppEnabledExtensionNames = exts.data(); + + if (vkCreateDevice(adapter->physicalDevice(), &dci, nullptr, &m_device) != VK_SUCCESS) + return ErrorCode::Unknown; + + type = DeviceType::Vulkan; + + // Retrieve queues. + auto retrieveQueues = [&](i32 family, u32 count, QueueType qt, u32 offset) { + if (family < 0 || count == 0) return; + f32 tsPeriod = adapter->properties().limits.timestampPeriod; + for (u32 i = 0; i < count; ++i) { + VkQueue q = VK_NULL_HANDLE; + vkGetDeviceQueue(m_device, static_cast(family), offset + i, &q); + auto* vkQ = new VkQueueImpl(q, qt, static_cast(family), tsPeriod, this, m_device, adapter->physicalDevice()); + m_allQueues.push_back(vkQ); + switch (qt) { + case QueueType::Graphics: m_gfxQueues.push_back(vkQ); break; + case QueueType::Compute: m_compQueues.push_back(vkQ); break; + case QueueType::Transfer: m_xferQueues.push_back(vkQ); break; + } + } + }; + // Track how many queues have been claimed from each family to avoid overrun. + auto familyAvail = [&](i32 family) -> u32 { + u32 total = adapter->queueFamilies()[family].queueCount; + for (auto& fr : familyReqs) if (fr.family == static_cast(family)) return fr.count; + return total; + }; + + u32 gfxCount = std::min(desc.graphicsQueueCount, familyAvail(gfxFamily)); + retrieveQueues(gfxFamily, gfxCount, QueueType::Graphics, 0); + + if (compFamily >= 0) { + u32 offset = (compFamily == gfxFamily) ? gfxCount : 0; + u32 avail = adapter->queueFamilies()[compFamily].queueCount - offset; + u32 compCount = std::min(desc.computeQueueCount, avail); + retrieveQueues(compFamily, compCount, QueueType::Compute, offset); + } + if (xferFamily >= 0) { + u32 offset = 0; + if (xferFamily == gfxFamily) offset = gfxCount + static_cast(m_compQueues.size()); + else if (xferFamily == compFamily) offset = static_cast(m_compQueues.size()); + u32 avail = adapter->queueFamilies()[xferFamily].queueCount - offset; + u32 xferCount = std::min(desc.transferQueueCount, avail); + retrieveQueues(xferFamily, xferCount, QueueType::Transfer, offset); + } + + // Descriptor pool manager. + m_poolManager = new VkDescriptorPoolManager(m_device, 256, rtEnabled); + + // Probe depth-stencil format support. D24_S8 is optional (unsupported on AMD/RADV). + // Configures toVkFormat() to substitute D32F/D32F_S8 when D24 variants aren't supported. + { + VkFormatProperties fp{}; + vkGetPhysicalDeviceFormatProperties(adapter->physicalDevice(), VK_FORMAT_D24_UNORM_S8_UINT, &fp); + bool d24s8 = (fp.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0; + vkGetPhysicalDeviceFormatProperties(adapter->physicalDevice(), VK_FORMAT_X8_D24_UNORM_PACK32, &fp); + bool d24 = (fp.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0; + setDepthFormatSupport(d24s8, d24); + } + + // RT properties. + if (rtEnabled) { + VkPhysicalDeviceRayTracingPipelinePropertiesKHR rtProps{}; + rtProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_PROPERTIES_KHR; + VkPhysicalDeviceProperties2 p2{}; p2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; p2.pNext = &rtProps; + vkGetPhysicalDeviceProperties2(adapter->physicalDevice(), &p2); + shaderGroupHandleSize = rtProps.shaderGroupHandleSize; + shaderGroupHandleAlignment = rtProps.shaderGroupHandleAlignment; + shaderGroupBaseAlignment = rtProps.shaderGroupBaseAlignment; + } + + return ErrorCode::Ok; + } + + // ---- Device interface ---- + + Queue* getQueue(QueueType t, u32 index) override { + switch (t) { + case QueueType::Graphics: return index < m_gfxQueues.size() ? m_gfxQueues[index] : nullptr; + case QueueType::Compute: return index < m_compQueues.size() ? m_compQueues[index] : nullptr; + case QueueType::Transfer: return index < m_xferQueues.size() ? m_xferQueues[index] : nullptr; + } return nullptr; + } + u32 getQueueCount(QueueType t) override { + switch (t) { + case QueueType::Graphics: return static_cast(m_gfxQueues.size()); + case QueueType::Compute: return static_cast(m_compQueues.size()); + case QueueType::Transfer: return static_cast(m_xferQueues.size()); + } return 0; + } + + FormatSupport getFormatSupport(TextureFormat format) override { + VkFormatProperties fp{}; + vkGetPhysicalDeviceFormatProperties(m_adapter->physicalDevice(), toVkFormat(format), &fp); + auto opt = fp.optimalTilingFeatures; + auto buf = fp.bufferFeatures; + FormatSupport s = FormatSupport::Unsupported; + if (opt & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) s = s | FormatSupport::Texture; + if (opt & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) s = s | FormatSupport::StorageTexture; + if (opt & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) s = s | FormatSupport::ColorAttachment; + if (opt & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) s = s | FormatSupport::DepthStencil; + if (opt & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) s = s | FormatSupport::BlendableColor; + if (opt & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)s = s | FormatSupport::LinearFilter; + if (buf & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) s = s | FormatSupport::Buffer; + if (buf & VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT) s = s | FormatSupport::StorageBuffer; + if (buf & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) s = s | FormatSupport::VertexBuffer; + return s; + } + + // ---- Resource creation ---- + Status createBuffer(const BufferDesc& d, Buffer*& out) override { + auto* b = new VkBufferImpl(); if (b->init(m_device, m_adapter, d) != ErrorCode::Ok) { delete b; out = nullptr; return ErrorCode::Unknown; } + out = b; return ErrorCode::Ok; + } + Status createTexture(const TextureDesc& d, Texture*& out) override { + auto* t = new VkTextureImpl(); if (t->init(m_device, m_adapter, d) != ErrorCode::Ok) { delete t; out = nullptr; return ErrorCode::Unknown; } + out = t; return ErrorCode::Ok; + } + Status createTextureView(Texture* tex, const TextureViewDesc& d, TextureView*& out) override { + auto* v = new VkTextureViewImpl(); if (v->init(m_device, static_cast(tex), d) != ErrorCode::Ok) { delete v; out = nullptr; return ErrorCode::Unknown; } + out = v; return ErrorCode::Ok; + } + Status createSampler(const SamplerDesc& d, Sampler*& out) override { + auto* s = new VkSamplerImpl(); if (s->init(m_device, d) != ErrorCode::Ok) { delete s; out = nullptr; return ErrorCode::Unknown; } + out = s; return ErrorCode::Ok; + } + Status createShaderModule(const ShaderModuleDesc& d, ShaderModule*& out) override { + auto* m = new VkShaderModuleImpl(); if (m->init(m_device, d) != ErrorCode::Ok) { delete m; out = nullptr; return ErrorCode::Unknown; } + out = m; return ErrorCode::Ok; + } + Status createBindGroupLayout(const BindGroupLayoutDesc& d, BindGroupLayout*& out) override { + auto* l = new VkBindGroupLayoutImpl(); if (l->init(m_device, d, m_bindingShifts) != ErrorCode::Ok) { delete l; out = nullptr; return ErrorCode::Unknown; } + out = l; return ErrorCode::Ok; + } + Status createBindGroup(const BindGroupDesc& d, BindGroup*& out) override { + auto* g = new VkBindGroupImpl(); if (g->init(m_device, m_poolManager, d, m_bindingShifts) != ErrorCode::Ok) { delete g; out = nullptr; return ErrorCode::Unknown; } + out = g; return ErrorCode::Ok; + } + Status createPipelineLayout(const PipelineLayoutDesc& d, PipelineLayout*& out) override { + auto* l = new VkPipelineLayoutImpl(); if (l->init(m_device, d) != ErrorCode::Ok) { delete l; out = nullptr; return ErrorCode::Unknown; } + out = l; return ErrorCode::Ok; + } + Status createPipelineCache(const PipelineCacheDesc& d, PipelineCache*& out) override { + auto* c = new VkPipelineCacheImpl(); if (c->init(m_device, d) != ErrorCode::Ok) { delete c; out = nullptr; return ErrorCode::Unknown; } + out = c; return ErrorCode::Ok; + } + Status createRenderPipeline(const RenderPipelineDesc& d, RenderPipeline*& out) override { + auto* p = new VkRenderPipelineImpl(); if (p->init(m_device, d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; return ErrorCode::Ok; + } + Status createComputePipeline(const ComputePipelineDesc& d, ComputePipeline*& out) override { + auto* p = new VkComputePipelineImpl(); if (p->init(m_device, d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; return ErrorCode::Ok; + } + Status createCommandPool(QueueType qt, CommandPool*& out) override { + auto* p = new VkCommandPoolImpl(); p->ownerDevice = this; + if (p->init(m_device, m_adapter, qt) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; return ErrorCode::Ok; + } + Status createFence(u64 initialValue, Fence*& out) override { + auto* f = new VkFenceImpl(); if (f->init(m_device, initialValue) != ErrorCode::Ok) { delete f; out = nullptr; return ErrorCode::Unknown; } + out = f; return ErrorCode::Ok; + } + Status createQuerySet(const QuerySetDesc& d, QuerySet*& out) override { + auto* q = new VkQuerySetImpl(); if (q->init(m_device, d) != ErrorCode::Ok) { delete q; out = nullptr; return ErrorCode::Unknown; } + out = q; return ErrorCode::Ok; + } + Status createSwapChain(Surface* surface, const SwapChainDesc& d, SwapChain*& out) override { + auto* sc = new VkSwapChainImpl(); + auto* vkSurf = static_cast(surface); + if (sc->init(m_device, m_adapter->physicalDevice(), vkSurf->handle(), d, this) != ErrorCode::Ok) + { delete sc; out = nullptr; return ErrorCode::Unknown; } + out = sc; return ErrorCode::Ok; + } + + // ---- Mesh shader (folded in) ---- + Status createMeshPipeline(const MeshPipelineDesc& d, MeshPipeline*& out) override { + if (!m_meshEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* p = new VkMeshPipelineImpl(); if (p->init(m_device, d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; return ErrorCode::Ok; + } + void destroyMeshPipeline(MeshPipeline*& p) override { + if (p) { static_cast(p)->cleanup(m_device); delete p; p = nullptr; } + } + + // ---- Ray tracing (folded in) ---- + Status createAccelStruct(const AccelStructDesc& d, AccelStruct*& out) override { + if (!m_rtEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* a = new VkAccelStructImpl(); if (a->init(m_device, m_adapter, d, 256 * 1024) != ErrorCode::Ok) { delete a; out = nullptr; return ErrorCode::Unknown; } + out = a; return ErrorCode::Ok; + } + void destroyAccelStruct(AccelStruct*& a) override { + if (a) { static_cast(a)->cleanup(m_device); delete a; a = nullptr; } + } + Status createRayTracingPipeline(const RayTracingPipelineDesc& d, RayTracingPipeline*& out) override { + if (!m_rtEnabled) { out = nullptr; return ErrorCode::NotSupported; } + auto* p = new VkRayTracingPipelineImpl(); if (p->init(m_device, d) != ErrorCode::Ok) { delete p; out = nullptr; return ErrorCode::Unknown; } + out = p; return ErrorCode::Ok; + } + void destroyRayTracingPipeline(RayTracingPipeline*& p) override { + if (p) { static_cast(p)->cleanup(m_device); delete p; p = nullptr; } + } + Status getShaderGroupHandles(RayTracingPipeline* pipeline, u32 firstGroup, u32 groupCount, std::span outData) override { + if (!m_rtEnabled) return ErrorCode::NotSupported; + auto* p = static_cast(pipeline); + auto pfn = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkGetRayTracingShaderGroupHandlesKHR")); + if (!pfn) return ErrorCode::Unknown; + return pfn(m_device, p->handle(), firstGroup, groupCount, outData.size(), outData.data()) == VK_SUCCESS + ? ErrorCode::Ok : ErrorCode::Unknown; + } + + // ---- Resource destruction ---- + void destroyBuffer(Buffer*& b) override { if (b) { static_cast(b)->cleanup(m_device); delete b; b = nullptr; } } + void destroyTexture(Texture*& t) override { if (t) { static_cast(t)->cleanup(m_device); delete t; t = nullptr; } } + void destroyTextureView(TextureView*& v) override { if (v) { static_cast(v)->cleanup(m_device); delete v; v = nullptr; } } + void destroySampler(Sampler*& s) override { if (s) { static_cast(s)->cleanup(m_device); delete s; s = nullptr; } } + void destroyShaderModule(ShaderModule*& m) override { if (m) { static_cast(m)->cleanup(m_device); delete m; m = nullptr; } } + void destroyBindGroupLayout(BindGroupLayout*& l) override { if (l) { static_cast(l)->cleanup(m_device); delete l; l = nullptr; } } + void destroyBindGroup(BindGroup*& g) override { if (g) { static_cast(g)->cleanup(m_device, m_poolManager); delete g; g = nullptr; } } + void destroyPipelineLayout(PipelineLayout*& l) override { if (l) { static_cast(l)->cleanup(m_device); delete l; l = nullptr; } } + void destroyPipelineCache(PipelineCache*& c) override { if (c) { static_cast(c)->cleanup(m_device); delete c; c = nullptr; } } + void destroyRenderPipeline(RenderPipeline*& p) override { if (p) { static_cast(p)->cleanup(m_device); delete p; p = nullptr; } } + void destroyComputePipeline(ComputePipeline*& p) override { if (p) { static_cast(p)->cleanup(m_device); delete p; p = nullptr; } } + void destroyCommandPool(CommandPool*& p) override { if (p) { static_cast(p)->cleanup(); delete p; p = nullptr; } } + void destroyFence(Fence*& f) override { if (f) { static_cast(f)->cleanup(m_device); delete f; f = nullptr; } } + void destroyQuerySet(QuerySet*& q) override { if (q) { static_cast(q)->cleanup(m_device); delete q; q = nullptr; } } + void destroySwapChain(SwapChain*& sc) override { if (sc) { static_cast(sc)->cleanup(); delete sc; sc = nullptr; } } + void destroySurface(Surface*& s) override { if (s) { static_cast(s)->destroy(); delete s; s = nullptr; } } + + void waitIdle() override { vkDeviceWaitIdle(m_device); } + void destroy() override { + waitIdle(); + if (m_poolManager) { m_poolManager->destroy(); delete m_poolManager; m_poolManager = nullptr; } + for (auto* q : m_allQueues) delete q; + m_allQueues.clear(); m_gfxQueues.clear(); m_compQueues.clear(); m_xferQueues.clear(); + if (m_device != VK_NULL_HANDLE) { vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; } + delete this; + } + + // ---- Swap chain sync ---- + void setPendingSwapChainSync(VkSemaphore acquire, VkSemaphore present) { + m_pendingAcquire = acquire; m_pendingPresent = present; m_hasPendingSync = true; + } + bool consumePendingSwapChainSync(VkSemaphore& acquire, VkSemaphore& present) { + if (!m_hasPendingSync) return false; + acquire = m_pendingAcquire; present = m_pendingPresent; m_hasPendingSync = false; return true; + } + + [[nodiscard]] VkDevice handle() const { return m_device; } + [[nodiscard]] VkAdapterImpl* adapter() const { return m_adapter; } + [[nodiscard]] bool validationEnabled() const { return m_validationEnabled; } + + [[nodiscard]] const BindingShifts& bindingShifts() const { return m_bindingShifts; } + void setBindingShifts(const BindingShifts& s) { m_bindingShifts = s; } + + /// Set a Vulkan debug name on an object (only when validation is enabled). + void setDebugName(VkObjectType objectType, u64 objectHandle, std::u8string_view name) { + if (!m_validationEnabled || name.empty()) return; + auto pfn = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkSetDebugUtilsObjectNameEXT")); + if (!pfn) return; + char buf[256]{}; + auto len = std::min(name.size(), static_cast(255)); + std::memcpy(buf, name.data(), len); + VkDebugUtilsObjectNameInfoEXT ni{}; + ni.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; + ni.objectType = objectType; + ni.objectHandle = objectHandle; + ni.pObjectName = buf; + pfn(m_device, &ni); + } + +private: + VkDevice m_device = VK_NULL_HANDLE; + VkAdapterImpl* m_adapter = nullptr; + bool m_meshEnabled = false; + bool m_rtEnabled = false; + bool m_bindlessEnabled = false; + bool m_validationEnabled= false; + BindingShifts m_bindingShifts = BindingShifts::standard(); + VkDescriptorPoolManager* m_poolManager = nullptr; + + std::vector m_allQueues, m_gfxQueues, m_compQueues, m_xferQueues; + + VkSemaphore m_pendingAcquire = VK_NULL_HANDLE; + VkSemaphore m_pendingPresent = VK_NULL_HANDLE; + bool m_hasPendingSync = false; +}; + +// ---- Adapter::CreateDevice implementation ---- + +Status VkAdapterImpl::createDevice(const DeviceDesc& desc, Device*& out) { + auto* dev = new VkDeviceImpl(); + if (dev->init(this, desc) != ErrorCode::Ok) { delete dev; out = nullptr; return ErrorCode::Unknown; } + out = dev; return ErrorCode::Ok; +} + +// ---- SwapChain acquire/present implementations ---- + +Status VkSwapChainImpl::acquireNextImage() { + VkSemaphore acquireSem = m_acquireSems[m_frameIndex]; + u32 imgIdx = 0; + VkResult vr = vkAcquireNextImageKHR(m_device, m_swapchain, ~0ull, acquireSem, VK_NULL_HANDLE, &imgIdx); + m_currentImageIndex = imgIdx; + if (vr == VK_ERROR_OUT_OF_DATE_KHR) return ErrorCode::Unknown; + if (vr != VK_SUCCESS && vr != VK_SUBOPTIMAL_KHR) return ErrorCode::Unknown; + VkSemaphore presentSem = m_presentSems[m_currentImageIndex]; + m_owner->setPendingSwapChainSync(acquireSem, presentSem); + return ErrorCode::Ok; +} + +Status VkSwapChainImpl::present(Queue* queue) { + auto* vkQ = static_cast(queue); + VkSemaphore waitSem = m_presentSems[m_currentImageIndex]; + VkPresentInfoKHR pi{}; pi.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &waitSem; + pi.swapchainCount = 1; pi.pSwapchains = &m_swapchain; pi.pImageIndices = &m_currentImageIndex; + VkResult vr = vkQueuePresentKHR(vkQ->handle(), &pi); + m_frameIndex = (m_frameIndex + 1) % m_bufferCount; + if (vr == VK_ERROR_OUT_OF_DATE_KHR || vr == VK_SUBOPTIMAL_KHR) return ErrorCode::Unknown; + return vr == VK_SUCCESS ? ErrorCode::Ok : ErrorCode::Unknown; +} + +// ---- Queue submit-with-fence (needs Device for swap chain sync) ---- + +void VkQueueImpl::submit(std::span cmdBufs, Fence* signalFence, u64 signalValue) { + if (cmdBufs.size() == 0) return; + std::vector bufs(cmdBufs.size()); + for (usize i = 0; i < cmdBufs.size(); ++i) + bufs[i] = static_cast(cmdBufs[i])->handle(); + + auto* vkFence = static_cast(signalFence); + if (!vkFence) return; + + VkSemaphore acquireSem = VK_NULL_HANDLE, presentSem = VK_NULL_HANDLE; + bool hasSync = m_device->consumePendingSwapChainSync(acquireSem, presentSem); + + VkSemaphore waitSems[1] = { acquireSem }; + VkPipelineStageFlags waitStages[1] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; + u64 waitValues[1] = { 0 }; + + auto timelineSem = vkFence->handle(); + VkSemaphore signalSems[2] = { timelineSem, presentSem }; + u64 signalValues[2] = { signalValue, 0 }; + + VkTimelineSemaphoreSubmitInfo tsi{}; tsi.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO; + tsi.signalSemaphoreValueCount = hasSync ? 2u : 1u; tsi.pSignalSemaphoreValues = signalValues; + tsi.waitSemaphoreValueCount = hasSync ? 1u : 0u; tsi.pWaitSemaphoreValues = waitValues; + + VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.pNext = &tsi; + si.commandBufferCount = static_cast(bufs.size()); si.pCommandBuffers = bufs.data(); + si.signalSemaphoreCount = hasSync ? 2u : 1u; si.pSignalSemaphores = signalSems; + if (hasSync) { si.waitSemaphoreCount = 1; si.pWaitSemaphores = waitSems; si.pWaitDstStageMask = waitStages; } + + vkQueueSubmit(m_queue, 1, &si, VK_NULL_HANDLE); +} + +void VkQueueImpl::submit(std::span cmdBufs, + std::span waitFences, std::span waitVals, + Fence* signalFence, u64 signalValue) { + if (cmdBufs.size() == 0) return; + std::vector bufs(cmdBufs.size()); + for (usize i = 0; i < cmdBufs.size(); ++i) + bufs[i] = static_cast(cmdBufs[i])->handle(); + + // 5-arg submit does NOT consume swap chain sync - matching Sedulous. + // The 2-arg submit (used by the first queue submit each frame) handles it. + std::vector waitSems(waitFences.size()); + std::vector waitStages(waitFences.size()); + for (usize i = 0; i < waitFences.size(); ++i) { + waitSems[i] = static_cast(waitFences[i])->handle(); + waitStages[i] = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + } + + VkTimelineSemaphoreSubmitInfo tsi{}; + tsi.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO; + tsi.waitSemaphoreValueCount = static_cast(waitVals.size()); + tsi.pWaitSemaphoreValues = waitVals.data(); + + VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.pNext = &tsi; + si.commandBufferCount = static_cast(bufs.size()); si.pCommandBuffers = bufs.data(); + si.waitSemaphoreCount = static_cast(waitSems.size()); si.pWaitSemaphores = waitSems.data(); + si.pWaitDstStageMask = waitStages.data(); + + VkSemaphore signalSem = VK_NULL_HANDLE; + if (signalFence) { + signalSem = static_cast(signalFence)->handle(); + tsi.signalSemaphoreValueCount = 1; + tsi.pSignalSemaphoreValues = &signalValue; + si.signalSemaphoreCount = 1; + si.pSignalSemaphores = &signalSem; + } + + vkQueueSubmit(m_queue, 1, &si, VK_NULL_HANDLE); +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkFence.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkFence.cppm new file mode 100644 index 00000000..9ed2d920 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkFence.cppm @@ -0,0 +1,65 @@ +/// Vulkan implementation of Fence (timeline semaphore). + +module; + +#include "VkIncludes.h" + +export module rhi.vk:fence; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkFenceImpl : public Fence { +public: + Status init(VkDevice device, u64 initialValue) { + m_device = device; + + VkSemaphoreTypeCreateInfo typeInfo{}; + typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + typeInfo.initialValue = initialValue; + + VkSemaphoreCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + ci.pNext = &typeInfo; + + if (vkCreateSemaphore(device, &ci, nullptr, &m_semaphore) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(device, m_semaphore, nullptr); + m_semaphore = VK_NULL_HANDLE; + } + } + + // ---- Fence interface ---- + u64 completedValue() override { + u64 value = 0; + vkGetSemaphoreCounterValue(m_device, m_semaphore, &value); + return value; + } + + bool wait(u64 value, u64 timeoutNs) override { + VkSemaphoreWaitInfo wi{}; + wi.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + wi.semaphoreCount = 1; + wi.pSemaphores = &m_semaphore; + wi.pValues = &value; + return vkWaitSemaphores(m_device, &wi, timeoutNs) == VK_SUCCESS; + } + + [[nodiscard]] VkSemaphore handle() const { return m_semaphore; } + +private: + VkSemaphore m_semaphore = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkIncludes.h b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkIncludes.h new file mode 100644 index 00000000..e018778e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkIncludes.h @@ -0,0 +1,19 @@ +#ifndef DRACO_RHI_VK_INCLUDES_H_ +#define DRACO_RHI_VK_INCLUDES_H_ + +#ifdef _WIN32 +# ifndef VK_USE_PLATFORM_WIN32_KHR +# define VK_USE_PLATFORM_WIN32_KHR +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#endif + +#include + +#endif // DRACO_RHI_VK_INCLUDES_H_ diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkMeshPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkMeshPipeline.cppm new file mode 100644 index 00000000..39be6721 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkMeshPipeline.cppm @@ -0,0 +1,166 @@ +/// Vulkan implementation of MeshPipeline. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:mesh_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :shader_module; +import :pipeline_layout; +import :pipeline_cache; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkMeshPipelineImpl : public MeshPipeline { +public: + Status init(VkDevice device, const MeshPipelineDesc& desc) { + auto* vkLayout = static_cast(desc.layout); + if (!vkLayout) return ErrorCode::Unknown; + layout = desc.layout; + m_layout = vkLayout; + + std::vector stages; + std::u8string meshEntry = std::u8string(desc.mesh.entryPoint); + std::u8string taskEntry, fsEntry; + + // Task shader (optional). + if (desc.task.has_value()) { + taskEntry = std::u8string(desc.task->entryPoint); + if (auto* mod = static_cast(desc.task->module)) { + VkPipelineShaderStageCreateInfo s{}; s.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + s.stage = VK_SHADER_STAGE_TASK_BIT_EXT; s.module = mod->handle(); s.pName = reinterpret_cast(taskEntry.c_str()); + stages.push_back(s); + } + } + + // Mesh shader (required). + auto* meshMod = static_cast(desc.mesh.module); + if (!meshMod) return ErrorCode::Unknown; + { VkPipelineShaderStageCreateInfo s{}; s.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + s.stage = VK_SHADER_STAGE_MESH_BIT_EXT; s.module = meshMod->handle(); s.pName = reinterpret_cast(meshEntry.c_str()); + stages.push_back(s); } + + // Fragment shader (optional). + if (desc.fragment.has_value()) { + fsEntry = std::u8string(desc.fragment->shader.entryPoint); + if (auto* mod = static_cast(desc.fragment->shader.module)) { + VkPipelineShaderStageCreateInfo s{}; s.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + s.stage = VK_SHADER_STAGE_FRAGMENT_BIT; s.module = mod->handle(); s.pName = reinterpret_cast(fsEntry.c_str()); + stages.push_back(s); + } + } + + // Viewport (dynamic). + VkPipelineViewportStateCreateInfo vp{}; vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + vp.viewportCount = 1; vp.scissorCount = 1; + + // Rasterization. + VkPipelineRasterizationStateCreateInfo rs{}; rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rs.depthClampEnable = desc.primitive.depthClipEnabled ? VK_FALSE : VK_TRUE; + rs.polygonMode = toVkPolygonMode(desc.primitive.fillMode); + rs.cullMode = toVkCullMode(desc.primitive.cullMode); + rs.frontFace = toVkFrontFace(desc.primitive.frontFace); + rs.lineWidth = 1.0f; + if (desc.depthStencil.has_value()) { + rs.depthBiasEnable = (desc.depthStencil->depthBias != 0 || desc.depthStencil->depthBiasSlopeScale != 0) ? VK_TRUE : VK_FALSE; + rs.depthBiasConstantFactor = static_cast(desc.depthStencil->depthBias); + rs.depthBiasSlopeFactor = desc.depthStencil->depthBiasSlopeScale; + rs.depthBiasClamp = desc.depthStencil->depthBiasClamp; + } + + // Multisample. + VkPipelineMultisampleStateCreateInfo ms{}; ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + ms.rasterizationSamples = toVkSampleCount(desc.multisample.count); + ms.alphaToCoverageEnable = desc.multisample.alphaToCoverageEnabled ? VK_TRUE : VK_FALSE; + u32 sampleMask = desc.multisample.mask; ms.pSampleMask = &sampleMask; + + // Color blend. + std::vector blendAtts(desc.colorTargets.size()); + for (usize i = 0; i < desc.colorTargets.size(); ++i) { + auto& ba = blendAtts[i]; ba = {}; + ba.colorWriteMask = toVkColorWriteMask(desc.colorTargets[i].writeMask); + if (desc.colorTargets[i].blend.has_value()) { + ba.blendEnable = VK_TRUE; + ba.srcColorBlendFactor = toVkBlendFactor(desc.colorTargets[i].blend->color.srcFactor); + ba.dstColorBlendFactor = toVkBlendFactor(desc.colorTargets[i].blend->color.dstFactor); + ba.colorBlendOp = toVkBlendOp(desc.colorTargets[i].blend->color.operation); + ba.srcAlphaBlendFactor = toVkBlendFactor(desc.colorTargets[i].blend->alpha.srcFactor); + ba.dstAlphaBlendFactor = toVkBlendFactor(desc.colorTargets[i].blend->alpha.dstFactor); + ba.alphaBlendOp = toVkBlendOp(desc.colorTargets[i].blend->alpha.operation); + } + } + VkPipelineColorBlendStateCreateInfo cb{}; cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + cb.attachmentCount = static_cast(blendAtts.size()); cb.pAttachments = blendAtts.data(); + + // Depth/stencil. + VkPipelineDepthStencilStateCreateInfo dss{}; dss.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + if (desc.depthStencil.has_value()) { + const auto& ds = *desc.depthStencil; + dss.depthTestEnable = ds.depthTestEnabled ? VK_TRUE : VK_FALSE; + dss.depthWriteEnable = ds.depthWriteEnabled ? VK_TRUE : VK_FALSE; + dss.depthCompareOp = toVkCompareOp(ds.depthCompare); + dss.stencilTestEnable = ds.stencilEnabled ? VK_TRUE : VK_FALSE; + dss.front.failOp = toVkStencilOp(ds.stencilFront.failOp); dss.front.passOp = toVkStencilOp(ds.stencilFront.passOp); + dss.front.depthFailOp = toVkStencilOp(ds.stencilFront.depthFailOp); dss.front.compareOp = toVkCompareOp(ds.stencilFront.compare); + dss.front.compareMask = ds.stencilReadMask; dss.front.writeMask = ds.stencilWriteMask; + dss.back = dss.front; + dss.back.failOp = toVkStencilOp(ds.stencilBack.failOp); dss.back.passOp = toVkStencilOp(ds.stencilBack.passOp); + dss.back.depthFailOp = toVkStencilOp(ds.stencilBack.depthFailOp); dss.back.compareOp = toVkCompareOp(ds.stencilBack.compare); + } + + // Dynamic state. + VkDynamicState dynStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_BLEND_CONSTANTS, VK_DYNAMIC_STATE_STENCIL_REFERENCE }; + VkPipelineDynamicStateCreateInfo dyn{}; dyn.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dyn.dynamicStateCount = 4; dyn.pDynamicStates = dynStates; + + // Dynamic rendering. + std::vector colorFmts(desc.colorTargets.size()); + for (usize i = 0; i < desc.colorTargets.size(); ++i) colorFmts[i] = toVkFormat(desc.colorTargets[i].format); + VkPipelineRenderingCreateInfo ri{}; ri.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + ri.colorAttachmentCount = static_cast(colorFmts.size()); ri.pColorAttachmentFormats = colorFmts.data(); + if (desc.depthStencil.has_value()) { + VkFormat dsf = toVkFormat(desc.depthStencil->format); + if (hasDepth(desc.depthStencil->format)) ri.depthAttachmentFormat = dsf; + if (hasStencil(desc.depthStencil->format)) ri.stencilAttachmentFormat = dsf; + } + + // Create pipeline (no vertex input / input assembly for mesh shaders). + VkGraphicsPipelineCreateInfo pi{}; pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pi.pNext = &ri; pi.stageCount = static_cast(stages.size()); pi.pStages = stages.data(); + pi.pVertexInputState = nullptr; pi.pInputAssemblyState = nullptr; + pi.pViewportState = &vp; pi.pRasterizationState = &rs; pi.pMultisampleState = &ms; + pi.pDepthStencilState = desc.depthStencil.has_value() ? &dss : nullptr; + pi.pColorBlendState = &cb; pi.pDynamicState = &dyn; pi.layout = vkLayout->handle(); + + VkPipelineCache cacheHandle = VK_NULL_HANDLE; + if (desc.cache) cacheHandle = static_cast(desc.cache)->handle(); + + if (vkCreateGraphicsPipelines(device, cacheHandle, 1, &pi, nullptr, &m_pipeline) != VK_SUCCESS) + return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, m_pipeline, nullptr); m_pipeline = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkPipeline handle() const { return m_pipeline; } + [[nodiscard]] VkPipelineLayoutImpl* vkLayout() const { return m_layout; } + +private: + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkModule.cppm new file mode 100644 index 00000000..ee9c63c5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkModule.cppm @@ -0,0 +1,35 @@ +export module rhi.vk; + +export import :conversions; +export import :surface; +export import :adapter; +export import :backend; +export import :buffer; +export import :texture; +export import :texture_view; +export import :sampler; +export import :shader_module; +export import :fence; +export import :query_set; +export import :barrier_helper; +export import :command_buffer; +export import :descriptor_pool_manager; +export import :binding_shifts; +export import :bind_group_layout; +export import :pipeline_layout; +export import :pipeline_cache; +export import :render_pipeline; +export import :compute_pipeline; +export import :bind_group; +export import :swap_chain; +export import :queue; +export import :command_pool; +export import :render_bundle_encoder; +export import :render_pass_encoder; +export import :compute_pass_encoder; +export import :mesh_pipeline; +export import :accel_struct; +export import :ray_tracing_pipeline; +export import :transfer_batch; +export import :command_encoder; +export import :device; diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineCache.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineCache.cppm new file mode 100644 index 00000000..d0034a87 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineCache.cppm @@ -0,0 +1,57 @@ +/// Vulkan implementation of PipelineCache. + +module; + +#include "VkIncludes.h" +#include + +export module rhi.vk:pipeline_cache; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkPipelineCacheImpl : public PipelineCache { +public: + Status init(VkDevice device, const PipelineCacheDesc& desc) { + m_device = device; + + VkPipelineCacheCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + if (desc.initialData.size() > 0) { + ci.initialDataSize = desc.initialData.size(); + ci.pInitialData = desc.initialData.data(); + } + + if (vkCreatePipelineCache(device, &ci, nullptr, &m_cache) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_cache != VK_NULL_HANDLE) { vkDestroyPipelineCache(device, m_cache, nullptr); m_cache = VK_NULL_HANDLE; } + } + + u32 getDataSize() override { + usize size = 0; + vkGetPipelineCacheData(m_device, m_cache, &size, nullptr); + return static_cast(size); + } + + Status getData(std::span outData) override { + usize size = outData.size(); + if (vkGetPipelineCacheData(m_device, m_cache, &size, outData.data()) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + [[nodiscard]] VkPipelineCache handle() const { return m_cache; } + +private: + VkPipelineCache m_cache = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineLayout.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineLayout.cppm new file mode 100644 index 00000000..3d793c4e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkPipelineLayout.cppm @@ -0,0 +1,60 @@ +/// Vulkan implementation of PipelineLayout. + +module; + +#include "VkIncludes.h" +#include + + +export module rhi.vk:pipeline_layout; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :bind_group_layout; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkPipelineLayoutImpl : public PipelineLayout { +public: + Status init(VkDevice device, const PipelineLayoutDesc& desc) { + std::vector setLayouts(desc.bindGroupLayouts.size()); + for (usize i = 0; i < desc.bindGroupLayouts.size(); ++i) { + auto* vkl = static_cast(desc.bindGroupLayouts[i]); + if (!vkl) return ErrorCode::Unknown; + setLayouts[i] = vkl->handle(); + } + + std::vector pushRanges(desc.pushConstantRanges.size()); + for (usize i = 0; i < desc.pushConstantRanges.size(); ++i) { + pushRanges[i] = {}; + pushRanges[i].stageFlags = toVkShaderStageFlags(desc.pushConstantRanges[i].stages); + pushRanges[i].offset = desc.pushConstantRanges[i].offset; + pushRanges[i].size = desc.pushConstantRanges[i].size; + } + + VkPipelineLayoutCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + ci.setLayoutCount = static_cast(setLayouts.size()); + ci.pSetLayouts = setLayouts.data(); + ci.pushConstantRangeCount = static_cast(pushRanges.size()); + ci.pPushConstantRanges = pushRanges.data(); + + if (vkCreatePipelineLayout(device, &ci, nullptr, &m_layout) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_layout != VK_NULL_HANDLE) { vkDestroyPipelineLayout(device, m_layout, nullptr); m_layout = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkPipelineLayout handle() const { return m_layout; } + +private: + VkPipelineLayout m_layout = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQuerySet.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQuerySet.cppm new file mode 100644 index 00000000..d6180eb5 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQuerySet.cppm @@ -0,0 +1,55 @@ +/// Vulkan implementation of QuerySet. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:query_set; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkQuerySetImpl : public QuerySet { +public: + Status init(VkDevice device, const QuerySetDesc& d) { + type = d.type; + count = d.count; + + VkQueryPoolCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + ci.queryCount = d.count; + + switch (d.type) { + case QueryType::Timestamp: ci.queryType = VK_QUERY_TYPE_TIMESTAMP; break; + case QueryType::Occlusion: ci.queryType = VK_QUERY_TYPE_OCCLUSION; break; + case QueryType::PipelineStatistics: + ci.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; + ci.pipelineStatistics = + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_VERTICES_BIT | + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_PRIMITIVES_BIT | + VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT; + break; + } + + if (vkCreateQueryPool(device, &ci, nullptr, &m_pool) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_pool != VK_NULL_HANDLE) { vkDestroyQueryPool(device, m_pool, nullptr); m_pool = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkQueryPool handle() const { return m_pool; } + +private: + VkQueryPool m_pool = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQueue.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQueue.cppm new file mode 100644 index 00000000..079a73e3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkQueue.cppm @@ -0,0 +1,77 @@ +/// Vulkan implementation of Queue. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:queue; + +import core.stdtypes; +import core.status; +import rhi; +import :command_buffer; +import :fence; +import :transfer_batch; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkDeviceImpl; // forward + +class VkQueueImpl : public Queue { +public: + VkQueueImpl(VkQueue queue, QueueType type, u32 familyIndex, f32 tsPeriod, VkDeviceImpl* device, VkDevice vkDevice, VkPhysicalDevice physDevice) + : m_queue(queue), m_familyIndex(familyIndex), m_tsPeriod(tsPeriod), m_device(device), m_vkDevice(vkDevice), m_physDevice(physDevice) + { queueType = type; } + + // ---- Queue interface ---- + + void submit(std::span cmdBufs) override { + if (cmdBufs.size() == 0) return; + std::vector bufs(cmdBufs.size()); + for (usize i = 0; i < cmdBufs.size(); ++i) + bufs[i] = static_cast(cmdBufs[i])->handle(); + + VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + si.commandBufferCount = static_cast(bufs.size()); + si.pCommandBuffers = bufs.data(); + vkQueueSubmit(m_queue, 1, &si, VK_NULL_HANDLE); + } + + void submit(std::span cmdBufs, Fence* signalFence, u64 signalValue) override; + + void submit(std::span cmdBufs, + std::span waitFences, std::span waitValues, + Fence* signalFence, u64 signalValue) override; + + + void waitIdle() override { vkQueueWaitIdle(m_queue); } + + Status createTransferBatch(TransferBatch*& out) override { + out = new VkTransferBatchImpl(m_vkDevice, m_queue, m_familyIndex, m_physDevice); + return ErrorCode::Ok; + } + void destroyTransferBatch(TransferBatch*& batch) override { + if (batch) { static_cast(batch)->destroy(); delete batch; batch = nullptr; } + } + + f32 timestampPeriod() const override { return m_tsPeriod; } + + // ---- Internal ---- + [[nodiscard]] VkQueue handle() const { return m_queue; } + [[nodiscard]] u32 familyIndex() const { return m_familyIndex; } + +private: + VkQueue m_queue = VK_NULL_HANDLE; + u32 m_familyIndex = 0; + f32 m_tsPeriod = 0.0f; + VkDeviceImpl* m_device = nullptr; + VkDevice m_vkDevice = VK_NULL_HANDLE; + VkPhysicalDevice m_physDevice = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRayTracingPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRayTracingPipeline.cppm new file mode 100644 index 00000000..21b6c67e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRayTracingPipeline.cppm @@ -0,0 +1,106 @@ +/// Vulkan implementation of RayTracingPipeline. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:ray_tracing_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :shader_module; +import :pipeline_layout; +import :pipeline_cache; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkRayTracingPipelineImpl : public RayTracingPipeline { +public: + Status init(VkDevice device, const RayTracingPipelineDesc& desc) { + auto* vkLayout = static_cast(desc.layout); + if (!vkLayout) return ErrorCode::Unknown; + layout = desc.layout; + m_layout = vkLayout; + + // Shader stages. + std::vector stages(desc.stages.size()); + std::vector entryStrings(desc.stages.size()); + for (usize i = 0; i < desc.stages.size(); ++i) { + entryStrings[i] = std::u8string(desc.stages[i].entryPoint); + auto* mod = static_cast(desc.stages[i].module); + if (!mod) return ErrorCode::Unknown; + stages[i] = {}; + stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[i].module = mod->handle(); + stages[i].pName = reinterpret_cast(entryStrings[i].c_str()); + // Map stage from desc. + auto s = desc.stages[i].stage; + if (s == ShaderStage::RayGen) stages[i].stage = VK_SHADER_STAGE_RAYGEN_BIT_KHR; + else if (s == ShaderStage::Miss) stages[i].stage = VK_SHADER_STAGE_MISS_BIT_KHR; + else if (s == ShaderStage::ClosestHit) stages[i].stage = VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR; + else if (s == ShaderStage::AnyHit) stages[i].stage = VK_SHADER_STAGE_ANY_HIT_BIT_KHR; + else if (s == ShaderStage::Intersection) stages[i].stage = VK_SHADER_STAGE_INTERSECTION_BIT_KHR; + else if (s == ShaderStage::Callable) stages[i].stage = VK_SHADER_STAGE_CALLABLE_BIT_KHR; + else stages[i].stage = VK_SHADER_STAGE_RAYGEN_BIT_KHR; + } + + // Shader groups. + std::vector groups(desc.groups.size()); + for (usize i = 0; i < desc.groups.size(); ++i) { + const auto& g = desc.groups[i]; + groups[i] = {}; + groups[i].sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR; + switch (g.type) { + case RayTracingShaderGroup::Type::General: + groups[i].type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR; break; + case RayTracingShaderGroup::Type::TrianglesHitGroup: + groups[i].type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR; break; + case RayTracingShaderGroup::Type::ProceduralHitGroup: + groups[i].type = VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR; break; + } + groups[i].generalShader = g.generalShaderIndex; + groups[i].closestHitShader = g.closestHitShaderIndex; + groups[i].anyHitShader = g.anyHitShaderIndex; + groups[i].intersectionShader = g.intersectionShaderIndex; + } + + VkRayTracingPipelineCreateInfoKHR ci{}; + ci.sType = VK_STRUCTURE_TYPE_RAY_TRACING_PIPELINE_CREATE_INFO_KHR; + ci.stageCount = static_cast(stages.size()); + ci.pStages = stages.data(); + ci.groupCount = static_cast(groups.size()); + ci.pGroups = groups.data(); + ci.maxPipelineRayRecursionDepth = desc.maxRecursionDepth; + ci.layout = vkLayout->handle(); + + auto pfn = reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCreateRayTracingPipelinesKHR")); + if (!pfn) return ErrorCode::Unknown; + + VkPipelineCache cacheHandle = VK_NULL_HANDLE; + if (desc.cache) cacheHandle = static_cast(desc.cache)->handle(); + + if (pfn(device, VK_NULL_HANDLE, cacheHandle, 1, &ci, nullptr, &m_pipeline) != VK_SUCCESS) + return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, m_pipeline, nullptr); m_pipeline = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkPipeline handle() const { return m_pipeline; } + [[nodiscard]] VkPipelineLayoutImpl* vkLayout() const { return m_layout; } + +private: + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderBundleEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderBundleEncoder.cppm new file mode 100644 index 00000000..3bf4627a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderBundleEncoder.cppm @@ -0,0 +1,119 @@ +/// Vulkan implementation of RenderBundleEncoder + RenderBundle. +/// +/// A render bundle is a SECONDARY VkCommandBuffer recorded with +/// VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT and dynamic-rendering inheritance info +/// (the attachment formats it is compatible with). It is replayed into a primary pass - +/// begun with VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT - via vkCmdExecuteCommands +/// (RenderPassEncoder::ExecuteBundles). Because bundles carry no pass-level dynamic state and +/// Vulkan secondaries don't inherit it, the encoder records a full-target viewport + scissor up +/// front (from the desc extent). The draw-recording methods mirror VkRenderPassEncoderImpl. + +module; + +#include "VkIncludes.h" +#include + +export module rhi.vk:render_bundle_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :bind_group; +import :render_pipeline; +import :pipeline_layout; + +using namespace draco; + +export namespace draco::rhi::vk { + +// An immutable, replayable secondary command buffer. Valid until the owning pool resets. +class VkRenderBundleImpl : public RenderBundle { +public: + explicit VkRenderBundleImpl(VkCommandBuffer cmdBuf) : m_cmdBuf(cmdBuf) {} + [[nodiscard]] VkCommandBuffer handle() const noexcept { return m_cmdBuf; } +private: + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; +}; + +// Records draws into a secondary command buffer. The owning command pool allocates/recycles the +// secondary handle and frees this wrapper (+ the produced bundle) on reset. +class VkRenderBundleEncoderImpl : public RenderBundleEncoder { +public: + explicit VkRenderBundleEncoderImpl(VkCommandBuffer cmdBuf) : m_cmdBuf(cmdBuf) {} + ~VkRenderBundleEncoderImpl() override { delete m_bundle; } // frees the produced bundle wrapper + + void setPipeline(RenderPipeline* pipeline) override { + m_currentPipeline = static_cast(pipeline); + if (m_currentPipeline) + vkCmdBindPipeline(m_cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_currentPipeline->handle()); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + auto* bg = static_cast(group); + auto* layout = m_currentPipeline ? m_currentPipeline->vkLayout() : nullptr; + if (!bg || !layout) return; + VkDescriptorSet set = bg->handle(); + vkCmdBindDescriptorSets(m_cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, layout->handle(), + index, 1, &set, static_cast(dynOffsets.size()), dynOffsets.data()); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + auto* layout = m_currentPipeline ? m_currentPipeline->vkLayout() : nullptr; + if (!layout) return; + vkCmdPushConstants(m_cmdBuf, layout->handle(), toVkShaderStageFlags(stages), offset, size, data); + } + + void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset) override { + auto* vkBuf = static_cast(buffer); + if (!vkBuf) return; + VkBuffer handle = vkBuf->handle(); + vkCmdBindVertexBuffers(m_cmdBuf, slot, 1, &handle, &offset); + } + + void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset) override { + auto* vkBuf = static_cast(buffer); + if (!vkBuf) return; + vkCmdBindIndexBuffer(m_cmdBuf, vkBuf->handle(), offset, toVkIndexType(format)); + } + + void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) override { + vkCmdDraw(m_cmdBuf, vertexCount, instanceCount, firstVertex, firstInstance); + } + + void drawIndexed(u32 indexCount, u32 instanceCount, u32 firstIndex, i32 baseVertex, u32 firstInstance) override { + vkCmdDrawIndexed(m_cmdBuf, indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + } + + void drawIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override { + auto* vkBuf = static_cast(buf); + if (!vkBuf) return; + vkCmdDrawIndirect(m_cmdBuf, vkBuf->handle(), offset, drawCount, stride > 0 ? stride : 16); + } + + void drawIndexedIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override { + auto* vkBuf = static_cast(buf); + if (!vkBuf) return; + vkCmdDrawIndexedIndirect(m_cmdBuf, vkBuf->handle(), offset, drawCount, stride > 0 ? stride : 20); + } + + // End recording; the produced bundle (owned by the pool) wraps the secondary buffer. + RenderBundle* finish() override { + if (m_finished) { return m_bundle; } + m_finished = true; + vkEndCommandBuffer(m_cmdBuf); + m_bundle = new VkRenderBundleImpl(m_cmdBuf); + return m_bundle; + } + + [[nodiscard]] VkRenderBundleImpl* producedBundle() const noexcept { return m_bundle; } + +private: + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; + VkRenderPipelineImpl* m_currentPipeline = nullptr; + VkRenderBundleImpl* m_bundle = nullptr; + bool m_finished = false; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPassEncoder.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPassEncoder.cppm new file mode 100644 index 00000000..bccdfd00 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPassEncoder.cppm @@ -0,0 +1,162 @@ +/// Vulkan implementation of RenderPassEncoder + MeshShaderPassExt. + +module; + +#include "VkIncludes.h" +#include +#include + +export module rhi.vk:render_pass_encoder; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :bind_group; +import :render_pipeline; +import :compute_pipeline; +import :pipeline_layout; +import :query_set; +import :mesh_pipeline; +import :render_bundle_encoder; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkRenderPassEncoderImpl : public RenderPassEncoder, public MeshShaderPassExt { +public: + MeshShaderPassExt* asMeshShaderExt() noexcept override { return this; } + VkRenderPassEncoderImpl(VkCommandBuffer cmdBuf, VkDevice device) + : m_cmdBuf(cmdBuf), m_device(device) {} + + // ---- RenderPassEncoder ---- + + void setPipeline(RenderPipeline* pipeline) override { + m_currentPipeline = static_cast(pipeline); + m_currentMeshPipeline = nullptr; + if (m_currentPipeline) + vkCmdBindPipeline(m_cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_currentPipeline->handle()); + } + + void setBindGroup(u32 index, BindGroup* group, std::span dynOffsets) override { + auto* bg = static_cast(group); + auto* layout = getCurrentLayout(); + if (!bg || !layout) return; + VkDescriptorSet set = bg->handle(); + vkCmdBindDescriptorSets(m_cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, layout->handle(), + index, 1, &set, static_cast(dynOffsets.size()), dynOffsets.data()); + } + + void setPushConstants(ShaderStage stages, u32 offset, u32 size, const void* data) override { + auto* layout = getCurrentLayout(); + if (!layout) return; + vkCmdPushConstants(m_cmdBuf, layout->handle(), toVkShaderStageFlags(stages), offset, size, data); + } + + void setVertexBuffer(u32 slot, Buffer* buffer, u64 offset) override { + auto* vkBuf = static_cast(buffer); + if (!vkBuf) return; + VkBuffer handle = vkBuf->handle(); + vkCmdBindVertexBuffers(m_cmdBuf, slot, 1, &handle, &offset); + } + + void setIndexBuffer(Buffer* buffer, IndexFormat format, u64 offset) override { + auto* vkBuf = static_cast(buffer); + if (!vkBuf) return; + vkCmdBindIndexBuffer(m_cmdBuf, vkBuf->handle(), offset, toVkIndexType(format)); + } + + void setViewport(f32 x, f32 y, f32 w, f32 h, f32 minDepth, f32 maxDepth) override { + // Flip Y via negative height to match DX12 coordinate system. + VkViewport vp{}; vp.x = x; vp.y = y + h; vp.width = w; vp.height = -h; + vp.minDepth = minDepth; vp.maxDepth = maxDepth; + vkCmdSetViewport(m_cmdBuf, 0, 1, &vp); + } + + void setScissor(i32 x, i32 y, u32 w, u32 h) override { + VkRect2D sc{}; sc.offset = {x, y}; sc.extent = {w, h}; + vkCmdSetScissor(m_cmdBuf, 0, 1, &sc); + } + + void setBlendConstant(f32 r, f32 g, f32 b, f32 a) override { + f32 c[4] = {r, g, b, a}; + vkCmdSetBlendConstants(m_cmdBuf, c); + } + + void setStencilReference(u32 ref) override { + vkCmdSetStencilReference(m_cmdBuf, VK_STENCIL_FACE_FRONT_AND_BACK, ref); + } + + void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) override { + vkCmdDraw(m_cmdBuf, vertexCount, instanceCount, firstVertex, firstInstance); + } + + void drawIndexed(u32 indexCount, u32 instanceCount, u32 firstIndex, i32 baseVertex, u32 firstInstance) override { + vkCmdDrawIndexed(m_cmdBuf, indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + } + + void drawIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override { + auto* vkBuf = static_cast(buf); + if (!vkBuf) return; + vkCmdDrawIndirect(m_cmdBuf, vkBuf->handle(), offset, drawCount, stride > 0 ? stride : 16); + } + + void drawIndexedIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override { + auto* vkBuf = static_cast(buf); + if (!vkBuf) return; + vkCmdDrawIndexedIndirect(m_cmdBuf, vkBuf->handle(), offset, drawCount, stride > 0 ? stride : 20); + } + + void writeTimestamp(QuerySet* qs, u32 index) override { + auto* q = static_cast(qs); + if (q) vkCmdWriteTimestamp(m_cmdBuf, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, q->handle(), index); + } + + void beginOcclusionQuery(QuerySet* qs, u32 index) override { + auto* q = static_cast(qs); + if (q) vkCmdBeginQuery(m_cmdBuf, q->handle(), index, 0); + } + + void endOcclusionQuery(QuerySet* qs, u32 index) override { + auto* q = static_cast(qs); + if (q) vkCmdEndQuery(m_cmdBuf, q->handle(), index); + } + + void executeBundles(std::span bundles) override { + if (bundles.empty()) return; + std::vector secs(bundles.size()); + for (usize i = 0; i < bundles.size(); ++i) + secs[i] = static_cast(bundles[i])->handle(); + vkCmdExecuteCommands(m_cmdBuf, static_cast(secs.size()), secs.data()); + } + + void end() override { + vkCmdEndRendering(m_cmdBuf); + m_currentPipeline = nullptr; + m_currentMeshPipeline = nullptr; + } + + // ---- MeshShaderPassExt ---- + + void setMeshPipeline(MeshPipeline* pipeline) override; + void drawMeshTasks(u32 gx, u32 gy, u32 gz) override; + void drawMeshTasksIndirect(Buffer* buf, u64 offset, u32 drawCount, u32 stride) override; + void drawMeshTasksIndirectCount(Buffer* buf, u64 offset, Buffer* countBuf, u64 countOffset, u32 maxDrawCount, u32 stride) override; + +private: + VkPipelineLayoutImpl* getCurrentLayout(); + + VkCommandBuffer m_cmdBuf = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + VkRenderPipelineImpl* m_currentPipeline = nullptr; + VkMeshPipelineImpl* m_currentMeshPipeline = nullptr; + + // Cached device-level mesh shader function pointers. + PFN_vkCmdDrawMeshTasksEXT m_pfnDrawMesh = nullptr; + PFN_vkCmdDrawMeshTasksIndirectEXT m_pfnDrawMeshIndirect = nullptr; + PFN_vkCmdDrawMeshTasksIndirectCountEXT m_pfnDrawMeshIndCount = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPipeline.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPipeline.cppm new file mode 100644 index 00000000..9df538e2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkRenderPipeline.cppm @@ -0,0 +1,222 @@ +/// Vulkan implementation of RenderPipeline. + +module; + +#include "VkIncludes.h" +#include +#include +#include + + +export module rhi.vk:render_pipeline; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :shader_module; +import :pipeline_layout; +import :pipeline_cache; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkRenderPipelineImpl : public RenderPipeline { +public: + Status init(VkDevice device, const RenderPipelineDesc& desc) { + auto* vkLayout = static_cast(desc.layout); + if (!vkLayout) return ErrorCode::Unknown; + layout = desc.layout; + m_layout = vkLayout; + + // Shader stages. + std::vector stages; + std::u8string vsEntry = std::u8string(desc.vertex.shader.entryPoint); + std::u8string fsEntry; + + auto* vsMod = static_cast(desc.vertex.shader.module); + if (!vsMod) return ErrorCode::Unknown; + VkPipelineShaderStageCreateInfo vsStage{}; + vsStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + vsStage.stage = VK_SHADER_STAGE_VERTEX_BIT; + vsStage.module = vsMod->handle(); + vsStage.pName = reinterpret_cast(vsEntry.c_str()); + stages.push_back(vsStage); + + if (desc.fragment.has_value()) { + fsEntry = std::u8string(desc.fragment->shader.entryPoint); + auto* fsMod = static_cast(desc.fragment->shader.module); + if (fsMod) { + VkPipelineShaderStageCreateInfo fsStage{}; + fsStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + fsStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + fsStage.module = fsMod->handle(); + fsStage.pName = reinterpret_cast(fsEntry.c_str()); + stages.push_back(fsStage); + } + } + + // Vertex input. + std::vector vertBindings; + std::vector vertAttribs; + for (usize i = 0; i < desc.vertex.buffers.size(); ++i) { + const auto& buf = desc.vertex.buffers[i]; + VkVertexInputBindingDescription b{}; + b.binding = static_cast(i); + b.stride = buf.stride; + b.inputRate = buf.stepMode == VertexStepMode::Instance ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX; + vertBindings.push_back(b); + for (usize j = 0; j < buf.attributes.size(); ++j) { + VkVertexInputAttributeDescription a{}; + a.location = buf.attributes[j].shaderLocation; + a.binding = static_cast(i); + a.format = toVkVertexFormat(buf.attributes[j].format); + a.offset = buf.attributes[j].offset; + vertAttribs.push_back(a); + } + } + VkPipelineVertexInputStateCreateInfo vertexInput{}; + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInput.vertexBindingDescriptionCount = static_cast(vertBindings.size()); + vertexInput.pVertexBindingDescriptions = vertBindings.data(); + vertexInput.vertexAttributeDescriptionCount = static_cast(vertAttribs.size()); + vertexInput.pVertexAttributeDescriptions = vertAttribs.data(); + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = toVkTopology(desc.primitive.topology); + + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo rasterization{}; + rasterization.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterization.depthClampEnable = desc.primitive.depthClipEnabled ? VK_FALSE : VK_TRUE; + rasterization.polygonMode = toVkPolygonMode(desc.primitive.fillMode); + rasterization.cullMode = toVkCullMode(desc.primitive.cullMode); + rasterization.frontFace = toVkFrontFace(desc.primitive.frontFace); + rasterization.lineWidth = 1.0f; + if (desc.depthStencil.has_value()) { + rasterization.depthBiasEnable = (desc.depthStencil->depthBias != 0 || desc.depthStencil->depthBiasSlopeScale != 0) ? VK_TRUE : VK_FALSE; + rasterization.depthBiasConstantFactor = static_cast(desc.depthStencil->depthBias); + rasterization.depthBiasSlopeFactor = desc.depthStencil->depthBiasSlopeScale; + rasterization.depthBiasClamp = desc.depthStencil->depthBiasClamp; + } + + VkPipelineMultisampleStateCreateInfo multisample{}; + multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = toVkSampleCount(desc.multisample.count); + multisample.alphaToCoverageEnable = desc.multisample.alphaToCoverageEnabled ? VK_TRUE : VK_FALSE; + u32 sampleMask = desc.multisample.mask; + multisample.pSampleMask = &sampleMask; + + // Color blend. + auto colorTargets = desc.fragment.has_value() ? desc.fragment->targets : std::span(); + std::vector blendAttachments(colorTargets.size()); + for (usize i = 0; i < colorTargets.size(); ++i) { + auto& ba = blendAttachments[i]; + ba = {}; + ba.colorWriteMask = toVkColorWriteMask(colorTargets[i].writeMask); + if (colorTargets[i].blend.has_value()) { + ba.blendEnable = VK_TRUE; + ba.srcColorBlendFactor = toVkBlendFactor(colorTargets[i].blend->color.srcFactor); + ba.dstColorBlendFactor = toVkBlendFactor(colorTargets[i].blend->color.dstFactor); + ba.colorBlendOp = toVkBlendOp(colorTargets[i].blend->color.operation); + ba.srcAlphaBlendFactor = toVkBlendFactor(colorTargets[i].blend->alpha.srcFactor); + ba.dstAlphaBlendFactor = toVkBlendFactor(colorTargets[i].blend->alpha.dstFactor); + ba.alphaBlendOp = toVkBlendOp(colorTargets[i].blend->alpha.operation); + } + } + VkPipelineColorBlendStateCreateInfo colorBlend{}; + colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + colorBlend.attachmentCount = static_cast(blendAttachments.size()); + colorBlend.pAttachments = blendAttachments.data(); + + // Depth/stencil. + VkPipelineDepthStencilStateCreateInfo depthStencil{}; + depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + if (desc.depthStencil.has_value()) { + const auto& ds = *desc.depthStencil; + depthStencil.depthTestEnable = ds.depthTestEnabled ? VK_TRUE : VK_FALSE; + depthStencil.depthWriteEnable = ds.depthWriteEnabled ? VK_TRUE : VK_FALSE; + depthStencil.depthCompareOp = toVkCompareOp(ds.depthCompare); + depthStencil.stencilTestEnable = ds.stencilEnabled ? VK_TRUE : VK_FALSE; + depthStencil.front.failOp = toVkStencilOp(ds.stencilFront.failOp); + depthStencil.front.passOp = toVkStencilOp(ds.stencilFront.passOp); + depthStencil.front.depthFailOp = toVkStencilOp(ds.stencilFront.depthFailOp); + depthStencil.front.compareOp = toVkCompareOp(ds.stencilFront.compare); + depthStencil.front.compareMask = ds.stencilReadMask; + depthStencil.front.writeMask = ds.stencilWriteMask; + depthStencil.back.failOp = toVkStencilOp(ds.stencilBack.failOp); + depthStencil.back.passOp = toVkStencilOp(ds.stencilBack.passOp); + depthStencil.back.depthFailOp = toVkStencilOp(ds.stencilBack.depthFailOp); + depthStencil.back.compareOp = toVkCompareOp(ds.stencilBack.compare); + depthStencil.back.compareMask = ds.stencilReadMask; + depthStencil.back.writeMask = ds.stencilWriteMask; + } + + // Dynamic state. + VkDynamicState dynStates[] = { + VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_BLEND_CONSTANTS, VK_DYNAMIC_STATE_STENCIL_REFERENCE + }; + VkPipelineDynamicStateCreateInfo dynState{}; + dynState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynState.dynamicStateCount = 4; + dynState.pDynamicStates = dynStates; + + // Dynamic rendering (Vulkan 1.3). + std::vector colorFormats(colorTargets.size()); + for (usize i = 0; i < colorTargets.size(); ++i) + colorFormats[i] = toVkFormat(colorTargets[i].format); + + VkPipelineRenderingCreateInfo renderingInfo{}; + renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + renderingInfo.colorAttachmentCount = static_cast(colorFormats.size()); + renderingInfo.pColorAttachmentFormats = colorFormats.data(); + if (desc.depthStencil.has_value()) { + VkFormat dsf = toVkFormat(desc.depthStencil->format); + if (hasDepth(desc.depthStencil->format)) renderingInfo.depthAttachmentFormat = dsf; + if (hasStencil(desc.depthStencil->format)) renderingInfo.stencilAttachmentFormat = dsf; + } + + // Create pipeline. + VkGraphicsPipelineCreateInfo pi{}; + pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pi.pNext = &renderingInfo; + pi.stageCount = static_cast(stages.size()); + pi.pStages = stages.data(); + pi.pVertexInputState = &vertexInput; + pi.pInputAssemblyState = &inputAssembly; + pi.pViewportState = &viewportState; + pi.pRasterizationState = &rasterization; + pi.pMultisampleState = &multisample; + pi.pDepthStencilState = desc.depthStencil.has_value() ? &depthStencil : nullptr; + pi.pColorBlendState = &colorBlend; + pi.pDynamicState = &dynState; + pi.layout = vkLayout->handle(); + + VkPipelineCache cacheHandle = VK_NULL_HANDLE; + if (desc.cache) cacheHandle = static_cast(desc.cache)->handle(); + + if (vkCreateGraphicsPipelines(device, cacheHandle, 1, &pi, nullptr, &m_pipeline) != VK_SUCCESS) + return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(device, m_pipeline, nullptr); m_pipeline = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkPipeline handle() const { return m_pipeline; } + [[nodiscard]] VkPipelineLayoutImpl* vkLayout() const { return m_layout; } + +private: + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayoutImpl* m_layout = nullptr; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSampler.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSampler.cppm new file mode 100644 index 00000000..96132d56 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSampler.cppm @@ -0,0 +1,58 @@ +/// Vulkan implementation of Sampler. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:sampler; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkSamplerImpl : public Sampler { +public: + Status init(VkDevice device, const SamplerDesc& d) { + desc = d; + + VkSamplerCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + ci.magFilter = toVkFilter(d.magFilter); + ci.minFilter = toVkFilter(d.minFilter); + ci.mipmapMode = toVkMipmapMode(d.mipmapFilter); + ci.addressModeU = toVkAddressMode(d.addressU); + ci.addressModeV = toVkAddressMode(d.addressV); + ci.addressModeW = toVkAddressMode(d.addressW); + ci.mipLodBias = d.mipLodBias; + ci.anisotropyEnable = d.maxAnisotropy > 1 ? VK_TRUE : VK_FALSE; + ci.maxAnisotropy = static_cast(d.maxAnisotropy); + ci.minLod = d.minLod; + ci.maxLod = d.maxLod; + ci.borderColor = toVkBorderColor(d.borderColor); + ci.unnormalizedCoordinates = VK_FALSE; + + if (d.compare.has_value()) { + ci.compareEnable = VK_TRUE; + ci.compareOp = toVkCompareOp(d.compare.value()); + } + + if (vkCreateSampler(device, &ci, nullptr, &m_sampler) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_sampler != VK_NULL_HANDLE) { vkDestroySampler(device, m_sampler, nullptr); m_sampler = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkSampler handle() const { return m_sampler; } + +private: + VkSampler m_sampler = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkShaderModule.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkShaderModule.cppm new file mode 100644 index 00000000..c1474371 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkShaderModule.cppm @@ -0,0 +1,39 @@ +/// Vulkan implementation of ShaderModule. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:shader_module; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkShaderModuleImpl : public ShaderModule { +public: + Status init(VkDevice device, const ShaderModuleDesc& d) { + VkShaderModuleCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + ci.codeSize = d.code.size(); + ci.pCode = reinterpret_cast(d.code.data()); + + if (vkCreateShaderModule(device, &ci, nullptr, &m_module) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_module != VK_NULL_HANDLE) { vkDestroyShaderModule(device, m_module, nullptr); m_module = VK_NULL_HANDLE; } + } + + [[nodiscard]] VkShaderModule handle() const { return m_module; } + +private: + VkShaderModule m_module = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSurface.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSurface.cppm new file mode 100644 index 00000000..8eb9307c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSurface.cppm @@ -0,0 +1,37 @@ +/// Vulkan implementation of Surface. +/// Wraps VkSurfaceKHR + parent VkInstance for cleanup. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:surface; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkSurfaceImpl : public Surface { +public: + VkSurfaceImpl(VkSurfaceKHR surface, VkInstance instance) + : m_surface(surface), m_instance(instance) {} + + [[nodiscard]] VkSurfaceKHR handle() const { return m_surface; } + + void destroy() { + if (m_surface != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(m_instance, m_surface, nullptr); + m_surface = VK_NULL_HANDLE; + } + } + +private: + VkSurfaceKHR m_surface = VK_NULL_HANDLE; + VkInstance m_instance = VK_NULL_HANDLE; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSwapChain.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSwapChain.cppm new file mode 100644 index 00000000..be27a6af --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkSwapChain.cppm @@ -0,0 +1,264 @@ +/// Vulkan implementation of SwapChain. + +module; + +#include "VkIncludes.h" +#include +#include +#include + + +export module rhi.vk:swap_chain; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :adapter; +import :surface; +import :texture; +import :texture_view; + +using namespace draco; + +export namespace draco::rhi::vk { + +// Minimal reverse format mapping for swap chain format negotiation. +inline TextureFormat fromVkFormat(VkFormat f) { + switch (f) { + case VK_FORMAT_R8G8B8A8_UNORM: return TextureFormat::RGBA8Unorm; + case VK_FORMAT_R8G8B8A8_SRGB: return TextureFormat::RGBA8UnormSrgb; + case VK_FORMAT_B8G8R8A8_UNORM: return TextureFormat::BGRA8Unorm; + case VK_FORMAT_B8G8R8A8_SRGB: return TextureFormat::BGRA8UnormSrgb; + case VK_FORMAT_R16G16B16A16_SFLOAT: return TextureFormat::RGBA16Float; + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return TextureFormat::RGB10A2Unorm; + default: return TextureFormat::Undefined; + } +} + +class VkDeviceImpl; // forward + +class VkSwapChainImpl : public SwapChain { +public: + Status init(VkDevice device, VkPhysicalDevice physDevice, VkSurfaceKHR surface, + const SwapChainDesc& desc, VkDeviceImpl* owner) { + m_device = device; + m_physDevice = physDevice; + m_surface = surface; + m_owner = owner; + m_presentMode= desc.presentMode; + return createSwapChain(desc.width, desc.height, desc.format, desc.bufferCount, VK_NULL_HANDLE); + } + + // ---- SwapChain interface ---- + TextureFormat format() const override { return m_format; } + u32 width() const override { return m_width; } + u32 height() const override { return m_height; } + u32 bufferCount() const override { return m_bufferCount; } + u32 currentImageIndex() const override { return m_currentImageIndex; } + + Status acquireNextImage() override; + Texture* currentTexture() override { return m_currentImageIndex < m_textures.size() ? m_textures[m_currentImageIndex] : nullptr; } + TextureView* currentTextureView() override { return m_currentImageIndex < m_views.size() ? m_views[m_currentImageIndex] : nullptr; } + Status present(Queue* queue) override; + Status resize(u32 w, u32 h) override; + + void cleanup(); + + // ---- Internal ---- + [[nodiscard]] VkSwapchainKHR handle() const { return m_swapchain; } + + // Semaphore accessors for queue submit integration. + VkSemaphore currentAcquireSemaphore() const { return m_acquireSems[m_frameIndex]; } + VkSemaphore currentPresentSemaphore() const { return m_presentSems[m_currentImageIndex]; } + +private: + Status createSwapChain(u32 w, u32 h, TextureFormat reqFormat, u32 reqCount, VkSwapchainKHR old); + Status retrieveImages(VkFormat format); + void createSyncObjects(); + void cleanupImages(); + void destroySyncObjects(); + + VkSurfaceFormatKHR chooseSurfaceFormat(TextureFormat requested); + VkPresentModeKHR choosePresentMode(PresentMode requested); + + VkDevice m_device = VK_NULL_HANDLE; + VkPhysicalDevice m_physDevice = VK_NULL_HANDLE; + VkSurfaceKHR m_surface = VK_NULL_HANDLE; + VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; + VkDeviceImpl* m_owner = nullptr; + + TextureFormat m_format = TextureFormat::Undefined; + PresentMode m_presentMode = PresentMode::Fifo; + u32 m_width = 0, m_height = 0, m_bufferCount = 0; + u32 m_currentImageIndex = 0; + u32 m_frameIndex = 0; + + std::vector m_textures; + std::vector m_views; + std::vector m_acquireSems; + std::vector m_presentSems; +}; + +// ---- Implementation (inline in module) ---- + +inline Status VkSwapChainImpl::createSwapChain(u32 w, u32 h, TextureFormat reqFormat, u32 reqCount, VkSwapchainKHR old) { + VkSurfaceCapabilitiesKHR caps{}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physDevice, m_surface, &caps); + + if (caps.currentExtent.width != ~0u) { m_width = caps.currentExtent.width; m_height = caps.currentExtent.height; } + else { m_width = std::clamp(w, caps.minImageExtent.width, caps.maxImageExtent.width); + m_height = std::clamp(h, caps.minImageExtent.height, caps.maxImageExtent.height); } + if (m_width == 0 || m_height == 0) return ErrorCode::Unknown; + + auto presentMode = choosePresentMode(m_presentMode); + + // Mailbox needs a 3rd image to race ahead of the display without stalling; 2 is fine for + // FIFO/Immediate. + u32 wantCount = reqCount; + if (presentMode == VK_PRESENT_MODE_MAILBOX_KHR && wantCount < 3) { wantCount = 3; } + m_bufferCount = std::max(wantCount, caps.minImageCount); + if (caps.maxImageCount > 0) m_bufferCount = std::min(m_bufferCount, caps.maxImageCount); + + auto surfFmt = chooseSurfaceFormat(reqFormat); + m_format = fromVkFormat(surfFmt.format); + if (m_format == TextureFormat::Undefined) m_format = reqFormat; + + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + if (caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + VkCompositeAlphaFlagBitsKHR compositeAlpha = (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) + ? VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR : VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + + VkSwapchainCreateInfoKHR ci{}; + ci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + ci.surface = m_surface; + ci.minImageCount = m_bufferCount; + ci.imageFormat = surfFmt.format; + ci.imageColorSpace = surfFmt.colorSpace; + ci.imageExtent = { m_width, m_height }; + ci.imageArrayLayers = 1; + ci.imageUsage = usage; + ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + ci.preTransform = caps.currentTransform; + ci.compositeAlpha = compositeAlpha; + ci.presentMode = presentMode; + ci.clipped = VK_TRUE; + ci.oldSwapchain = old; + + if (vkCreateSwapchainKHR(m_device, &ci, nullptr, &m_swapchain) != VK_SUCCESS) return ErrorCode::Unknown; + if (old != VK_NULL_HANDLE) vkDestroySwapchainKHR(m_device, old, nullptr); + + if (retrieveImages(surfFmt.format) != ErrorCode::Ok) return ErrorCode::Unknown; + createSyncObjects(); + m_frameIndex = 0; + return ErrorCode::Ok; +} + +inline VkSurfaceFormatKHR VkSwapChainImpl::chooseSurfaceFormat(TextureFormat requested) { + u32 count = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(m_physDevice, m_surface, &count, nullptr); + std::vector fmts(count); + vkGetPhysicalDeviceSurfaceFormatsKHR(m_physDevice, m_surface, &count, fmts.data()); + + VkFormat desired = toVkFormat(requested); + for (auto& f : fmts) if (f.format == desired) return f; + for (auto& f : fmts) if (f.format == VK_FORMAT_B8G8R8A8_SRGB && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) return f; + for (auto& f : fmts) if (f.format == VK_FORMAT_B8G8R8A8_UNORM) return f; + return fmts[0]; +} + +inline VkPresentModeKHR VkSwapChainImpl::choosePresentMode(PresentMode requested) { + u32 count = 0; + vkGetPhysicalDeviceSurfacePresentModesKHR(m_physDevice, m_surface, &count, nullptr); + std::vector modes(count); + vkGetPhysicalDeviceSurfacePresentModesKHR(m_physDevice, m_surface, &count, modes.data()); + auto has = [&](VkPresentModeKHR m) { for (auto x : modes) if (x == m) return true; return false; }; + + const VkPresentModeKHR desired = toVkPresentMode(requested); + if (has(desired)) { return desired; } + + // Requested mode unavailable: preserve the request's INTENT rather than dropping straight to + // vsync. Immediate and Mailbox both mean "don't block on the refresh" - so fall back to whichever + // uncapped mode the surface does expose (Wayland commonly offers Mailbox but NOT Immediate) before + // settling for FIFO. FIFO is the only mode guaranteed present by the spec. + VkPresentModeKHR chosen = VK_PRESENT_MODE_FIFO_KHR; + if (requested == PresentMode::Immediate || requested == PresentMode::Mailbox) { + if (has(VK_PRESENT_MODE_MAILBOX_KHR)) { chosen = VK_PRESENT_MODE_MAILBOX_KHR; } + else if (has(VK_PRESENT_MODE_IMMEDIATE_KHR)) { chosen = VK_PRESENT_MODE_IMMEDIATE_KHR; } + } else if (requested == PresentMode::FifoRelaxed && has(VK_PRESENT_MODE_FIFO_RELAXED_KHR)) { + chosen = VK_PRESENT_MODE_FIFO_RELAXED_KHR; + } + + logWarningf("[swapchain] requested present mode (vk %u) unavailable; using vk %u", + static_cast(desired), static_cast(chosen)); + return chosen; +} + +inline Status VkSwapChainImpl::retrieveImages(VkFormat format) { + u32 imgCount = 0; + vkGetSwapchainImagesKHR(m_device, m_swapchain, &imgCount, nullptr); + std::vector images(imgCount); + vkGetSwapchainImagesKHR(m_device, m_swapchain, &imgCount, images.data()); + m_bufferCount = imgCount; + + TextureFormat texFmt = fromVkFormat(format); + if (texFmt == TextureFormat::Undefined) texFmt = m_format; + + for (u32 i = 0; i < imgCount; ++i) { + TextureDesc td{}; td.dimension = TextureDimension::Texture2D; td.format = texFmt; + td.width = m_width; td.height = m_height; td.arrayLayerCount = 1; td.mipLevelCount = 1; + td.sampleCount = 1; td.usage = TextureUsage::RenderTarget; + + auto* tex = new VkTextureImpl(); + tex->initFromExisting(images[i], td); + m_textures.push_back(tex); + + TextureViewDesc vd{}; vd.format = texFmt; vd.dimension = TextureViewDimension::Texture2D; + vd.mipLevelCount = 1; vd.arrayLayerCount = 1; + auto* view = new VkTextureViewImpl(); + if (view->init(m_device, tex, vd) != ErrorCode::Ok) { delete view; return ErrorCode::Unknown; } + m_views.push_back(view); + } + return ErrorCode::Ok; +} + +inline void VkSwapChainImpl::createSyncObjects() { + VkSemaphoreCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + for (u32 i = 0; i < m_bufferCount; ++i) { + VkSemaphore a = VK_NULL_HANDLE, p = VK_NULL_HANDLE; + vkCreateSemaphore(m_device, &ci, nullptr, &a); + vkCreateSemaphore(m_device, &ci, nullptr, &p); + m_acquireSems.push_back(a); + m_presentSems.push_back(p); + } +} + +inline void VkSwapChainImpl::cleanupImages() { + for (auto* v : m_views) { v->cleanup(m_device); delete v; } m_views.clear(); + for (auto* t : m_textures) { t->cleanup(m_device); delete t; } m_textures.clear(); +} + +inline void VkSwapChainImpl::destroySyncObjects() { + for (auto s : m_acquireSems) { vkDestroySemaphore(m_device, s, nullptr); } + m_acquireSems.clear(); + for (auto s : m_presentSems) { vkDestroySemaphore(m_device, s, nullptr); } + m_presentSems.clear(); +} + +inline Status VkSwapChainImpl::resize(u32 w, u32 h) { + vkDeviceWaitIdle(m_device); + cleanupImages(); + destroySyncObjects(); + return createSwapChain(w, h, m_format, m_bufferCount, m_swapchain); +} + +inline void VkSwapChainImpl::cleanup() { + vkDeviceWaitIdle(m_device); + cleanupImages(); + destroySyncObjects(); + if (m_swapchain != VK_NULL_HANDLE) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; } +} + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTexture.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTexture.cppm new file mode 100644 index 00000000..f8227da3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTexture.cppm @@ -0,0 +1,141 @@ +/// Vulkan implementation of Texture. + +module; + +#include "VkIncludes.h" +#include +#include + + +export module rhi.vk:texture; + +import core.stdtypes; +import core.status; +import rhi; +import :adapter; +import :conversions; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkTextureImpl : public Texture { +public: + /// Initialize from a TextureDesc (creates VkImage + allocates memory). + Status init(VkDevice device, VkAdapterImpl* adapter, const TextureDesc& d) { + desc = d; + + VkImageCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + ci.imageType = toVkImageType(d.dimension); + ci.format = toVkFormat(d.format); + ci.extent = { d.width, d.height, d.depth }; + ci.mipLevels = d.mipLevelCount; + ci.arrayLayers = d.arrayLayerCount; + ci.samples = toVkSampleCount(d.sampleCount); + ci.tiling = VK_IMAGE_TILING_OPTIMAL; + ci.usage = toVkImageUsage(d.usage); + ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + if (d.arrayLayerCount >= 6 && d.dimension == TextureDimension::Texture2D) + ci.flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + + if (vkCreateImage(device, &ci, nullptr, &m_image) != VK_SUCCESS) return ErrorCode::Unknown; + + VkMemoryRequirements memReqs{}; + vkGetImageMemoryRequirements(device, m_image, &memReqs); + + i32 memType = adapter->findMemoryType( + static_cast(memReqs.memoryTypeBits), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (memType < 0) { + vkDestroyImage(device, m_image, nullptr); m_image = VK_NULL_HANDLE; + return ErrorCode::Unknown; + } + + VkMemoryAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.allocationSize = memReqs.size; + ai.memoryTypeIndex = static_cast(memType); + + if (vkAllocateMemory(device, &ai, nullptr, &m_memory) != VK_SUCCESS) { + vkDestroyImage(device, m_image, nullptr); m_image = VK_NULL_HANDLE; + return ErrorCode::Unknown; + } + + vkBindImageMemory(device, m_image, m_memory, 0); + return ErrorCode::Ok; + } + + /// Initialize from an existing VkImage (e.g. swap chain). Does not own the image. + void initFromExisting(VkImage image, const TextureDesc& d) { + m_image = image; + desc = d; + m_ownsImage = false; + } + + void cleanup(VkDevice device) { + if (m_memory != VK_NULL_HANDLE) { vkFreeMemory(device, m_memory, nullptr); m_memory = VK_NULL_HANDLE; } + if (m_ownsImage && m_image != VK_NULL_HANDLE) vkDestroyImage(device, m_image, nullptr); + m_image = VK_NULL_HANDLE; + m_subresourceLayouts.clear(); + } + + // ---- Internal ---- + [[nodiscard]] VkImage handle() const { return m_image; } + [[nodiscard]] VkFormat vkFormat() const { return toVkFormat(desc.format); } + + /// Whole-resource layout (uniform fast path). + VkImageLayout currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + /// Get layout for a specific subresource. + VkImageLayout getSubresourceLayout(u32 mip, u32 layer) const { + if (m_subresourceLayouts.empty()) return currentLayout; + u32 idx = mip + layer * desc.mipLevelCount; + if (idx >= static_cast(m_subresourceLayouts.size())) return currentLayout; + return m_subresourceLayouts[idx]; + } + + /// Update layout for a subresource range. Promotes to per-subresource + /// tracking when needed, collapses back to uniform when all match. + void setSubresourceLayout(u32 baseMip, u32 mipCount, u32 baseLayer, u32 layerCount, + VkImageLayout layout) { + u32 totalMips = desc.mipLevelCount; + u32 totalLayers = std::max(desc.arrayLayerCount, 1u); + u32 mipEnd = (mipCount == ~0u) ? totalMips : std::min(baseMip + mipCount, totalMips); + u32 layerEnd = (layerCount == ~0u) ? totalLayers : std::min(baseLayer + layerCount, totalLayers); + + // All subresources? Collapse to uniform. + if (baseMip == 0 && mipEnd >= totalMips && baseLayer == 0 && layerEnd >= totalLayers) { + currentLayout = layout; + m_subresourceLayouts.clear(); + return; + } + + // Promote to per-subresource. + if (m_subresourceLayouts.empty()) { + if (layout == currentLayout) return; + m_subresourceLayouts.resize(totalMips * totalLayers, currentLayout); + } + + for (u32 l = baseLayer; l < layerEnd; ++l) + for (u32 m = baseMip; m < mipEnd; ++m) + m_subresourceLayouts[m + l * totalMips] = layout; + + // Try to collapse back to uniform. + VkImageLayout first = m_subresourceLayouts[0]; + for (usize i = 1; i < m_subresourceLayouts.size(); ++i) { + if (m_subresourceLayouts[i] != first) return; + } + currentLayout = first; + m_subresourceLayouts.clear(); + } + +private: + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_memory = VK_NULL_HANDLE; + bool m_ownsImage = true; + std::vector m_subresourceLayouts; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTextureView.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTextureView.cppm new file mode 100644 index 00000000..794c936c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTextureView.cppm @@ -0,0 +1,77 @@ +/// Vulkan implementation of TextureView. + +module; + +#include "VkIncludes.h" + +export module rhi.vk:texture_view; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :texture; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkTextureViewImpl : public TextureView { +public: + Status init(VkDevice device, VkTextureImpl* tex, const TextureViewDesc& d) { + desc = d; + texture = tex; + // Dimensions are the view's BASE MIP extent, not the texture's mip-0 size - a view onto mip N + // is half-sized per level. Render-pass renderArea/framebuffer is derived from these, so a stale + // mip-0 size here overruns a mip>0 attachment (GPU fault). Shadows only ever target mip 0; the + // IBL prefilter is the first mip>0 render target to exercise this. + m_width = tex->desc.width >> d.baseMipLevel; if (m_width == 0) { m_width = 1; } + m_height = tex->desc.height >> d.baseMipLevel; if (m_height == 0) { m_height = 1; } + + TextureFormat fmt = (d.format == TextureFormat::Undefined) ? tex->desc.format : d.format; + m_format = fmt; + + VkImageViewCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + ci.image = tex->handle(); + ci.viewType = toVkImageViewType(d.dimension); + ci.format = toVkFormat(fmt); + ci.components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, + VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY }; + + u32 mipCount = d.mipLevelCount > 0 ? d.mipLevelCount : tex->desc.mipLevelCount - d.baseMipLevel; + u32 layerCount = d.arrayLayerCount > 0 ? d.arrayLayerCount : tex->desc.arrayLayerCount - d.baseArrayLayer; + + VkImageAspectFlags aspect; + switch (d.aspect) { + case TextureAspect::DepthOnly: aspect = VK_IMAGE_ASPECT_DEPTH_BIT; break; + case TextureAspect::StencilOnly: aspect = VK_IMAGE_ASPECT_STENCIL_BIT; break; + default: aspect = getAspectMask(fmt); break; + } + + ci.subresourceRange = { aspect, d.baseMipLevel, mipCount, d.baseArrayLayer, layerCount }; + + if (vkCreateImageView(device, &ci, nullptr, &m_imageView) != VK_SUCCESS) return ErrorCode::Unknown; + return ErrorCode::Ok; + } + + void cleanup(VkDevice device) { + if (m_imageView != VK_NULL_HANDLE) { + vkDestroyImageView(device, m_imageView, nullptr); + m_imageView = VK_NULL_HANDLE; + } + } + + [[nodiscard]] VkImageView handle() const { return m_imageView; } + [[nodiscard]] TextureFormat format() const { return m_format; } + [[nodiscard]] u32 width() const { return m_width; } + [[nodiscard]] u32 height() const { return m_height; } + +private: + VkImageView m_imageView = VK_NULL_HANDLE; + TextureFormat m_format = TextureFormat::Undefined; + u32 m_width = 0; + u32 m_height = 0; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTransferBatch.cppm b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTransferBatch.cppm new file mode 100644 index 00000000..f2d5df93 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VkTransferBatch.cppm @@ -0,0 +1,225 @@ +/// Vulkan implementation of TransferBatch. + +module; + +#include "VkIncludes.h" +#include +#include +#include + +#include + +export module rhi.vk:transfer_batch; + +import core.stdtypes; +import core.status; +import rhi; +import :conversions; +import :buffer; +import :texture; +import :fence; + +using namespace draco; + +export namespace draco::rhi::vk { + +class VkTransferBatchImpl : public TransferBatch { +public: + VkTransferBatchImpl(VkDevice device, VkQueue queue, u32 queueFamilyIndex, VkPhysicalDevice physDevice) + : m_device(device), m_physDevice(physDevice), m_queue(queue), m_queueFamilyIndex(queueFamilyIndex) {} + + void writeBuffer(Buffer* dst, u64 dstOffset, std::span data) override { + u64 needed = m_stagingOffset + data.size(); + if (ensureStagingBuffer(needed) != ErrorCode::Ok) return; + void* mapped = m_stagingMapped; + if (!mapped) return; + std::memcpy(static_cast(mapped) + m_stagingOffset, data.data(), data.size()); + m_bufferCopies.push_back({ dst, dstOffset, m_stagingOffset, data.size() }); + m_stagingOffset = (m_stagingOffset + data.size() + 15) & ~static_cast(15); + } + + void writeTexture(Texture* dst, std::span data, + const TextureDataLayout& layout, Extent3D extent, + u32 mipLevel, u32 arrayLayer) override { + u64 needed = m_stagingOffset + data.size(); + if (ensureStagingBuffer(needed) != ErrorCode::Ok) return; + void* mapped = m_stagingMapped; + if (!mapped) return; + std::memcpy(static_cast(mapped) + m_stagingOffset, data.data(), data.size()); + m_textureCopies.push_back({ dst, m_stagingOffset, mipLevel, arrayLayer, extent, layout }); + m_stagingOffset = (m_stagingOffset + data.size() + 15) & ~static_cast(15); + } + + Status submit() override { + if (m_bufferCopies.empty() && m_textureCopies.empty()) return ErrorCode::Ok; + VkCommandBuffer cmdBuf = recordCommands(); + if (cmdBuf == VK_NULL_HANDLE) return ErrorCode::Unknown; + + VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + si.commandBufferCount = 1; si.pCommandBuffers = &cmdBuf; + if (vkQueueSubmit(m_queue, 1, &si, VK_NULL_HANDLE) != VK_SUCCESS) return ErrorCode::Unknown; + vkQueueWaitIdle(m_queue); + cleanupCmdPool(); + return ErrorCode::Ok; + } + + Status submitAsync(Fence* fence, u64 signalValue) override { + if (m_bufferCopies.empty() && m_textureCopies.empty()) return ErrorCode::Ok; + VkCommandBuffer cmdBuf = recordCommands(); + if (cmdBuf == VK_NULL_HANDLE) return ErrorCode::Unknown; + + auto* vkFence = static_cast(fence); + if (!vkFence) return ErrorCode::Unknown; + + VkSemaphore sem = vkFence->handle(); + VkTimelineSemaphoreSubmitInfo tsi{}; tsi.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO; + tsi.signalSemaphoreValueCount = 1; tsi.pSignalSemaphoreValues = &signalValue; + + VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.pNext = &tsi; + si.commandBufferCount = 1; si.pCommandBuffers = &cmdBuf; + si.signalSemaphoreCount = 1; si.pSignalSemaphores = &sem; + + vkQueueSubmit(m_queue, 1, &si, VK_NULL_HANDLE); + m_asyncFence = vkFence; m_asyncValue = signalValue; + return ErrorCode::Ok; + } + + void reset() override { + m_bufferCopies.clear(); m_textureCopies.clear(); + m_stagingOffset = 0; + cleanupCmdPool(); + } + + void destroy() override { + if (m_asyncFence) { m_asyncFence->wait(m_asyncValue, ~0ull); m_asyncFence = nullptr; } + if (m_cmdPool != VK_NULL_HANDLE) { vkDestroyCommandPool(m_device, m_cmdPool, nullptr); m_cmdPool = VK_NULL_HANDLE; } + if (m_stagingMapped) { vkUnmapMemory(m_device, m_stagingMem); m_stagingMapped = nullptr; } + if (m_stagingMem != VK_NULL_HANDLE) { vkFreeMemory(m_device, m_stagingMem, nullptr); m_stagingMem = VK_NULL_HANDLE; } + if (m_stagingBuf != VK_NULL_HANDLE) { vkDestroyBuffer(m_device, m_stagingBuf, nullptr); m_stagingBuf = VK_NULL_HANDLE; } + } + +private: + struct BufCopy { Buffer* dst; u64 dstOffset; u64 stagingOffset; u64 size; }; + struct TexCopy { Texture* dst; u64 stagingOffset; u32 mipLevel; u32 arrayLayer; Extent3D extent; TextureDataLayout layout; }; + + Status ensureStagingBuffer(u64 required) { + if (m_stagingBuf != VK_NULL_HANDLE && m_stagingSize >= required) return ErrorCode::Ok; + u64 newSize = std::max(required, std::max(m_stagingSize * 2, static_cast(4 * 1024 * 1024))); + + // Create staging buffer directly. + VkBufferCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + ci.size = newSize; ci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VkBuffer newBuf = VK_NULL_HANDLE; + if (vkCreateBuffer(m_device, &ci, nullptr, &newBuf) != VK_SUCCESS) return ErrorCode::Unknown; + + VkMemoryRequirements memReqs{}; vkGetBufferMemoryRequirements(m_device, newBuf, &memReqs); + + // Find host-visible + host-coherent memory type. + VkPhysicalDeviceMemoryProperties memProps{}; + vkGetPhysicalDeviceMemoryProperties(m_physDevice, &memProps); + i32 memType = -1; + for (u32 i = 0; i < memProps.memoryTypeCount; ++i) { + if ((memReqs.memoryTypeBits & (1 << i)) && + (memProps.memoryTypes[i].propertyFlags & (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) + == (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) + { memType = static_cast(i); break; } + } + if (memType < 0) { vkDestroyBuffer(m_device, newBuf, nullptr); return ErrorCode::Unknown; } + + VkMemoryAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + ai.allocationSize = memReqs.size; ai.memoryTypeIndex = static_cast(memType); + VkDeviceMemory newMem = VK_NULL_HANDLE; + if (vkAllocateMemory(m_device, &ai, nullptr, &newMem) != VK_SUCCESS) { + vkDestroyBuffer(m_device, newBuf, nullptr); return ErrorCode::Unknown; + } + vkBindBufferMemory(m_device, newBuf, newMem, 0); + + void* newMapped = nullptr; + vkMapMemory(m_device, newMem, 0, newSize, 0, &newMapped); + + // Copy existing data. + if (m_stagingMapped && m_stagingOffset > 0 && newMapped) + std::memcpy(newMapped, m_stagingMapped, static_cast(m_stagingOffset)); + + // Free old. + if (m_stagingMapped) vkUnmapMemory(m_device, m_stagingMem); + if (m_stagingMem != VK_NULL_HANDLE) vkFreeMemory(m_device, m_stagingMem, nullptr); + if (m_stagingBuf != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_stagingBuf, nullptr); + + m_stagingBuf = newBuf; m_stagingMem = newMem; m_stagingMapped = newMapped; m_stagingSize = newSize; + return ErrorCode::Ok; + } + + VkCommandBuffer recordCommands() { + if (m_cmdPool == VK_NULL_HANDLE) { + VkCommandPoolCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + ci.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; ci.queueFamilyIndex = m_queueFamilyIndex; + if (vkCreateCommandPool(m_device, &ci, nullptr, &m_cmdPool) != VK_SUCCESS) return VK_NULL_HANDLE; + } + VkCommandBufferAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ai.commandPool = m_cmdPool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; + VkCommandBuffer cb = VK_NULL_HANDLE; + if (vkAllocateCommandBuffers(m_device, &ai, &cb) != VK_SUCCESS) return VK_NULL_HANDLE; + + VkCommandBufferBeginInfo bi{}; bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &bi); + + for (const auto& c : m_bufferCopies) { + auto* dst = static_cast(c.dst); + if (!dst) continue; + VkBufferCopy r{}; r.srcOffset = c.stagingOffset; r.dstOffset = c.dstOffset; r.size = c.size; + vkCmdCopyBuffer(cb, m_stagingBuf, dst->handle(), 1, &r); + } + + for (const auto& c : m_textureCopies) { + auto* dst = static_cast(c.dst); + if (!dst) continue; + auto aspect = getAspectMask(dst->desc.format); + + VkImageMemoryBarrier pre{}; pre.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + pre.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + pre.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; pre.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + pre.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; pre.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + pre.image = dst->handle(); + pre.subresourceRange = { aspect, c.mipLevel, 1, c.arrayLayer, 1 }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &pre); + + VkBufferImageCopy r{}; r.bufferOffset = c.stagingOffset; + r.imageSubresource = { aspect, c.mipLevel, c.arrayLayer, 1 }; + r.imageExtent = { c.extent.width, c.extent.height, c.extent.depth }; + vkCmdCopyBufferToImage(cb, m_stagingBuf, dst->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r); + + VkImageMemoryBarrier post = pre; + post.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; post.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + post.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; post.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &post); + } + + vkEndCommandBuffer(cb); + return cb; + } + + void cleanupCmdPool() { + if (m_cmdPool != VK_NULL_HANDLE) vkResetCommandPool(m_device, m_cmdPool, 0); + } + + VkDevice m_device = VK_NULL_HANDLE; + VkPhysicalDevice m_physDevice = VK_NULL_HANDLE; + VkQueue m_queue = VK_NULL_HANDLE; + u32 m_queueFamilyIndex = 0; + VkCommandPool m_cmdPool = VK_NULL_HANDLE; + VkBuffer m_stagingBuf = VK_NULL_HANDLE; + VkDeviceMemory m_stagingMem = VK_NULL_HANDLE; + void* m_stagingMapped = nullptr; + u64 m_stagingOffset = 0; + u64 m_stagingSize = 0; + std::vector m_bufferCopies; + std::vector m_textureCopies; + VkFenceImpl* m_asyncFence = nullptr; + u64 m_asyncValue = 0; +}; + +} // namespace draco::rhi::vk diff --git a/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VulkanRhi.test.cpp b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VulkanRhi.test.cpp new file mode 100644 index 00000000..91b787c1 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RHI/Vulkan/VulkanRhi.test.cpp @@ -0,0 +1,40 @@ +#include + +import core; +import rhi; +import rhi.vk; + +using namespace draco; +using namespace draco::rhi; + +// These tests require a working Vulkan loader + at least one ICD (a physical +// device or a software rasterizer such as lavapipe). Where no device is +// available the backend still initializes; adapter-dependent checks are guarded. + +TEST_CASE("rhi.vk: backend initializes and enumerates adapters") +{ + Backend* backend = nullptr; + vk::VkBackendDesc desc{}; + REQUIRE(vk::createBackend(desc, backend).isOk()); + REQUIRE(backend != nullptr); + CHECK(backend->isInitialized); + + auto adapters = backend->enumerateAdapters(); + INFO("adapter count: ", adapters.size()); + + for (Adapter* a : adapters) { + const AdapterInfo info = a->info(); + CHECK(!info.name.empty()); + } + + if (!adapters.empty()) { + // Adapters are ordered best-first; a device should be creatable from [0]. + Device* device = nullptr; + CHECK(adapters[0]->createDevice(DeviceDesc{}, device).isOk()); + CHECK(device != nullptr); + // The device must be torn down before the backend destroys the instance. + if (device) device->destroy(); + } + + backend->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/RHI/macros.h b/Engine/cpp/Runtime/Rendering/RHI/macros.h deleted file mode 100644 index d055a9cb..00000000 --- a/Engine/cpp/Runtime/Rendering/RHI/macros.h +++ /dev/null @@ -1,29 +0,0 @@ -// rendering/rhi/macros.h - -#pragma once -#include -#include - -#ifndef DRACO_RHI_VALIDATION -#define DRACO_RHI_VALIDATION 1 -#endif - -#if DRACO_RHI_VALIDATION - #define RHI_ASSERT(cond, msg, ...) \ - do { \ - if (!(cond)) { \ - std::println("[RHI ERROR] " msg, ##__VA_ARGS__); \ - std::abort(); \ - } \ - } while(0) - - #define RHI_WARN(cond, msg, ...) \ - do { \ - if (!(cond)) { \ - std::println("[RHI WARNING] " msg, ##__VA_ARGS__); \ - } \ - } while(0) -#else - #define RHI_ASSERT(cond, msg, ...) do { (void)(cond); } while(0) - #define RHI_WARN(cond, msg, ...) do { (void)(cond); } while(0) -#endif diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierSolver.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierSolver.cppm new file mode 100644 index 00000000..e306a108 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierSolver.cppm @@ -0,0 +1,376 @@ +// Draconic::RenderGraph - :barrier_solver partition +// +// Computes and emits resource barriers between passes. State is tracked at two +// levels: per-resource-handle (buffers; convenience for textures) and per- +// Texture with per-subresource granularity (source of truth for textures, keyed +// by the GPU texture pointer so the same texture unifies across handles). Ported +// from Sedulous.RenderGraph (BarrierSolver.bf). + +module; + +#include + +#include +#include +#include + +export module rendergraph:barrier_solver; + +import core; +import rhi; +import :types; +import :resource; +import :pass; +import :state_tracker; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class BarrierSolver + { + public: + BarrierSolver() = default; + ~BarrierSolver() { clearTrackers(); } + + BarrierSolver(const BarrierSolver&) = delete; + BarrierSolver& operator=(const BarrierSolver&) = delete; + + // Initialize states from the resource list (call once per frame). Persistent + // resources use their last-known state; transient resources start Undefined. + void reset(std::span resources) + { + m_resourceStates.clear(); + clearTrackers(); + + for (i32 i = 0; i < static_cast(resources.size()); ++i) + { + RenderGraphResource* res = resources[static_cast(i)]; + if (res == nullptr) { continue; } + + rhi::ResourceState initialState = rhi::ResourceState::Undefined; + + if (res->lifetime == RGResourceLifetime::Persistent && res->persistentData.get() != nullptr) + { + initialState = res->persistentData->firstFrame + ? (res->texture != nullptr ? res->texture->initialState : rhi::ResourceState::Undefined) + : res->persistentData->lastKnownState; + } + else if (res->lifetime == RGResourceLifetime::Imported) + { + initialState = res->lastKnownState; + } + else + { + // Transient: always start Undefined; the backend uses the texture's + // actual tracked state for the "before" side, so first access always + // gets a correct transition. + initialState = rhi::ResourceState::Undefined; + } + + m_resourceStates.insert_or_assign(i, initialState); + + if (res->resourceType == RGResourceType::Texture && res->texture != nullptr) + { + if (SubresourceStateTracker** existing = mapFind(m_textureStates, res->texture)) + { + // Same GPU texture via another handle - unify. + SubresourceStateTracker* tracker = *existing; + if (initialState == rhi::ResourceState::Undefined && tracker->isUniform() + && tracker->uniformState() != rhi::ResourceState::Undefined) + { + m_resourceStates.insert_or_assign(i, tracker->uniformState()); + } + else if (initialState != rhi::ResourceState::Undefined) + { + tracker->setAll(initialState); + } + } + else + { + const u32 mipCount = res->texture->desc.mipLevelCount; + const u32 layerCount = res->texture->desc.arrayLayerCount; + SubresourceStateTracker* tracker = + new SubresourceStateTracker(mipCount, layerCount, initialState); + + if (res->lifetime == RGResourceLifetime::Persistent && res->persistentData.get() != nullptr + && !res->persistentData->firstFrame + && !res->persistentData->subresourceStates.empty()) + { + tracker->initFromStates(res->persistentData->subresourceStates, initialState); + } + + m_textureStates.insert_or_assign(res->texture, tracker); + } + } + } + } + + // Emit barriers needed before executing `pass`. + void emitBarriers(const RenderGraphPass& pass, std::span resources, + rhi::CommandEncoder& encoder) + { + m_textureBarriers.clear(); + m_bufferBarriers.clear(); + + for (const RGResourceAccess& access : pass.accesses) + { + if (!access.handle.isValid()) { continue; } + const i32 resIdx = static_cast(access.handle.index); + if (static_cast(resIdx) >= resources.size()) { continue; } + + RenderGraphResource* res = resources[static_cast(resIdx)]; + if (res == nullptr) { continue; } + + const rhi::ResourceState requiredState = access.toResourceState(); + const bool accessIsReadWrite = isRead(access.type) && isWrite(access.type); + + if (res->resourceType == RGResourceType::Texture && res->texture != nullptr) + { + SubresourceStateTracker** found = mapFind(m_textureStates, res->texture); + if (found == nullptr) { continue; } + SubresourceStateTracker* tracker = *found; + + emitTextureBarriers(*tracker, res->texture, access.subresource, requiredState, accessIsReadWrite); + tracker->setState(access.subresource, requiredState); + m_resourceStates.insert_or_assign(resIdx, requiredState); + } + else if (res->resourceType == RGResourceType::Buffer && res->buffer != nullptr) + { + rhi::ResourceState currentState = rhi::ResourceState::Undefined; + if (rhi::ResourceState* p = mapFind(m_resourceStates, resIdx)) { currentState = *p; } + if (currentState == requiredState) { continue; } + + rhi::BufferBarrier bb{}; + bb.buffer = res->buffer; + bb.oldState = currentState; + bb.newState = requiredState; + m_bufferBarriers.push_back(bb); + + m_resourceStates.insert_or_assign(resIdx, requiredState); + } + } + + flushBarriers(encoder); + } + + // After a pass executes: transition resources marked ReadableAfterWrite + // (written by the pass) to ShaderRead so external bind groups can sample them. + void emitReadableAfterWriteBarriers(const RenderGraphPass& pass, + std::span resources, + rhi::CommandEncoder& encoder) + { + m_textureBarriers.clear(); + m_bufferBarriers.clear(); + + for (const RGResourceAccess& access : pass.accesses) + { + if (!access.isWrite() || !access.handle.isValid()) { continue; } + const i32 resIdx = static_cast(access.handle.index); + if (static_cast(resIdx) >= resources.size()) { continue; } + + RenderGraphResource* res = resources[static_cast(resIdx)]; + if (res == nullptr || !res->readableAfterWrite) { continue; } + if (res->resourceType != RGResourceType::Texture || res->texture == nullptr) { continue; } + + SubresourceStateTracker** found = mapFind(m_textureStates, res->texture); + if (found == nullptr) { continue; } + SubresourceStateTracker* tracker = *found; + + emitTextureBarriers(*tracker, res->texture, access.subresource, rhi::ResourceState::ShaderRead, false); + tracker->setState(access.subresource, rhi::ResourceState::ShaderRead); + m_resourceStates.insert_or_assign(resIdx, rhi::ResourceState::ShaderRead); + } + + flushBarriers(encoder); + } + + // Transition imported resources to their requested final state. + void emitFinalTransitions(std::span resources, rhi::CommandEncoder& encoder) + { + m_textureBarriers.clear(); + m_bufferBarriers.clear(); + + for (i32 i = 0; i < static_cast(resources.size()); ++i) + { + RenderGraphResource* res = resources[static_cast(i)]; + if (res == nullptr || !res->finalState.has_value()) { continue; } + + const rhi::ResourceState finalState = res->finalState.value(); + if (res->texture != nullptr) + { + SubresourceStateTracker** found = mapFind(m_textureStates, res->texture); + if (found == nullptr) { continue; } + SubresourceStateTracker* tracker = *found; + + emitTextureBarriers(*tracker, res->texture, RGSubresourceRange::all(), finalState, false); + tracker->setAll(finalState); + m_resourceStates.insert_or_assign(i, finalState); + } + } + + flushBarriers(encoder); + } + + // Write tracked states back into persistent/imported resources for next frame. + void updatePersistentStates(std::span resources) + { + for (i32 i = 0; i < static_cast(resources.size()); ++i) + { + RenderGraphResource* res = resources[static_cast(i)]; + if (res == nullptr) { continue; } + + if (res->resourceType == RGResourceType::Texture && res->texture != nullptr) + { + SubresourceStateTracker** found = mapFind(m_textureStates, res->texture); + if (found == nullptr) { continue; } + SubresourceStateTracker* tracker = *found; + + if (tracker->isUniform()) + { + res->lastKnownState = tracker->uniformState(); + if (res->persistentData.get() != nullptr) + { + res->persistentData->lastKnownState = tracker->uniformState(); + res->persistentData->firstFrame = false; + res->persistentData->subresourceStates.clear(); + } + } + else + { + res->lastKnownState = tracker->getState(0, 0); + if (res->persistentData.get() != nullptr) + { + res->persistentData->lastKnownState = tracker->getState(0, 0); + res->persistentData->firstFrame = false; + res->persistentData->subresourceStates = tracker->copyStates(); + } + } + } + else if (rhi::ResourceState* p = mapFind(m_resourceStates, i)) + { + res->lastKnownState = *p; + if (res->persistentData.get() != nullptr) + { + res->persistentData->lastKnownState = *p; + res->persistentData->firstFrame = false; + } + } + } + } + + [[nodiscard]] rhi::ResourceState getState(i32 resourceIndex) + { + if (rhi::ResourceState* p = mapFind(m_resourceStates, resourceIndex)) { return *p; } + return rhi::ResourceState::Undefined; + } + + [[nodiscard]] rhi::ResourceState getTextureState(rhi::Texture* texture) + { + if (texture == nullptr) { return rhi::ResourceState::Undefined; } + if (SubresourceStateTracker** found = mapFind(m_textureStates, texture)) + { + SubresourceStateTracker* tracker = *found; + return tracker->isUniform() ? tracker->uniformState() : tracker->getState(0, 0); + } + return rhi::ResourceState::Undefined; + } + + [[nodiscard]] SubresourceStateTracker* getTextureTracker(rhi::Texture* texture) + { + if (texture == nullptr) { return nullptr; } + SubresourceStateTracker** found = mapFind(m_textureStates, texture); + return found != nullptr ? *found : nullptr; + } + + private: + void emitTextureBarriers(SubresourceStateTracker& tracker, rhi::Texture* texture, + RGSubresourceRange subresource, rhi::ResourceState requiredState, + bool accessIsReadWrite) + { + const u32 totalMips = tracker.mipCount(); + const u32 totalLayers = tracker.layerCount(); + + if (tracker.isUniform()) + { + const rhi::ResourceState currentState = tracker.uniformState(); + if (currentState == requiredState && !accessIsReadWrite) { return; } + + rhi::TextureBarrier barrier{}; + barrier.texture = texture; + barrier.oldState = currentState; + barrier.newState = requiredState; + if (!subresource.isAll()) + { + barrier.baseMipLevel = subresource.baseMipLevel; + barrier.mipLevelCount = subresource.mipLevelCount == 0 ? 0xFFFFFFFFu : subresource.mipLevelCount; + barrier.baseArrayLayer = subresource.baseArrayLayer; + barrier.arrayLayerCount = subresource.arrayLayerCount == 0 ? 0xFFFFFFFFu : subresource.arrayLayerCount; + } + m_textureBarriers.push_back(barrier); + } + else + { + const u32 baseMip = subresource.baseMipLevel; + const u32 mipEnd = subresource.mipLevelCount == 0 ? totalMips + : std::min(baseMip + subresource.mipLevelCount, totalMips); + const u32 baseLayer = subresource.baseArrayLayer; + const u32 layerEnd = subresource.arrayLayerCount == 0 ? totalLayers + : std::min(baseLayer + subresource.arrayLayerCount, totalLayers); + + for (u32 layer = baseLayer; layer < layerEnd; ++layer) + { + for (u32 mip = baseMip; mip < mipEnd; ++mip) + { + const rhi::ResourceState currentState = tracker.getState(mip, layer); + if (currentState == requiredState && !accessIsReadWrite) { continue; } + + rhi::TextureBarrier barrier{}; + barrier.texture = texture; + barrier.oldState = currentState; + barrier.newState = requiredState; + barrier.baseMipLevel = mip; + barrier.mipLevelCount = 1; + barrier.baseArrayLayer = layer; + barrier.arrayLayerCount = 1; + m_textureBarriers.push_back(barrier); + } + } + } + } + + void flushBarriers(rhi::CommandEncoder& encoder) + { + if (m_textureBarriers.empty() && m_bufferBarriers.empty()) { return; } + + rhi::BarrierGroup group{}; + if (!m_textureBarriers.empty()) + { + group.textureBarriers = + std::span(m_textureBarriers.data(), m_textureBarriers.size()); + } + if (!m_bufferBarriers.empty()) + { + group.bufferBarriers = + std::span(m_bufferBarriers.data(), m_bufferBarriers.size()); + } + encoder.barrier(group); + } + + void clearTrackers() + { + for (auto& entry : m_textureStates) + { + delete (entry.second); + } + m_textureStates.clear(); + } + + std::unordered_map m_resourceStates; + std::unordered_map m_textureStates; + std::vector m_textureBarriers; + std::vector m_bufferBarriers; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierTests.test.cpp new file mode 100644 index 00000000..653ab7fc --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/BarrierTests.test.cpp @@ -0,0 +1,527 @@ +// BarrierSolver directly via a recording mock encoder and plain RHI textures +// (rhi::Texture is concrete, so no texture mock is needed; its pointer identity +// is what the solver keys on). + +#include +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; +using RS = rhi::ResourceState; +using AT = RGAccessType; + +namespace +{ + // Records emitted barriers; stubs the rest of the CommandEncoder interface. + class MockEncoder final : public rhi::CommandEncoder + { + public: + std::vector textureBarriers; + std::vector bufferBarriers; + + void barrier(const rhi::BarrierGroup& group) override + { + for (usize i = 0; i < group.textureBarriers.size(); ++i) { textureBarriers.push_back(group.textureBarriers[i]); } + for (usize i = 0; i < group.bufferBarriers.size(); ++i) { bufferBarriers.push_back(group.bufferBarriers[i]); } + } + + rhi::RenderPassEncoder* beginRenderPass(const rhi::RenderPassDesc&) override { return nullptr; } + rhi::ComputePassEncoder* beginComputePass(std::u8string_view) override { return nullptr; } + rhi::RenderBundleEncoder* createRenderBundleEncoder(const rhi::RenderBundleDesc&) override { return nullptr; } + void copyBufferToBuffer(rhi::Buffer*, u64, rhi::Buffer*, u64, u64) override {} + void copyBufferToTexture(rhi::Buffer*, rhi::Texture*, const rhi::BufferTextureCopyRegion&) override {} + void copyTextureToBuffer(rhi::Texture*, rhi::Buffer*, const rhi::BufferTextureCopyRegion&) override {} + void copyTextureToTexture(rhi::Texture*, rhi::Texture*, const rhi::TextureCopyRegion&) override {} + void blit(rhi::Texture*, rhi::Texture*) override {} + void generateMipmaps(rhi::Texture*) override {} + void resolveTexture(rhi::Texture*, rhi::Texture*) override {} + void resetQuerySet(rhi::QuerySet*, u32, u32) override {} + void writeTimestamp(rhi::QuerySet*, u32) override {} + void resolveQuerySet(rhi::QuerySet*, u32, u32, rhi::Buffer*, u64) override {} + void beginDebugLabel(std::u8string_view, f32, f32, f32, f32) override {} + void endDebugLabel() override {} + void insertDebugLabel(std::u8string_view, f32, f32, f32, f32) override {} + rhi::CommandBuffer* finish() override { return nullptr; } + }; + + rhi::Texture makeTexture(RS initialState, u32 mips = 1, u32 layers = 1) + { + rhi::Texture tex; + tex.desc.mipLevelCount = mips; + tex.desc.arrayLayerCount = layers; + tex.initialState = initialState; + return tex; + } + + RGResourceAccess access(u32 index, AT type, RGSubresourceRange sub = {}) + { + return RGResourceAccess{ RGHandle{ index, 0 }, type, sub }; + } +} + +TEST_CASE("barriers: same texture via two handles emits one transition") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture shadow = makeTexture(RS::Undefined); + + RenderGraphResource res0(u8"ShadowWrite", RGResourceType::Texture, RGResourceLifetime::Imported); + res0.texture = &shadow; + RenderGraphResource res1(u8"ShadowRead", RGResourceType::Texture, RGResourceLifetime::Imported); + res1.texture = &shadow; // same GPU texture + RenderGraphResource* resources[] = { &res0, &res1 }; + const std::span span(resources, 2); + + solver.reset(span); + + RenderGraphPass writePass(u8"ShadowPass", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteDepthTarget)); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"ForwardPass", RGPassType::Render); + readPass.accesses.push_back(access(1, AT::ReadTexture)); + solver.emitBarriers(readPass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::DepthStencilWrite); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); + CHECK(encoder.textureBarriers[0].texture == &shadow); +} + +TEST_CASE("barriers: single handle read-after-write") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); + + RenderGraphResource res(u8"Color", RGResourceType::Texture, RGResourceLifetime::Transient); + res.texture = &tex; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"Writer", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget)); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"Reader", RGPassType::Render); + readPass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(readPass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); +} + +TEST_CASE("barriers: compute write then render read") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); + + RenderGraphResource res(u8"Volume", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass computePass(u8"Compute", RGPassType::Compute); + computePass.accesses.push_back(access(0, AT::WriteStorage)); + solver.emitBarriers(computePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass renderPass(u8"Render", RGPassType::Render); + renderPass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(renderPass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::ShaderWrite); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); +} + +TEST_CASE("barriers: final transition uses texture-keyed state") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); + + RenderGraphResource res(u8"Backbuffer", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.finalState = RS::Present; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass pass(u8"FinalBlit", RGPassType::Render); + pass.accesses.push_back(access(0, AT::WriteColorTarget)); + solver.emitBarriers(pass, span, encoder); + encoder.textureBarriers.clear(); + + solver.emitFinalTransitions(span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].newState == RS::Present); +} + +TEST_CASE("barriers: none when already in the correct state") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); + + RenderGraphResource res(u8"Tex", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass pass(u8"Reader", RGPassType::Render); + pass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(pass, span, encoder); + + CHECK(encoder.textureBarriers.size() == 0u); +} + +TEST_CASE("barriers: per-layer writes emit individual barriers") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 4); // 4 cascades + + RenderGraphResource res(u8"ShadowArray", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass pass0(u8"Cascade0", RGPassType::Render); + pass0.accesses.push_back(access(0, AT::WriteDepthTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(pass0, span, encoder); + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::ShaderRead); + CHECK(encoder.textureBarriers[0].newState == RS::DepthStencilWrite); + CHECK(encoder.textureBarriers[0].baseArrayLayer == 0u); + CHECK(encoder.textureBarriers[0].arrayLayerCount == 1u); + encoder.textureBarriers.clear(); + + RenderGraphPass pass2(u8"Cascade2", RGPassType::Render); + pass2.accesses.push_back(access(0, AT::WriteDepthTarget, RGSubresourceRange{ 0, 1, 2, 1 })); + solver.emitBarriers(pass2, span, encoder); + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].baseArrayLayer == 2u); + CHECK(encoder.textureBarriers[0].arrayLayerCount == 1u); +} + +TEST_CASE("barriers: non-uniform whole-resource read emits per-subresource") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 2); + + RenderGraphResource res(u8"Tex", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"Writer", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"Reader", RGPassType::Render); + readPass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(readPass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); // only layer 0 transitions + CHECK(encoder.textureBarriers[0].oldState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); + CHECK(encoder.textureBarriers[0].baseArrayLayer == 0u); +} + +TEST_CASE("barriers: all layers written collapses to uniform") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 2); + + RenderGraphResource res(u8"Tex", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass p0(u8"W0", RGPassType::Render); + p0.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(p0, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass p1(u8"W1", RGPassType::Render); + p1.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 1, 1 })); + solver.emitBarriers(p1, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"Read", RGPassType::Render); + readPass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(readPass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); // whole-resource fast path + CHECK(encoder.textureBarriers[0].oldState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); + CHECK(encoder.textureBarriers[0].mipLevelCount == 0xFFFFFFFFu); + CHECK(encoder.textureBarriers[0].arrayLayerCount == 0xFFFFFFFFu); +} + +TEST_CASE("barriers: SampleDepth on a written cascade array reads as DepthStencilRead") +{ + // The real CSM scenario: each cascade writes one layer (DepthStencilWrite), then the + // forward pass samples the WHOLE array. SampleDepthStencil must transition it to + // DepthStencilRead (DEPTH_STENCIL_READ_ONLY_OPTIMAL) - the layout a depth sampler needs - + // not ShaderRead. After all layers are written uniform, the read is a whole-resource barrier. + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 4); // 4 cascades + + RenderGraphResource res(u8"ShadowArray", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::DepthStencilRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + for (u32 layer = 0; layer < 4; ++layer) + { + RenderGraphPass cascade(u8"Cascade", RGPassType::Render); + cascade.accesses.push_back(access(0, AT::WriteDepthTarget, RGSubresourceRange{ 0, 1, layer, 1 })); + solver.emitBarriers(cascade, span, encoder); + } + encoder.textureBarriers.clear(); + + RenderGraphPass forward(u8"Forward", RGPassType::Render); + forward.accesses.push_back(access(0, AT::SampleDepthStencil)); + solver.emitBarriers(forward, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); // whole-resource (all layers uniform) + CHECK(encoder.textureBarriers[0].oldState == RS::DepthStencilWrite); + CHECK(encoder.textureBarriers[0].newState == RS::DepthStencilRead); + CHECK(encoder.textureBarriers[0].arrayLayerCount == 0xFFFFFFFFu); +} + +TEST_CASE("barriers: per-mip different states") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 4, 1); + + RenderGraphResource res(u8"Tex", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::Undefined; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass p0(u8"WriteMip0", RGPassType::Render); + p0.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(p0, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass p1(u8"WriteMip1", RGPassType::Render); + p1.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 1, 1, 0, 1 })); + solver.emitBarriers(p1, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::Undefined); + CHECK(encoder.textureBarriers[0].newState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].baseMipLevel == 1u); + CHECK(encoder.textureBarriers[0].mipLevelCount == 1u); +} + +TEST_CASE("barriers: readable-after-write is subresource aware") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 4); + + RenderGraphResource res(u8"Tex", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + res.readableAfterWrite = true; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"Writer", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteDepthTarget, RGSubresourceRange{ 0, 1, 1, 1 })); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + solver.emitReadableAfterWriteBarriers(writePass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::DepthStencilWrite); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); + CHECK(encoder.textureBarriers[0].baseArrayLayer == 1u); + CHECK(encoder.textureBarriers[0].arrayLayerCount == 1u); +} + +TEST_CASE("barriers: final transition non-uniform emits per-subresource") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 2); + + RenderGraphResource res(u8"Swapchain", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::Undefined; + res.finalState = RS::Present; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"Blit", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + solver.emitFinalTransitions(span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 2u); + bool foundLayer0 = false, foundLayer1 = false; + for (usize i = 0; i < encoder.textureBarriers.size(); ++i) + { + const rhi::TextureBarrier& b = encoder.textureBarriers[i]; + if (b.baseArrayLayer == 0 && b.oldState == RS::RenderTarget && b.newState == RS::Present) { foundLayer0 = true; } + if (b.baseArrayLayer == 1 && b.oldState == RS::Undefined && b.newState == RS::Present) { foundLayer1 = true; } + } + CHECK(foundLayer0); + CHECK(foundLayer1); +} + +TEST_CASE("barriers: non-overlapping subresource access emits no false barrier") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 4); + + RenderGraphResource res(u8"Array", RGResourceType::Texture, RGResourceLifetime::Imported); + res.texture = &tex; + res.lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"WriteLayer0", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"ReadLayer2", RGPassType::Render); + readPass.accesses.push_back(access(0, AT::ReadTexture, RGSubresourceRange{ 0, 1, 2, 1 })); + solver.emitBarriers(readPass, span, encoder); + + CHECK(encoder.textureBarriers.size() == 0u); +} + +TEST_CASE("barriers: persistent resource preserves per-subresource state") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined, 1, 2); + + RenderGraphResource res(u8"Persistent", RGResourceType::Texture, RGResourceLifetime::Persistent); + res.texture = &tex; + res.persistentData = std::make_unique(&tex, static_cast(nullptr)); + res.persistentData->firstFrame = false; + res.persistentData->lastKnownState = RS::ShaderRead; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"Writer", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget, RGSubresourceRange{ 0, 1, 0, 1 })); + solver.emitBarriers(writePass, span, encoder); + + solver.updatePersistentStates(span); + + CHECK(res.persistentData->subresourceStates.size() == 2u); +} + +TEST_CASE("barriers: transient first access emits Undefined -> RenderTarget") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); + + RenderGraphResource res(u8"PipelineOutput", RGResourceType::Texture, RGResourceLifetime::Transient); + res.texture = &tex; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"ForwardOpaque", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget)); + solver.emitBarriers(writePass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::Undefined); + CHECK(encoder.textureBarriers[0].newState == RS::RenderTarget); +} + +TEST_CASE("barriers: transient reused across frames starts from Undefined") +{ + BarrierSolver solver; + MockEncoder encoder; + rhi::Texture tex = makeTexture(RS::Undefined); // same pooled texture both frames + + // Frame 1 + { + RenderGraphResource res(u8"PipelineOutput", RGResourceType::Texture, RGResourceLifetime::Transient); + res.texture = &tex; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"ForwardOpaque", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget)); + solver.emitBarriers(writePass, span, encoder); + encoder.textureBarriers.clear(); + + RenderGraphPass readPass(u8"PostProcess", RGPassType::Render); + readPass.accesses.push_back(access(0, AT::ReadTexture)); + solver.emitBarriers(readPass, span, encoder); + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::RenderTarget); + CHECK(encoder.textureBarriers[0].newState == RS::ShaderRead); + encoder.textureBarriers.clear(); + } + + // Frame 2: same texture, fresh resource - must restart from Undefined. + { + RenderGraphResource res(u8"PipelineOutput", RGResourceType::Texture, RGResourceLifetime::Transient); + res.texture = &tex; + RenderGraphResource* resources[] = { &res }; + const std::span span(resources, 1); + solver.reset(span); + + RenderGraphPass writePass(u8"ForwardOpaque", RGPassType::Render); + writePass.accesses.push_back(access(0, AT::WriteColorTarget)); + solver.emitBarriers(writePass, span, encoder); + + REQUIRE(encoder.textureBarriers.size() == 1u); + CHECK(encoder.textureBarriers[0].oldState == RS::Undefined); + CHECK(encoder.textureBarriers[0].newState == RS::RenderTarget); + } +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/BundlePassTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/BundlePassTests.test.cpp new file mode 100644 index 00000000..f49d7ba6 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/BundlePassTests.test.cpp @@ -0,0 +1,87 @@ +// Render-bundle pass: a render pass whose body is supplied by render bundles (the rendergraph +// extension that lets parallel command recording run inside the frame graph). Driven on the +// Null RHI so Execute actually runs the pass. +#include + +import core; +import rhi; +import rhi.null; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace { +struct GraphHarness { + rhi::null::NullDevice device; + rhi::Texture* tex = nullptr; + rhi::TextureView* view = nullptr; + rhi::CommandPool* pool = nullptr; + rhi::CommandEncoder* enc = nullptr; + + bool init() { + if (!device.createTexture(rhi::TextureDesc::renderTarget(rhi::TextureFormat::BGRA8Unorm, 64, 64), tex).isOk()) { return false; } + rhi::TextureViewDesc vd{}; vd.format = rhi::TextureFormat::BGRA8Unorm; + if (!device.createTextureView(tex, vd, view).isOk()) { return false; } + if (!device.createCommandPool(rhi::QueueType::Graphics, pool).isOk()) { return false; } + return pool->createEncoder(enc).isOk(); + } + ~GraphHarness() { + if (pool) { device.destroyCommandPool(pool); } + if (view) { device.destroyTextureView(view); } + if (tex) { device.destroyTexture(tex); } + } +}; +} + +TEST_CASE("rg.bundle: a bundle pass records bundles before the pass + executes them") +{ + GraphHarness h; + REQUIRE(h.init()); + + RenderGraph graph(&h.device); + graph.setOutputSize(64, 64); + graph.beginFrame(0); + const RGHandle color = graph.importTarget(u8"BB", h.tex, h.view, rhi::ResourceState::Present); + + bool ran = false; + usize bundleCount = 0; + graph.addRenderPass(u8"BundlePass", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + b.setBundleExecute([&](rhi::CommandEncoder& enc, std::vector& out) { + ran = true; // called with the encoder in the recording state, before the pass begins + rhi::RenderBundleDesc bd{}; + bd.colorFormats[0] = rhi::TextureFormat::BGRA8Unorm; bd.colorFormatCount = 1; + bd.width = 64; bd.height = 64; + if (rhi::RenderBundleEncoder* be = enc.createRenderBundleEncoder(bd)) { out.push_back(be->finish()); } + bundleCount = out.size(); + }); + }); + + CHECK(graph.execute(h.enc).isOk()); + CHECK(ran); // the bundle callback ran during execution + CHECK(bundleCount == 1u); // it produced a bundle for the graph to ExecuteBundles +} + +TEST_CASE("rg.bundle: a bundle pass that produces no bundles still runs (clears only)") +{ + GraphHarness h; + REQUIRE(h.init()); + + RenderGraph graph(&h.device); + graph.setOutputSize(64, 64); + graph.beginFrame(0); + const RGHandle color = graph.importTarget(u8"BB", h.tex, h.view, rhi::ResourceState::Present); + + bool ran = false; + graph.addRenderPass(u8"EmptyBundlePass", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + b.setBundleExecute([&](rhi::CommandEncoder&, std::vector&) { ran = true; }); + }); + + CHECK(graph.execute(h.enc).isOk()); + CHECK(ran); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/Callbacks.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/Callbacks.cppm new file mode 100644 index 00000000..fbd7545e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/Callbacks.cppm @@ -0,0 +1,32 @@ +// Draconic::RenderGraph - :callbacks partition +// +// Pass execution callbacks. Sedulous uses Beef delegates; here they are Core +// Function objects over the RHI encoder a pass records into. + +module; + +#include +#include + +export module rendergraph:callbacks; + +import core; +import rhi; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + using RenderPassExecuteCallback = std::function; + using ComputePassExecuteCallback = std::function; + using CopyPassExecuteCallback = std::function; + + // A render pass whose body is supplied by render bundles (recorded off-thread). Called with + // the command encoder in the RECORDING state (before the render pass begins, so bundles can + // be created) and an out-list to fill with the bundles to replay; the graph then begins the + // pass with secondary-command-buffer contents and ExecuteBundles them in order. This is what + // lets parallel command recording run inside the frame graph. + using RenderBundlePassCallback = std::function&)>; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/CullingTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/CullingTests.test.cpp new file mode 100644 index 00000000..b1b3673e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/CullingTests.test.cpp @@ -0,0 +1,88 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +TEST_CASE("rg.cull: unused pass is culled") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Unused", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + }); + + REQUIRE(graph.compile().isOk()); + CHECK(graph.culledPassCount() == 1u); +} + +TEST_CASE("rg.cull: NeverCull prevents culling") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Important", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + + REQUIRE(graph.compile().isOk()); + CHECK(graph.culledPassCount() == 0u); +} + +TEST_CASE("rg.cull: HasSideEffects prevents culling") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + graph.addComputePass(u8"SideEffect", [](PassBuilder& b) { b.hasSideEffects(); }); + + REQUIRE(graph.compile().isOk()); + CHECK(graph.culledPassCount() == 0u); +} + +TEST_CASE("rg.cull: backward propagation keeps dependencies alive") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle depth = graph.createTransient(u8"Depth", RGTextureDesc(rhi::TextureFormat::Depth32Float)); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"DepthPrepass", [&](PassBuilder& b) { + b.setDepthTarget(depth, rhi::LoadOp::Clear, rhi::StoreOp::Store); + }); + graph.addRenderPass(u8"ForwardOpaque", [&](PassBuilder& b) { + b.readTexture(depth); + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + + REQUIRE(graph.compile().isOk()); + CHECK(graph.culledPassCount() == 0u); // DepthPrepass kept alive by ForwardOpaque +} + +TEST_CASE("rg.cull: imported with final state prevents culling") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle backbuffer = graph.importTarget(u8"BB", nullptr, nullptr, rhi::ResourceState::Present); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Render", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + }); + graph.addRenderPass(u8"Blit", [&](PassBuilder& b) { + b.readTexture(color); + b.setColorTarget(0, backbuffer, rhi::LoadOp::Clear, rhi::StoreOp::Store); + }); + + REQUIRE(graph.compile().isOk()); + CHECK(graph.culledPassCount() == 0u); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/DebugTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/DebugTests.test.cpp new file mode 100644 index 00000000..b83aeeee --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/DebugTests.test.cpp @@ -0,0 +1,85 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace +{ + bool contains(std::u8string_view hay, std::u8string_view needle) + { + if (needle.size() > hay.size()) { return false; } + for (usize i = 0; i + needle.size() <= hay.size(); ++i) + { + if (hay.substr(i, needle.size()) == needle) { return true; } + } + return false; + } +} + +TEST_CASE("rg.debug: ExportDOT produces valid syntax") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"SceneColor", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + const RGHandle depth = graph.createTransient(u8"Depth", RGTextureDesc(rhi::TextureFormat::Depth32Float)); + + graph.addRenderPass(u8"DepthPrepass", [&](PassBuilder& b) { + b.setDepthTarget(depth, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addRenderPass(u8"ForwardOpaque", [&](PassBuilder& b) { + b.readTexture(depth); + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + + std::u8string dot; + GraphDebug::exportDOT(graph, dot); + const std::u8string_view v = dot; + CHECK(contains(v, u8"digraph")); + CHECK(contains(v, u8"DepthPrepass")); + CHECK(contains(v, u8"ForwardOpaque")); + CHECK(contains(v, u8"SceneColor")); + CHECK(contains(v, u8"}")); +} + +TEST_CASE("rg.debug: ExportSummary includes counts") +{ + RenderGraph graph(nullptr); + graph.setOutputSize(1920, 1080); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"Pass1", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + REQUIRE(graph.compile().isOk()); + + std::u8string summary; + GraphDebug::exportSummary(graph, summary); + const std::u8string_view v = summary; + CHECK(contains(v, u8"1920x1080")); + CHECK(contains(v, u8"Pass1")); + CHECK(contains(v, u8"Render")); +} + +TEST_CASE("rg.debug: DOT marks culled passes dashed") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"Culled", [&](PassBuilder& b) { + b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); + }); + REQUIRE(graph.compile().isOk()); + + std::u8string dot; + GraphDebug::exportDOT(graph, dot); + CHECK(contains(dot, u8"dashed")); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/DependencyTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/DependencyTests.test.cpp new file mode 100644 index 00000000..7ea4e9d2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/DependencyTests.test.cpp @@ -0,0 +1,145 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace +{ + std::u8string_view passName(RenderGraph& g, i32 orderSlot) + { + return g.passes()[static_cast(g.executionOrder()[static_cast(orderSlot)])]->name; + } +} + +TEST_CASE("rg.dep: reader depends on writer") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Writer", [&](PassBuilder& b) { + b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addRenderPass(u8"Reader", [&](PassBuilder& b) { + b.readTexture(tex); + b.neverCull(); + }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 2u); + CHECK(passName(graph, 0) == u8"Writer"); +} + +TEST_CASE("rg.dep: multiple readers fan out, writer first") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Writer", [&](PassBuilder& b) { + b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addRenderPass(u8"ReaderA", [&](PassBuilder& b) { b.readTexture(tex); b.neverCull(); }); + graph.addRenderPass(u8"ReaderB", [&](PassBuilder& b) { b.readTexture(tex); b.neverCull(); }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 3u); + CHECK(passName(graph, 0) == u8"Writer"); +} + +TEST_CASE("rg.dep: subresource writes are independent, reader last") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + RGTextureDesc atlasDesc(rhi::TextureFormat::Depth32Float); + atlasDesc.arrayLayerCount = 4; + const RGHandle atlas = graph.createTransient(u8"ShadowAtlas", atlasDesc); + + graph.addRenderPass(u8"Cascade0", [&](PassBuilder& b) { + b.setDepthTarget(atlas, rhi::LoadOp::Clear, rhi::StoreOp::Store, 1.0f, RGSubresourceRange{ 0, 1, 0, 1 }); + b.neverCull(); + }); + graph.addRenderPass(u8"Cascade1", [&](PassBuilder& b) { + b.setDepthTarget(atlas, rhi::LoadOp::Clear, rhi::StoreOp::Store, 1.0f, RGSubresourceRange{ 0, 1, 1, 1 }); + b.neverCull(); + }); + graph.addRenderPass(u8"Forward", [&](PassBuilder& b) { b.readTexture(atlas); b.neverCull(); }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 3u); + CHECK(passName(graph, 2) == u8"Forward"); +} + +TEST_CASE("rg.dep: LoadOp on a color target creates a dependency on the writer") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"SceneColor", RGTextureDesc(rhi::TextureFormat::RGBA16Float)); + + graph.addRenderPass(u8"ForwardOpaque", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addRenderPass(u8"Terrain", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Load, rhi::StoreOp::Store); + b.neverCull(); + }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 2u); + CHECK(passName(graph, 0) == u8"ForwardOpaque"); + CHECK(passName(graph, 1) == u8"Terrain"); +} + +TEST_CASE("rg.dep: LoadOp on a depth target creates a dependency on the writer") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle depth = graph.createTransient(u8"Depth", RGTextureDesc(rhi::TextureFormat::Depth32Float)); + + graph.addRenderPass(u8"DepthPrepass", [&](PassBuilder& b) { + b.setDepthTarget(depth, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addRenderPass(u8"ForwardOpaque", [&](PassBuilder& b) { + b.setDepthTarget(depth, rhi::LoadOp::Load, rhi::StoreOp::Store); + b.neverCull(); + }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 2u); + CHECK(passName(graph, 0) == u8"DepthPrepass"); + CHECK(passName(graph, 1) == u8"ForwardOpaque"); +} + +TEST_CASE("rg.dep: writer chain orders correctly") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Write1", [&](PassBuilder& b) { + b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addComputePass(u8"Process", [&](PassBuilder& b) { + b.readTexture(tex); + b.writeStorage(tex); + b.neverCull(); + }); + graph.addRenderPass(u8"FinalRead", [&](PassBuilder& b) { b.readTexture(tex); b.neverCull(); }); + + REQUIRE(graph.compile().isOk()); + REQUIRE(graph.executionOrder().size() == 3u); + CHECK(passName(graph, 0) == u8"Write1"); + CHECK(passName(graph, 1) == u8"Process"); + CHECK(passName(graph, 2) == u8"FinalRead"); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/DescriptorTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/DescriptorTests.test.cpp new file mode 100644 index 00000000..7d4f3d54 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/DescriptorTests.test.cpp @@ -0,0 +1,74 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +TEST_CASE("rg.descriptor: full size resolves") +{ + RGTextureDesc desc(rhi::TextureFormat::RGBA8Unorm, SizeMode::FullSize); + desc.resolve(1920, 1080); + CHECK(desc.width == 1920u); + CHECK(desc.height == 1080u); +} + +TEST_CASE("rg.descriptor: half size resolves") +{ + RGTextureDesc desc(rhi::TextureFormat::RGBA8Unorm, SizeMode::HalfSize); + desc.resolve(1920, 1080); + CHECK(desc.width == 960u); + CHECK(desc.height == 540u); +} + +TEST_CASE("rg.descriptor: quarter size resolves") +{ + RGTextureDesc desc(rhi::TextureFormat::RGBA8Unorm, SizeMode::QuarterSize); + desc.resolve(1920, 1080); + CHECK(desc.width == 480u); + CHECK(desc.height == 270u); +} + +TEST_CASE("rg.descriptor: custom is not resolved") +{ + RGTextureDesc desc(rhi::TextureFormat::RGBA8Unorm, 256, 256); + desc.resolve(1920, 1080); + CHECK(desc.width == 256u); + CHECK(desc.height == 256u); +} + +TEST_CASE("rg.descriptor: half size clamps to at least one") +{ + RGTextureDesc desc(rhi::TextureFormat::RGBA8Unorm, SizeMode::HalfSize); + desc.resolve(1, 1); + CHECK(desc.width >= 1u); + CHECK(desc.height >= 1u); +} + +TEST_CASE("rg.descriptor: color target defaults") +{ + RGColorTarget target{}; + target.handle = RGHandle{ 0, 1 }; + CHECK(target.loadOp == rhi::LoadOp::Clear); + CHECK(target.storeOp == rhi::StoreOp::Store); +} + +TEST_CASE("rg.descriptor: depth target defaults") +{ + RGDepthTarget target{}; + target.handle = RGHandle{ 0, 1 }; + CHECK(target.depthLoadOp == rhi::LoadOp::Clear); + CHECK(target.depthStoreOp == rhi::StoreOp::Store); + CHECK(target.depthClearValue == 1.0f); + CHECK_FALSE(target.readOnly); +} + +TEST_CASE("rg.descriptor: config defaults") +{ + RenderGraphConfig config{}; + CHECK(config.frameBufferCount == 2); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/Descriptors.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/Descriptors.cppm new file mode 100644 index 00000000..0cbb6b1a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/Descriptors.cppm @@ -0,0 +1,109 @@ +// Draconic::RenderGraph - :descriptors partition +// +// Render-graph resource descriptors (transient texture/buffer) and pass target +// resolves size-relative-to-output and converts to an RHI TextureDesc. + +module; + +#include + +#include + +export module rendergraph:descriptors; + +import core; +import rhi; +import :types; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + // Describes a transient texture resource in the graph. + struct RGTextureDesc + { + rhi::TextureFormat format = rhi::TextureFormat::Undefined; + SizeMode sizeMode = SizeMode::FullSize; + u32 width = 0; // used only when sizeMode == Custom + u32 height = 0; + u32 arrayLayerCount = 1; + u32 mipLevelCount = 1; + u32 sampleCount = 1; + rhi::TextureUsage usage = rhi::TextureUsage::None; + + RGTextureDesc() = default; + RGTextureDesc(rhi::TextureFormat fmt, SizeMode mode = SizeMode::FullSize) noexcept + : format(fmt), sizeMode(mode) {} + RGTextureDesc(rhi::TextureFormat fmt, u32 w, u32 h) noexcept + : format(fmt), sizeMode(SizeMode::Custom), width(w), height(h) {} + + // Resolves actual dimensions from the graph output size. + void resolve(u32 outputWidth, u32 outputHeight) noexcept + { + switch (sizeMode) + { + case SizeMode::FullSize: + width = outputWidth; height = outputHeight; break; + case SizeMode::HalfSize: + width = std::max(1u, outputWidth / 2u); height = std::max(1u, outputHeight / 2u); break; + case SizeMode::QuarterSize: + width = std::max(1u, outputWidth / 4u); height = std::max(1u, outputHeight / 4u); break; + case SizeMode::Custom: + break; // already set + } + } + + [[nodiscard]] rhi::TextureDesc toTextureDesc(std::u8string_view label) const + { + rhi::TextureDesc desc{}; + desc.format = format; + desc.width = width; + desc.height = height; + desc.arrayLayerCount = arrayLayerCount; + desc.mipLevelCount = mipLevelCount; + desc.sampleCount = sampleCount; + desc.usage = usage; + desc.label = label; + return desc; + } + }; + + // Describes a transient buffer resource in the graph. + struct RGBufferDesc + { + u64 size = 0; + rhi::BufferUsage usage = rhi::BufferUsage::None; + }; + + // Color target attachment for a render pass. + struct RGColorTarget + { + RGHandle handle = RGHandle::invalid(); + rhi::LoadOp loadOp = rhi::LoadOp::Clear; + rhi::StoreOp storeOp = rhi::StoreOp::Store; + rhi::ClearColor clearValue = rhi::ClearColor::black(); + RGSubresourceRange subresource; + }; + + // Depth/stencil target attachment for a render pass. + struct RGDepthTarget + { + RGHandle handle = RGHandle::invalid(); + rhi::LoadOp depthLoadOp = rhi::LoadOp::Clear; + rhi::StoreOp depthStoreOp = rhi::StoreOp::Store; + f32 depthClearValue = 1.0f; + bool readOnly = false; + rhi::LoadOp stencilLoadOp = rhi::LoadOp::DontCare; + rhi::StoreOp stencilStoreOp = rhi::StoreOp::DontCare; + u32 stencilClearValue = 0; + RGSubresourceRange subresource; + }; + + // Configuration for the render graph. + struct RenderGraphConfig + { + i32 frameBufferCount = 2; // multi-buffering slots (typically 2 or 3) + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/GraphCoreTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphCoreTests.test.cpp new file mode 100644 index 00000000..e68548f1 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphCoreTests.test.cpp @@ -0,0 +1,118 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +TEST_CASE("rg.graph: create transient returns a valid handle") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle handle = graph.createTransient(u8"Test", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm, SizeMode::FullSize)); + CHECK(handle.isValid()); + CHECK(graph.resourceCount() == 1u); +} + +TEST_CASE("rg.graph: multiple resources get unique handles") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle h1 = graph.createTransient(u8"A", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + const RGHandle h2 = graph.createTransient(u8"B", RGTextureDesc(rhi::TextureFormat::Depth32Float)); + CHECK(h1 != h2); + CHECK(graph.resourceCount() == 2u); +} + +TEST_CASE("rg.graph: get resource by name") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle h1 = graph.createTransient(u8"SceneColor", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + CHECK(graph.getResource(u8"SceneColor") == h1); + CHECK_FALSE(graph.getResource(u8"NonExistent").isValid()); +} + +TEST_CASE("rg.graph: pass count is correct") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"Pass1", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + graph.addComputePass(u8"Pass2", [](PassBuilder& b) { b.hasSideEffects(); }); + + CHECK(graph.passCount() == 2u); +} + +TEST_CASE("rg.graph: set output size affects resolution") +{ + RenderGraph graph(nullptr); + graph.setOutputSize(1920, 1080); + CHECK(graph.outputWidth() == 1920u); + CHECK(graph.outputHeight() == 1080u); +} + +TEST_CASE("rg.graph: import target with final state") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle handle = graph.importTarget(u8"Backbuffer", nullptr, nullptr, rhi::ResourceState::Present); + CHECK(handle.isValid()); +} + +TEST_CASE("rg.graph: reset keeps persistent, drops transient") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + graph.registerPersistent(u8"Shadow", nullptr, nullptr); + graph.createTransient(u8"Temp", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.reset(); + + CHECK(graph.getResource(u8"Shadow").isValid()); + CHECK_FALSE(graph.getResource(u8"Temp").isValid()); +} + +TEST_CASE("rg.graph: SetViewport records a per-pass viewport override") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"ViewportPass", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.setViewport(10, 20, 100, 200); + b.neverCull(); + }); + + REQUIRE(graph.passes().size() == 1u); + const RenderGraphPass* pass = graph.passes()[0]; + CHECK(pass->hasViewport); + CHECK(pass->viewportX == 10); + CHECK(pass->viewportY == 20); + CHECK(pass->viewportW == 100u); + CHECK(pass->viewportH == 200u); +} + +TEST_CASE("rg.graph: a pass without SetViewport has no viewport override (full attachment)") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle color = graph.createTransient(u8"Color", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + + graph.addRenderPass(u8"FullPass", [&](PassBuilder& b) { + b.setColorTarget(0, color, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + }); + + REQUIRE(graph.passes().size() == 1u); + CHECK_FALSE(graph.passes()[0]->hasViewport); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/GraphDebug.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphDebug.cppm new file mode 100644 index 00000000..82c471ce --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphDebug.cppm @@ -0,0 +1,182 @@ +// Draconic::RenderGraph - :debug partition +// +// Debug visualization/reporting: Graphviz DOT export and a text summary. Ported +// from Sedulous.RenderGraph (GraphDebug.bf). + +module; + +#include +#include +#include + +export module rendergraph:debug; + +import core; +import :types; +import :pass; +import :resource; +import :graph; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace detail + { + [[nodiscard]] inline std::u8string_view passColor(RGPassType type) + { + switch (type) + { + case RGPassType::Render: return u8"#4488cc"; + case RGPassType::Compute: return u8"#cc8844"; + case RGPassType::Copy: return u8"#44aa44"; + } + return u8"#888888"; + } + [[nodiscard]] inline std::u8string_view lifetimeLabel(RGResourceLifetime lifetime) + { + switch (lifetime) + { + case RGResourceLifetime::Transient: return u8"transient"; + case RGResourceLifetime::Persistent: return u8"persistent"; + case RGResourceLifetime::Imported: return u8"imported"; + } + return u8"?"; + } + [[nodiscard]] inline std::u8string_view accessLabel(RGAccessType type) + { + switch (type) + { + case RGAccessType::ReadTexture: return u8"read"; + case RGAccessType::ReadBuffer: return u8"read"; + case RGAccessType::ReadDepthStencil: return u8"depth-read"; + case RGAccessType::SampleDepthStencil: return u8"depth-sample"; + case RGAccessType::ReadCopySrc: return u8"copy-src"; + case RGAccessType::WriteColorTarget: return u8"color-out"; + case RGAccessType::WriteDepthTarget: return u8"depth-out"; + case RGAccessType::WriteStorage: return u8"storage-write"; + case RGAccessType::WriteCopyDst: return u8"copy-dst"; + case RGAccessType::ReadWriteStorage: return u8"rw-storage"; + case RGAccessType::ReadWriteDepthTarget: return u8"depth-rw"; + case RGAccessType::ReadWriteColorTarget: return u8"color-rw"; + } + return u8"?"; + } + [[nodiscard]] inline std::u8string_view passTypeLabel(RGPassType type) + { + switch (type) + { + case RGPassType::Render: return u8"Render"; + case RGPassType::Compute: return u8"Compute"; + case RGPassType::Copy: return u8"Copy"; + } + return u8"?"; + } + } + + class GraphDebug + { + public: + // Graphviz DOT: pass nodes (boxes) + resource nodes (ellipse/diamond) + + // access edges. Culled passes/edges are dashed/gray. + static void exportDOT(RenderGraph& graph, std::u8string& out) + { + const std::vector& passes = graph.passes(); + const std::vector& resources = graph.resources(); + + out.append(u8"digraph RenderGraph {\n"); + out.append(u8" rankdir=LR;\n"); + out.append(u8" node [fontname=\"Helvetica\"];\n\n"); + + for (usize i = 0; i < passes.size(); ++i) + { + RenderGraphPass* pass = passes[i]; + const std::u8string_view style = pass->isCulled ? std::u8string_view(u8"dashed") : std::u8string_view(u8"filled"); + const std::u8string_view fontColor = pass->isCulled ? std::u8string_view(u8"gray") : std::u8string_view(u8"white"); + appendFormat(out, + u8" pass{} [label=\"{}\" shape=box style={} fillcolor=\"{}\" fontcolor=\"{}\"", + i, pass->name, style, detail::passColor(pass->type), fontColor); + if (pass->isCulled) { out.append(u8" color=gray"); } + out.append(u8"];\n"); + } + out.append(u8"\n"); + + for (usize i = 0; i < resources.size(); ++i) + { + RenderGraphResource* res = resources[i]; + if (res == nullptr) { continue; } + const std::u8string_view shape = res->resourceType == RGResourceType::Texture + ? std::u8string_view(u8"ellipse") : std::u8string_view(u8"diamond"); + appendFormat(out, u8" res{} [label=\"{}\\n({})\" shape={}];\n", + i, res->name, detail::lifetimeLabel(res->lifetime), shape); + } + out.append(u8"\n"); + + for (usize passIdx = 0; passIdx < passes.size(); ++passIdx) + { + RenderGraphPass* pass = passes[passIdx]; + for (const RGResourceAccess& access : pass->accesses) + { + if (!access.handle.isValid() || access.handle.index >= resources.size()) { continue; } + if (resources[access.handle.index] == nullptr) { continue; } + + const std::u8string_view label = detail::accessLabel(access.type); + if (access.isRead()) + { + appendFormat(out, u8" res{} -> pass{} [label=\"{}\"", access.handle.index, passIdx, label); + if (pass->isCulled) { out.append(u8" style=dashed color=gray"); } + out.append(u8"];\n"); + } + if (access.isWrite()) + { + appendFormat(out, u8" pass{} -> res{} [label=\"{}\"", passIdx, access.handle.index, label); + if (pass->isCulled) { out.append(u8" style=dashed color=gray"); } + out.append(u8"];\n"); + } + } + } + + out.append(u8"}\n"); + } + + // Human-readable text summary (counts + execution order). + static void exportSummary(RenderGraph& graph, std::u8string& out) + { + const std::vector& passes = graph.passes(); + const std::vector& resources = graph.resources(); + const std::vector& executionOrder = graph.executionOrder(); + + usize activeCount = 0, culledCount = 0; + for (RenderGraphPass* p : passes) { if (p->isCulled) { ++culledCount; } else { ++activeCount; } } + + usize resCount = 0, transientCount = 0, persistentCount = 0, importedCount = 0; + for (RenderGraphResource* r : resources) + { + if (r == nullptr) { continue; } + ++resCount; + switch (r->lifetime) + { + case RGResourceLifetime::Transient: ++transientCount; break; + case RGResourceLifetime::Persistent: ++persistentCount; break; + case RGResourceLifetime::Imported: ++importedCount; break; + } + } + + out.append(u8"=== Render Graph Summary ===\n"); + appendFormat(out, u8"Passes: {} active, {} culled, {} total\n", activeCount, culledCount, passes.size()); + appendFormat(out, u8"Resources: {} total ({} transient, {} persistent, {} imported)\n", + resCount, transientCount, persistentCount, importedCount); + appendFormat(out, u8"Output: {}x{}\n\n", graph.outputWidth(), graph.outputHeight()); + + if (!executionOrder.empty()) + { + out.append(u8"Execution order:\n"); + for (usize i = 0; i < executionOrder.size(); ++i) + { + RenderGraphPass* pass = passes[static_cast(executionOrder[i])]; + appendFormat(out, u8" {}. [{}] {}\n", i + 1, detail::passTypeLabel(pass->type), pass->name); + } + } + } + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/GraphProfiler.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphProfiler.cppm new file mode 100644 index 00000000..598356b9 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphProfiler.cppm @@ -0,0 +1,171 @@ +// Draconic::RenderGraph - :profiler partition +// +// Optional GPU profiler: per-pass timing via timestamp queries. BeginPass/EndPass +// write timestamps around each pass; Resolve copies the query results to a +// readback buffer; ReadResults (after a fence wait) maps it and builds a report. + +module; + +#include + +#include +#include +#include + +export module rendergraph:profiler; + +import core; +import rhi; +import :types; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class GraphProfiler + { + public: + bool enabled = true; + + ~GraphProfiler() { destroy(); } + + GraphProfiler() = default; + GraphProfiler(const GraphProfiler&) = delete; + GraphProfiler& operator=(const GraphProfiler&) = delete; + + // Two timestamp queries per pass (begin + end) + a GpuToCpu readback buffer. + [[nodiscard]] Status init(rhi::Device& device, i32 maxPasses = 64) + { + m_device = &device; + m_maxPasses = maxPasses; + + rhi::QuerySetDesc queryDesc{}; + queryDesc.type = rhi::QueryType::Timestamp; + queryDesc.count = static_cast(maxPasses * 2); + queryDesc.label = u8"RG_Profiler_Queries"; + if (!device.createQuerySet(queryDesc, m_querySet).isOk()) { return Status{ ErrorCode::Unknown }; } + + rhi::BufferDesc bufDesc{}; + bufDesc.size = static_cast(maxPasses) * 2u * sizeof(u64); + bufDesc.usage = rhi::BufferUsage::CopyDst; + bufDesc.memory = rhi::MemoryLocation::GpuToCpu; + bufDesc.label = u8"RG_Profiler_Readback"; + if (!device.createBuffer(bufDesc, m_readbackBuffer).isOk()) { return Status{ ErrorCode::Unknown }; } + + m_passTimesMs.resize(static_cast(maxPasses)); + m_initialized = true; + return Status{}; + } + + // Reset the query set at the START of the frame's encoder - timestamps can only be written + // into a freshly-reset pool, and the reset must be outside any render pass. + void beginFrame(rhi::CommandEncoder& encoder) + { + if (!m_initialized || !enabled) { return; } + encoder.resetQuerySet(m_querySet, 0, static_cast(m_maxPasses * 2)); + m_passNames.clear(); + } + + void beginPass(rhi::CommandEncoder& encoder, i32 passIndex, std::u8string_view passName) + { + if (!m_initialized || !enabled || passIndex >= m_maxPasses) { return; } + while (static_cast(m_passNames.size()) <= passIndex) { m_passNames.push_back(std::u8string{}); } + m_passNames[static_cast(passIndex)] = std::u8string(passName); + encoder.writeTimestamp(m_querySet, static_cast(passIndex * 2)); + } + + void endPass(rhi::CommandEncoder& encoder, i32 passIndex) + { + if (!m_initialized || !enabled || passIndex >= m_maxPasses) { return; } + encoder.writeTimestamp(m_querySet, static_cast(passIndex * 2 + 1)); + } + + // Resolve queries into the readback buffer (call after recording all passes). The pool was + // already reset by BeginFrame, so this only copies the written timestamps out. + void resolve(rhi::CommandEncoder& encoder, i32 passCount) + { + if (!m_initialized || !enabled || passCount == 0) { return; } + const u32 queryCount = static_cast(std::min(passCount * 2, m_maxPasses * 2)); + encoder.resolveQuerySet(m_querySet, 0, queryCount, m_readbackBuffer, 0); + } + + // Read results and append a timing report (call after the GPU has finished). + void readResults(i32 passCount, std::u8string& outReport) + { + if (!m_initialized || !enabled || passCount == 0) { return; } + + const u64* mapped = static_cast(m_readbackBuffer->map()); + if (mapped == nullptr) { return; } + + const i32 count = std::min(passCount, m_maxPasses); + f32 totalMs = 0.0f; + + outReport.append(u8"=== GPU Pass Timing ===\n"); + for (i32 i = 0; i < count; ++i) + { + const u64 begin = mapped[i * 2]; + const u64 end = mapped[i * 2 + 1]; + const u64 ticks = end > begin ? end - begin : 0; + const f32 ms = static_cast(ticks) * m_gpuTimestampPeriod / 1000000.0f; + m_passTimesMs[static_cast(i)] = ms; + totalMs += ms; + + const std::u8string_view name = i < static_cast(m_passNames.size()) + ? m_passNames[static_cast(i)] : std::u8string_view(u8"???"); + appendFormat(outReport, u8" {} ms {}\n", ms, name); + } + appendFormat(outReport, u8" --------\n {} ms TOTAL\n", totalMs); + + // Aggregate by pass NAME so many same-named passes (e.g. 24x probes.prefilter) read as one + // line - a "which pass category is expensive" summary, sorted most-expensive first. + struct Agg { std::u8string_view name; f32 sum = 0.0f; i32 n = 0; }; + std::vector agg; + for (i32 i = 0; i < count; ++i) + { + const std::u8string_view nm = i < static_cast(m_passNames.size()) + ? m_passNames[static_cast(i)] : std::u8string_view(u8"???"); + bool found = false; + for (Agg& a : agg) { if (a.name == nm) { a.sum += m_passTimesMs[static_cast(i)]; ++a.n; found = true; break; } } + if (!found) { Agg a; a.name = nm; a.sum = m_passTimesMs[static_cast(i)]; a.n = 1; agg.push_back(a); } + } + for (usize i = 0; i < agg.size(); ++i) // selection sort by total (few entries) + for (usize j = i + 1; j < agg.size(); ++j) + if (agg[j].sum > agg[i].sum) { const Agg t = agg[i]; agg[i] = agg[j]; agg[j] = t; } + outReport.append(u8"=== GPU by pass name (expensive first) ===\n"); + for (const Agg& a : agg) { appendFormat(outReport, u8" {} ms (x{}) {}\n", a.sum, a.n, a.name); } + + m_readbackBuffer->unmap(); + } + + [[nodiscard]] f32 getPassTimeMs(i32 passIndex) const + { + if (passIndex < 0 || passIndex >= static_cast(m_passTimesMs.size())) { return 0.0f; } + return m_passTimesMs[static_cast(passIndex)]; + } + + // GPU timestamp period (nanoseconds per tick); backend-specific. + void setTimestampPeriod(f32 nanosecondsPerTick) noexcept { m_gpuTimestampPeriod = nanosecondsPerTick; } + + void destroy() + { + if (m_device != nullptr) + { + if (m_readbackBuffer != nullptr) { m_device->destroyBuffer(m_readbackBuffer); } + if (m_querySet != nullptr) { m_device->destroyQuerySet(m_querySet); } + } + m_initialized = false; + } + + private: + rhi::Device* m_device = nullptr; + rhi::QuerySet* m_querySet = nullptr; + rhi::Buffer* m_readbackBuffer = nullptr; + i32 m_maxPasses = 0; + bool m_initialized = false; + std::vector m_passNames; + std::vector m_passTimesMs; + f32 m_gpuTimestampPeriod = 0.0f; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/GraphValidator.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphValidator.cppm new file mode 100644 index 00000000..49c49321 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/GraphValidator.cppm @@ -0,0 +1,163 @@ +// Draconic::RenderGraph - :validator partition +// +// Validates a graph for common authoring errors: reads of never-written +// resources (error), passes with no execute callback (warning), and redundant +// (GraphValidator.bf). + +module; + +#include +#include +#include +#include +#include + +export module rendergraph:validator; + +import core; +import :types; +import :pass; +import :resource; +import :graph; + +using namespace draco; + +export namespace draco::rendergraph +{ + enum class ValidationSeverity { Warning, Error }; + + struct ValidationMessage + { + ValidationSeverity severity = ValidationSeverity::Warning; + std::u8string message; + }; + + namespace detail + { + [[nodiscard]] inline std::u8string_view resName(const std::vector& resources, u32 index) + { + return (index < resources.size() && resources[index] != nullptr) + ? resources[index]->name : std::u8string_view(u8"???"); + } + } + + class GraphValidator + { + public: + static void validate(RenderGraph& graph, std::vector& out) + { + checkUninitializedReads(graph, out); + checkEmptyPasses(graph, out); + checkRedundantWrites(graph, out); + } + + static void validateToString(RenderGraph& graph, std::u8string& out) + { + std::vector messages; + validate(graph, messages); + + if (messages.empty()) + { + out.append(u8"Render graph validation: OK (no issues)\n"); + return; + } + + appendFormat(out, u8"Render graph validation: {} issue(s)\n", messages.size()); + for (const ValidationMessage& msg : messages) + { + const std::u8string_view prefix = msg.severity == ValidationSeverity::Error ? std::u8string_view(u8"ERROR") + : std::u8string_view(u8"WARNING"); + appendFormat(out, u8" [{}] {}\n", prefix, msg.message); + } + } + + private: + // Reads of resources that no prior pass wrote (transient only - imported + // and persistent are considered externally initialized). + static void checkUninitializedReads(RenderGraph& graph, std::vector& out) + { + const std::vector& resources = graph.resources(); + std::unordered_set written; + + for (u32 i = 0; i < resources.size(); ++i) + { + RenderGraphResource* res = resources[i]; + if (res != nullptr && (res->lifetime == RGResourceLifetime::Imported + || res->lifetime == RGResourceLifetime::Persistent)) + { + written.insert(i); + } + } + + for (RenderGraphPass* pass : graph.passes()) + { + for (const RGResourceAccess& access : pass->accesses) + { + if (access.isRead() && access.handle.isValid() && !written.contains(access.handle.index)) + { + ValidationMessage msg; + msg.severity = ValidationSeverity::Error; + appendFormat(msg.message, + u8"Pass '{}' reads resource '{}' (index {}) which has not been written to", + pass->name, detail::resName(resources, access.handle.index), access.handle.index); + out.push_back(static_cast(msg)); + } + } + for (const RGResourceAccess& access : pass->accesses) + { + if (access.isWrite() && access.handle.isValid()) { written.insert(access.handle.index); } + } + } + } + + static void checkEmptyPasses(RenderGraph& graph, std::vector& out) + { + for (RenderGraphPass* pass : graph.passes()) + { + bool hasCallback = false; + switch (pass->type) + { + case RGPassType::Render: hasCallback = static_cast(pass->executeCallback); break; + case RGPassType::Compute: hasCallback = static_cast(pass->computeCallback); break; + case RGPassType::Copy: hasCallback = static_cast(pass->copyCallback); break; + } + if (!hasCallback) + { + ValidationMessage msg; + msg.severity = ValidationSeverity::Warning; + appendFormat(msg.message, u8"Pass '{}' has no execute callback", pass->name); + out.push_back(static_cast(msg)); + } + } + } + + // Resources written twice with no read in between. + static void checkRedundantWrites(RenderGraph& graph, std::vector& out) + { + const std::vector& resources = graph.resources(); + std::unordered_map lastWriter; + + for (RenderGraphPass* pass : graph.passes()) + { + for (const RGResourceAccess& access : pass->accesses) + { + if (access.isRead() && access.handle.isValid()) { lastWriter.erase(access.handle.index); } + } + for (const RGResourceAccess& access : pass->accesses) + { + if (!access.isWrite() || !access.handle.isValid()) { continue; } + if (std::u8string* prev = mapFind(lastWriter, access.handle.index)) + { + ValidationMessage msg; + msg.severity = ValidationSeverity::Warning; + appendFormat(msg.message, + u8"Resource '{}' written by pass '{}' was already written by '{}' without being read", + detail::resName(resources, access.handle.index), pass->name, *prev); + out.push_back(static_cast(msg)); + } + lastWriter.insert_or_assign(access.handle.index, std::u8string(pass->name)); + } + } + } + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilder.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilder.cppm new file mode 100644 index 00000000..c239632e --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilder.cppm @@ -0,0 +1,185 @@ +// Draconic::RenderGraph - :pass_builder partition +// +// Fluent builder handed to a pass's setup callback to declare reads/writes, +// attachments, dependencies, flags, and the execute callback. Ported from +// Sedulous.RenderGraph (PassBuilder.bf). Methods return *this for chaining. + +module; + +#include +#include + +export module rendergraph:pass_builder; + +import core; +import rhi; +import :types; +import :descriptors; +import :callbacks; +import :pass; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class PassBuilder + { + public: + explicit PassBuilder(RenderGraphPass& pass) noexcept : m_pass(&pass) {} + + // --- texture / buffer reads --- + PassBuilder& readTexture(RGHandle handle, RGSubresourceRange subresource = {}) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadTexture, subresource }); + return *this; + } + + // Sample a DEPTH texture in this pass's shader (e.g. a shadow map). Like ReadTexture - a read + // dependency + barrier, NOT a depth attachment (unlike ReadDepth) - but transitions to + // DepthStencilRead (DEPTH_STENCIL_READ_ONLY_OPTIMAL), the layout a depth sampler expects. + PassBuilder& sampleDepth(RGHandle handle, RGSubresourceRange subresource = {}) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::SampleDepthStencil, subresource }); + return *this; + } + + PassBuilder& readDepth(RGHandle handle, RGSubresourceRange subresource = {}) + { + RGDepthTarget dt{}; + dt.handle = handle; + dt.depthLoadOp = rhi::LoadOp::Load; + dt.depthStoreOp = rhi::StoreOp::Store; + dt.readOnly = true; + dt.subresource = subresource; + m_pass->depthTarget = dt; + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadDepthStencil, subresource }); + return *this; + } + + PassBuilder& readBuffer(RGHandle handle) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadBuffer, {} }); + return *this; + } + + // --- render targets --- + PassBuilder& setColorTarget(i32 slot, RGHandle handle, + rhi::LoadOp loadOp = rhi::LoadOp::Clear, + rhi::StoreOp storeOp = rhi::StoreOp::Store, + rhi::ClearColor clearValue = rhi::ClearColor::black(), + RGSubresourceRange subresource = {}) + { + RGColorTarget target{}; + target.handle = handle; + target.loadOp = loadOp; + target.storeOp = storeOp; + target.clearValue = clearValue; + target.subresource = subresource; + + while (static_cast(m_pass->colorTargets.size()) <= slot) { m_pass->colorTargets.push_back(RGColorTarget{}); } + m_pass->colorTargets[static_cast(slot)] = target; + + if (loadOp == rhi::LoadOp::Load && storeOp == rhi::StoreOp::Store) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadWriteColorTarget, subresource }); + } + else if (storeOp == rhi::StoreOp::Store) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::WriteColorTarget, subresource }); + } + return *this; + } + + PassBuilder& setDepthTarget(RGHandle handle, + rhi::LoadOp loadOp = rhi::LoadOp::Clear, + rhi::StoreOp storeOp = rhi::StoreOp::Store, + f32 clearDepth = 1.0f, + RGSubresourceRange subresource = {}) + { + RGDepthTarget dt{}; + dt.handle = handle; + dt.depthLoadOp = loadOp; + dt.depthStoreOp = storeOp; + dt.depthClearValue = clearDepth; + dt.readOnly = false; + dt.stencilLoadOp = rhi::LoadOp::DontCare; + dt.stencilStoreOp = rhi::StoreOp::DontCare; + dt.subresource = subresource; + m_pass->depthTarget = dt; + + if (loadOp == rhi::LoadOp::Load && storeOp == rhi::StoreOp::Store) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadWriteDepthTarget, subresource }); + } + else if (storeOp == rhi::StoreOp::Store) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::WriteDepthTarget, subresource }); + } + return *this; + } + + // Read-only depth (depth test, no write): transitions to DepthStencilRead. + PassBuilder& setReadOnlyDepthTarget(RGHandle handle, RGSubresourceRange subresource = {}) + { + RGDepthTarget dt{}; + dt.handle = handle; + dt.depthLoadOp = rhi::LoadOp::Load; + dt.depthStoreOp = rhi::StoreOp::Store; + dt.depthClearValue = 1.0f; + dt.readOnly = true; + dt.subresource = subresource; + m_pass->depthTarget = dt; + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadDepthStencil, subresource }); + return *this; + } + + // --- storage (UAV) --- + PassBuilder& writeStorage(RGHandle handle, RGSubresourceRange subresource = {}) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::WriteStorage, subresource }); + return *this; + } + PassBuilder& readWriteStorage(RGHandle handle, RGSubresourceRange subresource = {}) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadWriteStorage, subresource }); + return *this; + } + + // --- copy --- + PassBuilder& copySrc(RGHandle handle) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::ReadCopySrc, {} }); + return *this; + } + PassBuilder& copyDst(RGHandle handle) + { + m_pass->accesses.push_back(RGResourceAccess{ handle, RGAccessType::WriteCopyDst, {} }); + return *this; + } + + // --- dependencies / flags --- + PassBuilder& dependsOn(PassHandle pass) { m_pass->dependencies.push_back(pass); return *this; } + // Override the pass's viewport + scissor (a sub-rect of the attachment, e.g. split-screen). + // Without it the pass covers the full attachment. + PassBuilder& setViewport(i32 x, i32 y, u32 w, u32 h) { + m_pass->hasViewport = true; m_pass->viewportX = x; m_pass->viewportY = y; + m_pass->viewportW = w; m_pass->viewportH = h; return *this; + } + PassBuilder& neverCull() { m_pass->neverCull = true; return *this; } + PassBuilder& hasSideEffects() { m_pass->hasSideEffects = true; return *this; } + PassBuilder& enableIf(std::function condition) { m_pass->condition = std::move(condition); return *this; } + + // --- execute callbacks --- + PassBuilder& setExecute(RenderPassExecuteCallback callback) { m_pass->executeCallback = std::move(callback); return *this; } + // A render pass whose body is supplied by render bundles (parallel command recording). + // The graph begins the pass with secondary-command-buffer contents + ExecuteBundles them. + PassBuilder& setBundleExecute(RenderBundlePassCallback callback) { m_pass->bundleCallback = std::move(callback); return *this; } + PassBuilder& setComputeExecute(ComputePassExecuteCallback callback) { m_pass->computeCallback = std::move(callback); return *this; } + PassBuilder& setCopyExecute(CopyPassExecuteCallback callback) { m_pass->copyCallback = std::move(callback); return *this; } + + private: + RenderGraphPass* m_pass; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilderTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilderTests.test.cpp new file mode 100644 index 00000000..92757ec2 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/PassBuilderTests.test.cpp @@ -0,0 +1,179 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +TEST_CASE("rg.builder: ReadTexture adds an access") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + const RGHandle handle{ 0, 1 }; + builder.readTexture(handle); + + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].handle == handle); + CHECK(pass.accesses[0].type == RGAccessType::ReadTexture); + CHECK(pass.accesses[0].subresource.isAll()); +} + +TEST_CASE("rg.builder: ReadTexture with subresource") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + builder.readTexture(RGHandle{ 0, 1 }, RGSubresourceRange{ 0, 1, 2, 1 }); + + CHECK(pass.accesses[0].subresource.baseArrayLayer == 2u); + CHECK(pass.accesses[0].subresource.arrayLayerCount == 1u); +} + +TEST_CASE("rg.builder: SetColorTarget adds access + attachment") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + const RGHandle handle{ 0, 1 }; + builder.setColorTarget(0, handle, rhi::LoadOp::Clear, rhi::StoreOp::Store); + + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::WriteColorTarget); + REQUIRE(pass.colorTargets.size() == 1u); + CHECK(pass.colorTargets[0].handle == handle); + CHECK(pass.colorTargets[0].loadOp == rhi::LoadOp::Clear); +} + +TEST_CASE("rg.builder: SetDepthTarget adds access + attachment") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + const RGHandle handle{ 0, 1 }; + builder.setDepthTarget(handle, rhi::LoadOp::Clear, rhi::StoreOp::Store, 1.0f); + + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::WriteDepthTarget); + REQUIRE(pass.depthTarget.has_value()); + CHECK(pass.depthTarget.value().handle == handle); + CHECK(pass.depthTarget.value().depthClearValue == 1.0f); +} + +TEST_CASE("rg.builder: ReadDepth sets read-only") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + builder.readDepth(RGHandle{ 0, 1 }); + + REQUIRE(pass.depthTarget.has_value()); + CHECK(pass.depthTarget.value().readOnly); + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::ReadDepthStencil); +} + +TEST_CASE("rg.builder: SampleDepth reads without an attachment") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + builder.sampleDepth(RGHandle{ 0, 1 }); + + // Unlike ReadDepth, sampling a depth texture in a shader is NOT a depth attachment: + // it adds a read access only, leaving depthTarget unset. + CHECK_FALSE(pass.depthTarget.has_value()); + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::SampleDepthStencil); + CHECK(pass.accesses[0].isRead()); + CHECK_FALSE(pass.accesses[0].isWrite()); +} + +TEST_CASE("rg.builder: SampleDepth with subresource") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder builder(pass); + builder.sampleDepth(RGHandle{ 0, 1 }, RGSubresourceRange{ 0, 1, 3, 2 }); + + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].subresource.baseArrayLayer == 3u); + CHECK(pass.accesses[0].subresource.arrayLayerCount == 2u); +} + +TEST_CASE("rg.builder: NeverCull / HasSideEffects flags") +{ + { + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder(pass).neverCull(); + CHECK(pass.neverCull); + CHECK(pass.shouldSurviveCulling()); + } + { + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder(pass).hasSideEffects(); + CHECK(pass.hasSideEffects); + CHECK(pass.shouldSurviveCulling()); + } +} + +TEST_CASE("rg.builder: EnableIf stores a runtime condition") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder(pass).enableIf([]() { return true; }); + REQUIRE(static_cast(pass.condition)); + CHECK(pass.condition()); +} + +TEST_CASE("rg.builder: storage + copy accesses") +{ + { + RenderGraphPass pass(u8"Test", RGPassType::Compute); + PassBuilder(pass).writeStorage(RGHandle{ 0, 1 }); + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::WriteStorage); + } + { + RenderGraphPass pass(u8"Test", RGPassType::Compute); + PassBuilder(pass).readWriteStorage(RGHandle{ 0, 1 }); + REQUIRE(pass.accesses.size() == 1u); + CHECK(pass.accesses[0].type == RGAccessType::ReadWriteStorage); + CHECK(pass.accesses[0].isRead()); + CHECK(pass.accesses[0].isWrite()); + } + { + RenderGraphPass pass(u8"Test", RGPassType::Copy); + PassBuilder(pass).copySrc(RGHandle{ 0, 1 }).copyDst(RGHandle{ 1, 1 }); + REQUIRE(pass.accesses.size() == 2u); + CHECK(pass.accesses[0].type == RGAccessType::ReadCopySrc); + CHECK(pass.accesses[1].type == RGAccessType::WriteCopyDst); + } +} + +TEST_CASE("rg.builder: fluent chaining") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + PassBuilder(pass) + .readTexture(RGHandle{ 2, 1 }) + .setColorTarget(0, RGHandle{ 0, 1 }, rhi::LoadOp::Clear, rhi::StoreOp::Store) + .setDepthTarget(RGHandle{ 1, 1 }, rhi::LoadOp::Load, rhi::StoreOp::Store) + .neverCull(); + + CHECK(pass.accesses.size() == 3u); // read + write-color + readwrite-depth (Load+Store) + CHECK(pass.colorTargets.size() == 1u); + CHECK(pass.depthTarget.has_value()); + CHECK(pass.neverCull); +} + +TEST_CASE("rg.builder: GetInputs folds LoadOp into a read") +{ + RenderGraphPass pass(u8"Test", RGPassType::Render); + const RGHandle handle{ 0, 1 }; + PassBuilder(pass).setColorTarget(0, handle, rhi::LoadOp::Load, rhi::StoreOp::Store); + + std::vector inputs; + pass.getInputs(inputs); + bool hasRead = false; + for (const RGResourceAccess& input : inputs) + { + if (input.handle == handle && input.isRead()) { hasRead = true; } + } + CHECK(hasRead); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/PersistentResource.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/PersistentResource.cppm new file mode 100644 index 00000000..1bab4fc9 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/PersistentResource.cppm @@ -0,0 +1,81 @@ +// Draconic::RenderGraph - :persistent_resource partition +// +// A persistent resource that survives across frames with tracked state. +// Externally owned - the graph never creates or destroys these. The ping-pong +// variant carries two slots (current + previous frame) for temporal effects. + +module; + +#include + +export module rendergraph:persistent_resource; + +import core; +import rhi; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class PersistentResource + { + public: + static constexpr i32 kSlotCount = 2; // current + one previous + + PersistentResource(rhi::Texture* texture, rhi::TextureView* view) noexcept + { + m_textures[0] = texture; + m_views[0] = view; + } + + // Ping-pong (double-buffered) variant. + PersistentResource(rhi::Texture* tex0, rhi::Texture* tex1, + rhi::TextureView* view0, rhi::TextureView* view1) noexcept + : m_isPingPong(true) + { + m_textures[0] = tex0; m_textures[1] = tex1; + m_views[0] = view0; m_views[1] = view1; + } + + [[nodiscard]] rhi::Texture* currentTexture() const noexcept { return m_textures[m_currentIndex]; } + [[nodiscard]] rhi::TextureView* currentView() const noexcept { return m_views[m_currentIndex]; } + [[nodiscard]] rhi::Texture* previousTexture() const noexcept + { + return m_isPingPong ? m_textures[(m_currentIndex + kSlotCount - 1) % kSlotCount] + : m_textures[m_currentIndex]; + } + [[nodiscard]] rhi::TextureView* previousView() const noexcept + { + return m_isPingPong ? m_views[(m_currentIndex + kSlotCount - 1) % kSlotCount] + : m_views[m_currentIndex]; + } + [[nodiscard]] bool isPingPong() const noexcept { return m_isPingPong; } + + void swap() noexcept + { + if (m_isPingPong) { m_currentIndex = (m_currentIndex + 1) % kSlotCount; } + } + + // Re-point the active slot (e.g. when the external texture is recreated on resize). + void updateTexture(rhi::Texture* texture, rhi::TextureView* view) noexcept + { + m_textures[m_currentIndex] = texture; + m_views[m_currentIndex] = view; + } + + // Cross-frame tracking the graph reads/writes (persists across reset()). + bool firstFrame = true; + rhi::ResourceState lastKnownState = rhi::ResourceState::Undefined; + // Non-empty when the resource ended a frame in a non-uniform state; + // otherwise lastKnownState is the uniform value. + std::vector subresourceStates; + + private: + rhi::Texture* m_textures[kSlotCount] = {}; + rhi::TextureView* m_views[kSlotCount] = {}; + i32 m_currentIndex = 0; + bool m_isPingPong = false; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/ProfilerTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/ProfilerTests.test.cpp new file mode 100644 index 00000000..7a6adc5c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/ProfilerTests.test.cpp @@ -0,0 +1,25 @@ +// GPU-free guard/bounds coverage for GraphProfiler (Sedulous ships no profiler +// test; the timing path needs a real device + fence wait, exercised in samples). +#include + + +import core; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; + +TEST_CASE("rg.profiler: uninitialized is safe and reports zero") +{ + GraphProfiler profiler; + CHECK(profiler.enabled); + + // No init() called: queries are zero, out-of-range indices are clamped. + CHECK(profiler.getPassTimeMs(0) == 0.0f); + CHECK(profiler.getPassTimeMs(-1) == 0.0f); + CHECK(profiler.getPassTimeMs(1000) == 0.0f); + + profiler.setTimestampPeriod(1.0f); // no-op without GPU state + profiler.destroy(); // safe with no device + CHECK(profiler.getPassTimeMs(0) == 0.0f); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cpp deleted file mode 100644 index 31d7e2e1..00000000 --- a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cpp +++ /dev/null @@ -1,121 +0,0 @@ -module; - -#include -#include - -#include - -module rendering.rendergraph; - -import rendering.rhi; - -namespace draco::rendering::rendergraph { - - static void sortMaterial(std::vector& packets) - { - std::sort(packets.begin(), packets.end(), - [](const rhi::RenderPacket& a, const rhi::RenderPacket& b) - { - // Pipeline first - if (a.pipeline != b.pipeline) - return a.pipeline.value < b.pipeline.value; - - // Texture second - if (a.textureHandle != b.textureHandle) - return a.textureHandle.value < b.textureHandle.value; - - // Vertex buffer third - if (a.vertexBuffer != b.vertexBuffer) - return a.vertexBuffer.value < b.vertexBuffer.value; - - // Index buffer fallback - return a.indexBuffer.value < b.indexBuffer.value; - }); - } - - // Placeholder until depth sorting exists - static void sortFrontToBack(std::vector& packets) - { - sortMaterial(packets); - } - - static void sortBackToFront(std::vector& packets) - { - sortMaterial(packets); - } - - static void sortPackets(std::vector& packets, SortMode mode) - { - switch (mode) - { - case SortMode::None: - break; - - case SortMode::Material: - sortMaterial(packets); - break; - - case SortMode::FrontToBack: - sortFrontToBack(packets); - break; - - case SortMode::BackToFront: - sortBackToFront(packets); - break; - } - } - - void RenderGraph::reset() - { - passes.clear(); // Directly clear - } - - Pass& RenderGraph::addPass(const std::string& name) - { - passes.emplace_back(); - - auto& pass = passes.back(); - pass.name = name; - - return pass; - } - - Pass* RenderGraph::getPass(const std::string& name) - { - for (auto& p : passes) - { - if (p.name == name) - return &p; - } - - return nullptr; - } - - void RenderGraph::execute() - { - for (auto& pass : passes) - { - // Future dependency handling hook - for (const auto& dep : pass.dependencies) - { - (void)dep; - } - - sortPackets(pass.packets, pass.sortMode); - - rhi::applyView(pass.view, {pass.framebuffer, 0, 0, pass.width, pass.height, pass.clearFlags, pass.clearColor}); - - rhi::setViewProjection(pass.view, pass.viewMatrix, pass.projMatrix); - - if (pass.clearFlags) - { - bgfx::setViewClear(pass.view, pass.clearFlags, pass.clearColor); - } - - for (auto& pkt : pass.packets) - { - rhi::submit(pkt, pass.view); - } - } - } -} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cppm index a8616691..b70f0828 100644 --- a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cppm +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraph.cppm @@ -1,79 +1,888 @@ +// Draconic::RenderGraph - :graph partition +// +// The orchestrator. GPU work is declared as passes with resource accesses; the +// graph builds dependencies, culls unused work, topologically sorts, allocates/ +// aliases transient resources, inserts barriers (via BarrierSolver), and runs +// (RenderGraph.bf). compile() works without an encoder so graph logic is +// testable headless. + module; +#include #include +#include +#include +#include #include +#include + +export module rendergraph:graph; -export module rendering.rendergraph; +import core; +import rhi; +import :types; +import :descriptors; +import :callbacks; +import :resource; +import :persistent_resource; +import :pass; +import :pass_builder; +import :barrier_solver; +import :profiler; +import :transient_pool; -import core.stdtypes; -import rendering.rhi; +using namespace draco; -export namespace draco::rendering::rendergraph { +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; - enum class PassType : u8 + // NOTE: local stand-in. Replace with a core timing/clock utility once draco.core + // provides one (Sedulous's core::GetTicks). + // Monotonic CPU tick counter for the profiler (nanoseconds since the epoch). + [[nodiscard]] inline u64 getTicks() noexcept { - Graphics, - Transparent, - Shadow, - PostProcess, - UI - }; + return static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } - enum class SortMode : u8 + // getTicks() deltas are nanoseconds (steady_clock); convert to milliseconds for reports. + [[nodiscard]] inline f32 ticksToMilliseconds(u64 ticks) noexcept { - None, - Material, - FrontToBack, - BackToFront - }; + return static_cast(ticks) / 1000000.0f; + } - struct Pass + class RenderGraph { - std::string name; + public: + explicit RenderGraph(rhi::Device* device, RenderGraphConfig config = {}) + : m_device(device), m_config(config) + { + if (device != nullptr) { m_texturePool = std::make_unique(*device); } + const i32 slots = config.frameBufferCount > 0 ? config.frameBufferCount : 1; + for (i32 i = 0; i < slots; ++i) { m_deferredDeletions.push_back(std::vector{}); } + } - PassType type = PassType::Graphics; - SortMode sortMode = SortMode::Material; + // Turn on per-pass GPU timestamp profiling (lazy; needs the device). Idempotent. + void enableGpuProfiling() + { + if (m_gpuProfiler.get() != nullptr || m_device == nullptr) { return; } + m_gpuProfiler = std::make_unique(); + if (!m_gpuProfiler->init(*m_device).isOk()) { m_gpuProfiler.reset(); return; } + if (rhi::Queue* q = m_device->getQueue(rhi::QueueType::Graphics)) { m_gpuProfiler->setTimestampPeriod(q->timestampPeriod()); } + } - std::vector dependencies; + // The GPU profiler (null if not enabled). Read its results after the GPU has finished (e.g. + // after WaitIdle on a P-key dump). `LastProfiledPassCount` is how many passes were timed. + [[nodiscard]] GraphProfiler* gpuProfiler() noexcept { return m_gpuProfiler.get(); } + [[nodiscard]] i32 lastProfiledPassCount() const noexcept { return m_lastProfiledPassCount; } - rhi::ViewID view = 0; - rhi::FramebufferHandle framebuffer = rhi::InvalidFramebuffer; + // Aggregate this frame's per-pass CPU RECORD time by pass name (most-expensive first). The graph + // execute is often CPU-bound (recording/bundle build) while the GPU is idle - this shows where. + void appendCpuPassReport(std::u8string& out) const { + struct Agg { std::u8string_view name; u64 ticks = 0; i32 n = 0; }; + std::vector agg; u64 total = 0; + for (const PassCpu& p : m_passCpu) { + total += p.ticks; + bool found = false; + for (Agg& a : agg) { if (a.name == p.name) { a.ticks += p.ticks; ++a.n; found = true; break; } } + if (!found) { Agg a; a.name = p.name; a.ticks = p.ticks; a.n = 1; agg.push_back(a); } + } + for (usize i = 0; i < agg.size(); ++i) + for (usize j = i + 1; j < agg.size(); ++j) + if (agg[j].ticks > agg[i].ticks) { const Agg t = agg[i]; agg[i] = agg[j]; agg[j] = t; } + out.append(u8"=== CPU by pass name (record cost, expensive first) ===\n"); + for (const Agg& a : agg) { appendFormat(out, u8" {} ms (x{}) {}\n", ticksToMilliseconds(a.ticks), a.n, a.name); } + appendFormat(out, u8" --------\n {} ms TOTAL (pass record)\n", ticksToMilliseconds(total)); + } - std::vector packets; + ~RenderGraph() + { + for (std::vector& list : m_deferredDeletions) + { + for (DeferredDeletion& d : list) { d.execute(m_device); } + } + for (RenderGraphPass* pass : m_passes) { delete (pass); } + for (RenderGraphResource* res : m_resources) { if (res != nullptr) { delete (res); } } + } - f32 viewMatrix[16] = { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - }; - - f32 projMatrix[16] = { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - }; + RenderGraph(const RenderGraph&) = delete; + RenderGraph& operator=(const RenderGraph&) = delete; - u16 width = 0; - u16 height = 0; + // --- output dimensions (for SizeMode resolution) --- + void setOutputSize(u32 width, u32 height) noexcept { m_outputWidth = width; m_outputHeight = height; } + [[nodiscard]] u32 outputWidth() const noexcept { return m_outputWidth; } + [[nodiscard]] u32 outputHeight() const noexcept { return m_outputHeight; } - u32 clearFlags = 0; - u32 clearColor = 0; - }; + // --- frame lifecycle --- + void beginFrame(i32 frameIndex) + { + m_frameIndex = frameIndex; + flushDeferred(frameIndex); + clearPasses(); + recycleNonPersistent(); + m_isCompiled = false; + } - class RenderGraph - { - public: - void reset(); + // Compile only (cull, sort, allocate). Safe without an encoder (tests). + [[nodiscard]] Status compile() + { + if (m_passes.empty()) { return Status{}; } + + buildResourceReferences(); + cullPasses(); + buildDependencies(); + if (!topologicalSort().isOk()) { return Status{ ErrorCode::Unknown }; } + allocateTransientResources(); + + m_isCompiled = true; + return Status{}; + } + + // Compile (if needed) and execute into `encoder` (null = compile-only). + [[nodiscard]] Status execute(rhi::CommandEncoder* encoder) + { + if (!m_isCompiled) + { + if (!compile().isOk()) { return Status{ ErrorCode::Unknown }; } + } + if (encoder == nullptr) { return Status{}; } + + m_barrierSolver.reset(resourceSpan()); + + // GPU profiling: reset the timestamp pool up front (must be outside any render pass), + // then bracket each executed pass with begin/end timestamps. + const bool prof = (m_gpuProfiler.get() != nullptr); + if (prof) { m_gpuProfiler->beginFrame(*encoder); m_passCpu.clear(); } + i32 profiledPassCount = 0; + + for (i32 passIdx : m_executionOrder) + { + RenderGraphPass* pass = m_passes[static_cast(passIdx)]; + if (pass->isCulled) { continue; } + if (static_cast(pass->condition) && !pass->condition()) { continue; } + + // Per-pass CPU cost (barriers + record/execute, incl. any bundle build/wait): the graph's + // execute is often CPU-bound while the GPU is idle, so this shows which pass RECORDING is slow. + const u64 cpuStart = prof ? getTicks() : 0; + encoder->beginDebugLabel(pass->name); + if (prof) { m_gpuProfiler->beginPass(*encoder, profiledPassCount, pass->name); } + m_barrierSolver.emitBarriers(*pass, resourceSpan(), *encoder); + + switch (pass->type) + { + case RGPassType::Render: executeRenderPass(*pass, *encoder); break; + case RGPassType::Compute: executeComputePass(*pass, *encoder); break; + case RGPassType::Copy: executeCopyPass(*pass, *encoder); break; + } + + m_barrierSolver.emitReadableAfterWriteBarriers(*pass, resourceSpan(), *encoder); + if (prof) { m_gpuProfiler->endPass(*encoder, profiledPassCount); ++profiledPassCount; } + encoder->endDebugLabel(); + if (prof) { m_passCpu.push_back(PassCpu{ pass->name, getTicks() - cpuStart }); } + } - Pass& addPass(const std::string& name); + if (m_gpuProfiler.get() != nullptr) { m_gpuProfiler->resolve(*encoder, profiledPassCount); m_lastProfiledPassCount = profiledPassCount; } - Pass* getPass(const std::string& name); + m_barrierSolver.emitFinalTransitions(resourceSpan(), *encoder); + m_barrierSolver.updatePersistentStates(resourceSpan()); - void execute(); + // Defer-delete per-subresource views created this frame. + if (!m_subresourceViews.empty()) + { + std::vector& deletions = deferredSlot(); + for (rhi::TextureView* view : m_subresourceViews) + { + DeferredDeletion d{}; d.view = view; deletions.push_back(d); + } + m_subresourceViews.clear(); + } + + returnTransientResources(); + return Status{}; + } + + void endFrame() + { + m_isCompiled = false; + if (m_texturePool.get() != nullptr) { m_texturePool->endFrame(); } + } + + // Clear passes but keep persistent resource state (multi-view rendering). + void reset() + { + returnTransientResources(); + clearPasses(); + recycleNonPersistent(); + m_isCompiled = false; + } + + // --- resource creation --- + RGHandle createTransient(std::u8string_view name, RGTextureDesc desc) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Texture, RGResourceLifetime::Transient); + desc.resolve(m_outputWidth, m_outputHeight); + res->textureDesc = desc; + return addResource(res); + } + + RGHandle createTransientBuffer(std::u8string_view name, RGBufferDesc desc) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Buffer, RGResourceLifetime::Transient); + res->bufferDesc = desc; + return addResource(res); + } + + RGHandle registerPersistent(std::u8string_view name, rhi::Texture* texture, rhi::TextureView* view) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Texture, RGResourceLifetime::Persistent); + res->texture = texture; + res->textureView = view; + res->persistentData = std::make_unique(texture, view); + return addResource(res); + } + + RGHandle registerPersistentPingPong(std::u8string_view name, rhi::Texture* tex0, rhi::Texture* tex1, + rhi::TextureView* view0, rhi::TextureView* view1) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Texture, RGResourceLifetime::Persistent); + res->texture = tex0; + res->textureView = view0; + res->persistentData = std::make_unique(tex0, tex1, view0, view1); + return addResource(res); + } + + RGHandle importTarget(std::u8string_view name, rhi::Texture* texture, rhi::TextureView* view, + std::optional finalState = {}, + std::optional currentState = {}) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Texture, RGResourceLifetime::Imported); + res->texture = texture; + res->textureView = view; + res->finalState = finalState; + res->lastKnownState = currentState.has_value() ? currentState.value() + : (texture != nullptr ? texture->initialState : rhi::ResourceState::Undefined); + return addResource(res); + } + + // Depth import variant carrying a depth-only view for shader sampling. + RGHandle importTarget(std::u8string_view name, rhi::Texture* texture, rhi::TextureView* view, + rhi::TextureView* depthOnlyView, + std::optional finalState = {}, + std::optional currentState = {}) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Texture, RGResourceLifetime::Imported); + res->texture = texture; + res->textureView = view; + res->depthOnlyView = depthOnlyView; + res->finalState = finalState; + res->lastKnownState = currentState.has_value() ? currentState.value() + : (texture != nullptr ? texture->initialState : rhi::ResourceState::Undefined); + return addResource(res); + } + + RGHandle importBuffer(std::u8string_view name, rhi::Buffer* buffer) + { + RenderGraphResource* res = new RenderGraphResource(name, RGResourceType::Buffer, RGResourceLifetime::Imported); + res->buffer = buffer; + return addResource(res); + } + + void requireReadableAfterWrite(RGHandle handle) + { + if (RenderGraphResource* res = resolve(handle)) { res->readableAfterWrite = true; } + } + + // --- pass creation (setup callable receives PassBuilder&) --- + template + PassHandle addRenderPass(std::u8string_view name, Setup&& setup) { return addPassOfType(name, RGPassType::Render, setup); } + template + PassHandle addComputePass(std::u8string_view name, Setup&& setup) { return addPassOfType(name, RGPassType::Compute, setup); } + template + PassHandle addCopyPass(std::u8string_view name, Setup&& setup) { return addPassOfType(name, RGPassType::Copy, setup); } + + // --- resource access (during execute callbacks) --- + [[nodiscard]] rhi::Texture* getTexture(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + if (res == nullptr) { return nullptr; } + return res->persistentData.get() != nullptr ? res->persistentData->currentTexture() : res->texture; + } + [[nodiscard]] rhi::TextureView* getTextureView(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + if (res == nullptr) { return nullptr; } + return res->persistentData.get() != nullptr ? res->persistentData->currentView() : res->textureView; + } + + // A stable id for the GPU texture currently backing `handle`. For a transient it changes when + // the graph (re)allocates a different physical texture (e.g. on resize) - even if the new view + // happens to reuse a freed address. A bind-group cache over GetTextureView MUST also key on + // this to stay correct across resizes. 0 when unresolved. + [[nodiscard]] u64 getTextureGeneration(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + return res != nullptr ? res->textureGeneration : 0; + } + [[nodiscard]] rhi::TextureView* getDepthOnlyTextureView(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + return res != nullptr ? res->depthOnlyView : nullptr; + } + [[nodiscard]] rhi::Buffer* getBuffer(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + return res != nullptr ? res->buffer : nullptr; + } + + void swapPingPong(RGHandle handle) + { + RenderGraphResource* res = resolve(handle); + if (res != nullptr && res->persistentData.get() != nullptr) + { + res->persistentData->swap(); + res->texture = res->persistentData->currentTexture(); + res->textureView = res->persistentData->currentView(); + } + } + + // --- queries --- + [[nodiscard]] usize passCount() const noexcept { return m_passes.size(); } + [[nodiscard]] usize resourceCount() const noexcept + { + usize count = 0; + for (RenderGraphResource* r : m_resources) { if (r != nullptr) { ++count; } } + return count; + } + [[nodiscard]] usize culledPassCount() const noexcept + { + usize count = 0; + for (RenderGraphPass* p : m_passes) { if (p->isCulled) { ++count; } } + return count; + } + [[nodiscard]] RGHandle getResource(std::u8string_view name) const + { + for (usize i = 0; i < m_resources.size(); ++i) + { + RenderGraphResource* res = m_resources[i]; + if (res != nullptr && res->name == name) { return RGHandle{ static_cast(i), res->generation }; } + } + return RGHandle::invalid(); + } + [[nodiscard]] rhi::ResourceState getResourceState(RGHandle handle) + { + RenderGraphResource* res = resolveChecked(handle); + return res != nullptr ? res->lastKnownState : rhi::ResourceState::Undefined; + } + + [[nodiscard]] const std::vector& executionOrder() const noexcept { return m_executionOrder; } + [[nodiscard]] const std::vector& passes() const noexcept { return m_passes; } + [[nodiscard]] const std::vector& resources() const noexcept { return m_resources; } private: - std::vector passes; + struct DeferredDeletion + { + rhi::Texture* texture = nullptr; + rhi::TextureView* view = nullptr; + rhi::TextureView* view2 = nullptr; + rhi::Buffer* buffer = nullptr; + + void execute(rhi::Device* device) + { + if (device == nullptr) { return; } + if (view2 != nullptr) { device->destroyTextureView(view2); } + if (view != nullptr) { device->destroyTextureView(view); } + if (texture != nullptr) { device->destroyTexture(texture); } + if (buffer != nullptr) { device->destroyBuffer(buffer); } + } + }; + + [[nodiscard]] std::span resourceSpan() const + { + return std::span(m_resources.data(), m_resources.size()); + } + + [[nodiscard]] std::vector& deferredSlot() + { + const i32 slot = m_frameIndex % static_cast(m_deferredDeletions.size()); + return m_deferredDeletions[static_cast(slot)]; + } + + void flushDeferred(i32 frameIndex) + { + const i32 slot = frameIndex % static_cast(m_deferredDeletions.size()); + std::vector& deletions = m_deferredDeletions[static_cast(slot)]; + for (DeferredDeletion& d : deletions) { d.execute(m_device); } + deletions.clear(); + } + + // Validate a handle (bounds + generation); null on mismatch. + [[nodiscard]] RenderGraphResource* resolveChecked(RGHandle handle) + { + if (!handle.isValid() || handle.index >= m_resources.size()) { return nullptr; } + RenderGraphResource* res = m_resources[handle.index]; + if (res == nullptr || res->generation != handle.generation) { return nullptr; } + return res; + } + // Validate a handle (bounds only; ignores generation) - for mutators. + [[nodiscard]] RenderGraphResource* resolve(RGHandle handle) + { + if (!handle.isValid() || handle.index >= m_resources.size()) { return nullptr; } + return m_resources[handle.index]; + } + + RGHandle addResource(RenderGraphResource* res) + { + if (!m_freeResourceSlots.empty()) + { + const i32 idx = m_freeResourceSlots.back(); + m_freeResourceSlots.pop_back(); + m_resources[static_cast(idx)] = res; + return RGHandle{ static_cast(idx), res->generation }; + } + const u32 idx = static_cast(m_resources.size()); + m_resources.push_back(res); + return RGHandle{ idx, res->generation }; + } + + template + PassHandle addPassOfType(std::u8string_view name, RGPassType type, Setup&& setup) + { + RenderGraphPass* pass = new RenderGraphPass(name, type); + PassBuilder builder(*pass); + setup(builder); + const u32 idx = static_cast(m_passes.size()); + m_passes.push_back(pass); + return PassHandle{ idx }; + } + + void clearPasses() + { + for (RenderGraphPass* p : m_passes) { delete (p); } + m_passes.clear(); + m_executionOrder.clear(); + } + + void recycleNonPersistent() + { + for (usize i = 0; i < m_resources.size(); ++i) + { + RenderGraphResource* res = m_resources[i]; + if (res == nullptr) { continue; } + if (res->lifetime != RGResourceLifetime::Persistent) + { + m_freeResourceSlots.push_back(static_cast(i)); + delete (res); + m_resources[i] = nullptr; + } + else + { + res->resetTracking(); + } + } + } + + // === compilation pipeline === + void buildResourceReferences() + { + for (usize passIdx = 0; passIdx < m_passes.size(); ++passIdx) + { + RenderGraphPass* pass = m_passes[passIdx]; + const PassHandle passHandle{ static_cast(passIdx) }; + + for (const RGResourceAccess& access : pass->accesses) + { + if (!access.handle.isValid() || access.handle.index >= m_resources.size()) { continue; } + RenderGraphResource* res = m_resources[access.handle.index]; + if (res == nullptr) { continue; } + + ++res->refCount; + if (res->firstUsePass < 0 || static_cast(passIdx) < res->firstUsePass) { res->firstUsePass = static_cast(passIdx); } + if (static_cast(passIdx) > res->lastUsePass) { res->lastUsePass = static_cast(passIdx); } + if (access.isWrite()) { res->firstWriter = passHandle; } + if (access.isRead()) { res->lastReader = passHandle; } + } + } + } + + void cullPasses() + { + for (RenderGraphPass* pass : m_passes) { pass->isCulled = true; } + for (RenderGraphPass* pass : m_passes) { if (pass->shouldSurviveCulling()) { pass->isCulled = false; } } + + // Passes writing imported resources with a final state stay alive. + for (RenderGraphPass* pass : m_passes) + { + if (!pass->isCulled) { continue; } + std::vector outputs; + pass->getOutputs(outputs); + for (const RGResourceAccess& output : outputs) + { + if (output.handle.isValid() && output.handle.index < m_resources.size()) + { + RenderGraphResource* res = m_resources[output.handle.index]; + if (res != nullptr && res->finalState.has_value()) { pass->isCulled = false; break; } + } + } + } + + // Backward propagation: a live pass keeps its overlapping upstream writers alive. + bool changed = true; + while (changed) + { + changed = false; + for (RenderGraphPass* pass : m_passes) + { + if (pass->isCulled) { continue; } + std::vector inputs; + pass->getInputs(inputs); + + for (const RGResourceAccess& input : inputs) + { + if (!input.handle.isValid() || input.handle.index >= m_resources.size()) { continue; } + for (usize i = m_passes.size(); i-- > 0;) + { + RenderGraphPass* candidate = m_passes[i]; + if (!candidate->isCulled) { continue; } + std::vector candidateOutputs; + candidate->getOutputs(candidateOutputs); + for (const RGResourceAccess& output : candidateOutputs) + { + if (output.handle == input.handle + && (input.subresource.isAll() || output.subresource.isAll() + || input.subresource.overlaps(output.subresource))) + { + candidate->isCulled = false; + changed = true; + } + } + } + } + } + } + } + + void buildDependencies() + { + for (usize passIdx = 0; passIdx < m_passes.size(); ++passIdx) + { + RenderGraphPass* pass = m_passes[passIdx]; + if (pass->isCulled) { continue; } + + std::vector readAccesses; + pass->getInputs(readAccesses); + + for (const RGResourceAccess& readAccess : readAccesses) + { + if (!readAccess.handle.isValid() || readAccess.handle.index >= m_resources.size()) { continue; } + RenderGraphResource* res = m_resources[readAccess.handle.index]; + const u32 totalMips = res != nullptr ? res->totalMipLevels() : 1u; + const u32 totalLayers = res != nullptr ? res->totalArrayLayers() : 1u; + + for (usize j = passIdx; j-- > 0;) + { + RenderGraphPass* writer = m_passes[j]; + if (writer->isCulled) { continue; } + std::vector writerOutputs; + writer->getOutputs(writerOutputs); + + bool overlaps = false; + for (const RGResourceAccess& writerAccess : writerOutputs) + { + if (writerAccess.handle == readAccess.handle + && (readAccess.subresource.isAll() || writerAccess.subresource.isAll() + || readAccess.subresource.overlaps(writerAccess.subresource, totalMips, totalLayers))) + { + overlaps = true; + break; + } + } + if (overlaps) { addDependencyIfNew(*pass, PassHandle{ static_cast(j) }); break; } + } + } + } + } + + static void addDependencyIfNew(RenderGraphPass& pass, PassHandle dep) + { + for (PassHandle existing : pass.dependencies) { if (existing == dep) { return; } } + pass.dependencies.push_back(dep); + } + + [[nodiscard]] Status topologicalSort() + { + m_executionOrder.clear(); + const usize passCount = m_passes.size(); + + std::vector inDegree; + inDegree.resize(passCount); + std::vector> adjacency; + for (usize i = 0; i < passCount; ++i) { adjacency.push_back(std::vector{}); } + + for (usize i = 0; i < passCount; ++i) + { + RenderGraphPass* pass = m_passes[i]; + if (pass->isCulled) { continue; } + for (PassHandle dep : pass->dependencies) + { + if (dep.isValid() && dep.index < passCount) + { + adjacency[dep.index].push_back(static_cast(i)); + ++inDegree[i]; + } + } + } + + std::vector queue; + for (usize i = 0; i < passCount; ++i) + { + if (!m_passes[i]->isCulled && inDegree[i] == 0) { queue.push_back(static_cast(i)); } + } + + while (!queue.empty()) + { + const i32 node = queue[0]; + queue.erase(queue.begin()); + m_executionOrder.push_back(node); + m_passes[static_cast(node)]->executionOrder = static_cast(m_executionOrder.size()) - 1; + + for (i32 neighbor : adjacency[static_cast(node)]) + { + if (--inDegree[static_cast(neighbor)] == 0) { queue.push_back(neighbor); } + } + } + + usize nonCulled = 0; + for (RenderGraphPass* p : m_passes) { if (!p->isCulled) { ++nonCulled; } } + return m_executionOrder.size() == nonCulled ? Status{} : Status{ ErrorCode::Unknown }; // cycle + } + + void allocateTransientResources() + { + for (RenderGraphResource* res : m_resources) + { + if (res == nullptr || res->lifetime != RGResourceLifetime::Transient || res->refCount == 0) { continue; } + + if (res->resourceType == RGResourceType::Texture && m_device != nullptr) + { + rhi::TextureDesc rhiDesc = res->textureDesc.toTextureDesc(res->name); + rhi::Texture* tex = nullptr; + rhi::TextureView* view = nullptr; + u64 pooledGen = 0; + if (m_texturePool.get() != nullptr && m_texturePool->tryAcquire(rhiDesc, tex, view, pooledGen)) + { + res->texture = tex; + res->textureView = view; + res->textureGeneration = pooledGen; // reused physical texture keeps its id + if (rhi::isDepthFormat(res->textureDesc.format) && rhi::hasStencil(res->textureDesc.format)) + { + rhi::TextureViewDesc depthDesc{}; + depthDesc.aspect = rhi::TextureAspect::DepthOnly; + depthDesc.label = u8"RGDepthOnlyView"; + rhi::TextureView* depthOnly = nullptr; + if (m_device->createTextureView(tex, depthDesc, depthOnly).isOk()) { res->depthOnlyView = depthOnly; } + } + } + else + { + (void)res->allocateTexture(*m_device); + res->textureGeneration = ++m_nextTransientGeneration; // freshly created -> new id + } + } + else if (res->resourceType == RGResourceType::Buffer && m_device != nullptr) + { + (void)res->allocateBuffer(*m_device); + } + } + } + + void returnTransientResources() + { + std::vector& deletions = deferredSlot(); + for (RenderGraphResource* res : m_resources) + { + if (res == nullptr || res->lifetime != RGResourceLifetime::Transient) { continue; } + + if (res->resourceType == RGResourceType::Texture && res->texture != nullptr) + { + if (m_texturePool.get() != nullptr) + { + const rhi::TextureDesc rhiDesc = res->textureDesc.toTextureDesc(res->name); + m_texturePool->returnToPool(rhiDesc, res->texture, res->textureView, res->textureGeneration); + if (res->depthOnlyView != nullptr) { DeferredDeletion d{}; d.view = res->depthOnlyView; deletions.push_back(d); } + } + else + { + DeferredDeletion d{}; d.texture = res->texture; d.view = res->textureView; d.view2 = res->depthOnlyView; + deletions.push_back(d); + } + res->texture = nullptr; + res->textureView = nullptr; + res->depthOnlyView = nullptr; + } + else if (res->resourceType == RGResourceType::Buffer && res->buffer != nullptr) + { + DeferredDeletion d{}; d.buffer = res->buffer; deletions.push_back(d); + res->buffer = nullptr; + } + } + } + + // === pass execution === + rhi::TextureView* createSubresourceView(RGHandle handle, RGSubresourceRange subresource) + { + if (m_device == nullptr) { return nullptr; } + rhi::Texture* texture = getTexture(handle); + if (texture == nullptr) { return nullptr; } + + rhi::TextureViewDesc viewDesc{}; + viewDesc.baseMipLevel = subresource.baseMipLevel; + viewDesc.mipLevelCount = subresource.mipLevelCount == 0 ? 1u : subresource.mipLevelCount; + viewDesc.baseArrayLayer = subresource.baseArrayLayer; + viewDesc.arrayLayerCount = subresource.arrayLayerCount == 0 ? 1u : subresource.arrayLayerCount; + viewDesc.dimension = viewDesc.arrayLayerCount == 1 ? rhi::TextureViewDimension::Texture2D + : rhi::TextureViewDimension::Texture2DArray; + rhi::TextureView* view = nullptr; + if (m_device->createTextureView(texture, viewDesc, view).isOk()) + { + m_subresourceViews.push_back(view); + return view; + } + return nullptr; + } + + // The full-target render area for a pass, from an attachment's resolved dimensions (the + // transient desc, else the backing texture). Used to set the parent pass's viewport + + // scissor before ExecuteBundles: render bundles INHERIT viewport/scissor from the parent + // pass (WebGPU + DX12 bundles cannot set them), so the parent must. Returns false if no + // attachment yields dimensions. + [[nodiscard]] bool passRenderArea(RenderGraphPass& pass, u32& outW, u32& outH) + { + auto fromHandle = [&](RGHandle h, u32& w, u32& h2) -> bool { + if (RenderGraphResource* res = resolve(h)) { + if (res->textureDesc.width > 0 && res->textureDesc.height > 0) { + w = res->textureDesc.width; h2 = res->textureDesc.height; return true; + } + } + if (rhi::TextureView* v = getTextureView(h)) { + if (v->texture != nullptr && v->texture->desc.width > 0 && v->texture->desc.height > 0) { + w = v->texture->desc.width; h2 = v->texture->desc.height; return true; + } + } + return false; + }; + for (const RGColorTarget& ct : pass.colorTargets) { if (fromHandle(ct.handle, outW, outH)) { return true; } } + if (pass.depthTarget.has_value()) { if (fromHandle(pass.depthTarget.value().handle, outW, outH)) { return true; } } + return false; + } + + void executeRenderPass(RenderGraphPass& pass, rhi::CommandEncoder& encoder) + { + const bool hasBundles = static_cast(pass.bundleCallback); + if (!static_cast(pass.executeCallback) && !hasBundles) { return; } + + // A bundle pass records its bundles NOW (encoder in recording state, before the pass + // begins); the graph then begins with secondary contents + replays them. Done before + // building the pass desc so the encoder is still recording. + std::vector bundles; + if (hasBundles) { pass.bundleCallback(encoder, bundles); } + + rhi::RenderPassDesc rpDesc{}; + rpDesc.label = pass.name; + if (hasBundles) { rpDesc.contents = rhi::RenderPassContents::SecondaryCommandBuffers; } + + for (usize i = 0; i < pass.colorTargets.size(); ++i) + { + const RGColorTarget& ct = pass.colorTargets[i]; + rhi::TextureView* view = getTextureView(ct.handle); + if (view == nullptr) { continue; } + if (!ct.subresource.isAll()) + { + if (rhi::TextureView* subView = createSubresourceView(ct.handle, ct.subresource)) { view = subView; } + } + rhi::ColorAttachment attachment{}; + attachment.view = view; + attachment.loadOp = ct.loadOp; + attachment.storeOp = ct.storeOp; + attachment.clearValue = ct.clearValue; + rpDesc.colorAttachments.push_back(attachment); + } + + if (pass.depthTarget.has_value()) + { + const RGDepthTarget& dt = pass.depthTarget.value(); + rhi::TextureView* view = getTextureView(dt.handle); + if (view != nullptr) + { + if (!dt.subresource.isAll()) + { + if (rhi::TextureView* subView = createSubresourceView(dt.handle, dt.subresource)) { view = subView; } + } + rhi::DepthStencilAttachment dsa{}; + dsa.view = view; + dsa.depthLoadOp = dt.depthLoadOp; + dsa.depthStoreOp = dt.depthStoreOp; + dsa.depthClearValue = dt.depthClearValue; + dsa.depthReadOnly = dt.readOnly; + dsa.stencilLoadOp = dt.stencilLoadOp; + dsa.stencilStoreOp = dt.stencilStoreOp; + dsa.stencilClearValue = dt.stencilClearValue; + rpDesc.depthStencilAttachment = dsa; + } + } + + rhi::RenderPassEncoder* rp = encoder.beginRenderPass(rpDesc); + // Viewport/scissor: a per-pass override (split-screen sub-rect) if set, else the full + // attachment. Set here for bundle passes (bundles inherit it from the parent - WebGPU/ + // DX12 can't set it inside a bundle); a plain execute callback may also rely on it. + i32 vpX = 0, vpY = 0; u32 vpW = 0, vpH = 0; + if (pass.hasViewport) { vpX = pass.viewportX; vpY = pass.viewportY; vpW = pass.viewportW; vpH = pass.viewportH; } + else { (void)passRenderArea(pass, vpW, vpH); } + if (vpW > 0 && vpH > 0) { + rp->setViewport(static_cast(vpX), static_cast(vpY), static_cast(vpW), static_cast(vpH)); + rp->setScissor(vpX, vpY, vpW, vpH); + } + if (hasBundles) { + if (!bundles.empty()) { + rp->executeBundles(std::span{ bundles.data(), bundles.size() }); + } + } else { + pass.executeCallback(*rp); + } + rp->end(); + } + + void executeComputePass(RenderGraphPass& pass, rhi::CommandEncoder& encoder) + { + if (!static_cast(pass.computeCallback)) { return; } + rhi::ComputePassEncoder* cp = encoder.beginComputePass(pass.name); + pass.computeCallback(*cp); + cp->end(); + } + + void executeCopyPass(RenderGraphPass& pass, rhi::CommandEncoder& encoder) + { + if (!static_cast(pass.copyCallback)) { return; } + pass.copyCallback(encoder); + } + + rhi::Device* m_device; + RenderGraphConfig m_config; + std::vector m_resources; + std::vector m_freeResourceSlots; + std::vector m_passes; + std::vector m_executionOrder; + bool m_isCompiled = false; + std::unique_ptr m_gpuProfiler; // optional per-pass GPU timing + i32 m_lastProfiledPassCount = 0; + struct PassCpu { std::u8string_view name; u64 ticks = 0; }; // per-pass CPU record time (name -> pass->name, valid pre-Reset) + std::vector m_passCpu; + BarrierSolver m_barrierSolver; + std::unique_ptr m_texturePool; + std::vector> m_deferredDeletions; + std::vector m_subresourceViews; + i32 m_frameIndex = 0; + u32 m_outputWidth = 1920; + u32 m_outputHeight = 1080; + u64 m_nextTransientGeneration = 0; // monotonic id stamped on each freshly created transient texture }; } diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphModule.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphModule.cppm new file mode 100644 index 00000000..45727967 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphModule.cppm @@ -0,0 +1,23 @@ +// Draconic::RenderGraph - the `rendergraph` module. +// +// A render graph over the RHI: passes declare resource accesses, the graph +// resolves dependencies, allocates/aliases transient resources, and inserts the +// the same one Draconic's RHI is a faithful port of). One named module composed of +// partitions, re-exported here. + +export module rendergraph; + +export import :types; +export import :callbacks; +export import :descriptors; +export import :persistent_resource; +export import :resource; +export import :pass; +export import :state_tracker; +export import :barrier_solver; +export import :pass_builder; +export import :transient_pool; +export import :graph; +export import :validator; +export import :debug; +export import :profiler; diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphPass.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphPass.cppm new file mode 100644 index 00000000..0dd4242a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphPass.cppm @@ -0,0 +1,124 @@ +// Draconic::RenderGraph - :pass partition +// +// A single pass: its declared resource accesses, attachments, dependencies, and +// typed execute callback. GetInputs/GetOutputs fold attachment load/store ops +// into the read/write access set the compiler reasons about. Ported from +// Sedulous.RenderGraph (RenderGraphPass.bf). + +module; + +#include +#include +#include +#include +#include + +export module rendergraph:pass; + +import core; +import rhi; +import :types; +import :descriptors; +import :callbacks; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class RenderGraphPass + { + public: + RenderGraphPass(std::u8string_view passName, RGPassType passType) + : name(passName), type(passType) {} + + // Explicitly defaulted so the (move-only, due to Function members) special + // members are synthesized in this module and usable by importers - GCC's + // module support otherwise reports the implicit destructor as deleted. + ~RenderGraphPass() = default; + RenderGraphPass(RenderGraphPass&&) = default; + RenderGraphPass& operator=(RenderGraphPass&&) = default; + RenderGraphPass(const RenderGraphPass&) = delete; + RenderGraphPass& operator=(const RenderGraphPass&) = delete; + + // Resource handles this pass reads (declared accesses + Load attachments). + void getInputs(std::vector& out) const + { + for (const RGResourceAccess& access : accesses) + { + if (access.isRead()) { out.push_back(access); } + } + for (const RGColorTarget& ct : colorTargets) + { + if (ct.loadOp == rhi::LoadOp::Load) + { + out.push_back(RGResourceAccess{ ct.handle, RGAccessType::ReadTexture, ct.subresource }); + } + } + if (depthTarget.has_value()) + { + const RGDepthTarget& dt = depthTarget.value(); + if (dt.depthLoadOp == rhi::LoadOp::Load || dt.readOnly) + { + out.push_back(RGResourceAccess{ dt.handle, RGAccessType::ReadDepthStencil, dt.subresource }); + } + } + } + + // Resource handles this pass writes (declared accesses + Store attachments). + void getOutputs(std::vector& out) const + { + for (const RGResourceAccess& access : accesses) + { + if (access.isWrite()) { out.push_back(access); } + } + for (const RGColorTarget& ct : colorTargets) + { + if (ct.storeOp == rhi::StoreOp::Store) + { + out.push_back(RGResourceAccess{ ct.handle, RGAccessType::WriteColorTarget, ct.subresource }); + } + } + if (depthTarget.has_value()) + { + const RGDepthTarget& dt = depthTarget.value(); + if (dt.depthStoreOp == rhi::StoreOp::Store && !dt.readOnly) + { + out.push_back(RGResourceAccess{ dt.handle, RGAccessType::WriteDepthTarget, dt.subresource }); + } + } + } + + [[nodiscard]] bool shouldSurviveCulling() const noexcept { return neverCull || hasSideEffects; } + + // --- identity --- + std::u8string name; + RGPassType type; + rhi::QueueType queueType = rhi::QueueType::Graphics; + + // --- declared work --- + std::vector accesses; + std::vector colorTargets; + std::optional depthTarget; + std::vector dependencies; + + // --- optional per-pass viewport/scissor override (else the full attachment is used) --- + bool hasViewport = false; + i32 viewportX = 0, viewportY = 0; + u32 viewportW = 0, viewportH = 0; + + // --- compile flags --- + bool isCulled = false; + bool neverCull = false; + bool hasSideEffects = false; + std::function condition; // optional runtime skip condition + i32 executionOrder = -1; // assigned during topological sort + + // --- typed execute callbacks (one is set per pass type) --- + RenderPassExecuteCallback executeCallback; + RenderBundlePassCallback bundleCallback; // render pass whose body is executed bundles + ComputePassExecuteCallback computeCallback; + CopyPassExecuteCallback copyCallback; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphResource.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphResource.cppm new file mode 100644 index 00000000..0efb2e7a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphResource.cppm @@ -0,0 +1,146 @@ +// Draconic::RenderGraph - :resource partition +// +// A resource managed by the graph (texture or buffer): its descriptor, the +// allocated GPU object, reference/lifetime tracking computed during compile, and +// Fields are public data the graph orchestrator manipulates directly. + +module; + +#include +#include +#include +#include + +export module rendergraph:resource; + +import core; +import rhi; +import :types; +import :descriptors; +import :persistent_resource; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class RenderGraphResource + { + public: + RenderGraphResource(std::u8string_view resourceName, RGResourceType type, RGResourceLifetime life) + : name(resourceName), resourceType(type), lifetime(life) {} + + // Allocate GPU resources for a transient texture. + [[nodiscard]] Status allocateTexture(rhi::Device& device) + { + rhi::TextureDesc rhiDesc = textureDesc.toTextureDesc(name); + rhiDesc.usage = rhiDesc.usage | rhi::TextureUsage::Sampled; // may be sampled + if (rhi::isDepthFormat(textureDesc.format)) { rhiDesc.usage = rhiDesc.usage | rhi::TextureUsage::DepthStencil; } + else { rhiDesc.usage = rhiDesc.usage | rhi::TextureUsage::RenderTarget; } + + rhi::Texture* tex = nullptr; + if (!device.createTexture(rhiDesc, tex).isOk()) { return Status{ ErrorCode::Unknown }; } + texture = tex; + lastKnownState = tex->initialState; + + rhi::TextureView* view = nullptr; + if (!device.createTextureView(tex, rhi::TextureViewDesc{}, view).isOk()) { return Status{ ErrorCode::Unknown }; } + textureView = view; + + // Depth-only view for depth/stencil textures (shader sampling of depth). + if (rhi::isDepthFormat(textureDesc.format) && rhi::hasStencil(textureDesc.format)) + { + rhi::TextureViewDesc depthDesc{}; + depthDesc.aspect = rhi::TextureAspect::DepthOnly; + depthDesc.label = u8"RGDepthOnlyView"; + rhi::TextureView* depthOnly = nullptr; + if (!device.createTextureView(tex, depthDesc, depthOnly).isOk()) { return Status{ ErrorCode::Unknown }; } + depthOnlyView = depthOnly; + } + return Status{}; + } + + // Allocate GPU resources for a transient buffer. + [[nodiscard]] Status allocateBuffer(rhi::Device& device) + { + rhi::BufferDesc rhiDesc{}; + rhiDesc.size = bufferDesc.size; + rhiDesc.usage = bufferDesc.usage; + rhiDesc.label = name; + + rhi::Buffer* buf = nullptr; + if (!device.createBuffer(rhiDesc, buf).isOk()) { return Status{ ErrorCode::Unknown }; } + buffer = buf; + lastKnownState = rhi::ResourceState::Undefined; + return Status{}; + } + + // Release GPU resources for a transient resource (no-op otherwise). + void releaseTransient(rhi::Device& device) + { + if (lifetime != RGResourceLifetime::Transient) { return; } + if (depthOnlyView != nullptr) { device.destroyTextureView(depthOnlyView); } + if (textureView != nullptr) { device.destroyTextureView(textureView); } + if (texture != nullptr) { device.destroyTexture(texture); } + if (buffer != nullptr) { device.destroyBuffer(buffer); } + } + + [[nodiscard]] u32 totalMipLevels() const + { + if (texture != nullptr) { return texture->desc.mipLevelCount; } + if (resourceType == RGResourceType::Texture) { return textureDesc.mipLevelCount; } + return 1; + } + [[nodiscard]] u32 totalArrayLayers() const + { + if (texture != nullptr) { return texture->desc.arrayLayerCount; } + if (resourceType == RGResourceType::Texture) { return textureDesc.arrayLayerCount; } + return 1; + } + + void resetTracking() + { + refCount = 0; + firstWriter = PassHandle::invalid(); + lastReader = PassHandle::invalid(); + firstUsePass = -1; + lastUsePass = -1; + } + + // --- identity / lifetime --- + std::u8string name; + RGResourceType resourceType; + RGResourceLifetime lifetime; + u32 generation = 1; + + // --- reference tracking (computed during compile) --- + i32 refCount = 0; + PassHandle firstWriter = PassHandle::invalid(); + PassHandle lastReader = PassHandle::invalid(); + i32 firstUsePass = -1; // for aliasing + i32 lastUsePass = -1; + + // --- texture data --- + RGTextureDesc textureDesc; + rhi::Texture* texture = nullptr; + rhi::TextureView* textureView = nullptr; + rhi::TextureView* depthOnlyView = nullptr; + // Stable id of the backing physical texture; changes when a transient is (re)allocated a + // different texture (e.g. on resize). Consumers caching a bind group over textureView key on + // this so a reused-address view does not alias a stale, destroyed texture. + u64 textureGeneration = 0; + + // --- buffer data --- + RGBufferDesc bufferDesc; + rhi::Buffer* buffer = nullptr; + + // --- state tracking --- + rhi::ResourceState lastKnownState = rhi::ResourceState::Undefined; + std::optional finalState; // transition-to after last use (imported) + bool readableAfterWrite = false; // transition to ShaderRead after last writer + + // --- persistent data (null for transient/imported) --- + std::unique_ptr persistentData; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphTests.test.cpp new file mode 100644 index 00000000..cc7552f4 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/RenderGraphTests.test.cpp @@ -0,0 +1,87 @@ +// Direct unit tests for pieces without a dedicated Sedulous test file +// (SubresourceStateTracker, PersistentResource ping-pong, resource tracking). +// The Sedulous suite is ported in Type/Descriptor/PassBuilder/Dependency/ +// Culling/GraphCore/Barrier Tests. + +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +TEST_CASE("rg.persistent: ping-pong swap") +{ + auto* a = reinterpret_cast(0x10); + auto* b = reinterpret_cast(0x20); + auto* va = reinterpret_cast(0x30); + auto* vb = reinterpret_cast(0x40); + + PersistentResource single(a, va); + CHECK_FALSE(single.isPingPong()); + CHECK(single.currentTexture() == a); + CHECK(single.previousTexture() == a); + single.swap(); + CHECK(single.currentTexture() == a); + + PersistentResource pp(a, b, va, vb); + CHECK(pp.isPingPong()); + CHECK(pp.currentTexture() == a); + CHECK(pp.previousTexture() == b); + pp.swap(); + CHECK(pp.currentTexture() == b); + CHECK(pp.previousTexture() == a); +} + +TEST_CASE("rg.resource: tracking + totals from descriptor") +{ + RenderGraphResource res(u8"gbuffer", RGResourceType::Texture, RGResourceLifetime::Transient); + res.textureDesc.mipLevelCount = 4; + res.textureDesc.arrayLayerCount = 6; + + CHECK(res.totalMipLevels() == 4u); // no GPU texture -> from descriptor + CHECK(res.totalArrayLayers() == 6u); + + res.refCount = 3; + res.firstUsePass = 2; + res.resetTracking(); + CHECK(res.refCount == 0); + CHECK(res.firstUsePass == -1); + CHECK_FALSE(res.firstWriter.isValid()); + CHECK_FALSE(res.finalState.has_value()); +} + +TEST_CASE("rg.state_tracker: uniform fast path, divergence, collapse") +{ + using RS = rhi::ResourceState; + SubresourceStateTracker t(4, 2, RS::Undefined); + CHECK(t.isUniform()); + CHECK(t.getState(0, 0) == RS::Undefined); + + t.setState(RGSubresourceRange::all(), RS::ShaderRead); + CHECK(t.isUniform()); + CHECK(t.getState(3, 1) == RS::ShaderRead); + + t.setState(0, 1, 0, 1, RS::RenderTarget); + CHECK_FALSE(t.isUniform()); + CHECK(t.getState(0, 0) == RS::RenderTarget); + CHECK(t.getState(1, 0) == RS::ShaderRead); + + std::vector snapshot = t.copyStates(); + CHECK(snapshot.size() == 8u); + + t.setAll(RS::ShaderRead); + CHECK(t.isUniform()); + + SubresourceStateTracker restored(4, 2, RS::Undefined); + restored.initFromStates(snapshot, RS::Undefined); + CHECK_FALSE(restored.isUniform()); + CHECK(restored.getState(0, 0) == RS::RenderTarget); + + restored.setState(0, 1, 0, 1, RS::ShaderRead); + CHECK(restored.isUniform()); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/SubresourceStateTracker.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/SubresourceStateTracker.cppm new file mode 100644 index 00000000..cfc439eb --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/SubresourceStateTracker.cppm @@ -0,0 +1,141 @@ +// Draconic::RenderGraph - :state_tracker partition +// +// Tracks ResourceState per subresource (mip x layer) with a uniform fast path: +// while all subresources share a state, only one value is stored; a per- +// subresource array is materialized lazily when states diverge, and collapsed +// (SubresourceStateTracker.bf); Beef's null-means-uniform becomes an empty Array. + +module; + +#include + +#include + +export module rendergraph:state_tracker; + +import core; +import rhi; +import :types; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class SubresourceStateTracker + { + public: + SubresourceStateTracker(u32 mipCount, u32 layerCount, rhi::ResourceState initialState) + : m_mipCount(std::max(mipCount, 1u)), m_layerCount(std::max(layerCount, 1u)), m_uniformState(initialState) {} + + [[nodiscard]] u32 mipCount() const noexcept { return m_mipCount; } + [[nodiscard]] u32 layerCount() const noexcept { return m_layerCount; } + [[nodiscard]] bool isUniform() const noexcept { return m_states.empty(); } + [[nodiscard]] rhi::ResourceState uniformState() const noexcept { return m_uniformState; } + + [[nodiscard]] rhi::ResourceState getState(u32 mip, u32 layer) const + { + if (m_states.empty()) { return m_uniformState; } + const u32 idx = mip + layer * m_mipCount; + if (idx >= m_states.size()) { return m_uniformState; } + return m_states[idx]; + } + + // count of 0 or ~0u means "all remaining from base". + void setState(u32 baseMip, u32 mipCount, u32 baseLayer, u32 layerCount, rhi::ResourceState state) + { + const u32 mipEnd = resolveEnd(baseMip, mipCount, m_mipCount); + const u32 layerEnd = resolveEnd(baseLayer, layerCount, m_layerCount); + + // Whole resource? Collapse to uniform. + if (baseMip == 0 && mipEnd >= m_mipCount && baseLayer == 0 && layerEnd >= m_layerCount) + { + m_uniformState = state; + m_states.clear(); + return; + } + + // Materialize per-subresource storage on first divergence. + if (m_states.empty()) + { + if (state == m_uniformState) { return; } + m_states.resize(m_mipCount * m_layerCount); + for (u32 i = 0; i < m_states.size(); ++i) { m_states[i] = m_uniformState; } + } + + for (u32 layer = baseLayer; layer < layerEnd; ++layer) + { + for (u32 mip = baseMip; mip < mipEnd; ++mip) + { + m_states[mip + layer * m_mipCount] = state; + } + } + + tryCollapseToUniform(); + } + + void setState(RGSubresourceRange range, rhi::ResourceState state) + { + const u32 mipCount = range.mipLevelCount == 0 ? 0xFFFFFFFFu : range.mipLevelCount; + const u32 layerCount = range.arrayLayerCount == 0 ? 0xFFFFFFFFu : range.arrayLayerCount; + setState(range.baseMipLevel, mipCount, range.baseArrayLayer, layerCount, state); + } + + void setAll(rhi::ResourceState state) + { + m_uniformState = state; + m_states.clear(); + } + + // Snapshot per-subresource states; empty if uniform. Caller owns the copy. + [[nodiscard]] std::vector copyStates() const + { + std::vector copy; + if (!m_states.empty()) + { + copy.resize(m_states.size()); + for (u32 i = 0; i < m_states.size(); ++i) { copy[i] = m_states[i]; } + } + return copy; + } + + // Restore from a per-subresource snapshot (empty/mismatched => uniform fallback). + void initFromStates(const std::vector& states, rhi::ResourceState uniformFallback) + { + if (states.size() != static_cast(m_mipCount) * m_layerCount) + { + m_uniformState = uniformFallback; + m_states.clear(); + return; + } + m_states.resize(states.size()); + for (usize i = 0; i < states.size(); ++i) { m_states[i] = states[i]; } + tryCollapseToUniform(); + } + + private: + void tryCollapseToUniform() + { + if (m_states.empty()) { return; } + const rhi::ResourceState first = m_states[0]; + for (usize i = 1; i < m_states.size(); ++i) + { + if (m_states[i] != first) { return; } + } + m_uniformState = first; + m_states.clear(); + } + + static u32 resolveEnd(u32 base, u32 count, u32 total) noexcept + { + if (count == 0 || count == 0xFFFFFFFFu) { return total; } + return std::min(base + count, total); + } + + u32 m_mipCount; + u32 m_layerCount; + rhi::ResourceState m_uniformState; + std::vector m_states; // empty => uniform + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/TransientGenerationTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/TransientGenerationTests.test.cpp new file mode 100644 index 00000000..ea09906a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/TransientGenerationTests.test.cpp @@ -0,0 +1,110 @@ +// Transient texture generation: each transient carries a stable id for its backing physical texture, +// surfaced via GetTextureGeneration. A bind-group cache over a transient's view keys on this (not the +// raw pointer) so a reused-address view can't alias a stale, destroyed texture across a resize. +// Driven on the Null RHI so Execute actually allocates/returns the transient. +#include + +import core; +import rhi; +import rhi.null; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace { +struct Harness { + rhi::null::NullDevice device; + rhi::Texture* bb = nullptr; + rhi::TextureView* bbView = nullptr; + rhi::CommandPool* pool = nullptr; + rhi::CommandEncoder* enc = nullptr; + + bool init() { + if (!device.createTexture(rhi::TextureDesc::renderTarget(rhi::TextureFormat::BGRA8Unorm, 64, 64), bb).isOk()) { return false; } + rhi::TextureViewDesc vd{}; vd.format = rhi::TextureFormat::BGRA8Unorm; + if (!device.createTextureView(bb, vd, bbView).isOk()) { return false; } + if (!device.createCommandPool(rhi::QueueType::Graphics, pool).isOk()) { return false; } + return pool->createEncoder(enc).isOk(); + } + ~Harness() { + if (pool) { device.destroyCommandPool(pool); } + if (bbView) { device.destroyTextureView(bbView); } + if (bb) { device.destroyTexture(bb); } + } +}; + +// One frame: a transient HDR target written by one pass and read by a backbuffer pass (so it isn't +// culled). Captures the transient's generation + view inside the writing pass's execute. +void runFrame(RenderGraph& graph, Harness& h, i32 frameIndex, u64& outGen, rhi::TextureView*& outView) { + graph.setOutputSize(64, 64); + graph.beginFrame(frameIndex); + const RGHandle bbH = graph.importTarget(u8"BB", h.bb, h.bbView, rhi::ResourceState::Present); + const RGHandle hdr = graph.createTransient(u8"HDR", RGTextureDesc(rhi::TextureFormat::RGBA16Float, 64, 64)); + graph.addRenderPass(u8"WriteHDR", [&](PassBuilder& b) { + b.setColorTarget(0, hdr, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + b.setExecute([&](rhi::RenderPassEncoder&) { + outGen = graph.getTextureGeneration(hdr); + outView = graph.getTextureView(hdr); + }); + }); + graph.addRenderPass(u8"ReadHDR", [&](PassBuilder& b) { + b.setColorTarget(0, bbH, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.readTexture(hdr); + b.neverCull(); + b.setExecute([](rhi::RenderPassEncoder&) {}); + }); + CHECK(graph.execute(h.enc).isOk()); + graph.endFrame(); +} +} + +TEST_CASE("rg.transient: generation is non-zero and stable across pool reuse") +{ + Harness h; REQUIRE(h.init()); + RenderGraph graph(&h.device); + + u64 gen0 = 0, gen1 = 0; + rhi::TextureView* v0 = nullptr; rhi::TextureView* v1 = nullptr; + runFrame(graph, h, 0, gen0, v0); + runFrame(graph, h, 1, gen1, v1); + + CHECK(gen0 != 0); // a freshly allocated transient gets a real id + CHECK(v0 != nullptr); + CHECK(gen1 == gen0); // same desc -> pool reuse -> SAME physical texture -> stable generation + CHECK(v1 == v0); // the same pooled view comes back (the case the cache optimizes for) +} + +TEST_CASE("rg.transient: distinct transients get distinct generations") +{ + Harness h; REQUIRE(h.init()); + RenderGraph graph(&h.device); + graph.setOutputSize(64, 64); + graph.beginFrame(0); + + const RGHandle bbH = graph.importTarget(u8"BB", h.bb, h.bbView, rhi::ResourceState::Present); + const RGHandle a = graph.createTransient(u8"A", RGTextureDesc(rhi::TextureFormat::RGBA16Float, 64, 64)); + const RGHandle b = graph.createTransient(u8"B", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm, 32, 32)); + + u64 genA = 0, genB = 0; + graph.addRenderPass(u8"PA", [&](PassBuilder& pb) { + pb.setColorTarget(0, a, rhi::LoadOp::Clear, rhi::StoreOp::Store); pb.neverCull(); + pb.setExecute([&](rhi::RenderPassEncoder&) { genA = graph.getTextureGeneration(a); }); + }); + graph.addRenderPass(u8"PB", [&](PassBuilder& pb) { + pb.setColorTarget(0, b, rhi::LoadOp::Clear, rhi::StoreOp::Store); pb.neverCull(); + pb.setExecute([&](rhi::RenderPassEncoder&) { genB = graph.getTextureGeneration(b); }); + }); + graph.addRenderPass(u8"Sink", [&](PassBuilder& pb) { + pb.setColorTarget(0, bbH, rhi::LoadOp::Clear, rhi::StoreOp::Store); + pb.readTexture(a); pb.readTexture(b); pb.neverCull(); + pb.setExecute([](rhi::RenderPassEncoder&) {}); + }); + + CHECK(graph.execute(h.enc).isOk()); + CHECK(genA != 0); + CHECK(genB != 0); + CHECK(genA != genB); // two distinct physical allocations -> distinct ids +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/TransientTexturePool.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/TransientTexturePool.cppm new file mode 100644 index 00000000..51c59990 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/TransientTexturePool.cppm @@ -0,0 +1,112 @@ +// Draconic::RenderGraph - :transient_pool partition +// +// Pools GPU textures for reuse across frames by transient resources, avoiding +// per-frame allocation thrashing. Matches by exact descriptor; ages out unused + +module; + +#include + +export module rendergraph:transient_pool; + +import core; +import rhi; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + class TransientTexturePool + { + public: + explicit TransientTexturePool(rhi::Device& device) noexcept : m_device(&device) {} + ~TransientTexturePool() { destroyAll(); } + + TransientTexturePool(const TransientTexturePool&) = delete; + TransientTexturePool& operator=(const TransientTexturePool&) = delete; + + i32 maxUnusedFrames = 4; + + // Acquire a matching texture from the pool; true if one was found. `generation` receives the + // physical texture's stable id (preserved while it lives in the pool), so consumers can detect + // when a transient is backed by a DIFFERENT physical texture (a freed view address can reuse). + [[nodiscard]] bool tryAcquire(const rhi::TextureDesc& desc, rhi::Texture*& texture, rhi::TextureView*& view, u64& generation) + { + for (usize i = 0; i < m_pool.size(); ++i) + { + if (descriptorsMatch(m_pool[i].desc, desc)) + { + texture = m_pool[i].texture; + view = m_pool[i].view; + generation = m_pool[i].generation; + m_pool.erase(m_pool.begin() + i); + return true; + } + } + texture = nullptr; + view = nullptr; + generation = 0; + return false; + } + + void returnToPool(const rhi::TextureDesc& desc, rhi::Texture* texture, rhi::TextureView* view, u64 generation) + { + m_pool.push_back(PooledTexture{ desc, texture, view, generation, 0 }); + } + + // Age out entries unused for more than maxUnusedFrames. + void endFrame() + { + for (usize i = m_pool.size(); i-- > 0;) + { + ++m_pool[i].unusedFrames; + if (m_pool[i].unusedFrames > maxUnusedFrames) + { + rhi::Texture* tex = m_pool[i].texture; + rhi::TextureView* view = m_pool[i].view; + if (view != nullptr) { m_device->destroyTextureView(view); } + if (tex != nullptr) { m_device->destroyTexture(tex); } + m_pool.erase(m_pool.begin() + i); + } + } + } + + void destroyAll() + { + for (PooledTexture& entry : m_pool) + { + if (entry.view != nullptr) { m_device->destroyTextureView(entry.view); } + if (entry.texture != nullptr) { m_device->destroyTexture(entry.texture); } + } + m_pool.clear(); + } + + private: + struct PooledTexture + { + rhi::TextureDesc desc; + rhi::Texture* texture = nullptr; + rhi::TextureView* view = nullptr; + u64 generation = 0; // stable id of this physical texture (carried across pool reuse) + i32 unusedFrames = 0; + }; + + static bool descriptorsMatch(const rhi::TextureDesc& a, const rhi::TextureDesc& b) noexcept + { + return a.dimension == b.dimension + && a.format == b.format + && a.width == b.width + && a.height == b.height + && a.depth == b.depth + && a.arrayLayerCount == b.arrayLayerCount + && a.mipLevelCount == b.mipLevelCount + && a.sampleCount == b.sampleCount + && a.usage == b.usage; + } + + rhi::Device* m_device; + std::vector m_pool; + }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/TypeTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/TypeTests.test.cpp new file mode 100644 index 00000000..5922502f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/TypeTests.test.cpp @@ -0,0 +1,102 @@ +#include +#include +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace std { + template <> struct hash { + [[nodiscard]] std::size_t operator()(const draco::rendergraph::RGHandle& h) const noexcept { + return (static_cast(h.index) * 1099511628211ull) ^ h.generation; + } + }; +} + + +TEST_CASE("rg.type: handle equality") +{ + CHECK(RGHandle{ 1, 1 } == RGHandle{ 1, 1 }); + CHECK(RGHandle{ 1, 1 } != RGHandle{ 2, 1 }); + CHECK(RGHandle{ 1, 1 } != RGHandle{ 1, 2 }); // generation matters +} + +TEST_CASE("rg.type: handle validity") +{ + CHECK_FALSE(RGHandle::invalid().isValid()); + CHECK(RGHandle{ 0, 1 }.isValid()); + CHECK_FALSE(PassHandle::invalid().isValid()); + CHECK(PassHandle{ 0 }.isValid()); +} + +TEST_CASE("rg.type: handle works as a hash-map key") +{ + std::unordered_map map; + map.insert_or_assign(RGHandle{ 1, 1 }, 7); + const i32* found = mapFind(map, RGHandle{ 1, 1 }); // equal handle + REQUIRE(found != nullptr); + CHECK(*found == 7); + CHECK(mapFind(map, RGHandle{ 2, 1 }) == nullptr); +} + +TEST_CASE("rg.type: access IsRead / IsWrite") +{ + CHECK(isRead(RGAccessType::ReadTexture)); + CHECK(isRead(RGAccessType::ReadBuffer)); + CHECK(isRead(RGAccessType::ReadDepthStencil)); + CHECK(isRead(RGAccessType::SampleDepthStencil)); + CHECK(isRead(RGAccessType::ReadCopySrc)); + CHECK(isRead(RGAccessType::ReadWriteStorage)); + CHECK_FALSE(isRead(RGAccessType::WriteColorTarget)); + CHECK_FALSE(isRead(RGAccessType::WriteStorage)); + + CHECK(isWrite(RGAccessType::WriteColorTarget)); + CHECK(isWrite(RGAccessType::WriteDepthTarget)); + CHECK(isWrite(RGAccessType::WriteStorage)); + CHECK(isWrite(RGAccessType::WriteCopyDst)); + CHECK(isWrite(RGAccessType::ReadWriteStorage)); + CHECK_FALSE(isWrite(RGAccessType::ReadTexture)); + CHECK_FALSE(isWrite(RGAccessType::ReadBuffer)); + CHECK_FALSE(isWrite(RGAccessType::SampleDepthStencil)); +} + +TEST_CASE("rg.type: access -> resource state") +{ + using RS = rhi::ResourceState; + CHECK(toResourceState(RGAccessType::ReadTexture) == RS::ShaderRead); + CHECK(toResourceState(RGAccessType::WriteColorTarget) == RS::RenderTarget); + CHECK(toResourceState(RGAccessType::WriteDepthTarget) == RS::DepthStencilWrite); + CHECK(toResourceState(RGAccessType::ReadDepthStencil) == RS::DepthStencilRead); + // Sampling a depth texture in a shader uses the same read-only depth layout as a + // read-only depth attachment (DEPTH_STENCIL_READ_ONLY_OPTIMAL), not ShaderRead. + CHECK(toResourceState(RGAccessType::SampleDepthStencil) == RS::DepthStencilRead); + CHECK(toResourceState(RGAccessType::ReadCopySrc) == RS::CopySrc); + CHECK(toResourceState(RGAccessType::WriteCopyDst) == RS::CopyDst); + CHECK(toResourceState(RGAccessType::WriteStorage) == RS::ShaderWrite); +} + +TEST_CASE("rg.type: subresource All + overlap") +{ + const RGSubresourceRange all = RGSubresourceRange::all(); + CHECK(all.isAll()); + CHECK(all.baseMipLevel == 0u); + CHECK(all.mipLevelCount == 0u); + + const RGSubresourceRange layer0{ 0, 1, 0, 1 }; + const RGSubresourceRange layer1{ 0, 1, 1, 1 }; + CHECK_FALSE(layer0.overlaps(layer1, 1, 4)); + CHECK(all.overlaps(layer0, 1, 4)); + CHECK(all.overlaps(layer1, 1, 4)); + CHECK(layer0.overlaps(layer0, 1, 4)); + + const RGSubresourceRange mip0{ 0, 1, 0, 0 }; + const RGSubresourceRange mip1{ 1, 1, 0, 0 }; + CHECK_FALSE(mip0.overlaps(mip1, 4, 1)); + CHECK(mip0.overlaps(mip0, 4, 1)); +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/Types.cppm b/Engine/cpp/Runtime/Rendering/RenderGraph/Types.cppm new file mode 100644 index 00000000..a05a150c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/Types.cppm @@ -0,0 +1,216 @@ +// Draconic::RenderGraph - :types partition +// +// Core value types: resource/pass handles, pass + access enums, subresource +// ResourceState, the currency the barrier solver works in. + +module; + +#include +#include +#include +#include + +export module rendergraph:types; + +import core; +import rhi; + +using namespace draco; + +export namespace draco::rendergraph +{ + namespace rhi = draco::rhi; + + // NOTE: local stand-in. Replace with a core container helper once draco.core + // provides a map with pointer-or-null lookup (Sedulous's HashMap::Find). + // Look up a key in a std map/set-like container, returning a pointer to the + // mapped value or nullptr (mirrors the pointer-or-null lookup the solver uses). + template + [[nodiscard]] auto* mapFind(Map& m, const Key& k) noexcept + { + auto it = m.find(k); + using P = decltype(&it->second); + return it != m.end() ? &it->second : P{nullptr}; + } + + // NOTE: local stand-in. Replace with a core text-formatting facility once + // draco.core provides one (Sedulous's AppendFormat). Substitutes each "{}" in + // `fmt` with the next argument (integers, floats, and UTF-8 string views). + namespace detail + { + inline void rgAppendArg(std::u8string& out, std::u8string_view v) { out.append(v); } + template requires std::is_arithmetic_v + void rgAppendArg(std::u8string& out, T v) + { + const std::string s = std::to_string(v); + out.append(reinterpret_cast(s.data()), s.size()); + } + } + + inline void appendFormat(std::u8string& out, std::u8string_view fmt) { out.append(fmt); } + + template + void appendFormat(std::u8string& out, std::u8string_view fmt, A&& a, Rest&&... rest) + { + const auto pos = fmt.find(u8"{}"); + if (pos == std::u8string_view::npos) { out.append(fmt); return; } + out.append(fmt.substr(0, pos)); + detail::rgAppendArg(out, std::forward(a)); + appendFormat(out, fmt.substr(pos + 2), std::forward(rest)...); + } + + // Handle to a graph resource (texture or buffer); generation-checked for staleness. + struct RGHandle + { + u32 index = 0xFFFFFFFFu; + u32 generation = 0; + + [[nodiscard]] static constexpr RGHandle invalid() noexcept { return RGHandle{ 0xFFFFFFFFu, 0 }; } + [[nodiscard]] constexpr bool isValid() const noexcept { return index != 0xFFFFFFFFu; } + }; + + [[nodiscard]] constexpr bool operator==(RGHandle a, RGHandle b) noexcept + { + return a.index == b.index && a.generation == b.generation; + } + [[nodiscard]] constexpr bool operator!=(RGHandle a, RGHandle b) noexcept { return !(a == b); } + + // Handle to a graph pass. + struct PassHandle + { + u32 index = 0xFFFFFFFFu; + + [[nodiscard]] static constexpr PassHandle invalid() noexcept { return PassHandle{ 0xFFFFFFFFu }; } + [[nodiscard]] constexpr bool isValid() const noexcept { return index != 0xFFFFFFFFu; } + }; + + [[nodiscard]] constexpr bool operator==(PassHandle a, PassHandle b) noexcept { return a.index == b.index; } + [[nodiscard]] constexpr bool operator!=(PassHandle a, PassHandle b) noexcept { return a.index != b.index; } + + enum class RGPassType : u8 { Render, Compute, Copy }; + + // Type of resource access declared by a pass. + enum class RGAccessType : u8 + { + // Reads + ReadTexture, ReadBuffer, ReadDepthStencil, ReadCopySrc, + // Sample a DEPTH texture in a shader: like ReadTexture (sampled, not an attachment) but the + // layout must be DepthStencilRead (DEPTH_STENCIL_READ_ONLY_OPTIMAL), not ShaderRead. + SampleDepthStencil, + // Writes + WriteColorTarget, WriteDepthTarget, WriteStorage, WriteCopyDst, + // Read + Write + ReadWriteStorage, ReadWriteDepthTarget, ReadWriteColorTarget, + }; + + [[nodiscard]] constexpr bool isRead(RGAccessType type) noexcept + { + switch (type) + { + case RGAccessType::ReadTexture: + case RGAccessType::ReadBuffer: + case RGAccessType::ReadDepthStencil: + case RGAccessType::SampleDepthStencil: + case RGAccessType::ReadCopySrc: + case RGAccessType::ReadWriteStorage: + case RGAccessType::ReadWriteDepthTarget: + case RGAccessType::ReadWriteColorTarget: + return true; + default: + return false; + } + } + + [[nodiscard]] constexpr bool isWrite(RGAccessType type) noexcept + { + switch (type) + { + case RGAccessType::WriteColorTarget: + case RGAccessType::WriteDepthTarget: + case RGAccessType::WriteStorage: + case RGAccessType::WriteCopyDst: + case RGAccessType::ReadWriteStorage: + case RGAccessType::ReadWriteDepthTarget: + case RGAccessType::ReadWriteColorTarget: + return true; + default: + return false; + } + } + + [[nodiscard]] constexpr rhi::ResourceState toResourceState(RGAccessType type) noexcept + { + using RS = rhi::ResourceState; + switch (type) + { + case RGAccessType::ReadTexture: return RS::ShaderRead; + case RGAccessType::ReadBuffer: return RS::ShaderRead; + case RGAccessType::ReadDepthStencil: return RS::DepthStencilRead; + case RGAccessType::SampleDepthStencil: return RS::DepthStencilRead; + case RGAccessType::ReadCopySrc: return RS::CopySrc; + case RGAccessType::WriteColorTarget: return RS::RenderTarget; + case RGAccessType::WriteDepthTarget: return RS::DepthStencilWrite; + case RGAccessType::WriteStorage: return RS::ShaderWrite; + case RGAccessType::WriteCopyDst: return RS::CopyDst; + case RGAccessType::ReadWriteStorage: return RS::ShaderWrite | RS::ShaderRead; + case RGAccessType::ReadWriteDepthTarget: return RS::DepthStencilWrite; + case RGAccessType::ReadWriteColorTarget: return RS::RenderTarget; + } + return RS::Undefined; + } + + // Subresource range for fine-grained access tracking (e.g. shadow cascades). + // A count of 0 means "all remaining from the base". + struct RGSubresourceRange + { + u32 baseMipLevel = 0; + u32 mipLevelCount = 0; + u32 baseArrayLayer = 0; + u32 arrayLayerCount = 0; + + [[nodiscard]] static constexpr RGSubresourceRange all() noexcept { return RGSubresourceRange{}; } + + [[nodiscard]] constexpr bool isAll() const noexcept + { + return baseMipLevel == 0 && mipLevelCount == 0 && baseArrayLayer == 0 && arrayLayerCount == 0; + } + + [[nodiscard]] constexpr bool overlaps(RGSubresourceRange other, u32 totalMips = 1, u32 totalLayers = 1) const noexcept + { + const u32 myMipEnd = mipLevelCount == 0 ? totalMips : baseMipLevel + mipLevelCount; + const u32 otherMipEnd = other.mipLevelCount == 0 ? totalMips : other.baseMipLevel + other.mipLevelCount; + const u32 myLayerEnd = arrayLayerCount == 0 ? totalLayers : baseArrayLayer + arrayLayerCount; + const u32 otherLayerEnd = other.arrayLayerCount == 0 ? totalLayers : other.baseArrayLayer + other.arrayLayerCount; + + const bool mipOverlap = baseMipLevel < otherMipEnd && other.baseMipLevel < myMipEnd; + const bool layerOverlap = baseArrayLayer < otherLayerEnd && other.baseArrayLayer < myLayerEnd; + return mipOverlap && layerOverlap; + } + }; + + [[nodiscard]] constexpr bool operator==(RGSubresourceRange a, RGSubresourceRange b) noexcept + { + return a.baseMipLevel == b.baseMipLevel && a.mipLevelCount == b.mipLevelCount + && a.baseArrayLayer == b.baseArrayLayer && a.arrayLayerCount == b.arrayLayerCount; + } + [[nodiscard]] constexpr bool operator!=(RGSubresourceRange a, RGSubresourceRange b) noexcept { return !(a == b); } + + // A single resource access declared by a pass. + struct RGResourceAccess + { + RGHandle handle = RGHandle::invalid(); + RGAccessType type = RGAccessType::ReadTexture; + RGSubresourceRange subresource; + + [[nodiscard]] bool isRead() const noexcept { return rendergraph::isRead(type); } + [[nodiscard]] bool isWrite() const noexcept { return rendergraph::isWrite(type); } + [[nodiscard]] rhi::ResourceState toResourceState() const noexcept { return rendergraph::toResourceState(type); } + }; + + // How transient resource dimensions resolve relative to the graph output. + enum class SizeMode : u8 { FullSize, HalfSize, QuarterSize, Custom }; + + enum class RGResourceLifetime : u8 { Transient, Persistent, Imported }; + + enum class RGResourceType : u8 { Texture, Buffer }; +} diff --git a/Engine/cpp/Runtime/Rendering/RenderGraph/ValidationTests.test.cpp b/Engine/cpp/Runtime/Rendering/RenderGraph/ValidationTests.test.cpp new file mode 100644 index 00000000..d210a4c9 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/RenderGraph/ValidationTests.test.cpp @@ -0,0 +1,110 @@ +#include + + +import core; +import rhi; +import rendergraph; + +using namespace draco; +using namespace draco::rendergraph; +namespace rhi = draco::rhi; + +namespace +{ + bool contains(std::u8string_view hay, std::u8string_view needle) + { + if (needle.size() > hay.size()) { return false; } + for (usize i = 0; i + needle.size() <= hay.size(); ++i) + { + if (hay.substr(i, needle.size()) == needle) { return true; } + } + return false; + } + + bool hasSeverity(const std::vector& messages, ValidationSeverity severity) + { + for (const ValidationMessage& m : messages) { if (m.severity == severity) { return true; } } + return false; + } +} + +TEST_CASE("rg.validation: uninitialized read is an error") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"BadPass", [&](PassBuilder& b) { b.readTexture(tex); b.neverCull(); }); + + std::vector messages; + GraphValidator::validate(graph, messages); + CHECK(hasSeverity(messages, ValidationSeverity::Error)); +} + +TEST_CASE("rg.validation: reading an imported resource is fine") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle imported = graph.importTarget(u8"External", nullptr, nullptr); + graph.addRenderPass(u8"ReadImported", [&](PassBuilder& b) { b.readTexture(imported); b.neverCull(); }); + + std::vector messages; + GraphValidator::validate(graph, messages); + CHECK_FALSE(hasSeverity(messages, ValidationSeverity::Error)); +} + +TEST_CASE("rg.validation: empty pass is a warning") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + graph.addRenderPass(u8"Empty", [](PassBuilder& b) { b.neverCull(); }); + + std::vector messages; + GraphValidator::validate(graph, messages); + CHECK(hasSeverity(messages, ValidationSeverity::Warning)); +} + +TEST_CASE("rg.validation: redundant write is a warning") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"Write1", [&](PassBuilder& b) { b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); b.neverCull(); }); + graph.addRenderPass(u8"Write2", [&](PassBuilder& b) { b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); b.neverCull(); }); + + std::vector messages; + GraphValidator::validate(graph, messages); + CHECK(hasSeverity(messages, ValidationSeverity::Warning)); +} + +TEST_CASE("rg.validation: a clean graph produces no messages") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"Write", [&](PassBuilder& b) { + b.setColorTarget(0, tex, rhi::LoadOp::Clear, rhi::StoreOp::Store); + b.neverCull(); + b.setExecute([](rhi::RenderPassEncoder&) {}); + }); + graph.addRenderPass(u8"Read", [&](PassBuilder& b) { + b.readTexture(tex); + b.neverCull(); + b.setExecute([](rhi::RenderPassEncoder&) {}); + }); + + std::vector messages; + GraphValidator::validate(graph, messages); + CHECK(messages.size() == 0u); +} + +TEST_CASE("rg.validation: ValidateToString formats output") +{ + RenderGraph graph(nullptr); + graph.beginFrame(0); + const RGHandle tex = graph.createTransient(u8"Tex", RGTextureDesc(rhi::TextureFormat::RGBA8Unorm)); + graph.addRenderPass(u8"BadRead", [&](PassBuilder& b) { b.readTexture(tex); b.neverCull(); }); + + std::u8string result; + GraphValidator::validateToString(graph, result); + CHECK(contains(result, u8"issue")); +} diff --git a/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cpp b/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cpp deleted file mode 100644 index eaea237b..00000000 --- a/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cpp +++ /dev/null @@ -1,150 +0,0 @@ -module; - -#include -#include -#include -#include -#include - -#include - -module rendering.renderer; - -import core.stdtypes; -import core.math.transform; -import rendering.rhi; -import rendering.rhi.uniform_registry; -import rendering.rendergraph; -import rendering.mesh; -import rendering.material; -import rendering.quad; - -namespace draco::rendering::renderer -{ - static constexpr const char* MAIN_PASS = "MainPass"; - - void init(u16 width, u16 height) - { - g_ctx.screenWidth = width; - g_ctx.screenHeight = height; - } - - void resize(u16 width, u16 height) - { - g_ctx.screenWidth = width; - g_ctx.screenHeight = height; - } - - void beginFrame(const Camera& cam) - { - rhi::beginFrame(); - - g_ctx.mainCamera = cam; - g_ctx.graph.reset(); - - // Create main pass once per frame - auto& pass = g_ctx.graph.addPass(MAIN_PASS); - - pass.view = 0; - pass.framebuffer = rhi::InvalidFramebuffer; - - pass.width = g_ctx.screenWidth; - pass.height = g_ctx.screenHeight; - - pass.clearFlags = BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH; - pass.clearColor = 0x303030ff; - - rhi::lookAt(pass.viewMatrix, cam.position.data(), cam.target.data(), cam.up.data()); - const f32 aspect = static_cast(g_ctx.screenWidth) / static_cast(std::max(g_ctx.screenHeight, 1)); - rhi::perspective(pass.projMatrix, cam.fov, aspect, cam.nearPlane, cam.farPlane); - } - - static void buildUniforms(const material::Material& mat, std::vector& out) - { - out.clear(); - out.reserve(mat.uniforms.size()); - - for (const auto& u : mat.uniforms) - { - rhi::UniformBind bind{}; - - bind.handle = rhi::getUniform(u.nameHash); - - bind.data = u.data; - bind.num = u.count; - - if (bind.handle == rhi::InvalidUniform) - { - std::println("[Renderer] Missing uniform hash: {}", u.nameHash); - continue; - } - - out.push_back(bind); - } - } - - void submitEntity(const rhi::RenderPacket& packet) - { - auto* pass = g_ctx.graph.getPass(MAIN_PASS); - if (!pass) return; - - pass->packets.push_back(packet); - } - - void submitRenderable(const draco::math::Transform& transform, const material::Material& material, mesh::MeshHandle mesh_id) - { - const auto* m = mesh::get(mesh_id); - if (!m) return; - - rhi::RenderPacket p{}; - - p.vertexBuffer = m->vbh; - p.indexBuffer = m->ibh; - - p.pipeline = material.pipeline; - p.textureHandle = material.texture; - p.textureUnit = material.texture_unit; - p.samplerUniform = material.sampler; - - buildUniforms(material, p.uniforms); - - transform.toMatrix(p.model); - - submitEntity(p); - } - - void submitUI(quad::QuadRenderer& quad_renderer) - { - auto& ui_pass = g_ctx.graph.addPass("UIPass"); - - ui_pass.view = 1; - ui_pass.sortMode = rendergraph::SortMode::None; - - ui_pass.framebuffer = rhi::InvalidFramebuffer; - - ui_pass.width = g_ctx.screenWidth; - ui_pass.height = g_ctx.screenHeight; - - ui_pass.clearFlags = 0; - - quad::OrthoCamera ortho; - - quad::QuadRenderer::buildOrtho(ortho, g_ctx.screenWidth, g_ctx.screenHeight); - - std::memcpy(ui_pass.viewMatrix, ortho.view, sizeof(f32) * 16); - std::memcpy(ui_pass.projMatrix, ortho.proj, sizeof(f32) * 16); - - quad_renderer.flushToPass(ui_pass); - } - - void endFrame() - { - g_ctx.graph.execute(); - rhi::endFrame(); - } - - rendergraph::RenderGraph& getGraph() - { - return g_ctx.graph; - } -} diff --git a/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cppm b/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cppm deleted file mode 100644 index 6ab3efb8..00000000 --- a/Engine/cpp/Runtime/Rendering/Renderer/Renderer.cppm +++ /dev/null @@ -1,48 +0,0 @@ -module; - -#include - -export module rendering.renderer; - -import core.stdtypes; -import core.math.transform; -import rendering.rhi; -import rendering.rendergraph; -import rendering.quad; -import rendering.material; -import rendering.mesh; - -export namespace draco::rendering::renderer { - - struct Camera { - std::array position = {0.0f, 0.0f, 0.0f}; - std::array target = {0.0f, 0.0f, 0.0f}; - std::array up = {0.0f, 1.0f, 0.0f}; - f32 fov = 60.0f; - f32 nearPlane = 0.1f; - f32 farPlane = 1000.0f; - }; - - struct SceneContext { - u16 screenWidth = 0; - u16 screenHeight = 0; - Camera mainCamera; - - rendergraph::RenderGraph graph; - }; - - inline SceneContext g_ctx; - - void init(u16 width, u16 height); - void resize(u16 width, u16 height); - - void beginFrame(const Camera& cam); - - void submitEntity(rhi::RenderPacket& packet, u16 view); - void submitRenderable(const math::Transform& transform, const material::Material& material, mesh::MeshHandle mesh_id); - void submitUI(quad::QuadRenderer& quad_renderer); - - void endFrame(); - - rendergraph::RenderGraph& getGraph(); -} diff --git a/Engine/cpp/Runtime/Rendering/Rendering.cppm b/Engine/cpp/Runtime/Rendering/Rendering.cppm deleted file mode 100644 index 36f000b2..00000000 --- a/Engine/cpp/Runtime/Rendering/Rendering.cppm +++ /dev/null @@ -1,9 +0,0 @@ -export module rendering; -export import rendering.rhi; -export import rendering.rhi.uniform_registry; -export import rendering.rhi.vertex; -export import rendering.rendergraph; -export import rendering.renderer; -export import rendering.mesh; -export import rendering.material; -export import rendering.quad; diff --git a/Engine/cpp/Runtime/Rendering/Shaders/Compiler.cppm b/Engine/cpp/Runtime/Rendering/Shaders/Compiler.cppm new file mode 100644 index 00000000..9db2bb14 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/Compiler.cppm @@ -0,0 +1,295 @@ +/// DXC shader compiler - loads dxcompiler.dll/libdxcompiler.so at runtime. + +module; + +#include "DxcIncludes.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#endif + +export module shaders:compiler; + +import core.stdtypes; +import core.status; +import :types; + +using namespace draco; + +export namespace draco::shaders { + +/// Configuration for compiler creation. +struct CompilerDesc { + /// Optional override path to the DXC shared library. + std::u8string_view dxcompilerPath{}; +}; + +/// HLSL shader compiler backed by DXC (IDxcCompiler3). +struct Compiler { + void* state = nullptr; + + [[nodiscard]] Status compile(const u8* source, usize sourceSize, + ShaderStage stage, std::u8string_view entryPoint, + ShaderTarget target, const CompileOptions& options, + CompileResult& out); + + void freeResult(CompileResult& result); + void destroy(); +}; + +[[nodiscard]] Status createCompiler(const CompilerDesc& desc, Compiler*& out); + +} // namespace draco::shaders (exported) + +// ---- Implementation ---- + +namespace draco::shaders { + +#ifdef _WIN32 +using DynLibHandle = HMODULE; +#else +using DynLibHandle = void*; +#endif + +struct CompilerState { + DynLibHandle dxcompiler = nullptr; + DxcCreateInstanceProc createInst = nullptr; + IDxcCompiler3* dxc = nullptr; + IDxcUtils* utils = nullptr; + IDxcIncludeHandler* includeHdlr = nullptr; +}; + +static CompilerState* stateOf(Compiler* c) { return static_cast(c->state); } + +// UTF-8 view -> std::wstring for DXC. Decodes UTF-8 codepoints, then on Windows +// (wchar_t = UTF-16) emits surrogate pairs for astral code points; on Linux +// (wchar_t = UTF-32) emits the codepoint directly. +static std::wstring widen(std::u8string_view s) { + std::wstring out; + out.reserve(s.size()); + usize i = 0; + while (i < s.size()) { + const u8 lead = static_cast(s.data()[i]); ++i; + char32_t cp; int extra; + if (lead < 0x80u) { cp = lead; extra = 0; } + else if ((lead & 0xE0u) == 0xC0u) { cp = lead & 0x1Fu; extra = 1; } + else if ((lead & 0xF0u) == 0xE0u) { cp = lead & 0x0Fu; extra = 2; } + else if ((lead & 0xF8u) == 0xF0u) { cp = lead & 0x07u; extra = 3; } + else { cp = 0xFFFDu; extra = 0; } + for (int k = 0; k < extra && i < s.size(); ++k) { + cp = (cp << 6) | (static_cast(s.data()[i]) & 0x3Fu); ++i; + } +#ifdef _WIN32 + if (cp <= 0xFFFFu) { out.push_back(static_cast(cp)); } + else { + cp -= 0x10000u; + out.push_back(static_cast(0xD800u + (cp >> 10))); + out.push_back(static_cast(0xDC00u + (cp & 0x3FFu))); + } +#else + out.push_back(static_cast(cp)); +#endif + } + return out; +} + +static const wchar_t* stagePrefix(ShaderStage stage) { + switch (stage) { + case ShaderStage::Vertex: return L"vs"; + case ShaderStage::Fragment: return L"ps"; + case ShaderStage::Compute: return L"cs"; + case ShaderStage::Mesh: return L"ms"; + case ShaderStage::Task: return L"as"; + case ShaderStage::RayGen: + case ShaderStage::ClosestHit: + case ShaderStage::AnyHit: + case ShaderStage::Miss: + case ShaderStage::Intersection: + case ShaderStage::Callable: return L"lib"; + } + return L"vs"; +} + +Status Compiler::compile(const u8* source, usize sourceSize, + ShaderStage stage, std::u8string_view entryPoint, + ShaderTarget target, const CompileOptions& options, + CompileResult& out) { + auto* s = stateOf(this); + out = {}; + + std::vector argStorage; + argStorage.reserve(64); + auto push = [&](const wchar_t* a) { argStorage.emplace_back(a); }; + auto pushS = [&](std::wstring a) { argStorage.emplace_back(std::move(a)); }; + + push(L"-E"); + pushS(entryPoint.empty() ? L"main" : widen(entryPoint)); + + std::wstring profile; + profile.append(stagePrefix(stage)); + profile.append(L"_"); + profile.append(widen(options.shaderModel)); + push(L"-T"); pushS(std::move(profile)); + + if (target == ShaderTarget::SPIRV) { + push(L"-spirv"); + push(L"-fspv-target-env=vulkan1.3"); + for (u32 set = 0; set < options.bindingShiftSets; ++set) { + wchar_t setBuf[16]; swprintf(setBuf, 16, L"%u", set); + std::wstring setStr = setBuf; + auto pushShift = [&](const wchar_t* flag, u32 shift) { + if (shift == 0) return; + wchar_t shBuf[16]; swprintf(shBuf, 16, L"%u", shift); + push(flag); pushS(shBuf); pushS(setStr); + }; + pushShift(L"-fvk-b-shift", options.bindingShifts.constantBufferShift); + pushShift(L"-fvk-t-shift", options.bindingShifts.textureShift); + pushShift(L"-fvk-s-shift", options.bindingShifts.samplerShift); + pushShift(L"-fvk-u-shift", options.bindingShifts.uavShift); + } + } + + push(options.rowMajorMatrices ? L"-Zpr" : L"-Zpc"); + + switch (options.optimizationLevel) { + case 0: push(L"-O0"); break; case 1: push(L"-O1"); break; + case 2: push(L"-O2"); break; default: push(L"-O3"); break; + } + if (options.enableDebugInfo) push(L"-Zi"); + + for (usize i = 0; i < options.defines.size(); ++i) { + std::wstring arg = L"-D"; + arg.append(widen(options.defines[i].name)); + if (!options.defines[i].value.empty()) { arg.append(L"="); arg.append(widen(options.defines[i].value)); } + pushS(std::move(arg)); + } + for (usize i = 0; i < options.includePaths.size(); ++i) { + push(L"-I"); pushS(widen(options.includePaths[i])); + } + push(L"-Wno-ignored-attributes"); + + std::vector args; + args.reserve(argStorage.size()); + for (const auto& w : argStorage) args.push_back(w.c_str()); + + DxcBuffer src{}; src.Ptr = source; src.Size = sourceSize; src.Encoding = DXC_CP_UTF8; + IDxcResult* result = nullptr; + HRESULT hr = s->dxc->Compile(&src, args.data(), static_cast(args.size()), s->includeHdlr, IID_PPV_ARGS(&result)); + + if (FAILED(hr) || !result) { + std::string msg = "IDxcCompiler3::Compile returned HRESULT 0x" + std::to_string(static_cast(hr)); + out.messagesSize = msg.size(); + out.messages = new char[msg.size() + 1]; + std::memcpy(out.messages, msg.data(), msg.size()); out.messages[msg.size()] = '\0'; + return ErrorCode::Unknown; + } + + IDxcBlobUtf8* errBlob = nullptr; IDxcBlobWide* errName = nullptr; + if (SUCCEEDED(result->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&errBlob), &errName)) && errBlob && errBlob->GetStringLength() > 0) { + auto n = errBlob->GetStringLength(); + out.messagesSize = n; out.messages = new char[n + 1]; + std::memcpy(out.messages, errBlob->GetStringPointer(), n); out.messages[n] = '\0'; + } else { out.messagesSize = 0; out.messages = new char[1]; out.messages[0] = '\0'; } + if (errBlob) { errBlob->Release(); } + if (errName) { errName->Release(); } + + HRESULT status = S_OK; result->GetStatus(&status); + if (SUCCEEDED(status)) { + IDxcBlob* objBlob = nullptr; IDxcBlobWide* objName = nullptr; + if (SUCCEEDED(result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&objBlob), &objName)) && objBlob && objBlob->GetBufferSize() > 0) { + auto sz = objBlob->GetBufferSize(); + out.bytecodeSize = sz; out.bytecode = new u8[sz]; + std::memcpy(out.bytecode, objBlob->GetBufferPointer(), sz); out.success = true; + } + if (objBlob) { objBlob->Release(); } + if (objName) { objName->Release(); } + } + result->Release(); + return out.success ? ErrorCode::Ok : ErrorCode::Unknown; +} + +void Compiler::freeResult(CompileResult& r) { delete[] r.bytecode; delete[] r.messages; r = {}; } + +void Compiler::destroy() { + auto* s = stateOf(this); + if (!s) return; + if (s->includeHdlr) { s->includeHdlr->Release(); s->includeHdlr = nullptr; } + if (s->utils) { s->utils->Release(); s->utils = nullptr; } + if (s->dxc) { s->dxc->Release(); s->dxc = nullptr; } +#ifdef _WIN32 + if (s->dxcompiler) { FreeLibrary(s->dxcompiler); s->dxcompiler = nullptr; } +#else + if (s->dxcompiler) { dlclose(s->dxcompiler); s->dxcompiler = nullptr; } +#endif + delete s; + this->state = nullptr; +} + +Status createCompiler(const CompilerDesc& desc, Compiler*& out) { + out = nullptr; + auto* c = new Compiler(); + auto* s = new CompilerState(); + c->state = s; + +#ifdef _WIN32 + { + std::wstring wpath; + if (!desc.dxcompilerPath.empty()) { + wpath = widen(desc.dxcompilerPath); +#ifdef DRACO_DXC_PATH + } else { + wpath = widen(reinterpret_cast(DRACO_DXC_PATH)); + s->dxcompiler = LoadLibraryW(wpath.c_str()); + if (!s->dxcompiler) wpath = L"dxcompiler.dll"; +#else + } else { + wpath = L"dxcompiler.dll"; +#endif + } + if (!s->dxcompiler) s->dxcompiler = LoadLibraryW(wpath.c_str()); + } + if (!s->dxcompiler) { + std::fprintf(stderr, "draco.shaders: LoadLibraryW(dxcompiler.dll) failed (error %lu)\n", GetLastError()); + c->destroy(); delete c; return ErrorCode::Unknown; + } + s->createInst = reinterpret_cast(GetProcAddress(s->dxcompiler, "DxcCreateInstance")); +#else + { + std::string path; + if (desc.dxcompilerPath.empty()) { +#ifdef DRACO_DXC_PATH + path = DRACO_DXC_PATH; +#else + path = "libdxcompiler.so"; +#endif + } else { + // dxcompilerPath is already UTF-8 - feed it to dlopen directly. + path.assign(reinterpret_cast(desc.dxcompilerPath.data()), desc.dxcompilerPath.size()); + } + s->dxcompiler = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL); + } + if (!s->dxcompiler) { + std::fprintf(stderr, "draco.shaders: dlopen(libdxcompiler.so) failed: %s\n", dlerror()); + c->destroy(); delete c; return ErrorCode::Unknown; + } + s->createInst = reinterpret_cast(dlsym(s->dxcompiler, "DxcCreateInstance")); +#endif + + if (!s->createInst) { std::fprintf(stderr, "draco.shaders: DXC library missing DxcCreateInstance\n"); c->destroy(); delete c; return ErrorCode::Unknown; } + if (FAILED(s->createInst(CLSID_DxcCompiler, IID_PPV_ARGS(&s->dxc)))) { std::fprintf(stderr, "draco.shaders: DxcCreateInstance(IDxcCompiler3) failed\n"); c->destroy(); delete c; return ErrorCode::Unknown; } + if (FAILED(s->createInst(CLSID_DxcUtils, IID_PPV_ARGS(&s->utils)))) { std::fprintf(stderr, "draco.shaders: DxcCreateInstance(IDxcUtils) failed\n"); c->destroy(); delete c; return ErrorCode::Unknown; } + if (FAILED(s->utils->CreateDefaultIncludeHandler(&s->includeHdlr))) { std::fprintf(stderr, "draco.shaders: CreateDefaultIncludeHandler failed\n"); c->destroy(); delete c; return ErrorCode::Unknown; } + + out = c; + return ErrorCode::Ok; +} + +} // namespace draco::shaders diff --git a/Engine/cpp/Runtime/Rendering/Shaders/DxcIncludes.h b/Engine/cpp/Runtime/Rendering/Shaders/DxcIncludes.h new file mode 100644 index 00000000..618cf76c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/DxcIncludes.h @@ -0,0 +1,25 @@ +#ifndef DRACO_SHADERS_DXC_INCLUDES_H_ +#define DRACO_SHADERS_DXC_INCLUDES_H_ + +// DXC uses __uuidof (MSVC extension) and has non-standard enum values. +// Suppress these diagnostics for all TUs that include this header. +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wlanguage-extension-token" +# pragma clang diagnostic ignored "-Wmicrosoft-enum-value" +#endif + +#ifdef _WIN32 +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +#endif + +#include + +#endif // DRACO_SHADERS_DXC_INCLUDES_H_ diff --git a/Engine/cpp/Runtime/Rendering/Shaders/Flags.cppm b/Engine/cpp/Runtime/Rendering/Shaders/Flags.cppm new file mode 100644 index 00000000..21d3607c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/Flags.cppm @@ -0,0 +1,82 @@ +/// Shader feature flags + variant key. Flags are compile-time permutation bits: +/// each set flag becomes a `#define` prepended before compilation, so shaders +/// #ifdef-gate features into specialized, branch-free permutations. Render state +/// (blend/cull/depth) is NOT here - that's PipelineConfig in the material layer. + +module; + +#include +#include + +export module shaders:flags; + +import core.stdtypes; +import :types; + +using namespace draco; + +export namespace draco::shaders { + +enum class ShaderFlags : u32 { + None = 0, + Skinned = 1u << 0, // -> #define SKINNED + Instanced = 1u << 1, // -> #define INSTANCED + AlphaTest = 1u << 2, // -> #define ALPHA_TEST + NormalMap = 1u << 3, // -> #define NORMAL_MAP + Emissive = 1u << 4, // -> #define EMISSIVE + VertexColors = 1u << 5, // -> #define VERTEX_COLORS + ReceiveShadows = 1u << 6, // -> #define RECEIVE_SHADOWS + GBuffer = 1u << 7, // -> #define GBUFFER (forward MRT: also output view-normal + motion) +}; + +[[nodiscard]] constexpr ShaderFlags operator|(ShaderFlags a, ShaderFlags b) noexcept +{ + return static_cast(static_cast(a) | static_cast(b)); +} +[[nodiscard]] constexpr ShaderFlags operator&(ShaderFlags a, ShaderFlags b) noexcept +{ + return static_cast(static_cast(a) & static_cast(b)); +} +constexpr ShaderFlags& operator|=(ShaderFlags& a, ShaderFlags b) noexcept { a = a | b; return a; } +[[nodiscard]] constexpr bool hasFlag(ShaderFlags v, ShaderFlags f) noexcept +{ + return (static_cast(v) & static_cast(f)) != 0u; +} + +// Append a `#define NAME 1` for each set flag (static-literal names - safe to +// reference for the duration of a compile). +inline void appendDefines(ShaderFlags flags, std::vector& out) +{ + if (hasFlag(flags, ShaderFlags::Skinned)) { out.push_back(ShaderDefine{ u8"SKINNED", u8"1" }); } + if (hasFlag(flags, ShaderFlags::Instanced)) { out.push_back(ShaderDefine{ u8"INSTANCED", u8"1" }); } + if (hasFlag(flags, ShaderFlags::AlphaTest)) { out.push_back(ShaderDefine{ u8"ALPHA_TEST", u8"1" }); } + if (hasFlag(flags, ShaderFlags::GBuffer)) { out.push_back(ShaderDefine{ u8"GBUFFER", u8"1" }); } + if (hasFlag(flags, ShaderFlags::NormalMap)) { out.push_back(ShaderDefine{ u8"NORMAL_MAP", u8"1" }); } + if (hasFlag(flags, ShaderFlags::Emissive)) { out.push_back(ShaderDefine{ u8"EMISSIVE", u8"1" }); } + if (hasFlag(flags, ShaderFlags::VertexColors)) { out.push_back(ShaderDefine{ u8"VERTEX_COLORS", u8"1" }); } + if (hasFlag(flags, ShaderFlags::ReceiveShadows)) { out.push_back(ShaderDefine{ u8"RECEIVE_SHADOWS", u8"1" }); } +} + +// Stable FNV-1a hash of a shader name (used to key sources + variants without +// storing the name string in every key). +[[nodiscard]] inline u64 shaderNameHash(std::u8string_view name) noexcept +{ + u64 h = 1469598103934665603ull; // FNV offset basis + for (char8_t c : name) { h ^= static_cast(c); h *= 1099511628211ull; } // FNV prime + return h; +} + +// Identifies one compiled permutation of a named shader. Trivially copyable +// (16 bytes, no padding) so the generic hash keys it directly. +struct ShaderVariantKey { + u64 nameHash = 0; + ShaderStage stage = ShaderStage::Vertex; + ShaderFlags flags = ShaderFlags::None; + + [[nodiscard]] bool operator==(const ShaderVariantKey& o) const noexcept + { + return nameHash == o.nameHash && stage == o.stage && flags == o.flags; + } +}; + +} // namespace draco::shaders diff --git a/Engine/cpp/Runtime/Rendering/Shaders/Shaders.test.cpp b/Engine/cpp/Runtime/Rendering/Shaders/Shaders.test.cpp new file mode 100644 index 00000000..45fa6e8f --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/Shaders.test.cpp @@ -0,0 +1,63 @@ +#include + +#include + +import core; +import shaders; + +using namespace draco; +using namespace draco::shaders; + +namespace +{ + constexpr const char* kVertexHlsl = + "float4 main(uint id : SV_VertexID) : SV_Position {\n" + " return float4(0.0, 0.0, 0.0, 1.0);\n" + "}\n"; +} + +TEST_CASE("shaders: DXC compiles HLSL to SPIR-V") +{ + Compiler* compiler = nullptr; + if (!createCompiler(CompilerDesc{}, compiler).isOk() || compiler == nullptr) + { + MESSAGE("DXC runtime unavailable; skipping shader compilation test"); + return; + } + + const auto* source = reinterpret_cast(kVertexHlsl); + const usize sourceSize = std::strlen(kVertexHlsl); + + CompileResult result{}; + const Status status = compiler->compile( + source, sourceSize, ShaderStage::Vertex, u8"main", + ShaderTarget::SPIRV, CompileOptions{}, result); + + CHECK(status.isOk()); + CHECK(result.success); + CHECK(result.bytecode != nullptr); + CHECK(result.bytecodeSize > 0u); + // SPIR-V magic number (0x07230203) in the first word. + if (result.bytecode != nullptr && result.bytecodeSize >= 4) + { + u32 magic = 0; + std::memcpy(&magic, result.bytecode, 4); + CHECK(magic == 0x07230203u); + } + + compiler->destroy(); +} + +TEST_CASE("shaders: a compile error is reported, not a crash") +{ + Compiler* compiler = nullptr; + if (!createCompiler(CompilerDesc{}, compiler).isOk() || compiler == nullptr) { return; } + + const char* bad = "this is not valid hlsl @#$"; + CompileResult result{}; + (void)compiler->compile(reinterpret_cast(bad), std::strlen(bad), + ShaderStage::Vertex, u8"main", ShaderTarget::SPIRV, CompileOptions{}, result); + CHECK_FALSE(result.success); + + compiler->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/Shaders/ShadersModule.cppm b/Engine/cpp/Runtime/Rendering/Shaders/ShadersModule.cppm new file mode 100644 index 00000000..116f9256 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/ShadersModule.cppm @@ -0,0 +1,5 @@ +export module shaders; + +export import :types; +export import :flags; +export import :compiler; diff --git a/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.cppm b/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.cppm new file mode 100644 index 00000000..0f07a06c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.cppm @@ -0,0 +1,186 @@ +/// The `shaders.system` module. +/// +/// Compile-on-demand + cache for shader VARIANTS. A shader is registered by name +/// per stage (its HLSL source); getVariant(name, stage, flags) compiles the +/// permutation (flags -> #defines) via DXC, creates the GPU ShaderModule, and +/// caches it by (nameHash, stage, flags). This is the layer above the stateless +/// shaders Compiler that the material/PSO layers build on. Needs the RHI to +/// create modules, so it is separate from the RHI-free shaders module. + +module; + +#include +#include +#include +#include +#include + +export module shaders.system; + +import core.stdtypes; +import core.status; +import rhi; +import shaders; + +using namespace draco; +namespace rhi = draco::rhi; + +export namespace draco::shaders { + +// Hash for the variant key so it can key an unordered_map (ShaderVariantKey is a +// trivially-copyable 16-byte struct with operator==). +struct ShaderVariantKeyHash { + usize operator()(const ShaderVariantKey& k) const noexcept { + u64 h = k.nameHash; + h = (h * 1099511628211ull) ^ static_cast(k.stage); + h = (h * 1099511628211ull) ^ static_cast(k.flags); + return static_cast(h); + } +}; + +// Compile-on-demand variant cache. The Compiler and Device are borrowed (owned by +// the caller). Sources are registered per (name, stage) - vertex and fragment are +// separate HLSL with `main` entry points. +class ShaderSystem { +public: + ShaderSystem(Compiler& compiler, rhi::Device& device) noexcept + : m_compiler(&compiler), m_device(&device) {} + + ~ShaderSystem() { destroyAll(); } + + ShaderSystem(const ShaderSystem&) = delete; + ShaderSystem& operator=(const ShaderSystem&) = delete; + + // Register a shader's HLSL source for a stage (owned copy). + void registerSource(std::u8string_view name, ShaderStage stage, std::u8string_view hlsl) + { + m_sources.insert_or_assign(sourceKey(name, stage), std::u8string(hlsl)); + } + + // Include search paths for DXC #include resolution of shared .hlsli (owned). + void setIncludePaths(std::span paths) + { + m_includePaths.clear(); + for (usize i = 0; i < paths.size(); ++i) { m_includePaths.push_back(std::u8string(paths[i])); } + } + + // Get (compile-on-demand + cache) the GPU module for a variant. Returns null + // if the source is unknown or compilation fails (failures are NOT cached, so a + // later request retries - e.g. after a fix). + [[nodiscard]] rhi::ShaderModule* getVariant(std::u8string_view name, ShaderStage stage, ShaderFlags flags) + { + const ShaderVariantKey key{ shaderNameHash(name), stage, flags }; + if (auto it = m_cache.find(key); it != m_cache.end()) { return it->second; } + + auto sit = m_sources.find(sourceKey(name, stage)); + if (sit == m_sources.end()) { return nullptr; } + + rhi::ShaderModule* module = compile(sit->second, stage, flags); + if (module == nullptr) { return nullptr; } + + m_cache.insert_or_assign(key, module); + return module; + } + + // Drop + destroy every cached variant of a shader and BUMP its version (the + // reload signal consumers poll). Call on a shader reload. The next getVariant + // recompiles. Returns how many variants were invalidated. + usize invalidateShader(std::u8string_view name) + { + const u64 nameHash = shaderNameHash(name); + std::vector toRemove; + for (auto& [k, v] : m_cache) + { + if (k.nameHash == nameHash) + { + if (v != nullptr) { m_device->destroyShaderModule(v); } + toRemove.push_back(k); + } + } + for (const ShaderVariantKey& k : toRemove) { m_cache.erase(k); } + bumpVersion(nameHash); + return toRemove.size(); + } + + // Monotonic version of a shader: bumped each invalidateShader (i.e. each + // reload). The PSO cache stamps pipelines with this and rebuilds when it + // changes. 0 if the shader was never registered/invalidated. + [[nodiscard]] u64 version(std::u8string_view name) noexcept + { + auto it = m_versions.find(shaderNameHash(name)); + return (it != m_versions.end()) ? it->second : 0ull; + } + +private: + [[nodiscard]] rhi::ShaderModule* compile(std::u8string_view source, ShaderStage stage, ShaderFlags flags) + { + const bool isDX12 = (m_device->type == rhi::DeviceType::DX12); + const ShaderTarget target = isDX12 ? ShaderTarget::DXIL : ShaderTarget::SPIRV; + + std::vector defines; + appendDefines(flags, defines); + + std::vector includeViews; + for (const std::u8string& p : m_includePaths) { includeViews.push_back(p); } + + CompileOptions opts{}; + opts.shaderModel = u8"6_0"; + opts.optimizationLevel = 3; + opts.defines = std::span(defines.data(), defines.size()); + opts.includePaths = std::span(includeViews.data(), includeViews.size()); + if (!isDX12) + { + // Vulkan: shift register spaces so HLSL b/t/u/s registers don't collide + // in SPIR-V (matches the sample framework's compileToModule). + opts.bindingShifts.constantBufferShift = 0; + opts.bindingShifts.textureShift = 1000; + opts.bindingShifts.uavShift = 2000; + opts.bindingShifts.samplerShift = 3000; + opts.bindingShiftSets = 4; + } + + CompileResult cr{}; + const Status r = m_compiler->compile( + reinterpret_cast(source.data()), source.size(), + stage, u8"main", target, opts, cr); + + if (r != ErrorCode::Ok || !cr.success) + { + if (cr.messages != nullptr) { rhi::logErrorf("Shader variant compile failed: %s", cr.messages); } + m_compiler->freeResult(cr); + return nullptr; + } + + rhi::ShaderModuleDesc desc{}; + desc.code = std::span(cr.bytecode, cr.bytecodeSize); + rhi::ShaderModule* module = nullptr; + const Status mr = m_device->createShaderModule(desc, module); + m_compiler->freeResult(cr); + return (mr == ErrorCode::Ok) ? module : nullptr; + } + + void destroyAll() + { + for (auto& kv : m_cache) { if (kv.second != nullptr) { m_device->destroyShaderModule(kv.second); } } + m_cache.clear(); + } + + [[nodiscard]] static u64 sourceKey(std::u8string_view name, ShaderStage stage) noexcept + { + return (shaderNameHash(name) * 1099511628211ull) ^ static_cast(stage); + } + + void bumpVersion(u64 nameHash) + { + ++m_versions[nameHash]; // default-constructs to 0, then increments + } + + Compiler* m_compiler; // borrowed + rhi::Device* m_device; // borrowed + std::unordered_map m_sources; // (name,stage) -> HLSL + std::unordered_map m_cache; // variant -> GPU module (owned) + std::unordered_map m_versions; // nameHash -> version + std::vector m_includePaths; +}; + +} // namespace draco::shaders diff --git a/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.test.cpp b/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.test.cpp new file mode 100644 index 00000000..faa0ff04 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/System/ShaderSystem.test.cpp @@ -0,0 +1,94 @@ +// Headless tests for the shader variant system: compile-on-demand, flags->defines, +// caching, and invalidation. Compiles real SPIR-V via DXC; creates modules on the +// Null RHI backend (so distinct compiles yield distinct module objects). +#include + +import core; +import rhi; +import rhi.null; +import shaders; +import shaders.system; + +using namespace draco; +using namespace draco::shaders; +namespace rhi = draco::rhi; + +namespace +{ + // Fails to compile unless NORMAL_MAP is defined - proves flags->defines apply. + constexpr const char8_t* kNeedsNormalMap = + u8"float4 main() : SV_Target {\n" + u8"#ifndef NORMAL_MAP\n" + u8"#error NORMAL_MAP required\n" + u8"#endif\n" + u8" return float4(1, 0, 0, 1);\n" + u8"}\n"; + + // Compiles regardless of flags. + constexpr const char8_t* kTrivialVertex = + u8"float4 main(uint id : SV_VertexID) : SV_Position { return float4(0, 0, 0, 1); }\n"; + + Compiler* makeCompiler() + { + Compiler* c = nullptr; + if (!createCompiler(CompilerDesc{}, c).isOk()) { return nullptr; } + return c; + } +} + +TEST_CASE("shader system: flags become defines; failures aren't cached") +{ + Compiler* compiler = makeCompiler(); + if (compiler == nullptr) { MESSAGE("DXC unavailable; skipping"); return; } + + rhi::null::NullDevice device; + { + ShaderSystem ss(*compiler, device); + ss.registerSource(u8"guarded", ShaderStage::Fragment, kNeedsNormalMap); + + // Without the flag, NORMAL_MAP is undefined -> compile error -> null (not cached). + CHECK(ss.getVariant(u8"guarded", ShaderStage::Fragment, ShaderFlags::None) == nullptr); + + // With NormalMap -> #define NORMAL_MAP -> compiles. + rhi::ShaderModule* m = ss.getVariant(u8"guarded", ShaderStage::Fragment, ShaderFlags::NormalMap); + CHECK(m != nullptr); + + // Same variant is cached (same module object). + CHECK(ss.getVariant(u8"guarded", ShaderStage::Fragment, ShaderFlags::NormalMap) == m); + + // A different flag doesn't define NORMAL_MAP -> still fails (specific mapping). + CHECK(ss.getVariant(u8"guarded", ShaderStage::Fragment, ShaderFlags::Emissive) == nullptr); + + // Unknown shader / wrong stage -> null. + CHECK(ss.getVariant(u8"missing", ShaderStage::Fragment, ShaderFlags::NormalMap) == nullptr); + CHECK(ss.getVariant(u8"guarded", ShaderStage::Vertex, ShaderFlags::NormalMap) == nullptr); + } + + compiler->destroy(); +} + +TEST_CASE("shader system: distinct variants cache separately; invalidate recompiles") +{ + Compiler* compiler = makeCompiler(); + if (compiler == nullptr) { return; } + + rhi::null::NullDevice device; + { + ShaderSystem ss(*compiler, device); + ss.registerSource(u8"vs", ShaderStage::Vertex, kTrivialVertex); + + rhi::ShaderModule* a = ss.getVariant(u8"vs", ShaderStage::Vertex, ShaderFlags::None); + rhi::ShaderModule* b = ss.getVariant(u8"vs", ShaderStage::Vertex, ShaderFlags::Skinned); + REQUIRE(a != nullptr); + REQUIRE(b != nullptr); + CHECK(a != b); // different variants + CHECK(ss.getVariant(u8"vs", ShaderStage::Vertex, ShaderFlags::None) == a); // cached + + // Invalidate drops the cached variants; a later request recompiles. + CHECK(ss.invalidateShader(u8"vs") == 2u); + rhi::ShaderModule* a2 = ss.getVariant(u8"vs", ShaderStage::Vertex, ShaderFlags::None); + CHECK(a2 != nullptr); + } + + compiler->destroy(); +} diff --git a/Engine/cpp/Runtime/Rendering/Shaders/Types.cppm b/Engine/cpp/Runtime/Rendering/Shaders/Types.cppm new file mode 100644 index 00000000..c31f35c6 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Shaders/Types.cppm @@ -0,0 +1,57 @@ +/// Shader compilation types. + +module; + +#include +#include + +export module shaders:types; + +import core.stdtypes; + +using namespace draco; + +export namespace draco::shaders { + +enum class ShaderStage : u32 { + Vertex, Fragment, Compute, Mesh, Task, + RayGen, ClosestHit, AnyHit, Miss, Intersection, Callable, +}; + +enum class ShaderTarget : u32 { + SPIRV, + DXIL, +}; + +struct ShaderDefine { + std::u8string_view name; + std::u8string_view value; +}; + +struct BindingShifts { + u32 constantBufferShift = 0; + u32 textureShift = 0; + u32 samplerShift = 0; + u32 uavShift = 0; +}; + +struct CompileOptions { + std::u8string_view shaderModel = u8"6_0"; + i32 optimizationLevel = 3; + bool enableDebugInfo = false; + bool rowMajorMatrices = false; + std::span defines; + std::span includePaths; + BindingShifts bindingShifts; + u32 bindingShiftSets = 1; +}; + +struct CompileResult { + u8* bytecode = nullptr; + usize bytecodeSize = 0; + char* messages = nullptr; + usize messagesSize = 0; + bool success = false; +}; + +} // namespace draco::shaders diff --git a/Engine/cpp/Runtime/Rendering/Shaders/fs.sc b/Engine/cpp/Runtime/Rendering/Shaders/fs.sc deleted file mode 100644 index c707ade0..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/fs.sc +++ /dev/null @@ -1,18 +0,0 @@ -$input v_normal, v_texcoord0 - -#include - -SAMPLER2D(s_texColor, 0); - -uniform vec4 u_tint; - -void main() -{ - vec3 lightDir = normalize(vec3(0.4, 1.0, 0.2)); - - float NdotL = max(dot(normalize(v_normal), lightDir), 0.2); - - vec4 texColor = texture2D(s_texColor, v_texcoord0); - - gl_FragColor = texColor * u_tint * NdotL; -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Rendering/Shaders/fs_quad.sc b/Engine/cpp/Runtime/Rendering/Shaders/fs_quad.sc deleted file mode 100644 index 9dc0e8ba..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/fs_quad.sc +++ /dev/null @@ -1,12 +0,0 @@ -$input v_texcoord0, v_color0 - -#include - -SAMPLER2D(s_texColor, 0); - -void main() -{ - vec4 tex = texture2D(s_texColor, v_texcoord0); - - gl_FragColor = tex * v_color0; -} diff --git a/Engine/cpp/Runtime/Rendering/Shaders/varying.def.sc b/Engine/cpp/Runtime/Rendering/Shaders/varying.def.sc deleted file mode 100644 index 4e4a36e2..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/varying.def.sc +++ /dev/null @@ -1,6 +0,0 @@ -vec3 a_position : POSITION; -vec3 a_normal : NORMAL; -vec2 a_texcoord0 : TEXCOORD0; - -vec3 v_normal : NORMAL; -vec2 v_texcoord0 : TEXCOORD0; \ No newline at end of file diff --git a/Engine/cpp/Runtime/Rendering/Shaders/varying_quad.def.sc b/Engine/cpp/Runtime/Rendering/Shaders/varying_quad.def.sc deleted file mode 100644 index a80b6016..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/varying_quad.def.sc +++ /dev/null @@ -1,6 +0,0 @@ -vec3 a_position : POSITION; -vec2 a_texcoord0 : TEXCOORD0; -vec4 a_color0 : COLOR0; - -vec2 v_texcoord0 : TEXCOORD0; -vec4 v_color0 : COLOR0; diff --git a/Engine/cpp/Runtime/Rendering/Shaders/vs.sc b/Engine/cpp/Runtime/Rendering/Shaders/vs.sc deleted file mode 100644 index f9ebb00f..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/vs.sc +++ /dev/null @@ -1,12 +0,0 @@ -$input a_position, a_normal, a_texcoord0 -$output v_normal, v_texcoord0 - -#include - -void main() -{ - gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); - - v_normal = a_normal; - v_texcoord0 = a_texcoord0; -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Rendering/Shaders/vs_quad.sc b/Engine/cpp/Runtime/Rendering/Shaders/vs_quad.sc deleted file mode 100644 index 92a21415..00000000 --- a/Engine/cpp/Runtime/Rendering/Shaders/vs_quad.sc +++ /dev/null @@ -1,21 +0,0 @@ -$input a_position, a_texcoord0, a_color0 -$output v_texcoord0, v_color0 - -#include - -void main() -{ - // Grab the actual screen dimensions automatically tracked by bgfx - float width = u_viewRect.z; - float height = u_viewRect.w; - - // Convert pixel coordinates [0, width] and [0, height] directly to NDC [-1, 1] - float ndcX = (a_position.x / width) * 2.0 - 1.0; - float ndcY = 1.0 - (a_position.y / height) * 2.0; // Flip Y so 0 is top of the screen - - // Lock Z to 0.0 and W to 1.0 - gl_Position = vec4(ndcX, ndcY, 0.0, 1.0); - - v_texcoord0 = a_texcoord0; - v_color0 = a_color0; -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Rendering/Texture/FormatUtils.cppm b/Engine/cpp/Runtime/Rendering/Texture/FormatUtils.cppm new file mode 100644 index 00000000..72c6a2c3 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Texture/FormatUtils.cppm @@ -0,0 +1,62 @@ +// Draconic::Texture - :format_utils partition +// +// Maps an image PixelFormat to the RHI TextureFormat, honoring the source + +module; + +export module texture:format_utils; + +import core; +import rhi; +import image; + +using namespace draco; + +export namespace draco::texture +{ + namespace rhi = draco::rhi; + namespace image = draco::image; + + class TextureFormatUtils + { + public: + // Image PixelFormat -> RHI TextureFormat. When the data is sRGB and the + // format has an sRGB variant (8-bit RGB/RGBA/BGR/BGRA), the sRGB GPU + // format is returned so hardware decodes sRGB->linear on sample. Float + // and 1/2-channel formats pass through. 3-channel maps to RGBA (GPUs + // don't support 3-channel). + [[nodiscard]] static rhi::TextureFormat convert(image::PixelFormat format, image::ImageColorSpace colorSpace) + { + if (colorSpace == image::ImageColorSpace::Srgb) + { + switch (format) + { + case image::PixelFormat::RGB8: + case image::PixelFormat::RGBA8: return rhi::TextureFormat::RGBA8UnormSrgb; + case image::PixelFormat::BGR8: + case image::PixelFormat::BGRA8: return rhi::TextureFormat::BGRA8UnormSrgb; + default: break; + } + } + + switch (format) + { + case image::PixelFormat::R8: return rhi::TextureFormat::R8Unorm; + case image::PixelFormat::RG8: return rhi::TextureFormat::RG8Unorm; + case image::PixelFormat::RGB8: return rhi::TextureFormat::RGBA8Unorm; + case image::PixelFormat::RGBA8: return rhi::TextureFormat::RGBA8Unorm; + case image::PixelFormat::BGR8: return rhi::TextureFormat::BGRA8Unorm; + case image::PixelFormat::BGRA8: return rhi::TextureFormat::BGRA8Unorm; + case image::PixelFormat::R16F: return rhi::TextureFormat::R16Float; + case image::PixelFormat::RG16F: return rhi::TextureFormat::RG16Float; + case image::PixelFormat::RGB16F: return rhi::TextureFormat::RGBA16Float; + case image::PixelFormat::RGBA16F: return rhi::TextureFormat::RGBA16Float; + case image::PixelFormat::R32F: return rhi::TextureFormat::R32Float; + case image::PixelFormat::RG32F: return rhi::TextureFormat::RG32Float; + case image::PixelFormat::RGB32F: return rhi::TextureFormat::RGBA32Float; + case image::PixelFormat::RGBA32F: return rhi::TextureFormat::RGBA32Float; + default: return rhi::TextureFormat::RGBA8Unorm; + } + } + }; +} diff --git a/Engine/cpp/Runtime/Rendering/Texture/TextureData.cppm b/Engine/cpp/Runtime/Rendering/Texture/TextureData.cppm new file mode 100644 index 00000000..a449863c --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Texture/TextureData.cppm @@ -0,0 +1,113 @@ +// Draconic::Texture - :data partition +// +// TextureData: a CPU-side descriptor of pixel data staged for GPU upload (it +// owns no GPU handle - the consumer creates the rhi::Texture from this). Ported +// from Sedulous.Textures/TextureData.bf. + +module; + +#include + +export module texture:data; + +import core; +import rhi; +import image; +import :format_utils; + +using namespace draco; + +export namespace draco::texture +{ + namespace rhi = draco::rhi; + namespace image = draco::image; + + // Raw texture data for upload to the GPU. Fields are lowercase (descriptor + // convention, matching RHI descs); the caller provides correctly-formatted + // pixel data and owns its lifetime. + struct TextureData + { + const u8* pixels = nullptr; // pixel data (not owned) + u64 size = 0; // total bytes + u32 width = 0; + u32 height = 0; + u32 depthOrArrayLayers = 1; // depth (3D) or layer count + u32 mipLevels = 1; // data must contain all mips if > 1 + rhi::TextureFormat format = rhi::TextureFormat::RGBA8Unorm; + rhi::TextureDimension dimension = rhi::TextureDimension::Texture2D; + u32 bytesPerRow = 0; // 0 = auto + u32 rowsPerImage = 0; // 0 = auto + + [[nodiscard]] static TextureData create2D(const u8* pixels, u64 size, u32 width, u32 height, rhi::TextureFormat format) + { + return TextureData{ pixels, size, width, height, 1, 1, format, rhi::TextureDimension::Texture2D, 0, 0 }; + } + + [[nodiscard]] static TextureData create2DWithMips(const u8* pixels, u64 size, u32 width, u32 height, u32 mipLevels, rhi::TextureFormat format) + { + return TextureData{ pixels, size, width, height, 1, mipLevels, format, rhi::TextureDimension::Texture2D, 0, 0 }; + } + + // Cubemap: 6 square faces packed as a 2D array of 6 layers. + [[nodiscard]] static TextureData createCube(const u8* pixels, u64 size, u32 faceSize, rhi::TextureFormat format) + { + return TextureData{ pixels, size, faceSize, faceSize, 6, 1, format, rhi::TextureDimension::Texture2D, 0, 0 }; + } + + [[nodiscard]] static TextureData create2DArray(const u8* pixels, u64 size, u32 width, u32 height, u32 layers, rhi::TextureFormat format) + { + return TextureData{ pixels, size, width, height, layers, 1, format, rhi::TextureDimension::Texture2D, 0, 0 }; + } + + // Builds 2D texture data from an image. `colorSpace` selects the sRGB GPU + // format (hardware sRGB->linear on sample) for color imagery, or a linear + // format for data textures (normal maps, masks, HDR). + [[nodiscard]] static TextureData fromImage(const image::Image& image, image::ImageColorSpace colorSpace) + { + const std::span data = image.pixelData(); + return create2D(data.data(), static_cast(data.size()), image.width(), image.height(), + TextureFormatUtils::convert(image.format(), colorSpace)); + } + + // Bytes per pixel for an RHI format (uncompressed formats; default 4). + [[nodiscard]] static u32 getBytesPerPixel(rhi::TextureFormat format) + { + switch (format) + { + case rhi::TextureFormat::R8Unorm: case rhi::TextureFormat::R8Snorm: + case rhi::TextureFormat::R8Uint: case rhi::TextureFormat::R8Sint: + return 1; + case rhi::TextureFormat::R16Uint: case rhi::TextureFormat::R16Sint: case rhi::TextureFormat::R16Float: + case rhi::TextureFormat::RG8Unorm: case rhi::TextureFormat::RG8Snorm: + case rhi::TextureFormat::RG8Uint: case rhi::TextureFormat::RG8Sint: + case rhi::TextureFormat::Depth16Unorm: + return 2; + case rhi::TextureFormat::R32Uint: case rhi::TextureFormat::R32Sint: case rhi::TextureFormat::R32Float: + case rhi::TextureFormat::RG16Uint: case rhi::TextureFormat::RG16Sint: case rhi::TextureFormat::RG16Float: + case rhi::TextureFormat::RGBA8Unorm: case rhi::TextureFormat::RGBA8UnormSrgb: case rhi::TextureFormat::RGBA8Snorm: + case rhi::TextureFormat::RGBA8Uint: case rhi::TextureFormat::RGBA8Sint: + case rhi::TextureFormat::BGRA8Unorm: case rhi::TextureFormat::BGRA8UnormSrgb: + case rhi::TextureFormat::Depth24Plus: case rhi::TextureFormat::Depth24PlusStencil8: case rhi::TextureFormat::Depth32Float: + return 4; + case rhi::TextureFormat::RG32Uint: case rhi::TextureFormat::RG32Sint: case rhi::TextureFormat::RG32Float: + case rhi::TextureFormat::RGBA16Uint: case rhi::TextureFormat::RGBA16Sint: case rhi::TextureFormat::RGBA16Float: + return 8; + case rhi::TextureFormat::RGBA32Uint: case rhi::TextureFormat::RGBA32Sint: case rhi::TextureFormat::RGBA32Float: + return 16; + case rhi::TextureFormat::Depth32FloatStencil8: + return 8; + default: + return 4; + } + } + + // Expected byte size of a mip level. + [[nodiscard]] u64 calculateMipSize(u32 mipLevel) const + { + const u32 mipWidth = std::max(1u, width >> mipLevel); + const u32 mipHeight = std::max(1u, height >> mipLevel); + const u32 bpp = getBytesPerPixel(format); + return static_cast(mipWidth) * mipHeight * depthOrArrayLayers * bpp; + } + }; +} diff --git a/Engine/cpp/Runtime/Rendering/Texture/TextureDataTests.test.cpp b/Engine/cpp/Runtime/Rendering/Texture/TextureDataTests.test.cpp new file mode 100644 index 00000000..5e5361d7 --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Texture/TextureDataTests.test.cpp @@ -0,0 +1,81 @@ +// Tests for texture: descriptor factories, format conversion, mip sizes. +#include +import core; +import rhi; +import image; +import texture; +using namespace draco; +using namespace draco::texture; +namespace rhi = draco::rhi; +namespace image = draco::image; + +TEST_CASE("textures.data: Create2D / mips / cube / array") +{ + u8 pixels[16] = {}; + TextureData t = TextureData::create2D(pixels, 16, 2, 2, rhi::TextureFormat::RGBA8Unorm); + CHECK(t.width == 2u); + CHECK(t.height == 2u); + CHECK(t.depthOrArrayLayers == 1u); + CHECK(t.mipLevels == 1u); + CHECK(t.dimension == rhi::TextureDimension::Texture2D); + CHECK(t.format == rhi::TextureFormat::RGBA8Unorm); + + TextureData mips = TextureData::create2DWithMips(pixels, 16, 4, 4, 3, rhi::TextureFormat::RGBA8Unorm); + CHECK(mips.mipLevels == 3u); + + TextureData cube = TextureData::createCube(pixels, 16, 8, rhi::TextureFormat::RGBA8Unorm); + CHECK(cube.depthOrArrayLayers == 6u); + CHECK(cube.width == 8u); + CHECK(cube.height == 8u); + + TextureData arr = TextureData::create2DArray(pixels, 16, 4, 4, 5, rhi::TextureFormat::RGBA8Unorm); + CHECK(arr.depthOrArrayLayers == 5u); +} + +TEST_CASE("textures.data: bytes per pixel") +{ + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::R8Unorm) == 1u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::RG8Unorm) == 2u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::RGBA8Unorm) == 4u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::RGBA8UnormSrgb) == 4u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::RGBA16Float) == 8u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::RGBA32Float) == 16u); + CHECK(TextureData::getBytesPerPixel(rhi::TextureFormat::Depth16Unorm) == 2u); +} + +TEST_CASE("textures.data: mip size halves") +{ + u8 pixels[1] = {}; + TextureData t = TextureData::create2D(pixels, 0, 8, 8, rhi::TextureFormat::RGBA8Unorm); + CHECK(t.calculateMipSize(0) == static_cast(8 * 8 * 4)); // 256 + CHECK(t.calculateMipSize(1) == static_cast(4 * 4 * 4)); // 64 + CHECK(t.calculateMipSize(3) == static_cast(1 * 1 * 4)); // clamps to 1x1 +} + +TEST_CASE("textures.format: PixelFormat -> TextureFormat with color space") +{ + using image::PixelFormat; + using image::ImageColorSpace; + // sRGB color imagery selects the sRGB GPU format. + CHECK(TextureFormatUtils::convert(PixelFormat::RGBA8, ImageColorSpace::Srgb) == rhi::TextureFormat::RGBA8UnormSrgb); + CHECK(TextureFormatUtils::convert(PixelFormat::RGB8, ImageColorSpace::Srgb) == rhi::TextureFormat::RGBA8UnormSrgb); + CHECK(TextureFormatUtils::convert(PixelFormat::BGRA8, ImageColorSpace::Srgb) == rhi::TextureFormat::BGRA8UnormSrgb); + // Linear data textures stay unorm. + CHECK(TextureFormatUtils::convert(PixelFormat::RGBA8, ImageColorSpace::Linear) == rhi::TextureFormat::RGBA8Unorm); + CHECK(TextureFormatUtils::convert(PixelFormat::R8, ImageColorSpace::Linear) == rhi::TextureFormat::R8Unorm); + // 3-channel maps to RGBA; floats pass through (no sRGB variant). + CHECK(TextureFormatUtils::convert(PixelFormat::RGB8, ImageColorSpace::Linear) == rhi::TextureFormat::RGBA8Unorm); + CHECK(TextureFormatUtils::convert(PixelFormat::RGB16F, ImageColorSpace::Srgb) == rhi::TextureFormat::RGBA16Float); + CHECK(TextureFormatUtils::convert(PixelFormat::R32F, ImageColorSpace::Linear) == rhi::TextureFormat::R32Float); +} + +TEST_CASE("textures.data: FromImage") +{ + image::Image image(2, 2, image::PixelFormat::RGBA8); + TextureData t = TextureData::fromImage(image, image::ImageColorSpace::Srgb); + CHECK(t.width == 2u); + CHECK(t.height == 2u); + CHECK(t.format == rhi::TextureFormat::RGBA8UnormSrgb); + CHECK(t.pixels == image.pixelData().data()); + CHECK(t.size == static_cast(image.pixelData().size())); +} diff --git a/Engine/cpp/Runtime/Rendering/Texture/TextureModule.cppm b/Engine/cpp/Runtime/Rendering/Texture/TextureModule.cppm new file mode 100644 index 00000000..8308ac1a --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Texture/TextureModule.cppm @@ -0,0 +1,13 @@ +// Draconic::Texture - the `texture` module. +// +// Logical texture types + a CPU-side upload descriptor (TextureData) and +// image->RHI format conversion. The descriptor layer between draco.image (CPU) +// and draco.rhi (GPU); consumers (VG renderer, the texture resource factory) +// create the actual rhi::Texture from a TextureData. Ported from +// Sedulous.Textures. One named module composed of partitions. + +export module texture; + +export import :types; +export import :format_utils; +export import :data; diff --git a/Engine/cpp/Runtime/Rendering/Texture/Types.cppm b/Engine/cpp/Runtime/Rendering/Texture/Types.cppm new file mode 100644 index 00000000..b5098b7d --- /dev/null +++ b/Engine/cpp/Runtime/Rendering/Texture/Types.cppm @@ -0,0 +1,42 @@ +// Draconic::Texture - :types partition +// +// Logical texture descriptors: shape, filter, wrap. Used by TextureResource to +// say how pixel data should be interpreted and sampled. Ported from +// Sedulous.Textures/TextureTypes.bf. + +module; + +export module texture:types; + +import core; + +using namespace draco; + +export namespace draco::texture +{ + // The logical shape of a texture asset. + enum class TextureShape : u8 + { + Texture2D, // standard 2D image + Texture2DArray, // multiple same-size layers + Texture3D, // volume + Cubemap, // 6 square faces (+X,-X,+Y,-Y,+Z,-Z) + CubemapArray, // array of cubemaps + }; + + enum class TextureFilter : u8 + { + Nearest, + Linear, + MipmapNearest, + MipmapLinear, + }; + + enum class TextureWrap : u8 + { + Repeat, + ClampToEdge, + ClampToBorder, + MirroredRepeat, + }; +} diff --git a/Engine/cpp/Runtime/Scene/CMakeLists.txt b/Engine/cpp/Runtime/Scene/CMakeLists.txt deleted file mode 100644 index 4ae18357..00000000 --- a/Engine/cpp/Runtime/Scene/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -add_modules_library(Camera) -add_modules_library(TransformComponent) -add_modules_library(Renderable) - -target_link_libraries(Camera PUBLIC Core Rendering bx) -target_link_libraries(TransformComponent PUBLIC Core) -target_link_libraries(Renderable PUBLIC TransformComponent Core Rendering) diff --git a/Engine/cpp/Runtime/Scene/Camera/CameraController.cpp b/Engine/cpp/Runtime/Scene/Camera/CameraController.cpp deleted file mode 100644 index 9ede1ef5..00000000 --- a/Engine/cpp/Runtime/Scene/Camera/CameraController.cpp +++ /dev/null @@ -1,98 +0,0 @@ -module; - -#include -#include - -module scene.camera.controller; - -namespace draco::scene -{ - void CameraController::init(f32 px, f32 py, f32 pz) - { - x = px; - y = py; - z = pz; - - yaw = 0.0f; - pitch = 0.0f; - - speed = 5.0f; // units per second - sensitivity = 0.002f; // mouse sensitivity - } - - void CameraController::update(f32 dt, const CameraInput& input) - { - yaw += input.mouseDx * sensitivity; - pitch -= input.mouseDy * sensitivity; // Temp fix to flip mouse input - - // Clamp pitch - if (pitch > 1.5f) pitch = 1.5f; - if (pitch < -1.5f) pitch = -1.5f; - - bx::Vec3 forward = { - cosf(pitch) * sinf(yaw), - sinf(pitch), - cosf(pitch) * cosf(yaw) - }; - - bx::Vec3 right = { - sinf(yaw - bx::kPiHalf), - 0.0f, - cosf(yaw - bx::kPiHalf) - }; - - f32 velocity = speed * dt; - - if (input.moveForward) - { - x += forward.x * velocity; - y += forward.y * velocity; - z += forward.z * velocity; - } - - if (input.moveBackward) - { - x -= forward.x * velocity; - y -= forward.y * velocity; - z -= forward.z * velocity; - } - - if (input.moveLeft) - { - x += right.x * velocity; - z += right.z * velocity; - } - - if (input.moveRight) - { - x -= right.x * velocity; - z -= right.z * velocity; - } - } - - rendering::renderer::Camera CameraController::getCamera() const - { - const bx::Vec3 forward = { - cosf(pitch) * sinf(yaw), - sinf(pitch), - cosf(pitch) * cosf(yaw) - }; - - rendering::renderer::Camera cam{}; - - cam.position = { x, y, z }; - cam.target = { - x + forward.x, - y + forward.y, - z + forward.z - }; - - cam.up = { 0.0f, 1.0f, 0.0f }; - - cam.fov = 60.0f; - cam.nearPlane = 0.1f; - cam.farPlane = 100.0f; - - return cam; - } -} diff --git a/Engine/cpp/Runtime/Scene/Camera/CameraController.cppm b/Engine/cpp/Runtime/Scene/Camera/CameraController.cppm deleted file mode 100644 index 669719a9..00000000 --- a/Engine/cpp/Runtime/Scene/Camera/CameraController.cppm +++ /dev/null @@ -1,35 +0,0 @@ -export module scene.camera.controller; - -import core.stdtypes; -import rendering; - -export namespace draco::scene -{ - // Per-frame input for the camera, supplied by the caller. Keeps the - // controller decoupled from any specific input/shell backend. - struct CameraInput - { - f32 mouseDx = 0.0f; - f32 mouseDy = 0.0f; - bool moveForward = false; - bool moveBackward = false; - bool moveLeft = false; - bool moveRight = false; - }; - - struct CameraController - { - void init(f32 x = 0.0f, f32 y = 0.0f, f32 z = -2.0f); - - void update(f32 dt, const CameraInput& input); - - [[nodiscard]] rendering::renderer::Camera getCamera() const; - - private: - f32 x = 0.0f, y = 0.0f, z = 0.0f; - f32 yaw = 0.0f; - f32 pitch = 0.0f; - f32 speed = 5.0f; - f32 sensitivity = 0.1f; - }; -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Scene/Renderable/Renderable.cppm b/Engine/cpp/Runtime/Scene/Renderable/Renderable.cppm deleted file mode 100644 index 599fcb35..00000000 --- a/Engine/cpp/Runtime/Scene/Renderable/Renderable.cppm +++ /dev/null @@ -1,15 +0,0 @@ -export module scene.renderable; - -import rendering.mesh; -import rendering.material; -import core.math.transform; - -export namespace draco::scene::renderable -{ - struct Renderable - { - rendering::mesh::MeshHandle mesh{}; - math::Transform transform{}; - rendering::material::Material material{}; - }; -} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Scene/Scene.cppm b/Engine/cpp/Runtime/Scene/Scene.cppm deleted file mode 100644 index 00768e1f..00000000 --- a/Engine/cpp/Runtime/Scene/Scene.cppm +++ /dev/null @@ -1,17 +0,0 @@ -module; - -#include - -export module scene; - -export import scene.renderable; -export import scene.transform_component; -export import scene.camera.controller; - -export namespace draco::scene -{ - struct Scene - { - std::vector renderables; - }; -} diff --git a/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cpp b/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cpp deleted file mode 100644 index fbadc4ef..00000000 --- a/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cpp +++ /dev/null @@ -1,9 +0,0 @@ -module scene.transform_component; - -namespace draco::scene -{ - void markDirty(TransformComponent& t) - { - t.dirty = true; - } -} diff --git a/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cppm b/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cppm deleted file mode 100644 index d867f1bd..00000000 --- a/Engine/cpp/Runtime/Scene/TransformComponent/TransformComponent.cppm +++ /dev/null @@ -1,15 +0,0 @@ -export module scene.transform_component; - -import core.math.transform; - -export namespace draco::scene -{ - struct TransformComponent - { - math::Transform local{}; - math::Transform world{}; - bool dirty = true; - }; - - void markDirty(TransformComponent& t); -} diff --git a/Engine/cpp/ThirdParty/CMakeLists.txt b/Engine/cpp/ThirdParty/CMakeLists.txt index b76d5b72..c180887f 100644 --- a/Engine/cpp/ThirdParty/CMakeLists.txt +++ b/Engine/cpp/ThirdParty/CMakeLists.txt @@ -1,23 +1,14 @@ -set(BGFX_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(BGFX_BUILD_TOOLS OFF CACHE BOOL "" FORCE) -set(BGFX_BUILD_TOOLS_SHADER ON CACHE BOOL "" FORCE) -set(BGFX_BUILD_TOOLS_TEXTURE OFF CACHE BOOL "" FORCE) -set(BGFX_BUILD_TOOLS_GEOMETRY OFF CACHE BOOL "" FORCE) - -set(BGFX_INSTALL OFF CACHE BOOL "" FORCE) -set(BGFX_LIBRARY_TYPE "STATIC") - set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_MAIN_HANDLED ON CACHE BOOL "" FORCE) set(SDL_STATIC_PIC ON CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) -set(SDL_AUDIO OFF CACHE BOOL "" FORCE) -set(SDL_VIDEO ON CACHE BOOL "" FORCE) -set(SDL_RENDER OFF CACHE BOOL "" FORCE) # Off because bgfx handles the rendering! -set(SDL_CAMERA OFF CACHE BOOL "" FORCE) -set(SDL_JOYSTICK ON CACHE BOOL "" FORCE) # Required by SDL_INIT_GAMEPAD (shell.desktop input) +set(SDL_AUDIO OFF CACHE BOOL "" FORCE) +set(SDL_VIDEO ON CACHE BOOL "" FORCE) +set(SDL_RENDER OFF CACHE BOOL "" FORCE) # Off: the engine owns rendering +set(SDL_CAMERA OFF CACHE BOOL "" FORCE) +set(SDL_JOYSTICK ON CACHE BOOL "" FORCE) # Required by SDL_INIT_GAMEPAD (shell.desktop input) set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) # Not needed: gamepad rumble uses SDL_RumbleGamepad (joystick), not the haptic subsystem set(SDL_HIDAPI ON CACHE BOOL "" FORCE) # Controller backends (PS/Switch/Xbox over USB/BT) @@ -32,7 +23,7 @@ elseif(WIN32) set(SDL_X11 OFF CACHE BOOL "" FORCE) else() set(SDL_WAYLAND ON CACHE BOOL "" FORCE) - set(SDL_LIBDECOR ON CACHE BOOL "" FORCE) + set(SDL_LIBDECOR ON CACHE BOOL "" FORCE) set(SDL_X11 ON CACHE BOOL "" FORCE) set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE) set(SDL_WAYLAND_SHARED OFF CACHE BOOL "" FORCE) @@ -41,14 +32,9 @@ else() set(SDL_PULSEAUDIO_SHARED OFF CACHE BOOL "" FORCE) endif() -set(BX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/bx CACHE STRING "" FORCE) -set(BIMG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/bimg CACHE STRING "" FORCE) -set(BGFX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/bgfx CACHE STRING "" FORCE) - -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/cmake/bx bx-build) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/cmake/bimg bimg-build) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/cmake/bgfx bgfx-build) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/sdl sdl-build) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/stb) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/DXC) -target_link_libraries(bgfx PUBLIC bx bimg) +add_subdirectory(ufbx) +add_subdirectory(cgltf) diff --git a/Engine/cpp/ThirdParty/DXC/CMakeLists.txt b/Engine/cpp/ThirdParty/DXC/CMakeLists.txt new file mode 100644 index 00000000..11089a06 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/CMakeLists.txt @@ -0,0 +1,20 @@ +# DXC (DirectX Shader Compiler). The headers are used at compile time; the runtime +# library (dxcompiler.dll / libdxcompiler.so) is loaded on demand by draco.shaders +# via LoadLibrary/dlopen, so there is no import library to link. +# +# Draco::DXCHeaders carries the include dir AND the DRACO_DXC_PATH definition (the +# absolute path draco.shaders falls back to when no override is given), so a target +# that links it gets both. + +add_library(DracoDXCHeaders INTERFACE) +add_library(Draco::DXCHeaders ALIAS DracoDXCHeaders) + +target_include_directories(DracoDXCHeaders INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +if(WIN32) + set(_dxc_runtime "${CMAKE_CURRENT_SOURCE_DIR}/lib/win-x64/dxcompiler.dll") +else() + set(_dxc_runtime "${CMAKE_CURRENT_SOURCE_DIR}/lib/linux-x86_64/libdxcompiler.so") +endif() + +target_compile_definitions(DracoDXCHeaders INTERFACE DRACO_DXC_PATH="${_dxc_runtime}") diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/Support/ErrorCodes.h b/Engine/cpp/ThirdParty/DXC/include/dxc/Support/ErrorCodes.h new file mode 100644 index 00000000..5239c811 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/Support/ErrorCodes.h @@ -0,0 +1,160 @@ +/////////////////////////////////////////////////////////////////////////////// +// // +// ErrorCodes.h // +// Copyright (C) Microsoft Corporation. All rights reserved. // +// This file is distributed under the University of Illinois Open Source // +// License. See LICENSE.TXT for details. // +// // +// Provides error code values for the DirectX compiler and tools. // +// // +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Redeclare some macros to not depend on winerror.h +#define DXC_SEVERITY_ERROR 1 +#define DXC_MAKE_HRESULT(sev, fac, code) \ + ((HRESULT)(((unsigned long)(sev) << 31) | ((unsigned long)(fac) << 16) | \ + ((unsigned long)(code)))) + +#define HRESULT_IS_WIN32ERR(hr) \ + ((HRESULT)(hr & 0xFFFF0000) == \ + MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, 0)) +#define HRESULT_AS_WIN32ERR(hr) (HRESULT_CODE(hr)) + +// Error codes from C libraries (0n150) - 0x8096xxxx +#define FACILITY_ERRNO (0x96) +#define HRESULT_FROM_ERRNO(x) \ + MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_ERRNO, (x)) + +// Error codes from DXC libraries (0n170) - 0x8013xxxx +#define FACILITY_DXC (0xAA) + +// 0x00000000 - The operation succeeded. +#define DXC_S_OK 0 // _HRESULT_TYPEDEF_(0x00000000L) + +// 0x80AA0001 - The operation failed because overlapping semantics were found. +#define DXC_E_OVERLAPPING_SEMANTICS \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0001)) + +// 0x80AA0002 - The operation failed because multiple depth semantics were +// found. +#define DXC_E_MULTIPLE_DEPTH_SEMANTICS \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0002)) + +// 0x80AA0003 - Input file is too large. +#define DXC_E_INPUT_FILE_TOO_LARGE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0003)) + +// 0x80AA0004 - Error parsing DXBC container. +#define DXC_E_INCORRECT_DXBC \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0004)) + +// 0x80AA0005 - Error parsing DXBC bytecode. +#define DXC_E_ERROR_PARSING_DXBC_BYTECODE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0005)) + +// 0x80AA0006 - Data is too large. +#define DXC_E_DATA_TOO_LARGE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0006)) + +// 0x80AA0007 - Incompatible converter options. +#define DXC_E_INCOMPATIBLE_CONVERTER_OPTIONS \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0007)) + +// 0x80AA0008 - Irreducible control flow graph. +#define DXC_E_IRREDUCIBLE_CFG \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0008)) + +// 0x80AA0009 - IR verification error. +#define DXC_E_IR_VERIFICATION_FAILED \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0009)) + +// 0x80AA000A - Scope-nested control flow recovery failed. +#define DXC_E_SCOPE_NESTED_FAILED \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000A)) + +// 0x80AA000B - Operation is not supported. +#define DXC_E_NOT_SUPPORTED \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000B)) + +// 0x80AA000C - Unable to encode string. +#define DXC_E_STRING_ENCODING_FAILED \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000C)) + +// 0x80AA000D - DXIL container is invalid. +#define DXC_E_CONTAINER_INVALID \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000D)) + +// 0x80AA000E - DXIL container is missing the DXIL part. +#define DXC_E_CONTAINER_MISSING_DXIL \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000E)) + +// 0x80AA000F - Unable to parse DxilModule metadata. +#define DXC_E_INCORRECT_DXIL_METADATA \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x000F)) + +// 0x80AA0010 - Error parsing DDI signature. +#define DXC_E_INCORRECT_DDI_SIGNATURE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0010)) + +// 0x80AA0011 - Duplicate part exists in dxil container. +#define DXC_E_DUPLICATE_PART \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0011)) + +// 0x80AA0012 - Error finding part in dxil container. +#define DXC_E_MISSING_PART \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0012)) + +// 0x80AA0013 - Malformed DXIL Container. +#define DXC_E_MALFORMED_CONTAINER \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0013)) + +// 0x80AA0014 - Incorrect Root Signature for shader. +#define DXC_E_INCORRECT_ROOT_SIGNATURE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0014)) + +// 0X80AA0015 - DXIL container is missing DebugInfo part. +#define DXC_E_CONTAINER_MISSING_DEBUG \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0015)) + +// 0X80AA0016 - Unexpected failure in macro expansion. +#define DXC_E_MACRO_EXPANSION_FAILURE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0016)) + +// 0X80AA0017 - DXIL optimization pass failed. +#define DXC_E_OPTIMIZATION_FAILED \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0017)) + +// 0X80AA0018 - General internal error. +#define DXC_E_GENERAL_INTERNAL_ERROR \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0018)) + +// 0X80AA0019 - Abort compilation error. +#define DXC_E_ABORT_COMPILATION_ERROR \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x0019)) + +// 0X80AA001A - Error in extension mechanism. +#define DXC_E_EXTENSION_ERROR \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001A)) + +// 0X80AA001B - LLVM Fatal Error +#define DXC_E_LLVM_FATAL_ERROR \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001B)) + +// 0X80AA001C - LLVM Unreachable code +#define DXC_E_LLVM_UNREACHABLE \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001C)) + +// 0X80AA001D - LLVM Cast Failure +#define DXC_E_LLVM_CAST_ERROR \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001D)) + +// 0X80AA001E - External validator (DXIL.dll) required, and missing. +#define DXC_E_VALIDATOR_MISSING \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001E)) + +// 0X80AA001F - DXIL container Program Version mismatches Dxil module shader +// model +#define DXC_E_INCORRECT_PROGRAM_VERSION \ + DXC_MAKE_HRESULT(DXC_SEVERITY_ERROR, FACILITY_DXC, (0x001F)) \ No newline at end of file diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/WinAdapter.h b/Engine/cpp/ThirdParty/DXC/include/dxc/WinAdapter.h new file mode 100644 index 00000000..98614921 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/WinAdapter.h @@ -0,0 +1,1056 @@ +//===- WinAdapter.h - Windows Adapter for non-Windows platforms -*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file defines Windows-specific types, macros, and SAL annotations used +// in the codebase for non-Windows platforms. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_SUPPORT_WIN_ADAPTER_H +#define LLVM_SUPPORT_WIN_ADAPTER_H + +#ifndef _WIN32 + +#ifdef __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif // __cplusplus + +#define COM_NO_WINDOWS_H // needed to inform d3d headers that this isn't windows + +//===----------------------------------------------------------------------===// +// +// Begin: Macro Definitions +// +//===----------------------------------------------------------------------===// +#define C_ASSERT(expr) static_assert((expr), "") +#define ATLASSERT assert + +#define CoTaskMemAlloc malloc +#define CoTaskMemFree free + +#define ARRAYSIZE(array) (sizeof(array) / sizeof(array[0])) + +#define _countof(a) (sizeof(a) / sizeof(*(a))) + +// If it is GCC, there is no UUID support and we must emulate it. +// Clang support depends on the -fms-extensions compiler flag. +#if !defined(__clang__) || !defined(_MSC_EXTENSIONS) +#define __EMULATE_UUID 1 +#endif // __clang__ + +#ifdef __EMULATE_UUID +#define __declspec(x) +#endif // __EMULATE_UUID + +#define DECLSPEC_SELECTANY + +#ifdef __EMULATE_UUID +#define uuid(id) +#endif // __EMULATE_UUID + +#define STDMETHODCALLTYPE +#define STDMETHODIMP_(type) type STDMETHODCALLTYPE +#define STDMETHODIMP STDMETHODIMP_(HRESULT) +#define STDMETHOD_(type, name) virtual STDMETHODIMP_(type) name +#define STDMETHOD(name) STDMETHOD_(HRESULT, name) +#define EXTERN_C extern "C" + +#define UNREFERENCED_PARAMETER(P) (void)(P) + +#define RtlEqualMemory(Destination, Source, Length) \ + (!memcmp((Destination), (Source), (Length))) +#define RtlMoveMemory(Destination, Source, Length) \ + memmove((Destination), (Source), (Length)) +#define RtlCopyMemory(Destination, Source, Length) \ + memcpy((Destination), (Source), (Length)) +#define RtlFillMemory(Destination, Length, Fill) \ + memset((Destination), (Fill), (Length)) +#define RtlZeroMemory(Destination, Length) memset((Destination), 0, (Length)) +#define MoveMemory RtlMoveMemory +#define CopyMemory RtlCopyMemory +#define FillMemory RtlFillMemory +#define ZeroMemory RtlZeroMemory + +#define FALSE 0 +#define TRUE 1 + +// We ignore the code page completely on Linux. +#define GetConsoleOutputCP() 0 + +#define _HRESULT_TYPEDEF_(_sc) ((HRESULT)_sc) +#define DISP_E_BADINDEX _HRESULT_TYPEDEF_(0x8002000BL) +#define REGDB_E_CLASSNOTREG _HRESULT_TYPEDEF_(0x80040154L) + +// This is an unsafe conversion. If needed, we can later implement a safe +// conversion that throws exceptions for overflow cases. +#define UIntToInt(uint_arg, int_ptr_arg) *int_ptr_arg = uint_arg + +#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) + +// Use errno to implement {Get|Set}LastError +#define GetLastError() errno +#define SetLastError(ERR) errno = ERR + +// Map these errors to equivalent errnos. +#define ERROR_SUCCESS 0L +#define ERROR_ARITHMETIC_OVERFLOW EOVERFLOW +#define ERROR_FILE_NOT_FOUND ENOENT +#define ERROR_FUNCTION_NOT_CALLED ENOSYS +#define ERROR_IO_DEVICE EIO +#define ERROR_INSUFFICIENT_BUFFER ENOBUFS +#define ERROR_INVALID_HANDLE EBADF +#define ERROR_INVALID_PARAMETER EINVAL +#define ERROR_OUT_OF_STRUCTURES ENOMEM +#define ERROR_NOT_CAPABLE EPERM +#define ERROR_NOT_FOUND ENOTSUP +#define ERROR_UNHANDLED_EXCEPTION EBADF +#define ERROR_BROKEN_PIPE EPIPE + +// Used by HRESULT <--> WIN32 error code conversion +#define SEVERITY_ERROR 1 +#define FACILITY_WIN32 7 +#define HRESULT_CODE(hr) ((hr) & 0xFFFF) +#define MAKE_HRESULT(severity, facility, code) \ + ((HRESULT)(((unsigned long)(severity) << 31) | \ + ((unsigned long)(facility) << 16) | ((unsigned long)(code)))) + +#define FILE_TYPE_UNKNOWN 0x0000 +#define FILE_TYPE_DISK 0x0001 +#define FILE_TYPE_CHAR 0x0002 +#define FILE_TYPE_PIPE 0x0003 +#define FILE_TYPE_REMOTE 0x8000 + +#define FILE_ATTRIBUTE_NORMAL 0x00000080 +#define FILE_ATTRIBUTE_DIRECTORY 0x00000010 +#define INVALID_FILE_ATTRIBUTES ((DWORD)-1) + +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +// STGTY ENUMS +#define STGTY_STORAGE 1 +#define STGTY_STREAM 2 +#define STGTY_LOCKBYTES 3 +#define STGTY_PROPERTY 4 + +// Storage errors +#define STG_E_INVALIDFUNCTION 1L +#define STG_E_ACCESSDENIED 2L + +#define STREAM_SEEK_SET 0 +#define STREAM_SEEK_CUR 1 +#define STREAM_SEEK_END 2 + +#define HEAP_NO_SERIALIZE 0x1 +#define HEAP_ZERO_MEMORY 0x8 + +#define MB_ERR_INVALID_CHARS 0x00000008 // error for invalid chars + +// File IO + +#define CREATE_ALWAYS 2 +#define CREATE_NEW 1 +#define OPEN_ALWAYS 4 +#define OPEN_EXISTING 3 +#define TRUNCATE_EXISTING 5 + +#define FILE_SHARE_DELETE 0x00000004 +#define FILE_SHARE_READ 0x00000001 +#define FILE_SHARE_WRITE 0x00000002 + +#define GENERIC_READ 0x80000000 +#define GENERIC_WRITE 0x40000000 + +#define _atoi64 atoll +#define sprintf_s snprintf +#define _strdup strdup +#define _strnicmp strnicmp + +#define vsnprintf_s vsnprintf +#define strcat_s strcat +#define strcpy_s(dst, n, src) strncpy(dst, src, n) +#define _vscwprintf vwprintf +#define vswprintf_s vswprintf +#define swprintf_s swprintf + +#define StringCchCopyW(dst, n, src) wcsncpy(dst, src, n) + +#define OutputDebugStringW(msg) fputws(msg, stderr) + +#define OutputDebugStringA(msg) fputs(msg, stderr) +#define OutputDebugFormatA(...) fprintf(stderr, __VA_ARGS__) + +// Event Tracing for Windows (ETW) provides application programmers the ability +// to start and stop event tracing sessions, instrument an application to +// provide trace events, and consume trace events. +#define DxcEtw_DXCompilerCreateInstance_Start() +#define DxcEtw_DXCompilerCreateInstance_Stop(hr) +#define DxcEtw_DXCompilerCompile_Start() +#define DxcEtw_DXCompilerCompile_Stop(hr) +#define DxcEtw_DXCompilerDisassemble_Start() +#define DxcEtw_DXCompilerDisassemble_Stop(hr) +#define DxcEtw_DXCompilerPreprocess_Start() +#define DxcEtw_DXCompilerPreprocess_Stop(hr) +#define DxcEtw_DxcValidation_Start() +#define DxcEtw_DxcValidation_Stop(hr) + +#define UInt32Add UIntAdd +#define Int32ToUInt32 IntToUInt + +//===--------------------- HRESULT Related Macros -------------------------===// + +#define S_OK ((HRESULT)0L) +#define S_FALSE ((HRESULT)1L) + +#define E_ABORT (HRESULT)0x80004004 +#define E_ACCESSDENIED (HRESULT)0x80070005 +#define E_BOUNDS (HRESULT)0x8000000B +#define E_FAIL (HRESULT)0x80004005 +#define E_HANDLE (HRESULT)0x80070006 +#define E_INVALIDARG (HRESULT)0x80070057 +#define E_NOINTERFACE (HRESULT)0x80004002 +#define E_NOTIMPL (HRESULT)0x80004001 +#define E_NOT_VALID_STATE (HRESULT)0x8007139F +#define E_OUTOFMEMORY (HRESULT)0x8007000E +#define E_POINTER (HRESULT)0x80004003 +#define E_UNEXPECTED (HRESULT)0x8000FFFF + +#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0) +#define FAILED(hr) (((HRESULT)(hr)) < 0) +#define DXC_FAILED(hr) (((HRESULT)(hr)) < 0) + +#define HRESULT_FROM_WIN32(x) \ + (HRESULT)(x) <= 0 ? (HRESULT)(x) \ + : (HRESULT)(((x) & 0x0000FFFF) | (7 << 16) | 0x80000000) + +//===----------------------------------------------------------------------===// +// +// Begin: Disable SAL Annotations +// +//===----------------------------------------------------------------------===// +#define _In_ +#define _In_z_ +#define _In_opt_ +#define _In_opt_count_(size) +#define _In_opt_z_ +#define _In_count_(size) +#define _In_bytecount_(size) + +#define _Out_ +#define _Out_opt_ +#define _Outptr_ +#define _Outptr_opt_ +#define _Outptr_result_z_ +#define _Outptr_opt_result_z_ +#define _Outptr_result_maybenull_ +#define _Outptr_result_nullonfailure_ +#define _Outptr_result_buffer_maybenull_(ptr) +#define _Outptr_result_buffer_(ptr) + +#define _COM_Outptr_ +#define _COM_Outptr_opt_ +#define _COM_Outptr_result_maybenull_ +#define _COM_Outptr_opt_result_maybenull_ + +#define THIS_ +#define THIS +#define PURE = 0 + +#define _Maybenull_ + +#define __debugbreak() + +// GCC produces erros on calling convention attributes. +#ifdef __GNUC__ +#define __cdecl +#define __CRTDECL +#define __stdcall +#define __vectorcall +#define __thiscall +#define __fastcall +#define __clrcall +#endif // __GNUC__ + +//===----------------------------------------------------------------------===// +// +// Begin: Type Definitions +// +//===----------------------------------------------------------------------===// + +#ifdef __cplusplus + +typedef unsigned char BYTE, UINT8; +typedef unsigned char *LPBYTE; + +typedef BYTE BOOLEAN; +typedef BOOLEAN *PBOOLEAN; + +typedef bool BOOL; +typedef BOOL *LPBOOL; + +typedef int INT; +typedef long LONG; +typedef unsigned int UINT; +typedef unsigned long ULONG; +typedef long long LONGLONG; +typedef long long LONG_PTR; +typedef unsigned long long ULONG_PTR; +typedef unsigned long long ULONGLONG; + +typedef uint16_t WORD; +typedef uint32_t DWORD; +typedef DWORD *LPDWORD; + +typedef uint32_t UINT32; +typedef uint64_t UINT64; + +typedef signed char INT8, *PINT8; +typedef signed int INT32, *PINT32; + +typedef size_t SIZE_T; +typedef const char *LPCSTR; +typedef const char *PCSTR; + +typedef int errno_t; + +typedef wchar_t WCHAR; +typedef wchar_t *LPWSTR; +typedef wchar_t *PWCHAR; +typedef const wchar_t *LPCWSTR; +typedef const wchar_t *PCWSTR; + +typedef WCHAR OLECHAR; +typedef OLECHAR *BSTR; +typedef OLECHAR *LPOLESTR; +typedef char *LPSTR; + +typedef void *LPVOID; +typedef const void *LPCVOID; + +typedef std::nullptr_t nullptr_t; + +typedef signed int HRESULT; + +//===--------------------- Handle Types -----------------------------------===// + +typedef void *HANDLE; +typedef void *RPC_IF_HANDLE; + +#define DECLARE_HANDLE(name) \ + struct name##__ { \ + int unused; \ + }; \ + typedef struct name##__ *name +DECLARE_HANDLE(HINSTANCE); + +typedef void *HMODULE; + +#define STD_INPUT_HANDLE ((DWORD)-10) +#define STD_OUTPUT_HANDLE ((DWORD)-11) +#define STD_ERROR_HANDLE ((DWORD)-12) + +//===--------------------- ID Types and Macros for COM --------------------===// + +#ifdef __EMULATE_UUID +struct GUID +#else // __EMULATE_UUID +// These specific definitions are required by clang -fms-extensions. +typedef struct _GUID +#endif // __EMULATE_UUID +{ + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; +} +#ifdef __EMULATE_UUID +; +#else // __EMULATE_UUID +GUID; +#endif // __EMULATE_UUID +typedef GUID CLSID; +typedef const GUID &REFGUID; +typedef const GUID &REFCLSID; + +typedef GUID IID; +typedef IID *LPIID; +typedef const IID &REFIID; +inline bool IsEqualGUID(REFGUID rguid1, REFGUID rguid2) { + // Optimization: + if (&rguid1 == &rguid2) + return true; + + return !memcmp(&rguid1, &rguid2, sizeof(GUID)); +} + +inline bool operator==(REFGUID guidOne, REFGUID guidOther) { + return !!IsEqualGUID(guidOne, guidOther); +} + +inline bool operator!=(REFGUID guidOne, REFGUID guidOther) { + return !(guidOne == guidOther); +} + +inline bool IsEqualIID(REFIID riid1, REFIID riid2) { + return IsEqualGUID(riid1, riid2); +} + +inline bool IsEqualCLSID(REFCLSID rclsid1, REFCLSID rclsid2) { + return IsEqualGUID(rclsid1, rclsid2); +} + +//===--------------------- Struct Types -----------------------------------===// + +typedef struct _FILETIME { + DWORD dwLowDateTime; + DWORD dwHighDateTime; +} FILETIME, *PFILETIME, *LPFILETIME; + +typedef struct _BY_HANDLE_FILE_INFORMATION { + DWORD dwFileAttributes; + FILETIME ftCreationTime; + FILETIME ftLastAccessTime; + FILETIME ftLastWriteTime; + DWORD dwVolumeSerialNumber; + DWORD nFileSizeHigh; + DWORD nFileSizeLow; + DWORD nNumberOfLinks; + DWORD nFileIndexHigh; + DWORD nFileIndexLow; +} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, + *LPBY_HANDLE_FILE_INFORMATION; + +typedef struct _WIN32_FIND_DATAW { + DWORD dwFileAttributes; + FILETIME ftCreationTime; + FILETIME ftLastAccessTime; + FILETIME ftLastWriteTime; + DWORD nFileSizeHigh; + DWORD nFileSizeLow; + DWORD dwReserved0; + DWORD dwReserved1; + WCHAR cFileName[260]; + WCHAR cAlternateFileName[14]; +} WIN32_FIND_DATAW, *PWIN32_FIND_DATAW, *LPWIN32_FIND_DATAW; + +typedef union _LARGE_INTEGER { + struct { + DWORD LowPart; + DWORD HighPart; + } u; + LONGLONG QuadPart; +} LARGE_INTEGER; + +typedef LARGE_INTEGER *PLARGE_INTEGER; + +typedef union _ULARGE_INTEGER { + struct { + DWORD LowPart; + DWORD HighPart; + } u; + ULONGLONG QuadPart; +} ULARGE_INTEGER; + +typedef ULARGE_INTEGER *PULARGE_INTEGER; + +typedef struct tagSTATSTG { + LPOLESTR pwcsName; + DWORD type; + ULARGE_INTEGER cbSize; + FILETIME mtime; + FILETIME ctime; + FILETIME atime; + DWORD grfMode; + DWORD grfLocksSupported; + CLSID clsid; + DWORD grfStateBits; + DWORD reserved; +} STATSTG; + +enum tagSTATFLAG { + STATFLAG_DEFAULT = 0, + STATFLAG_NONAME = 1, + STATFLAG_NOOPEN = 2 +}; + +//===--------------------- UUID Related Macros ----------------------------===// + +#ifdef __EMULATE_UUID + +// The following macros are defined to facilitate the lack of 'uuid' on Linux. + +constexpr uint8_t nybble_from_hex(char c) { + return ((c >= '0' && c <= '9') + ? (c - '0') + : ((c >= 'a' && c <= 'f') + ? (c - 'a' + 10) + : ((c >= 'A' && c <= 'F') ? (c - 'A' + 10) + : /* Should be an error */ -1))); +} + +constexpr uint8_t byte_from_hex(char c1, char c2) { + return nybble_from_hex(c1) << 4 | nybble_from_hex(c2); +} + +constexpr uint8_t byte_from_hexstr(const char str[2]) { + return nybble_from_hex(str[0]) << 4 | nybble_from_hex(str[1]); +} + +constexpr GUID guid_from_string(const char str[37]) { + return GUID{static_cast(byte_from_hexstr(str)) << 24 | + static_cast(byte_from_hexstr(str + 2)) << 16 | + static_cast(byte_from_hexstr(str + 4)) << 8 | + byte_from_hexstr(str + 6), + static_cast( + static_cast(byte_from_hexstr(str + 9)) << 8 | + byte_from_hexstr(str + 11)), + static_cast( + static_cast(byte_from_hexstr(str + 14)) << 8 | + byte_from_hexstr(str + 16)), + {byte_from_hexstr(str + 19), byte_from_hexstr(str + 21), + byte_from_hexstr(str + 24), byte_from_hexstr(str + 26), + byte_from_hexstr(str + 28), byte_from_hexstr(str + 30), + byte_from_hexstr(str + 32), byte_from_hexstr(str + 34)}}; +} + +template inline GUID __emulated_uuidof(); + +#define CROSS_PLATFORM_UUIDOF(interface, spec) \ + struct interface; \ + template <> inline GUID __emulated_uuidof() { \ + static const IID _IID = guid_from_string(spec); \ + return _IID; \ + } + +#define __uuidof(T) __emulated_uuidof::type>() + +#define IID_PPV_ARGS(ppType) \ + __uuidof(decltype(**(ppType))), reinterpret_cast(ppType) + +#else // __EMULATE_UUID + +#ifndef CROSS_PLATFORM_UUIDOF +// Warning: This macro exists in dxcapi.h as well +#define CROSS_PLATFORM_UUIDOF(interface, spec) \ + struct __declspec(uuid(spec)) interface; +#endif + +template inline void **IID_PPV_ARGS_Helper(T **pp) { + return reinterpret_cast(pp); +} +#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType) + +#endif // __EMULATE_UUID + +// Needed for d3d headers, but fail to create actual interfaces +#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + const GUID name = {l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}} +#define DECLSPEC_UUID(x) +#define MIDL_INTERFACE(x) struct DECLSPEC_UUID(x) +#define DECLARE_INTERFACE(iface) struct iface +#define DECLARE_INTERFACE_(iface, parent) DECLARE_INTERFACE(iface) : parent + +//===--------------------- COM Interfaces ---------------------------------===// + +CROSS_PLATFORM_UUIDOF(IUnknown, "00000000-0000-0000-C000-000000000046") +struct IUnknown { + IUnknown(){}; + virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0; + virtual ULONG AddRef() = 0; + virtual ULONG Release() = 0; + template HRESULT QueryInterface(Q **pp) { + return QueryInterface(__uuidof(Q), (void **)pp); + } +}; + +CROSS_PLATFORM_UUIDOF(INoMarshal, "ECC8691B-C1DB-4DC0-855E-65F6C551AF49") +struct INoMarshal : public IUnknown {}; + +CROSS_PLATFORM_UUIDOF(IMalloc, "00000002-0000-0000-C000-000000000046") +struct IMalloc : public IUnknown { + virtual void *Alloc(SIZE_T size) = 0; + virtual void *Realloc(void *ptr, SIZE_T size) = 0; + virtual void Free(void *ptr) = 0; + virtual SIZE_T GetSize(void *pv) = 0; + virtual int DidAlloc(void *pv) = 0; + virtual void HeapMinimize(void) = 0; +}; + +CROSS_PLATFORM_UUIDOF(ISequentialStream, "0C733A30-2A1C-11CE-ADE5-00AA0044773D") +struct ISequentialStream : public IUnknown { + virtual HRESULT Read(void *pv, ULONG cb, ULONG *pcbRead) = 0; + virtual HRESULT Write(const void *pv, ULONG cb, ULONG *pcbWritten) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IStream, "0000000c-0000-0000-C000-000000000046") +struct IStream : public ISequentialStream { + virtual HRESULT Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, + ULARGE_INTEGER *plibNewPosition) = 0; + virtual HRESULT SetSize(ULARGE_INTEGER libNewSize) = 0; + virtual HRESULT CopyTo(IStream *pstm, ULARGE_INTEGER cb, + ULARGE_INTEGER *pcbRead, + ULARGE_INTEGER *pcbWritten) = 0; + + virtual HRESULT Commit(DWORD grfCommitFlags) = 0; + + virtual HRESULT Revert(void) = 0; + + virtual HRESULT LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, + DWORD dwLockType) = 0; + + virtual HRESULT UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, + DWORD dwLockType) = 0; + + virtual HRESULT Stat(STATSTG *pstatstg, DWORD grfStatFlag) = 0; + + virtual HRESULT Clone(IStream **ppstm) = 0; +}; + +// These don't need stub implementations as they come from the DirectX Headers +// They still need the __uuidof() though +CROSS_PLATFORM_UUIDOF(ID3D12LibraryReflection, + "8E349D19-54DB-4A56-9DC9-119D87BDB804") +CROSS_PLATFORM_UUIDOF(ID3D12ShaderReflection, + "5A58797D-A72C-478D-8BA2-EFC6B0EFE88E") + +//===--------------------- COM Pointer Types ------------------------------===// + +class CAllocator { +public: + static void *Reallocate(void *p, size_t nBytes) throw(); + static void *Allocate(size_t nBytes) throw(); + static void Free(void *p) throw(); +}; + +template class CComPtrBase { +protected: + CComPtrBase() throw() { p = nullptr; } + CComPtrBase(T *lp) throw() { + p = lp; + if (p != nullptr) + p->AddRef(); + } + void Swap(CComPtrBase &other) { + T *pTemp = p; + p = other.p; + other.p = pTemp; + } + +public: + ~CComPtrBase() throw() { + if (p) { + p->Release(); + p = nullptr; + } + } + operator T *() const throw() { return p; } + T &operator*() const { return *p; } + T *operator->() const { return p; } + T **operator&() throw() { + assert(p == nullptr); + return &p; + } + bool operator!() const throw() { return (p == nullptr); } + bool operator<(T *pT) const throw() { return p < pT; } + bool operator!=(T *pT) const { return !operator==(pT); } + bool operator==(T *pT) const throw() { return p == pT; } + + // Release the interface and set to nullptr + void Release() throw() { + T *pTemp = p; + if (pTemp) { + p = nullptr; + pTemp->Release(); + } + } + + // Attach to an existing interface (does not AddRef) + void Attach(T *p2) throw() { + if (p) { + ULONG ref = p->Release(); + (void)(ref); + // Attaching to the same object only works if duplicate references are + // being coalesced. Otherwise re-attaching will cause the pointer to be + // released and may cause a crash on a subsequent dereference. + assert(ref != 0 || p2 != p); + } + p = p2; + } + + // Detach the interface (does not Release) + T *Detach() throw() { + T *pt = p; + p = nullptr; + return pt; + } + + HRESULT CopyTo(T **ppT) throw() { + assert(ppT != nullptr); + if (ppT == nullptr) + return E_POINTER; + *ppT = p; + if (p) + p->AddRef(); + return S_OK; + } + + template HRESULT QueryInterface(Q **pp) const throw() { + assert(pp != nullptr); + return p->QueryInterface(__uuidof(Q), (void **)pp); + } + + T *p; +}; + +template class CComPtr : public CComPtrBase { +public: + CComPtr() throw() {} + CComPtr(T *lp) throw() : CComPtrBase(lp) {} + CComPtr(const CComPtr &lp) throw() : CComPtrBase(lp.p) {} + T *operator=(T *lp) throw() { + if (*this != lp) { + CComPtr(lp).Swap(*this); + } + return *this; + } + + inline bool IsEqualObject(IUnknown *pOther) throw() { + if (this->p == nullptr && pOther == nullptr) + return true; // They are both NULL objects + + if (this->p == nullptr || pOther == nullptr) + return false; // One is NULL the other is not + + CComPtr punk1; + CComPtr punk2; + this->p->QueryInterface(__uuidof(IUnknown), (void **)&punk1); + pOther->QueryInterface(__uuidof(IUnknown), (void **)&punk2); + return punk1 == punk2; + } + + void ComPtrAssign(IUnknown **pp, IUnknown *lp, REFIID riid) { + IUnknown *pTemp = *pp; // takes ownership + if (lp == nullptr || FAILED(lp->QueryInterface(riid, (void **)pp))) + *pp = nullptr; + if (pTemp) + pTemp->Release(); + } + + template T *operator=(const CComPtr &lp) throw() { + if (!this->IsEqualObject(lp)) { + ComPtrAssign((IUnknown **)&this->p, lp, __uuidof(T)); + } + return *this; + } + + // NOTE: This conversion constructor is not part of the official CComPtr spec; + // however, it is needed to convert CComPtr to CComPtr where T derives + // from Q on Clang. MSVC compiles this conversion as first a call to + // CComPtr::operator T*, followed by CComPtr(T*), but Clang fails to + // compile with error: no viable conversion from 'CComPtr' to 'CComPtr'. + template + CComPtr(const CComPtr &lp) throw() : CComPtrBase(lp.p) {} + + T *operator=(const CComPtr &lp) throw() { + if (*this != lp) { + CComPtr(lp).Swap(*this); + } + return *this; + } + + CComPtr(CComPtr &&lp) throw() : CComPtrBase() { lp.Swap(*this); } + + T *operator=(CComPtr &&lp) throw() { + if (*this != lp) { + CComPtr(static_cast(lp)).Swap(*this); + } + return *this; + } +}; + +template class CSimpleArray : public std::vector { +public: + bool Add(const T &t) { + this->push_back(t); + return true; + } + int GetSize() { return this->size(); } + T *GetData() { return this->data(); } + void RemoveAll() { this->clear(); } +}; + +template class CHeapPtrBase { +protected: + CHeapPtrBase() throw() : m_pData(NULL) {} + CHeapPtrBase(CHeapPtrBase &p) throw() { + m_pData = p.Detach(); // Transfer ownership + } + explicit CHeapPtrBase(T *pData) throw() : m_pData(pData) {} + +public: + ~CHeapPtrBase() throw() { Free(); } + +protected: + CHeapPtrBase &operator=(CHeapPtrBase &p) throw() { + if (m_pData != p.m_pData) + Attach(p.Detach()); // Transfer ownership + return *this; + } + +public: + operator T *() const throw() { return m_pData; } + T *operator->() const throw() { + assert(m_pData != NULL); + return m_pData; + } + + T **operator&() throw() { + assert(m_pData == NULL); + return &m_pData; + } + + // Allocate a buffer with the given number of bytes + bool AllocateBytes(size_t nBytes) throw() { + assert(m_pData == NULL); + m_pData = static_cast(Allocator::Allocate(nBytes * sizeof(char))); + if (m_pData == NULL) + return false; + + return true; + } + + // Attach to an existing pointer (takes ownership) + void Attach(T *pData) throw() { + Allocator::Free(m_pData); + m_pData = pData; + } + + // Detach the pointer (releases ownership) + T *Detach() throw() { + T *pTemp = m_pData; + m_pData = NULL; + return pTemp; + } + + // Free the memory pointed to, and set the pointer to NULL + void Free() throw() { + Allocator::Free(m_pData); + m_pData = NULL; + } + + // Reallocate the buffer to hold a given number of bytes + bool ReallocateBytes(size_t nBytes) throw() { + T *pNew; + pNew = + static_cast(Allocator::Reallocate(m_pData, nBytes * sizeof(char))); + if (pNew == NULL) + return false; + m_pData = pNew; + + return true; + } + +public: + T *m_pData; +}; + +template +class CHeapPtr : public CHeapPtrBase { +public: + CHeapPtr() throw() {} + CHeapPtr(CHeapPtr &p) throw() : CHeapPtrBase(p) {} + explicit CHeapPtr(T *p) throw() : CHeapPtrBase(p) {} + CHeapPtr &operator=(CHeapPtr &p) throw() { + CHeapPtrBase::operator=(p); + return *this; + } + + // Allocate a buffer with the given number of elements + bool Allocate(size_t nElements = 1) throw() { + size_t nBytes = nElements * sizeof(T); + return this->AllocateBytes(nBytes); + } + + // Reallocate the buffer to hold a given number of elements + bool Reallocate(size_t nElements) throw() { + size_t nBytes = nElements * sizeof(T); + return this->ReallocateBytes(nBytes); + } +}; + +#define CComHeapPtr CHeapPtr + +//===--------------------------- BSTR Allocation --------------------------===// + +void SysFreeString(BSTR bstrString); +// Allocate string with length prefix +BSTR SysAllocStringLen(const OLECHAR *strIn, UINT ui); + +//===--------------------------- BSTR Length ------------------------------===// +unsigned int SysStringLen(const BSTR bstrString); + +//===--------------------- UTF-8 Related Types ----------------------------===// + +// Code Page +#define CP_ACP 0 +#define CP_UTF8 65001 // UTF-8 translation. + +// RAII style mechanism for setting/unsetting a locale for the specified Windows +// codepage +class ScopedLocale { + locale_t Utf8Locale = nullptr; + locale_t PrevLocale = nullptr; + +public: + explicit ScopedLocale(uint32_t CodePage) { + assert((CodePage == CP_UTF8) && + "Support for Linux only handles UTF8 code pages"); + Utf8Locale = newlocale(LC_CTYPE_MASK, "C.UTF-8", NULL); + if (!Utf8Locale) + Utf8Locale = newlocale(LC_CTYPE_MASK, "C.utf8", NULL); + if (!Utf8Locale) + Utf8Locale = newlocale(LC_CTYPE_MASK, "en_US.UTF-8", NULL); + assert(Utf8Locale && "Failed to create UTF-8 locale"); + if (!Utf8Locale) + return; + PrevLocale = uselocale(Utf8Locale); + assert(PrevLocale && "Failed to set locale to UTF-8"); + if (!PrevLocale) { + freelocale(Utf8Locale); + Utf8Locale = nullptr; + } + } + ~ScopedLocale() { + if (PrevLocale != nullptr) + uselocale(PrevLocale); + if (Utf8Locale) + freelocale(Utf8Locale); + PrevLocale = nullptr; + Utf8Locale = nullptr; + } +}; + +// The t_nBufferLength parameter is part of the published interface, but not +// used here. +template class CW2AEX { +public: + CW2AEX(LPCWSTR psz) { + ScopedLocale locale(CP_UTF8); + + if (!psz) { + m_psz = NULL; + return; + } + + int len = (wcslen(psz) + 1) * 4; + m_psz = new char[len]; + std::wcstombs(m_psz, psz, len); + } + + ~CW2AEX() { delete[] m_psz; } + + operator LPSTR() const { return m_psz; } + + char *m_psz; +}; +typedef CW2AEX<> CW2A; + +// The t_nBufferLength parameter is part of the published interface, but not +// used here. +template class CA2WEX { +public: + CA2WEX(LPCSTR psz) { + ScopedLocale locale(CP_UTF8); + + if (!psz) { + m_psz = NULL; + return; + } + + int len = strlen(psz) + 1; + m_psz = new wchar_t[len]; + std::mbstowcs(m_psz, psz, len); + } + + ~CA2WEX() { delete[] m_psz; } + + operator LPWSTR() const { return m_psz; } + + wchar_t *m_psz; +}; + +typedef CA2WEX<> CA2W; + +//===--------- File IO Related Types ----------------===// + +class CHandle { +public: + CHandle(HANDLE h); + ~CHandle(); + operator HANDLE() const throw(); + +private: + HANDLE m_h; +}; + +///////////////////////////////////////////////////////////////////////////// +// CComBSTR + +class CComBSTR { +public: + BSTR m_str; + CComBSTR() : m_str(nullptr){}; + CComBSTR(int nSize, LPCWSTR sz); + ~CComBSTR() throw() { SysFreeString(m_str); } + unsigned int Length() const throw() { return SysStringLen(m_str); } + operator BSTR() const throw() { return m_str; } + + bool operator==(const CComBSTR &bstrSrc) const throw(); + + BSTR *operator&() throw() { return &m_str; } + + BSTR Detach() throw() { + BSTR s = m_str; + m_str = NULL; + return s; + } + + void Empty() throw() { + SysFreeString(m_str); + m_str = NULL; + } +}; + +//===--------- Convert argv to wchar ----------------===// +class WArgV { + std::vector WStringVector; + std::vector WCharPtrVector; + +public: + WArgV(int argc, const char **argv); + const wchar_t **argv() { return WCharPtrVector.data(); } +}; + +#endif // __cplusplus + +#endif // _WIN32 + +#endif // LLVM_SUPPORT_WIN_ADAPTER_H diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/dxcapi.h b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcapi.h new file mode 100644 index 00000000..95cc56a0 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcapi.h @@ -0,0 +1,1309 @@ + +/////////////////////////////////////////////////////////////////////////////// +// // +// dxcapi.h // +// Copyright (C) Microsoft Corporation. All rights reserved. // +// This file is distributed under the University of Illinois Open Source // +// License. See LICENSE.TXT for details. // +// // +// Provides declarations for the DirectX Compiler API entry point. // +// // +/////////////////////////////////////////////////////////////////////////////// + +#ifndef __DXC_API__ +#define __DXC_API__ + +#ifdef _WIN32 +#ifndef DXC_API_IMPORT +#define DXC_API_IMPORT __declspec(dllimport) +#endif +#else +#ifndef DXC_API_IMPORT +#define DXC_API_IMPORT __attribute__((visibility("default"))) +#endif +#endif + +#ifdef _WIN32 + +#ifndef CROSS_PLATFORM_UUIDOF +// Warning: This macro exists in WinAdapter.h as well +#define CROSS_PLATFORM_UUIDOF(interface, spec) \ + struct __declspec(uuid(spec)) interface; +#endif + +#else + +#include "WinAdapter.h" +#include +#endif + +struct IMalloc; + +struct IDxcIncludeHandler; + +/// \brief Typedef for DxcCreateInstance function pointer. +/// +/// This can be used with GetProcAddress to get the DxcCreateInstance function. +typedef HRESULT(__stdcall *DxcCreateInstanceProc)(_In_ REFCLSID rclsid, + _In_ REFIID riid, + _Out_ LPVOID *ppv); + +/// \brief Typedef for DxcCreateInstance2 function pointer. +/// +/// This can be used with GetProcAddress to get the DxcCreateInstance2 function. +typedef HRESULT(__stdcall *DxcCreateInstance2Proc)(_In_ IMalloc *pMalloc, + _In_ REFCLSID rclsid, + _In_ REFIID riid, + _Out_ LPVOID *ppv); + +/// \brief Creates a single uninitialized object of the class associated with a +/// specified CLSID. +/// +/// \param rclsid The CLSID associated with the data and code that will be used +/// to create the object. +/// +/// \param riid A reference to the identifier of the interface to be used to +/// communicate with the object. +/// +/// \param ppv Address of pointer variable that receives the interface pointer +/// requested in riid. Upon successful return, *ppv contains the requested +/// interface pointer. Upon failure, *ppv contains NULL. +/// +/// While this function is similar to CoCreateInstance, there is no COM +/// involvement. +extern "C" DXC_API_IMPORT + HRESULT __stdcall DxcCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, + _Out_ LPVOID *ppv); + +/// \brief Version of DxcCreateInstance that takes an IMalloc interface. +/// +/// This can be used to create an instance of the compiler with a custom memory +/// allocator. +extern "C" DXC_API_IMPORT + HRESULT __stdcall DxcCreateInstance2(_In_ IMalloc *pMalloc, + _In_ REFCLSID rclsid, _In_ REFIID riid, + _Out_ LPVOID *ppv); + +// For convenience, equivalent definitions to CP_UTF8 and CP_UTF16. +#define DXC_CP_UTF8 65001 +#define DXC_CP_UTF16 1200 +#define DXC_CP_UTF32 12000 +// Use DXC_CP_ACP for: Binary; ANSI Text; Autodetect UTF with BOM +#define DXC_CP_ACP 0 + +/// Codepage for "wide" characters - UTF16 on Windows, UTF32 on other platforms. +#ifdef _WIN32 +#define DXC_CP_WIDE DXC_CP_UTF16 +#else +#define DXC_CP_WIDE DXC_CP_UTF32 +#endif + +/// Indicates that the shader hash was computed taking into account source +/// information (-Zss). +#define DXC_HASHFLAG_INCLUDES_SOURCE 1 + +/// Hash digest type for ShaderHash. +typedef struct DxcShaderHash { + UINT32 Flags; ///< DXC_HASHFLAG_* + BYTE HashDigest[16]; ///< The hash digest +} DxcShaderHash; + +#define DXC_FOURCC(ch0, ch1, ch2, ch3) \ + ((UINT32)(UINT8)(ch0) | (UINT32)(UINT8)(ch1) << 8 | \ + (UINT32)(UINT8)(ch2) << 16 | (UINT32)(UINT8)(ch3) << 24) +#define DXC_PART_PDB DXC_FOURCC('I', 'L', 'D', 'B') +#define DXC_PART_PDB_NAME DXC_FOURCC('I', 'L', 'D', 'N') +#define DXC_PART_PRIVATE_DATA DXC_FOURCC('P', 'R', 'I', 'V') +#define DXC_PART_ROOT_SIGNATURE DXC_FOURCC('R', 'T', 'S', '0') +#define DXC_PART_DXIL DXC_FOURCC('D', 'X', 'I', 'L') +#define DXC_PART_REFLECTION_DATA DXC_FOURCC('S', 'T', 'A', 'T') +#define DXC_PART_SHADER_HASH DXC_FOURCC('H', 'A', 'S', 'H') +#define DXC_PART_INPUT_SIGNATURE DXC_FOURCC('I', 'S', 'G', '1') +#define DXC_PART_OUTPUT_SIGNATURE DXC_FOURCC('O', 'S', 'G', '1') +#define DXC_PART_PATCH_CONSTANT_SIGNATURE DXC_FOURCC('P', 'S', 'G', '1') + +// Some option arguments are defined here for continuity with D3DCompile +// interface. +#define DXC_ARG_DEBUG L"-Zi" +#define DXC_ARG_SKIP_VALIDATION L"-Vd" +#define DXC_ARG_SKIP_OPTIMIZATIONS L"-Od" +#define DXC_ARG_PACK_MATRIX_ROW_MAJOR L"-Zpr" +#define DXC_ARG_PACK_MATRIX_COLUMN_MAJOR L"-Zpc" +#define DXC_ARG_AVOID_FLOW_CONTROL L"-Gfa" +#define DXC_ARG_PREFER_FLOW_CONTROL L"-Gfp" +#define DXC_ARG_ENABLE_STRICTNESS L"-Ges" +#define DXC_ARG_ENABLE_BACKWARDS_COMPATIBILITY L"-Gec" +#define DXC_ARG_IEEE_STRICTNESS L"-Gis" +#define DXC_ARG_OPTIMIZATION_LEVEL0 L"-O0" +#define DXC_ARG_OPTIMIZATION_LEVEL1 L"-O1" +#define DXC_ARG_OPTIMIZATION_LEVEL2 L"-O2" +#define DXC_ARG_OPTIMIZATION_LEVEL3 L"-O3" +#define DXC_ARG_WARNINGS_ARE_ERRORS L"-WX" +#define DXC_ARG_RESOURCES_MAY_ALIAS L"-res_may_alias" +#define DXC_ARG_ALL_RESOURCES_BOUND L"-all_resources_bound" +#define DXC_ARG_DEBUG_NAME_FOR_SOURCE L"-Zss" +#define DXC_ARG_DEBUG_NAME_FOR_BINARY L"-Zsb" + +CROSS_PLATFORM_UUIDOF(IDxcBlob, "8BA5FB08-5195-40e2-AC58-0D989C3A0102") +/// \brief A sized buffer that can be passed in and out of DXC APIs. +/// +/// This is an alias of ID3D10Blob and ID3DBlob. +struct IDxcBlob : public IUnknown { +public: + /// \brief Retrieves a pointer to the blob's data. + virtual LPVOID STDMETHODCALLTYPE GetBufferPointer(void) = 0; + + /// \brief Retrieves the size, in bytes, of the blob's data. + virtual SIZE_T STDMETHODCALLTYPE GetBufferSize(void) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcBlobEncoding, "7241d424-2646-4191-97c0-98e96e42fc68") +/// \brief A blob that might have a known encoding. +struct IDxcBlobEncoding : public IDxcBlob { +public: + /// \brief Retrieve the encoding for this blob. + /// + /// \param pKnown Pointer to a variable that will be set to TRUE if the + /// encoding is known. + /// + /// \param pCodePage Pointer to variable that will be set to the encoding used + /// for this blog. + /// + /// If the encoding is not known then pCodePage will be set to CP_ACP. + virtual HRESULT STDMETHODCALLTYPE GetEncoding(_Out_ BOOL *pKnown, + _Out_ UINT32 *pCodePage) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcBlobWide, "A3F84EAB-0FAA-497E-A39C-EE6ED60B2D84") +/// \brief A blob containing a null-terminated wide string. +/// +/// This uses the native wide character encoding (utf16 on Windows, utf32 on +/// Linux). +/// +/// The value returned by GetBufferSize() is the size of the buffer, in bytes, +/// including the null-terminator. +/// +/// This interface is used to return output name strings DXC. Other string +/// output blobs, such as errors/warnings, preprocessed HLSL, or other text are +/// returned using encodings based on the -encoding option passed to the +/// compiler. +struct IDxcBlobWide : public IDxcBlobEncoding { +public: + /// \brief Retrieves a pointer to the string stored in this blob. + virtual LPCWSTR STDMETHODCALLTYPE GetStringPointer(void) = 0; + + /// \brief Retrieves the length of the string stored in this blob, in + /// characters, excluding the null-terminator. + virtual SIZE_T STDMETHODCALLTYPE GetStringLength(void) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcBlobUtf8, "3DA636C9-BA71-4024-A301-30CBF125305B") +/// \brief A blob containing a UTF-8 encoded string. +/// +/// The value returned by GetBufferSize() is the size of the buffer, in bytes, +/// including the null-terminator. +/// +/// Depending on the -encoding option passed to the compiler, this interface is +/// used to return string output blobs, such as errors/warnings, preprocessed +/// HLSL, or other text. Output name strings always use IDxcBlobWide. +struct IDxcBlobUtf8 : public IDxcBlobEncoding { +public: + /// \brief Retrieves a pointer to the string stored in this blob. + virtual LPCSTR STDMETHODCALLTYPE GetStringPointer(void) = 0; + + /// \brief Retrieves the length of the string stored in this blob, in + /// characters, excluding the null-terminator. + virtual SIZE_T STDMETHODCALLTYPE GetStringLength(void) = 0; +}; + +#ifdef _WIN32 +/// IDxcBlobUtf16 is a legacy alias for IDxcBlobWide on Win32. +typedef IDxcBlobWide IDxcBlobUtf16; +#endif + +CROSS_PLATFORM_UUIDOF(IDxcIncludeHandler, + "7f61fc7d-950d-467f-b3e3-3c02fb49187c") +/// \brief Interface for handling include directives. +/// +/// This interface can be implemented to customize handling of include +/// directives. +/// +/// Use IDxcUtils::CreateDefaultIncludeHandler to create a default +/// implementation that reads include files from the filesystem. +/// +struct IDxcIncludeHandler : public IUnknown { + /// \brief Load a source file to be included by the compiler. + /// + /// \param pFilename Candidate filename. + /// + /// \param ppIncludeSource Resultant source object for included file, nullptr + /// if not found. + virtual HRESULT STDMETHODCALLTYPE + LoadSource(_In_z_ LPCWSTR pFilename, + _COM_Outptr_result_maybenull_ IDxcBlob **ppIncludeSource) = 0; +}; + +/// \brief Structure for supplying bytes or text input to Dxc APIs. +typedef struct DxcBuffer { + /// \brief Pointer to the start of the buffer. + LPCVOID Ptr; + + /// \brief Size of the buffer in bytes. + SIZE_T Size; + + /// \brief Encoding of the buffer. + /// + /// Use Encoding = 0 for non-text bytes, ANSI text, or unknown with BOM. + UINT Encoding; +} DxcText; + +/// \brief Structure for supplying defines to Dxc APIs. +struct DxcDefine { + LPCWSTR Name; ///< The define name. + _Maybenull_ LPCWSTR Value; ///< Optional value for the define. +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompilerArgs, "73EFFE2A-70DC-45F8-9690-EFF64C02429D") +/// \brief Interface for managing arguments passed to DXC. +/// +/// Use IDxcUtils::BuildArguments to create an instance of this interface. +struct IDxcCompilerArgs : public IUnknown { + /// \brief Retrieve the array of arguments. + /// + /// This can be passed directly to the pArguments parameter of the Compile() + /// method. + virtual LPCWSTR *STDMETHODCALLTYPE GetArguments() = 0; + + /// \brief Retrieve the number of arguments. + /// + /// This can be passed directly to the argCount parameter of the Compile() + /// method. + virtual UINT32 STDMETHODCALLTYPE GetCount() = 0; + + /// \brief Add additional arguments to this list of compiler arguments. + virtual HRESULT STDMETHODCALLTYPE AddArguments( + _In_opt_count_(argCount) + LPCWSTR *pArguments, ///< Array of pointers to arguments to add. + _In_ UINT32 argCount ///< Number of arguments to add. + ) = 0; + + /// \brief Add additional UTF-8 encoded arguments to this list of compiler + /// arguments. + virtual HRESULT STDMETHODCALLTYPE AddArgumentsUTF8( + _In_opt_count_(argCount) + LPCSTR *pArguments, ///< Array of pointers to UTF-8 arguments to add. + _In_ UINT32 argCount ///< Number of arguments to add. + ) = 0; + + /// \brief Add additional defines to this list of compiler arguments. + virtual HRESULT STDMETHODCALLTYPE AddDefines( + _In_count_(defineCount) const DxcDefine *pDefines, ///< Array of defines. + _In_ UINT32 defineCount ///< Number of defines. + ) = 0; +}; + +////////////////////////// +// Legacy Interfaces +///////////////////////// + +CROSS_PLATFORM_UUIDOF(IDxcLibrary, "e5204dc7-d18c-4c3c-bdfb-851673980fe7") +/// \deprecated IDxcUtils replaces IDxcLibrary; please use IDxcUtils insted. +struct IDxcLibrary : public IUnknown { + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE SetMalloc(_In_opt_ IMalloc *pMalloc) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE + CreateBlobFromBlob(_In_ IDxcBlob *pBlob, UINT32 offset, UINT32 length, + _COM_Outptr_ IDxcBlob **ppResult) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE + CreateBlobFromFile(_In_z_ LPCWSTR pFileName, _In_opt_ UINT32 *codePage, + _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE CreateBlobWithEncodingFromPinned( + _In_bytecount_(size) LPCVOID pText, UINT32 size, UINT32 codePage, + _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE CreateBlobWithEncodingOnHeapCopy( + _In_bytecount_(size) LPCVOID pText, UINT32 size, UINT32 codePage, + _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE CreateBlobWithEncodingOnMalloc( + _In_bytecount_(size) LPCVOID pText, IMalloc *pIMalloc, UINT32 size, + UINT32 codePage, _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE + CreateIncludeHandler(_COM_Outptr_ IDxcIncludeHandler **ppResult) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE CreateStreamFromBlobReadOnly( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IStream **ppStream) = 0; + + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE GetBlobAsUtf8( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + + // Renamed from GetBlobAsUtf16 to GetBlobAsWide + /// \deprecated + virtual HRESULT STDMETHODCALLTYPE GetBlobAsWide( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) = 0; + +#ifdef _WIN32 + // Alias to GetBlobAsWide on Win32 + /// \deprecated + inline HRESULT GetBlobAsUtf16(_In_ IDxcBlob *pBlob, + _COM_Outptr_ IDxcBlobEncoding **pBlobEncoding) { + return this->GetBlobAsWide(pBlob, pBlobEncoding); + } +#endif +}; + +CROSS_PLATFORM_UUIDOF(IDxcOperationResult, + "CEDB484A-D4E9-445A-B991-CA21CA157DC2") +/// \brief The results of a DXC operation. +/// +/// Note: IDxcResult replaces IDxcOperationResult and should be used wherever +/// possible. +struct IDxcOperationResult : public IUnknown { + /// \brief Retrieve the overall status of the operation. + virtual HRESULT STDMETHODCALLTYPE GetStatus(_Out_ HRESULT *pStatus) = 0; + + /// \brief Retrieve the primary output of the operation. + /// + /// This corresponds to: + /// * DXC_OUT_OBJECT - Compile() with shader or library target + /// * DXC_OUT_DISASSEMBLY - Disassemble() + /// * DXC_OUT_HLSL - Compile() with -P + /// * DXC_OUT_ROOT_SIGNATURE - Compile() with rootsig_* target + virtual HRESULT STDMETHODCALLTYPE + GetResult(_COM_Outptr_result_maybenull_ IDxcBlob **ppResult) = 0; + + /// \brief Retrieves the error buffer from the operation, if there is one. + /// + // This corresponds to calling IDxcResult::GetOutput() with DXC_OUT_ERRORS. + virtual HRESULT STDMETHODCALLTYPE + GetErrorBuffer(_COM_Outptr_result_maybenull_ IDxcBlobEncoding **ppErrors) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompiler, "8c210bf3-011f-4422-8d70-6f9acb8db617") +/// \deprecated Please use IDxcCompiler3 instead. +struct IDxcCompiler : public IUnknown { + /// \brief Compile a single entry point to the target shader model. + /// + /// \deprecated Please use IDxcCompiler3::Compile() instead. + virtual HRESULT STDMETHODCALLTYPE Compile( + _In_ IDxcBlob *pSource, // Source text to compile. + _In_opt_z_ LPCWSTR pSourceName, // Optional file name for pSource. Used in + // errors and include handlers. + _In_opt_z_ LPCWSTR pEntryPoint, // Entry point name. + _In_z_ LPCWSTR pTargetProfile, // Shader profile to compile. + _In_opt_count_(argCount) + LPCWSTR *pArguments, // Array of pointers to arguments. + _In_ UINT32 argCount, // Number of arguments. + _In_count_(defineCount) const DxcDefine *pDefines, // Array of defines. + _In_ UINT32 defineCount, // Number of defines. + _In_opt_ IDxcIncludeHandler + *pIncludeHandler, // User-provided interface to handle #include + // directives (optional). + _COM_Outptr_ IDxcOperationResult * + *ppResult // Compiler output status, buffer, and errors. + ) = 0; + + /// \brief Preprocess source text. + /// + /// \deprecated Please use IDxcCompiler3::Compile() with the "-P" argument + /// instead. + virtual HRESULT STDMETHODCALLTYPE Preprocess( + _In_ IDxcBlob *pSource, // Source text to preprocess. + _In_opt_z_ LPCWSTR pSourceName, // Optional file name for pSource. Used in + // errors and include handlers. + _In_opt_count_(argCount) + LPCWSTR *pArguments, // Array of pointers to arguments. + _In_ UINT32 argCount, // Number of arguments. + _In_count_(defineCount) const DxcDefine *pDefines, // Array of defines. + _In_ UINT32 defineCount, // Number of defines. + _In_opt_ IDxcIncludeHandler + *pIncludeHandler, // user-provided interface to handle #include + // directives (optional). + _COM_Outptr_ IDxcOperationResult * + *ppResult // Preprocessor output status, buffer, and errors. + ) = 0; + + /// \brief Disassemble a program. + /// + /// \deprecated Please use IDxcCompiler3::Disassemble() instead. + virtual HRESULT STDMETHODCALLTYPE Disassemble( + _In_ IDxcBlob *pSource, // Program to disassemble. + _COM_Outptr_ IDxcBlobEncoding **ppDisassembly // Disassembly text. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompiler2, "A005A9D9-B8BB-4594-B5C9-0E633BEC4D37") +/// \deprecated Please use IDxcCompiler3 instead. +struct IDxcCompiler2 : public IDxcCompiler { + /// \brief Compile a single entry point to the target shader model with debug + /// information. + /// + /// \deprecated Please use IDxcCompiler3::Compile() instead. + virtual HRESULT STDMETHODCALLTYPE CompileWithDebug( + _In_ IDxcBlob *pSource, // Source text to compile. + _In_opt_z_ LPCWSTR pSourceName, // Optional file name for pSource. Used in + // errors and include handlers. + _In_opt_z_ LPCWSTR pEntryPoint, // Entry point name. + _In_z_ LPCWSTR pTargetProfile, // Shader profile to compile. + _In_opt_count_(argCount) + LPCWSTR *pArguments, // Array of pointers to arguments. + _In_ UINT32 argCount, // Number of arguments. + _In_count_(defineCount) const DxcDefine *pDefines, // Array of defines. + _In_ UINT32 defineCount, // Number of defines. + _In_opt_ IDxcIncludeHandler + *pIncludeHandler, // user-provided interface to handle #include + // directives (optional). + _COM_Outptr_ IDxcOperationResult * + *ppResult, // Compiler output status, buffer, and errors. + _Outptr_opt_result_z_ LPWSTR + *ppDebugBlobName, // Suggested file name for debug blob. Must be + // CoTaskMemFree()'d. + _COM_Outptr_opt_ IDxcBlob **ppDebugBlob // Debug blob. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcLinker, "F1B5BE2A-62DD-4327-A1C2-42AC1E1E78E6") +/// \brief DXC linker interface. +/// +/// Use DxcCreateInstance with CLSID_DxcLinker to obtain an instance of this +/// interface. +struct IDxcLinker : public IUnknown { +public: + /// \brief Register a library with name to reference it later. + virtual HRESULT + RegisterLibrary(_In_opt_ LPCWSTR pLibName, ///< Name of the library. + _In_ IDxcBlob *pLib ///< Library blob. + ) = 0; + + /// \brief Links the shader and produces a shader blob that the Direct3D + /// runtime can use. + virtual HRESULT STDMETHODCALLTYPE Link( + _In_opt_ LPCWSTR pEntryName, ///< Entry point name. + _In_ LPCWSTR pTargetProfile, ///< shader profile to link. + _In_count_(libCount) + const LPCWSTR *pLibNames, ///< Array of library names to link. + _In_ UINT32 libCount, ///< Number of libraries to link. + _In_opt_count_(argCount) + const LPCWSTR *pArguments, ///< Array of pointers to arguments. + _In_ UINT32 argCount, ///< Number of arguments. + _COM_Outptr_ IDxcOperationResult * + *ppResult ///< Linker output status, buffer, and errors. + ) = 0; +}; + +///////////////////////// +// Latest interfaces. Please use these. +//////////////////////// + +CROSS_PLATFORM_UUIDOF(IDxcUtils, "4605C4CB-2019-492A-ADA4-65F20BB7D67F") +/// \brief Various utility functions for DXC. +/// +/// Use DxcCreateInstance with CLSID_DxcUtils to obtain an instance of this +/// interface. +/// +/// IDxcUtils replaces IDxcLibrary. +struct IDxcUtils : public IUnknown { + /// \brief Create a sub-blob that holds a reference to the outer blob and + /// points to its memory. + /// + /// \param pBlob The outer blob. + /// + /// \param offset The offset inside the outer blob. + /// + /// \param length The size, in bytes, of the buffer to reference from the + /// output blob. + /// + /// \param ppResult Address of the pointer that receives a pointer to the + /// newly created blob. + virtual HRESULT STDMETHODCALLTYPE + CreateBlobFromBlob(_In_ IDxcBlob *pBlob, UINT32 offset, UINT32 length, + _COM_Outptr_ IDxcBlob **ppResult) = 0; + + // For codePage, use 0 (or DXC_CP_ACP) for raw binary or ANSI code page. + + /// \brief Create a blob referencing existing memory, with no copy. + /// + /// \param pData Pointer to buffer containing the contents of the new blob. + /// + /// \param size The size of the pData buffer, in bytes. + /// + /// \param codePage The code page to use if the blob contains text. Use + /// DXC_CP_ACP for binary or ANSI code page. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + /// + /// The user must manage the memory lifetime separately. + /// + /// This replaces IDxcLibrary::CreateBlobWithEncodingFromPinned. + virtual HRESULT STDMETHODCALLTYPE CreateBlobFromPinned( + _In_bytecount_(size) LPCVOID pData, UINT32 size, UINT32 codePage, + _COM_Outptr_ IDxcBlobEncoding **ppBlobEncoding) = 0; + + /// \brief Create a blob, taking ownership of memory allocated with the + /// supplied allocator. + /// + /// \param pData Pointer to buffer containing the contents of the new blob. + /// + /// \param pIMalloc The memory allocator to use. + /// + /// \param size The size of thee pData buffer, in bytes. + /// + /// \param codePage The code page to use if the blob contains text. Use + /// DXC_CP_ACP for binary or ANSI code page. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + /// + /// This replaces IDxcLibrary::CreateBlobWithEncodingOnMalloc. + virtual HRESULT STDMETHODCALLTYPE MoveToBlob( + _In_bytecount_(size) LPCVOID pData, IMalloc *pIMalloc, UINT32 size, + UINT32 codePage, _COM_Outptr_ IDxcBlobEncoding **ppBlobEncoding) = 0; + + /// \brief Create a blob containing a copy of the existing data. + /// + /// \param pData Pointer to buffer containing the contents of the new blob. + /// + /// \param size The size of thee pData buffer, in bytes. + /// + /// \param codePage The code page to use if the blob contains text. Use + /// DXC_CP_ACP for binary or ANSI code page. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + /// + /// The new blob and its contents are allocated with the current allocator. + /// This replaces IDxcLibrary::CreateBlobWithEncodingOnHeapCopy. + virtual HRESULT STDMETHODCALLTYPE + CreateBlob(_In_bytecount_(size) LPCVOID pData, UINT32 size, UINT32 codePage, + _COM_Outptr_ IDxcBlobEncoding **ppBlobEncoding) = 0; + + /// \brief Create a blob with data loaded from a file. + /// + /// \param pFileName The name of the file to load from. + /// + /// \param pCodePage Optional code page to use if the blob contains text. Pass + /// NULL for binary data. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + /// + /// The new blob and its contents are allocated with the current allocator. + /// This replaces IDxcLibrary::CreateBlobFromFile. + virtual HRESULT STDMETHODCALLTYPE + LoadFile(_In_z_ LPCWSTR pFileName, _In_opt_ UINT32 *pCodePage, + _COM_Outptr_ IDxcBlobEncoding **ppBlobEncoding) = 0; + + /// \brief Create a stream that reads data from a blob. + /// + /// \param pBlob The blob to read from. + /// + /// \param ppStream Address of the pointer that receives a pointer to the + /// newly created stream. + virtual HRESULT STDMETHODCALLTYPE CreateReadOnlyStreamFromBlob( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IStream **ppStream) = 0; + + /// \brief Create default file-based include handler. + /// + /// \param ppResult Address of the pointer that receives a pointer to the + /// newly created include handler. + virtual HRESULT STDMETHODCALLTYPE + CreateDefaultIncludeHandler(_COM_Outptr_ IDxcIncludeHandler **ppResult) = 0; + + /// \brief Convert or return matching encoded text blob as UTF-8. + /// + /// \param pBlob The blob to convert. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + virtual HRESULT STDMETHODCALLTYPE GetBlobAsUtf8( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IDxcBlobUtf8 **ppBlobEncoding) = 0; + + /// \brief Convert or return matching encoded text blob as UTF-16. + /// + /// \param pBlob The blob to convert. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + virtual HRESULT STDMETHODCALLTYPE GetBlobAsWide( + _In_ IDxcBlob *pBlob, _COM_Outptr_ IDxcBlobWide **ppBlobEncoding) = 0; + +#ifdef _WIN32 + /// \brief Convert or return matching encoded text blob as UTF-16. + /// + /// \param pBlob The blob to convert. + /// + /// \param ppBlobEncoding Address of the pointer that receives a pointer to + /// the newly created blob. + /// + /// Alias to GetBlobAsWide on Win32. + inline HRESULT GetBlobAsUtf16(_In_ IDxcBlob *pBlob, + _COM_Outptr_ IDxcBlobWide **ppBlobEncoding) { + return this->GetBlobAsWide(pBlob, ppBlobEncoding); + } +#endif + + /// \brief Retrieve a single part from a DXIL container. + /// + /// \param pShader The shader to retrieve the part from. + /// + /// \param DxcPart The part to retrieve (eg DXC_PART_ROOT_SIGNATURE). + /// + /// \param ppPartData Address of the pointer that receives a pointer to the + /// part. + /// + /// \param pPartSizeInBytes Address of the pointer that receives the size of + /// the part. + /// + /// The returned pointer points inside the buffer passed in pShader. + virtual HRESULT STDMETHODCALLTYPE + GetDxilContainerPart(_In_ const DxcBuffer *pShader, _In_ UINT32 DxcPart, + _Outptr_result_nullonfailure_ void **ppPartData, + _Out_ UINT32 *pPartSizeInBytes) = 0; + + /// \brief Create reflection interface from serialized DXIL container or the + /// DXC_OUT_REFLECTION blob contents. + /// + /// \param pData The source data. + /// + /// \param iid The interface ID of the reflection interface to create. + /// + /// \param ppvReflection Address of the pointer that receives a pointer to the + /// newly created reflection interface. + /// + /// Use this with interfaces such as ID3D12ShaderReflection. + virtual HRESULT STDMETHODCALLTYPE CreateReflection( + _In_ const DxcBuffer *pData, REFIID iid, void **ppvReflection) = 0; + + /// \brief Build arguments that can be passed to the Compile method. + virtual HRESULT STDMETHODCALLTYPE BuildArguments( + _In_opt_z_ LPCWSTR pSourceName, ///< Optional file name for pSource. Used + ///< in errors and include handlers. + _In_opt_z_ LPCWSTR pEntryPoint, ///< Entry point name (-E). + _In_z_ LPCWSTR pTargetProfile, ///< Shader profile to compile (-T). + _In_opt_count_(argCount) + LPCWSTR *pArguments, ///< Array of pointers to arguments. + _In_ UINT32 argCount, ///< Number of arguments. + _In_count_(defineCount) const DxcDefine *pDefines, ///< Array of defines. + _In_ UINT32 defineCount, ///< Number of defines. + _COM_Outptr_ IDxcCompilerArgs * + *ppArgs ///< Arguments you can use with Compile() method. + ) = 0; + + /// \brief Retrieve the hash and contents of a shader PDB. + /// + /// \param pPDBBlob The blob containing the PDB. + /// + /// \param ppHash Address of the pointer that receives a pointer to the hash + /// blob. + /// + /// \param ppContainer Address of the pointer that receives a pointer to the + /// bloc containing the contents of the PDB. + /// + virtual HRESULT STDMETHODCALLTYPE + GetPDBContents(_In_ IDxcBlob *pPDBBlob, _COM_Outptr_ IDxcBlob **ppHash, + _COM_Outptr_ IDxcBlob **ppContainer) = 0; +}; + +/// \brief Specifies the kind of output to retrieve from a IDxcResult. +/// +/// Note: text outputs returned from version 2 APIs are UTF-8 or UTF-16 based on +/// the -encoding option passed to the compiler. +typedef enum DXC_OUT_KIND { + DXC_OUT_NONE = 0, ///< No output. + DXC_OUT_OBJECT = 1, ///< IDxcBlob - Shader or library object. + DXC_OUT_ERRORS = 2, ///< IDxcBlobUtf8 or IDxcBlobWide. + DXC_OUT_PDB = 3, ///< IDxcBlob. + DXC_OUT_SHADER_HASH = 4, ///< IDxcBlob - DxcShaderHash of shader or shader + ///< with source info (-Zsb/-Zss). + DXC_OUT_DISASSEMBLY = 5, ///< IDxcBlobUtf8 or IDxcBlobWide - from Disassemble. + DXC_OUT_HLSL = + 6, ///< IDxcBlobUtf8 or IDxcBlobWide - from Preprocessor or Rewriter. + DXC_OUT_TEXT = 7, ///< IDxcBlobUtf8 or IDxcBlobWide - other text, such as + ///< -ast-dump or -Odump. + DXC_OUT_REFLECTION = 8, ///< IDxcBlob - RDAT part with reflection data. + DXC_OUT_ROOT_SIGNATURE = 9, ///< IDxcBlob - Serialized root signature output. + DXC_OUT_EXTRA_OUTPUTS = 10, ///< IDxcExtraOutputs - Extra outputs. + DXC_OUT_REMARKS = + 11, ///< IDxcBlobUtf8 or IDxcBlobWide - text directed at stdout. + DXC_OUT_TIME_REPORT = + 12, ///< IDxcBlobUtf8 or IDxcBlobWide - text directed at stdout. + DXC_OUT_TIME_TRACE = + 13, ///< IDxcBlobUtf8 or IDxcBlobWide - text directed at stdout. + + DXC_OUT_LAST = DXC_OUT_TIME_TRACE, ///< Last value for a counter. + + DXC_OUT_NUM_ENUMS, + DXC_OUT_FORCE_DWORD = 0xFFFFFFFF +} DXC_OUT_KIND; + +static_assert(DXC_OUT_NUM_ENUMS == DXC_OUT_LAST + 1, + "DXC_OUT_* Enum added and last value not updated."); + +CROSS_PLATFORM_UUIDOF(IDxcResult, "58346CDA-DDE7-4497-9461-6F87AF5E0659") +/// \brief Result of a DXC operation. +/// +/// DXC operations may have multiple outputs, such as a shader object and +/// errors. This interface provides access to the outputs. +struct IDxcResult : public IDxcOperationResult { + /// \brief Determines whether or not this result has the specified output. + /// + /// \param dxcOutKind The kind of output to check for. + virtual BOOL STDMETHODCALLTYPE HasOutput(_In_ DXC_OUT_KIND dxcOutKind) = 0; + + /// \brief Retrieves the specified output. + /// + /// \param dxcOutKind The kind of output to retrieve. + /// + /// \param iid The interface ID of the output interface. + /// + /// \param ppvObject Address of the pointer that receives a pointer to the + /// output. + /// + /// \param ppOutputName Optional address of a pointer to receive the name + /// blob, if there is one. + virtual HRESULT STDMETHODCALLTYPE + GetOutput(_In_ DXC_OUT_KIND dxcOutKind, _In_ REFIID iid, + _COM_Outptr_opt_result_maybenull_ void **ppvObject, + _COM_Outptr_opt_result_maybenull_ IDxcBlobWide **ppOutputName) = 0; + + /// \brief Retrieves the number of outputs available in this result. + virtual UINT32 GetNumOutputs() = 0; + + /// \brief Retrieves the output kind at the specified index. + virtual DXC_OUT_KIND GetOutputByIndex(UINT32 Index) = 0; + + /// \brief Retrieves the primary output kind for this result. + /// + /// See IDxcOperationResult::GetResult() for more information on the primary + /// output kinds. + virtual DXC_OUT_KIND PrimaryOutput() = 0; +}; + +// Special names for extra output that should get written to specific streams. +#define DXC_EXTRA_OUTPUT_NAME_STDOUT L"*stdout*" +#define DXC_EXTRA_OUTPUT_NAME_STDERR L"*stderr*" + +CROSS_PLATFORM_UUIDOF(IDxcExtraOutputs, "319b37a2-a5c2-494a-a5de-4801b2faf989") +/// \brief Additional outputs from a DXC operation. +/// +/// This can be used to obtain outputs that don't have an explicit DXC_OUT_KIND. +/// Use DXC_OUT_EXTRA_OUTPUTS to obtain instances of this. +struct IDxcExtraOutputs : public IUnknown { + /// \brief Retrieves the number of outputs available + virtual UINT32 STDMETHODCALLTYPE GetOutputCount() = 0; + + /// \brief Retrieves the specified output. + /// + /// \param uIndex The index of the output to retrieve. + /// + /// \param iid The interface ID of the output interface. + /// + /// \param ppvObject Optional address of the pointer that receives a pointer + /// to the output if there is one. + /// + /// \param ppOutputType Optional address of the pointer that receives the + /// output type name blob if there is one. + /// + /// \param ppOutputName Optional address of the pointer that receives the + /// output name blob if there is one. + virtual HRESULT STDMETHODCALLTYPE + GetOutput(_In_ UINT32 uIndex, _In_ REFIID iid, + _COM_Outptr_opt_result_maybenull_ void **ppvObject, + _COM_Outptr_opt_result_maybenull_ IDxcBlobWide **ppOutputType, + _COM_Outptr_opt_result_maybenull_ IDxcBlobWide **ppOutputName) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompiler3, "228B4687-5A6A-4730-900C-9702B2203F54") +/// \brief Interface to the DirectX Shader Compiler. +/// +/// Use DxcCreateInstance with CLSID_DxcCompiler to obtain an instance of this +/// interface. +struct IDxcCompiler3 : public IUnknown { + /// \brief Compile a shader. + /// + /// IDxcUtils::BuildArguments can be used to assist building the pArguments + /// and argCount parameters. + /// + /// Depending on the arguments, this method can be used to: + /// + /// * Compile a single entry point to the target shader model, + /// * Compile a library to a library target (-T lib_*) + /// * Compile a root signature (-T rootsig_*), + /// * Preprocess HLSL source (-P). + virtual HRESULT STDMETHODCALLTYPE Compile( + _In_ const DxcBuffer *pSource, ///< Source text to compile. + _In_opt_count_(argCount) + LPCWSTR *pArguments, ///< Array of pointers to arguments. + _In_ UINT32 argCount, ///< Number of arguments. + _In_opt_ IDxcIncludeHandler + *pIncludeHandler, ///< user-provided interface to handle include + ///< directives (optional). + _In_ REFIID riid, ///< Interface ID for the result. + _Out_ LPVOID *ppResult ///< IDxcResult: status, buffer, and errors. + ) = 0; + + /// \brief Disassemble a program. + virtual HRESULT STDMETHODCALLTYPE Disassemble( + _In_ const DxcBuffer + *pObject, ///< Program to disassemble: dxil container or bitcode. + _In_ REFIID riid, ///< Interface ID for the result. + _Out_ LPVOID + *ppResult ///< IDxcResult: status, disassembly text, and errors. + ) = 0; +}; + +static const UINT32 DxcValidatorFlags_Default = 0; +static const UINT32 DxcValidatorFlags_InPlaceEdit = + 1; // Validator is allowed to update shader blob in-place. +static const UINT32 DxcValidatorFlags_RootSignatureOnly = 2; +static const UINT32 DxcValidatorFlags_ModuleOnly = 4; +static const UINT32 DxcValidatorFlags_ValidMask = 0x7; + +CROSS_PLATFORM_UUIDOF(IDxcValidator, "A6E82BD2-1FD7-4826-9811-2857E797F49A") +/// \brief Interface to DXC shader validator. +/// +/// Use DxcCreateInstance with CLSID_DxcValidator to obtain an instance of this. +struct IDxcValidator : public IUnknown { + /// \brief Validate a shader. + virtual HRESULT STDMETHODCALLTYPE Validate( + _In_ IDxcBlob *pShader, ///< Shader to validate. + _In_ UINT32 Flags, ///< Validation flags. + _COM_Outptr_ IDxcOperationResult * + *ppResult ///< Validation output status, buffer, and errors. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcValidator2, "458e1fd1-b1b2-4750-a6e1-9c10f03bed92") +/// \brief Interface to DXC shader validator. +/// +/// Use DxcCreateInstance with CLSID_DxcValidator to obtain an instance of this. +struct IDxcValidator2 : public IDxcValidator { + /// \brief Validate a shader with optional debug bitcode. + virtual HRESULT STDMETHODCALLTYPE ValidateWithDebug( + _In_ IDxcBlob *pShader, ///< Shader to validate. + _In_ UINT32 Flags, ///< Validation flags. + _In_opt_ DxcBuffer *pOptDebugBitcode, ///< Optional debug module bitcode + ///< to provide line numbers. + _COM_Outptr_ IDxcOperationResult * + *ppResult ///< Validation output status, buffer, and errors. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcContainerBuilder, + "334b1f50-2292-4b35-99a1-25588d8c17fe") +/// \brief Interface to DXC container builder. +/// +/// Use DxcCreateInstance with CLSID_DxcContainerBuilder to obtain an instance +/// of this. +struct IDxcContainerBuilder : public IUnknown { + /// \brief Load a DxilContainer to the builder. + virtual HRESULT STDMETHODCALLTYPE + Load(_In_ IDxcBlob *pDxilContainerHeader) = 0; + + /// \brief Add a part to the container. + /// + /// \param fourCC The part identifier (eg DXC_PART_PDB). + /// + /// \param pSource The source blob. + virtual HRESULT STDMETHODCALLTYPE AddPart(_In_ UINT32 fourCC, + _In_ IDxcBlob *pSource) = 0; + + /// \brief Remove a part from the container. + /// + /// \param fourCC The part identifier (eg DXC_PART_PDB). + /// + /// \return S_OK on success, DXC_E_MISSING_PART if the part was not found, or + /// other standard HRESULT error code. + virtual HRESULT STDMETHODCALLTYPE RemovePart(_In_ UINT32 fourCC) = 0; + + /// \brief Build the container. + /// + /// \param ppResult Pointer to variable to receive the result. + virtual HRESULT STDMETHODCALLTYPE + SerializeContainer(_Out_ IDxcOperationResult **ppResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcAssembler, "091f7a26-1c1f-4948-904b-e6e3a8a771d5") +/// \brief Interface to DxcAssembler. +/// +/// Use DxcCreateInstance with CLSID_DxcAssembler to obtain an instance of this. +struct IDxcAssembler : public IUnknown { + /// \brief Assemble DXIL in LL or LLVM bitcode to DXIL container. + virtual HRESULT STDMETHODCALLTYPE AssembleToContainer( + _In_ IDxcBlob *pShader, ///< Shader to assemble. + _COM_Outptr_ IDxcOperationResult * + *ppResult ///< Assembly output status, buffer, and errors. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcContainerReflection, + "d2c21b26-8350-4bdc-976a-331ce6f4c54c") +/// \brief Interface to DxcContainerReflection. +/// +/// Use DxcCreateInstance with CLSID_DxcContainerReflection to obtain an +/// instance of this. +struct IDxcContainerReflection : public IUnknown { + /// \brief Choose the container to perform reflection on + /// + /// \param pContainer The container to load. If null is passed then this + /// instance will release any held resources. + virtual HRESULT STDMETHODCALLTYPE Load(_In_ IDxcBlob *pContainer) = 0; + + /// \brief Retrieves the number of parts in the container. + /// + /// \param pResult Pointer to variable to receive the result. + /// + /// \return S_OK on success, E_NOT_VALID_STATE if a container has not been + /// loaded using Load(), or other standard HRESULT error codes. + virtual HRESULT STDMETHODCALLTYPE GetPartCount(_Out_ UINT32 *pResult) = 0; + + /// \brief Retrieve the kind of a specified part. + /// + /// \param idx The index of the part to retrieve the kind of. + /// + /// \param pResult Pointer to variable to receive the result. + /// + /// \return S_OK on success, E_NOT_VALID_STATE if a container has not been + /// loaded using Load(), E_BOUND if idx is out of bounds, or other standard + /// HRESULT error codes. + virtual HRESULT STDMETHODCALLTYPE GetPartKind(UINT32 idx, + _Out_ UINT32 *pResult) = 0; + + /// \brief Retrieve the content of a specified part. + /// + /// \param idx The index of the part to retrieve. + /// + /// \param ppResult Pointer to variable to receive the result. + /// + /// \return S_OK on success, E_NOT_VALID_STATE if a container has not been + /// loaded using Load(), E_BOUND if idx is out of bounds, or other standard + /// HRESULT error codes. + virtual HRESULT STDMETHODCALLTYPE + GetPartContent(UINT32 idx, _COM_Outptr_ IDxcBlob **ppResult) = 0; + + /// \brief Retrieve the index of the first part with the specified kind. + /// + /// \param kind The kind to search for. + /// + /// \param pResult Pointer to variable to receive the index of the matching + /// part. + /// + /// \return S_OK on success, E_NOT_VALID_STATE if a container has not been + /// loaded using Load(), HRESULT_FROM_WIN32(ERROR_NOT_FOUND) if there is no + /// part with the specified kind, or other standard HRESULT error codes. + virtual HRESULT STDMETHODCALLTYPE + FindFirstPartKind(UINT32 kind, _Out_ UINT32 *pResult) = 0; + + /// \brief Retrieve the reflection interface for a specified part. + /// + /// \param idx The index of the part to retrieve the reflection interface of. + /// + /// \param iid The IID of the interface to retrieve. + /// + /// \param ppvObject Pointer to variable to receive the result. + /// + /// Use this with interfaces such as ID3D12ShaderReflection. + /// + /// \return S_OK on success, E_NOT_VALID_STATE if a container has not been + /// loaded using Load(), E_BOUND if idx is out of bounds, or other standard + /// HRESULT error codes. + virtual HRESULT STDMETHODCALLTYPE GetPartReflection(UINT32 idx, REFIID iid, + void **ppvObject) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcOptimizerPass, "AE2CD79F-CC22-453F-9B6B-B124E7A5204C") +/// \brief An optimizer pass. +/// +/// Instances of this can be obtained via IDxcOptimizer::GetAvailablePass. +struct IDxcOptimizerPass : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetOptionName(_COM_Outptr_ LPWSTR *ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDescription(_COM_Outptr_ LPWSTR *ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetOptionArgCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetOptionArgName(UINT32 argIndex, _COM_Outptr_ LPWSTR *ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetOptionArgDescription(UINT32 argIndex, _COM_Outptr_ LPWSTR *ppResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcOptimizer, "25740E2E-9CBA-401B-9119-4FB42F39F270") +/// \brief Interface to DxcOptimizer. +/// +/// Use DxcCreateInstance with CLSID_DxcOptimizer to obtain an instance of this. +struct IDxcOptimizer : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetAvailablePassCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetAvailablePass(UINT32 index, _COM_Outptr_ IDxcOptimizerPass **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + RunOptimizer(IDxcBlob *pBlob, _In_count_(optionCount) LPCWSTR *ppOptions, + UINT32 optionCount, _COM_Outptr_ IDxcBlob **pOutputModule, + _COM_Outptr_opt_ IDxcBlobEncoding **ppOutputText) = 0; +}; + +static const UINT32 DxcVersionInfoFlags_None = 0; +static const UINT32 DxcVersionInfoFlags_Debug = 1; // Matches VS_FF_DEBUG +static const UINT32 DxcVersionInfoFlags_Internal = + 2; // Internal Validator (non-signing) + +CROSS_PLATFORM_UUIDOF(IDxcVersionInfo, "b04f5b50-2059-4f12-a8ff-a1e0cde1cc7e") +/// \brief PDB Version information. +/// +/// Use IDxcPdbUtils2::GetVersionInfo to obtain an instance of this. +struct IDxcVersionInfo : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetVersion(_Out_ UINT32 *pMajor, + _Out_ UINT32 *pMinor) = 0; + virtual HRESULT STDMETHODCALLTYPE GetFlags(_Out_ UINT32 *pFlags) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcVersionInfo2, "fb6904c4-42f0-4b62-9c46-983af7da7c83") +/// \brief PDB Version Information. +/// +/// Use IDxcPdbUtils2::GetVersionInfo to obtain a IDxcVersionInfo interface, and +/// then use QueryInterface to obtain an instance of this interface from it. +struct IDxcVersionInfo2 : public IDxcVersionInfo { + virtual HRESULT STDMETHODCALLTYPE GetCommitInfo( + _Out_ UINT32 *pCommitCount, ///< The total number commits. + _Outptr_result_z_ char **pCommitHash ///< The SHA of the latest commit. + ///< Must be CoTaskMemFree()'d. + ) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcVersionInfo3, "5e13e843-9d25-473c-9ad2-03b2d0b44b1e") +/// \brief PDB Version Information. +/// +/// Use IDxcPdbUtils2::GetVersionInfo to obtain a IDxcVersionInfo interface, and +/// then use QueryInterface to obtain an instance of this interface from it. +struct IDxcVersionInfo3 : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetCustomVersionString( + _Outptr_result_z_ char * + *pVersionString ///< Custom version string for compiler. Must be + ///< CoTaskMemFree()'d. + ) = 0; +}; + +struct DxcArgPair { + const WCHAR *pName; + const WCHAR *pValue; +}; + +CROSS_PLATFORM_UUIDOF(IDxcPdbUtils, "E6C9647E-9D6A-4C3B-B94C-524B5A6C343D") +/// \deprecated Please use IDxcPdbUtils2 instead. +struct IDxcPdbUtils : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE Load(_In_ IDxcBlob *pPdbOrDxil) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetSourceCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSource(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobEncoding **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSourceName(_In_ UINT32 uIndex, _Outptr_result_z_ BSTR *pResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetFlagCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFlag(_In_ UINT32 uIndex, _Outptr_result_z_ BSTR *pResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetArgCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE GetArg(_In_ UINT32 uIndex, + _Outptr_result_z_ BSTR *pResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetArgPairCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetArgPair(_In_ UINT32 uIndex, _Outptr_result_z_ BSTR *pName, + _Outptr_result_z_ BSTR *pValue) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetDefineCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDefine(_In_ UINT32 uIndex, _Outptr_result_z_ BSTR *pResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetTargetProfile(_Outptr_result_z_ BSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetEntryPoint(_Outptr_result_z_ BSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetMainFileName(_Outptr_result_z_ BSTR *pResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetHash(_COM_Outptr_ IDxcBlob **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetName(_Outptr_result_z_ BSTR *pResult) = 0; + + virtual BOOL STDMETHODCALLTYPE IsFullPDB() = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFullPDB(_COM_Outptr_ IDxcBlob **ppFullPDB) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetVersionInfo(_COM_Outptr_ IDxcVersionInfo **ppVersionInfo) = 0; + + virtual HRESULT STDMETHODCALLTYPE + SetCompiler(_In_ IDxcCompiler3 *pCompiler) = 0; + virtual HRESULT STDMETHODCALLTYPE + CompileForFullPDB(_COM_Outptr_ IDxcResult **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE OverrideArgs(_In_ DxcArgPair *pArgPairs, + UINT32 uNumArgPairs) = 0; + virtual HRESULT STDMETHODCALLTYPE + OverrideRootSignature(_In_ const WCHAR *pRootSignature) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcPdbUtils2, "4315D938-F369-4F93-95A2-252017CC3807") +/// \brief DxcPdbUtils interface. +/// +/// Use DxcCreateInstance with CLSID_DxcPdbUtils to create an instance of this. +struct IDxcPdbUtils2 : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE Load(_In_ IDxcBlob *pPdbOrDxil) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetSourceCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSource(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobEncoding **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSourceName(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetLibraryPDBCount(UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE GetLibraryPDB( + _In_ UINT32 uIndex, _COM_Outptr_ IDxcPdbUtils2 **ppOutPdbUtils, + _COM_Outptr_opt_result_maybenull_ IDxcBlobWide **ppLibraryName) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetFlagCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFlag(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetArgCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetArg(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetArgPairCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE GetArgPair( + _In_ UINT32 uIndex, _COM_Outptr_result_maybenull_ IDxcBlobWide **ppName, + _COM_Outptr_result_maybenull_ IDxcBlobWide **ppValue) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetDefineCount(_Out_ UINT32 *pCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDefine(_In_ UINT32 uIndex, _COM_Outptr_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetTargetProfile(_COM_Outptr_result_maybenull_ IDxcBlobWide **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetEntryPoint(_COM_Outptr_result_maybenull_ IDxcBlobWide **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetMainFileName(_COM_Outptr_result_maybenull_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetHash(_COM_Outptr_result_maybenull_ IDxcBlob **ppResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetName(_COM_Outptr_result_maybenull_ IDxcBlobWide **ppResult) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetVersionInfo( + _COM_Outptr_result_maybenull_ IDxcVersionInfo **ppVersionInfo) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetCustomToolchainID(_Out_ UINT32 *pID) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetCustomToolchainData(_COM_Outptr_result_maybenull_ IDxcBlob **ppBlob) = 0; + + virtual HRESULT STDMETHODCALLTYPE + GetWholeDxil(_COM_Outptr_result_maybenull_ IDxcBlob **ppResult) = 0; + + virtual BOOL STDMETHODCALLTYPE IsFullPDB() = 0; + virtual BOOL STDMETHODCALLTYPE IsPDBRef() = 0; +}; + +// Note: __declspec(selectany) requires 'extern' +// On Linux __declspec(selectany) is removed and using 'extern' results in link +// error. +#ifdef _MSC_VER +#define CLSID_SCOPE __declspec(selectany) extern +#else +#define CLSID_SCOPE +#endif + +CLSID_SCOPE const CLSID CLSID_DxcCompiler = { + 0x73e22d93, + 0xe6ce, + 0x47f3, + {0xb5, 0xbf, 0xf0, 0x66, 0x4f, 0x39, 0xc1, 0xb0}}; + +// {EF6A8087-B0EA-4D56-9E45-D07E1A8B7806} +CLSID_SCOPE const GUID CLSID_DxcLinker = { + 0xef6a8087, + 0xb0ea, + 0x4d56, + {0x9e, 0x45, 0xd0, 0x7e, 0x1a, 0x8b, 0x78, 0x6}}; + +// {CD1F6B73-2AB0-484D-8EDC-EBE7A43CA09F} +CLSID_SCOPE const CLSID CLSID_DxcDiaDataSource = { + 0xcd1f6b73, + 0x2ab0, + 0x484d, + {0x8e, 0xdc, 0xeb, 0xe7, 0xa4, 0x3c, 0xa0, 0x9f}}; + +// {3E56AE82-224D-470F-A1A1-FE3016EE9F9D} +CLSID_SCOPE const CLSID CLSID_DxcCompilerArgs = { + 0x3e56ae82, + 0x224d, + 0x470f, + {0xa1, 0xa1, 0xfe, 0x30, 0x16, 0xee, 0x9f, 0x9d}}; + +// {6245D6AF-66E0-48FD-80B4-4D271796748C} +CLSID_SCOPE const GUID CLSID_DxcLibrary = { + 0x6245d6af, + 0x66e0, + 0x48fd, + {0x80, 0xb4, 0x4d, 0x27, 0x17, 0x96, 0x74, 0x8c}}; + +CLSID_SCOPE const GUID CLSID_DxcUtils = CLSID_DxcLibrary; + +// {8CA3E215-F728-4CF3-8CDD-88AF917587A1} +CLSID_SCOPE const GUID CLSID_DxcValidator = { + 0x8ca3e215, + 0xf728, + 0x4cf3, + {0x8c, 0xdd, 0x88, 0xaf, 0x91, 0x75, 0x87, 0xa1}}; + +// {D728DB68-F903-4F80-94CD-DCCF76EC7151} +CLSID_SCOPE const GUID CLSID_DxcAssembler = { + 0xd728db68, + 0xf903, + 0x4f80, + {0x94, 0xcd, 0xdc, 0xcf, 0x76, 0xec, 0x71, 0x51}}; + +// {b9f54489-55b8-400c-ba3a-1675e4728b91} +CLSID_SCOPE const GUID CLSID_DxcContainerReflection = { + 0xb9f54489, + 0x55b8, + 0x400c, + {0xba, 0x3a, 0x16, 0x75, 0xe4, 0x72, 0x8b, 0x91}}; + +// {AE2CD79F-CC22-453F-9B6B-B124E7A5204C} +CLSID_SCOPE const GUID CLSID_DxcOptimizer = { + 0xae2cd79f, + 0xcc22, + 0x453f, + {0x9b, 0x6b, 0xb1, 0x24, 0xe7, 0xa5, 0x20, 0x4c}}; + +// {94134294-411f-4574-b4d0-8741e25240d2} +CLSID_SCOPE const GUID CLSID_DxcContainerBuilder = { + 0x94134294, + 0x411f, + 0x4574, + {0xb4, 0xd0, 0x87, 0x41, 0xe2, 0x52, 0x40, 0xd2}}; + +// {54621dfb-f2ce-457e-ae8c-ec355faeec7c} +CLSID_SCOPE const GUID CLSID_DxcPdbUtils = { + 0x54621dfb, + 0xf2ce, + 0x457e, + {0xae, 0x8c, 0xec, 0x35, 0x5f, 0xae, 0xec, 0x7c}}; + +#endif diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/dxcerrors.h b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcerrors.h new file mode 100644 index 00000000..ce50b83e --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcerrors.h @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////// +// // +// dxcerror.h // +// Copyright (C) Microsoft Corporation. All rights reserved. // +// This file is distributed under the University of Illinois Open Source // +// License. See LICENSE.TXT for details. // +// // +// Provides definition of error codes. // +// // +/////////////////////////////////////////////////////////////////////////////// + +#ifndef __DXC_ERRORS__ +#define __DXC_ERRORS__ + +#ifndef FACILITY_GRAPHICS +#define FACILITY_GRAPHICS 36 +#endif + +#define DXC_EXCEPTION_CODE(name, status) \ + static constexpr DWORD EXCEPTION_##name = \ + (0xc0000000u | (FACILITY_GRAPHICS << 16) | \ + (0xff00u | (status & 0xffu))); + +DXC_EXCEPTION_CODE(LOAD_LIBRARY_FAILED, 0x00u) +DXC_EXCEPTION_CODE(NO_HMODULE, 0x01u) +DXC_EXCEPTION_CODE(GET_PROC_FAILED, 0x02u) + +#undef DXC_EXCEPTION_CODE + +#endif diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/dxcisense.h b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcisense.h new file mode 100644 index 00000000..661de168 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcisense.h @@ -0,0 +1,959 @@ +/////////////////////////////////////////////////////////////////////////////// +// // +// dxcisense.h // +// Copyright (C) Microsoft Corporation. All rights reserved. // +// This file is distributed under the University of Illinois Open Source // +// License. See LICENSE.TXT for details. // +// // +// Provides declarations for the DirectX Compiler IntelliSense component. // +// // +/////////////////////////////////////////////////////////////////////////////// + +#ifndef __DXC_ISENSE__ +#define __DXC_ISENSE__ + +#include "dxcapi.h" +#ifndef _WIN32 +#include "WinAdapter.h" +#endif + +typedef enum DxcGlobalOptions { + DxcGlobalOpt_None = 0x0, + DxcGlobalOpt_ThreadBackgroundPriorityForIndexing = 0x1, + DxcGlobalOpt_ThreadBackgroundPriorityForEditing = 0x2, + DxcGlobalOpt_ThreadBackgroundPriorityForAll = + DxcGlobalOpt_ThreadBackgroundPriorityForIndexing | + DxcGlobalOpt_ThreadBackgroundPriorityForEditing +} DxcGlobalOptions; + +typedef enum DxcTokenKind { + DxcTokenKind_Punctuation = + 0, // A token that contains some kind of punctuation. + DxcTokenKind_Keyword = 1, // A language keyword. + DxcTokenKind_Identifier = 2, // An identifier (that is not a keyword). + DxcTokenKind_Literal = 3, // A numeric, string, or character literal. + DxcTokenKind_Comment = 4, // A comment. + DxcTokenKind_Unknown = + 5, // An unknown token (possibly known to a future version). + DxcTokenKind_BuiltInType = 6, // A built-in type like int, void or float3. +} DxcTokenKind; + +typedef enum DxcTypeKind { + DxcTypeKind_Invalid = + 0, // Reprents an invalid type (e.g., where no type is available). + DxcTypeKind_Unexposed = + 1, // A type whose specific kind is not exposed via this interface. + // Builtin types + DxcTypeKind_Void = 2, + DxcTypeKind_Bool = 3, + DxcTypeKind_Char_U = 4, + DxcTypeKind_UChar = 5, + DxcTypeKind_Char16 = 6, + DxcTypeKind_Char32 = 7, + DxcTypeKind_UShort = 8, + DxcTypeKind_UInt = 9, + DxcTypeKind_ULong = 10, + DxcTypeKind_ULongLong = 11, + DxcTypeKind_UInt128 = 12, + DxcTypeKind_Char_S = 13, + DxcTypeKind_SChar = 14, + DxcTypeKind_WChar = 15, + DxcTypeKind_Short = 16, + DxcTypeKind_Int = 17, + DxcTypeKind_Long = 18, + DxcTypeKind_LongLong = 19, + DxcTypeKind_Int128 = 20, + DxcTypeKind_Float = 21, + DxcTypeKind_Double = 22, + DxcTypeKind_LongDouble = 23, + DxcTypeKind_NullPtr = 24, + DxcTypeKind_Overload = 25, + DxcTypeKind_Dependent = 26, + DxcTypeKind_ObjCId = 27, + DxcTypeKind_ObjCClass = 28, + DxcTypeKind_ObjCSel = 29, + DxcTypeKind_FirstBuiltin = DxcTypeKind_Void, + DxcTypeKind_LastBuiltin = DxcTypeKind_ObjCSel, + + DxcTypeKind_Complex = 100, + DxcTypeKind_Pointer = 101, + DxcTypeKind_BlockPointer = 102, + DxcTypeKind_LValueReference = 103, + DxcTypeKind_RValueReference = 104, + DxcTypeKind_Record = 105, + DxcTypeKind_Enum = 106, + DxcTypeKind_Typedef = 107, + DxcTypeKind_ObjCInterface = 108, + DxcTypeKind_ObjCObjectPointer = 109, + DxcTypeKind_FunctionNoProto = 110, + DxcTypeKind_FunctionProto = 111, + DxcTypeKind_ConstantArray = 112, + DxcTypeKind_Vector = 113, + DxcTypeKind_IncompleteArray = 114, + DxcTypeKind_VariableArray = 115, + DxcTypeKind_DependentSizedArray = 116, + DxcTypeKind_MemberPointer = 117 +} DxcTypeKind; + +// Describes the severity of a particular diagnostic. +typedef enum DxcDiagnosticSeverity { + // A diagnostic that has been suppressed, e.g., by a command-line option. + DxcDiagnostic_Ignored = 0, + + // This diagnostic is a note that should be attached to the previous + // (non-note) diagnostic. + DxcDiagnostic_Note = 1, + + // This diagnostic indicates suspicious code that may not be wrong. + DxcDiagnostic_Warning = 2, + + // This diagnostic indicates that the code is ill-formed. + DxcDiagnostic_Error = 3, + + // This diagnostic indicates that the code is ill-formed such that future + // parser rec unlikely to produce useful results. + DxcDiagnostic_Fatal = 4 + +} DxcDiagnosticSeverity; + +// Options to control the display of diagnostics. +typedef enum DxcDiagnosticDisplayOptions { + // Display the source-location information where the diagnostic was located. + DxcDiagnostic_DisplaySourceLocation = 0x01, + + // If displaying the source-location information of the diagnostic, + // also include the column number. + DxcDiagnostic_DisplayColumn = 0x02, + + // If displaying the source-location information of the diagnostic, + // also include information about source ranges in a machine-parsable format. + DxcDiagnostic_DisplaySourceRanges = 0x04, + + // Display the option name associated with this diagnostic, if any. + DxcDiagnostic_DisplayOption = 0x08, + + // Display the category number associated with this diagnostic, if any. + DxcDiagnostic_DisplayCategoryId = 0x10, + + // Display the category name associated with this diagnostic, if any. + DxcDiagnostic_DisplayCategoryName = 0x20, + + // Display the severity of the diagnostic message. + DxcDiagnostic_DisplaySeverity = 0x200 +} DxcDiagnosticDisplayOptions; + +typedef enum DxcTranslationUnitFlags { + // Used to indicate that no special translation-unit options are needed. + DxcTranslationUnitFlags_None = 0x0, + + // Used to indicate that the parser should construct a "detailed" + // preprocessing record, including all macro definitions and instantiations. + DxcTranslationUnitFlags_DetailedPreprocessingRecord = 0x01, + + // Used to indicate that the translation unit is incomplete. + DxcTranslationUnitFlags_Incomplete = 0x02, + + // Used to indicate that the translation unit should be built with an + // implicit precompiled header for the preamble. + DxcTranslationUnitFlags_PrecompiledPreamble = 0x04, + + // Used to indicate that the translation unit should cache some + // code-completion results with each reparse of the source file. + DxcTranslationUnitFlags_CacheCompletionResults = 0x08, + + // Used to indicate that the translation unit will be serialized with + // SaveTranslationUnit. + DxcTranslationUnitFlags_ForSerialization = 0x10, + + // DEPRECATED + DxcTranslationUnitFlags_CXXChainedPCH = 0x20, + + // Used to indicate that function/method bodies should be skipped while + // parsing. + DxcTranslationUnitFlags_SkipFunctionBodies = 0x40, + + // Used to indicate that brief documentation comments should be + // included into the set of code completions returned from this translation + // unit. + DxcTranslationUnitFlags_IncludeBriefCommentsInCodeCompletion = 0x80, + + // Used to indicate that compilation should occur on the caller's thread. + DxcTranslationUnitFlags_UseCallerThread = 0x800 +} DxcTranslationUnitFlags; + +typedef enum DxcCursorFormatting { + DxcCursorFormatting_Default = + 0x0, // Default rules, language-insensitive formatting. + DxcCursorFormatting_UseLanguageOptions = + 0x1, // Language-sensitive formatting. + DxcCursorFormatting_SuppressSpecifiers = 0x2, // Supresses type specifiers. + DxcCursorFormatting_SuppressTagKeyword = + 0x4, // Suppressed tag keyword (eg, 'class'). + DxcCursorFormatting_IncludeNamespaceKeyword = + 0x8, // Include namespace keyword. +} DxcCursorFormatting; + +enum DxcCursorKind { + /* Declarations */ + DxcCursor_UnexposedDecl = + 1, // A declaration whose specific kind is not exposed via this interface. + DxcCursor_StructDecl = 2, // A C or C++ struct. + DxcCursor_UnionDecl = 3, // A C or C++ union. + DxcCursor_ClassDecl = 4, // A C++ class. + DxcCursor_EnumDecl = 5, // An enumeration. + DxcCursor_FieldDecl = 6, // A field (in C) or non-static data member (in C++) + // in a struct, union, or C++ class. + DxcCursor_EnumConstantDecl = 7, // An enumerator constant. + DxcCursor_FunctionDecl = 8, // A function. + DxcCursor_VarDecl = 9, // A variable. + DxcCursor_ParmDecl = 10, // A function or method parameter. + DxcCursor_ObjCInterfaceDecl = 11, // An Objective-C interface. + DxcCursor_ObjCCategoryDecl = 12, // An Objective-C interface for a category. + DxcCursor_ObjCProtocolDecl = 13, // An Objective-C protocol declaration. + DxcCursor_ObjCPropertyDecl = 14, // An Objective-C property declaration. + DxcCursor_ObjCIvarDecl = 15, // An Objective-C instance variable. + DxcCursor_ObjCInstanceMethodDecl = 16, // An Objective-C instance method. + DxcCursor_ObjCClassMethodDecl = 17, // An Objective-C class method. + DxcCursor_ObjCImplementationDecl = 18, // An Objective-C \@implementation. + DxcCursor_ObjCCategoryImplDecl = + 19, // An Objective-C \@implementation for a category. + DxcCursor_TypedefDecl = 20, // A typedef + DxcCursor_CXXMethod = 21, // A C++ class method. + DxcCursor_Namespace = 22, // A C++ namespace. + DxcCursor_LinkageSpec = 23, // A linkage specification, e.g. 'extern "C"'. + DxcCursor_Constructor = 24, // A C++ constructor. + DxcCursor_Destructor = 25, // A C++ destructor. + DxcCursor_ConversionFunction = 26, // A C++ conversion function. + DxcCursor_TemplateTypeParameter = 27, // A C++ template type parameter. + DxcCursor_NonTypeTemplateParameter = 28, // A C++ non-type template parameter. + DxcCursor_TemplateTemplateParameter = + 29, // A C++ template template parameter. + DxcCursor_FunctionTemplate = 30, // A C++ function template. + DxcCursor_ClassTemplate = 31, // A C++ class template. + DxcCursor_ClassTemplatePartialSpecialization = + 32, // A C++ class template partial specialization. + DxcCursor_NamespaceAlias = 33, // A C++ namespace alias declaration. + DxcCursor_UsingDirective = 34, // A C++ using directive. + DxcCursor_UsingDeclaration = 35, // A C++ using declaration. + DxcCursor_TypeAliasDecl = 36, // A C++ alias declaration + DxcCursor_ObjCSynthesizeDecl = 37, // An Objective-C \@synthesize definition. + DxcCursor_ObjCDynamicDecl = 38, // An Objective-C \@dynamic definition. + DxcCursor_CXXAccessSpecifier = 39, // An access specifier. + + DxcCursor_FirstDecl = DxcCursor_UnexposedDecl, + DxcCursor_LastDecl = DxcCursor_CXXAccessSpecifier, + + /* References */ + DxcCursor_FirstRef = 40, /* Decl references */ + DxcCursor_ObjCSuperClassRef = 40, + DxcCursor_ObjCProtocolRef = 41, + DxcCursor_ObjCClassRef = 42, + /** + * \brief A reference to a type declaration. + * + * A type reference occurs anywhere where a type is named but not + * declared. For example, given: + * + * \code + * typedef unsigned size_type; + * size_type size; + * \endcode + * + * The typedef is a declaration of size_type (DxcCursor_TypedefDecl), + * while the type of the variable "size" is referenced. The cursor + * referenced by the type of size is the typedef for size_type. + */ + DxcCursor_TypeRef = 43, // A reference to a type declaration. + DxcCursor_CXXBaseSpecifier = 44, + DxcCursor_TemplateRef = + 45, // A reference to a class template, function template, template + // template parameter, or class template partial specialization. + DxcCursor_NamespaceRef = 46, // A reference to a namespace or namespace alias. + DxcCursor_MemberRef = + 47, // A reference to a member of a struct, union, or class that occurs in + // some non-expression context, e.g., a designated initializer. + /** + * \brief A reference to a labeled statement. + * + * This cursor kind is used to describe the jump to "start_over" in the + * goto statement in the following example: + * + * \code + * start_over: + * ++counter; + * + * goto start_over; + * \endcode + * + * A label reference cursor refers to a label statement. + */ + DxcCursor_LabelRef = 48, // A reference to a labeled statement. + + // A reference to a set of overloaded functions or function templates + // that has not yet been resolved to a specific function or function template. + // + // An overloaded declaration reference cursor occurs in C++ templates where + // a dependent name refers to a function. + DxcCursor_OverloadedDeclRef = 49, + DxcCursor_VariableRef = + 50, // A reference to a variable that occurs in some non-expression + // context, e.g., a C++ lambda capture list. + + DxcCursor_LastRef = DxcCursor_VariableRef, + + /* Error conditions */ + DxcCursor_FirstInvalid = 70, + DxcCursor_InvalidFile = 70, + DxcCursor_NoDeclFound = 71, + DxcCursor_NotImplemented = 72, + DxcCursor_InvalidCode = 73, + DxcCursor_LastInvalid = DxcCursor_InvalidCode, + + /* Expressions */ + DxcCursor_FirstExpr = 100, + + /** + * \brief An expression whose specific kind is not exposed via this + * interface. + * + * Unexposed expressions have the same operations as any other kind + * of expression; one can extract their location information, + * spelling, children, etc. However, the specific kind of the + * expression is not reported. + */ + DxcCursor_UnexposedExpr = 100, // An expression whose specific kind is not + // exposed via this interface. + DxcCursor_DeclRefExpr = + 101, // An expression that refers to some value declaration, such as a + // function, varible, or enumerator. + DxcCursor_MemberRefExpr = + 102, // An expression that refers to a member of a struct, union, class, + // Objective-C class, etc. + DxcCursor_CallExpr = 103, // An expression that calls a function. + DxcCursor_ObjCMessageExpr = 104, // An expression that sends a message to an + // Objective-C object or class. + DxcCursor_BlockExpr = 105, // An expression that represents a block literal. + DxcCursor_IntegerLiteral = 106, // An integer literal. + DxcCursor_FloatingLiteral = 107, // A floating point number literal. + DxcCursor_ImaginaryLiteral = 108, // An imaginary number literal. + DxcCursor_StringLiteral = 109, // A string literal. + DxcCursor_CharacterLiteral = 110, // A character literal. + DxcCursor_ParenExpr = + 111, // A parenthesized expression, e.g. "(1)". This AST node is only + // formed if full location information is requested. + DxcCursor_UnaryOperator = 112, // This represents the unary-expression's + // (except sizeof and alignof). + DxcCursor_ArraySubscriptExpr = 113, // [C99 6.5.2.1] Array Subscripting. + DxcCursor_BinaryOperator = + 114, // A builtin binary operation expression such as "x + y" or "x <= y". + DxcCursor_CompoundAssignOperator = 115, // Compound assignment such as "+=". + DxcCursor_ConditionalOperator = 116, // The ?: ternary operator. + DxcCursor_CStyleCastExpr = + 117, // An explicit cast in C (C99 6.5.4) or a C-style cast in C++ (C++ + // [expr.cast]), which uses the syntax (Type)expr, eg: (int)f. + DxcCursor_CompoundLiteralExpr = 118, // [C99 6.5.2.5] + DxcCursor_InitListExpr = 119, // Describes an C or C++ initializer list. + DxcCursor_AddrLabelExpr = + 120, // The GNU address of label extension, representing &&label. + DxcCursor_StmtExpr = + 121, // This is the GNU Statement Expression extension: ({int X=4; X;}) + DxcCursor_GenericSelectionExpr = 122, // Represents a C11 generic selection. + + /** \brief Implements the GNU __null extension, which is a name for a null + * pointer constant that has integral type (e.g., int or long) and is the same + * size and alignment as a pointer. + * + * The __null extension is typically only used by system headers, which define + * NULL as __null in C++ rather than using 0 (which is an integer that may not + * match the size of a pointer). + */ + DxcCursor_GNUNullExpr = 123, + DxcCursor_CXXStaticCastExpr = 124, // C++'s static_cast<> expression. + DxcCursor_CXXDynamicCastExpr = 125, // C++'s dynamic_cast<> expression. + DxcCursor_CXXReinterpretCastExpr = + 126, // C++'s reinterpret_cast<> expression. + DxcCursor_CXXConstCastExpr = 127, // C++'s const_cast<> expression. + + /** \brief Represents an explicit C++ type conversion that uses "functional" + * notion (C++ [expr.type.conv]). + * + * Example: + * \code + * x = int(0.5); + * \endcode + */ + DxcCursor_CXXFunctionalCastExpr = 128, + DxcCursor_CXXTypeidExpr = 129, // A C++ typeid expression (C++ [expr.typeid]). + DxcCursor_CXXBoolLiteralExpr = 130, // [C++ 2.13.5] C++ Boolean Literal. + DxcCursor_CXXNullPtrLiteralExpr = 131, // [C++0x 2.14.7] C++ Pointer Literal. + DxcCursor_CXXThisExpr = 132, // Represents the "this" expression in C++ + DxcCursor_CXXThrowExpr = 133, // [C++ 15] C++ Throw Expression, both 'throw' + // and 'throw' assignment-expression. + DxcCursor_CXXNewExpr = 134, // A new expression for memory allocation and + // constructor calls, e.g: "new CXXNewExpr(foo)". + DxcCursor_CXXDeleteExpr = + 135, // A delete expression for memory deallocation and destructor calls, + // e.g. "delete[] pArray". + DxcCursor_UnaryExpr = 136, // A unary expression. + DxcCursor_ObjCStringLiteral = + 137, // An Objective-C string literal i.e. @"foo". + DxcCursor_ObjCEncodeExpr = 138, // An Objective-C \@encode expression. + DxcCursor_ObjCSelectorExpr = 139, // An Objective-C \@selector expression. + DxcCursor_ObjCProtocolExpr = 140, // An Objective-C \@protocol expression. + + /** \brief An Objective-C "bridged" cast expression, which casts between + * Objective-C pointers and C pointers, transferring ownership in the process. + * + * \code + * NSString *str = (__bridge_transfer NSString *)CFCreateString(); + * \endcode + */ + DxcCursor_ObjCBridgedCastExpr = 141, + + /** \brief Represents a C++0x pack expansion that produces a sequence of + * expressions. + * + * A pack expansion expression contains a pattern (which itself is an + * expression) followed by an ellipsis. For example: + * + * \code + * template + * void forward(F f, Types &&...args) { + * f(static_cast(args)...); + * } + * \endcode + */ + DxcCursor_PackExpansionExpr = 142, + + /** \brief Represents an expression that computes the length of a parameter + * pack. + * + * \code + * template + * struct count { + * static const unsigned value = sizeof...(Types); + * }; + * \endcode + */ + DxcCursor_SizeOfPackExpr = 143, + + /* \brief Represents a C++ lambda expression that produces a local function + * object. + * + * \code + * void abssort(float *x, unsigned N) { + * std::sort(x, x + N, + * [](float a, float b) { + * return std::abs(a) < std::abs(b); + * }); + * } + * \endcode + */ + DxcCursor_LambdaExpr = 144, + DxcCursor_ObjCBoolLiteralExpr = 145, // Objective-c Boolean Literal. + DxcCursor_ObjCSelfExpr = + 146, // Represents the "self" expression in a ObjC method. + DxcCursor_LastExpr = DxcCursor_ObjCSelfExpr, + + /* Statements */ + DxcCursor_FirstStmt = 200, + /** + * \brief A statement whose specific kind is not exposed via this + * interface. + * + * Unexposed statements have the same operations as any other kind of + * statement; one can extract their location information, spelling, + * children, etc. However, the specific kind of the statement is not + * reported. + */ + DxcCursor_UnexposedStmt = 200, + + /** \brief A labelled statement in a function. + * + * This cursor kind is used to describe the "start_over:" label statement in + * the following example: + * + * \code + * start_over: + * ++counter; + * \endcode + * + */ + DxcCursor_LabelStmt = 201, + DxcCursor_CompoundStmt = + 202, // A group of statements like { stmt stmt }. This cursor kind is used + // to describe compound statements, e.g. function bodies. + DxcCursor_CaseStmt = 203, // A case statement. + DxcCursor_DefaultStmt = 204, // A default statement. + DxcCursor_IfStmt = 205, // An if statement + DxcCursor_SwitchStmt = 206, // A switch statement. + DxcCursor_WhileStmt = 207, // A while statement. + DxcCursor_DoStmt = 208, // A do statement. + DxcCursor_ForStmt = 209, // A for statement. + DxcCursor_GotoStmt = 210, // A goto statement. + DxcCursor_IndirectGotoStmt = 211, // An indirect goto statement. + DxcCursor_ContinueStmt = 212, // A continue statement. + DxcCursor_BreakStmt = 213, // A break statement. + DxcCursor_ReturnStmt = 214, // A return statement. + DxcCursor_GCCAsmStmt = 215, // A GCC inline assembly statement extension. + DxcCursor_AsmStmt = DxcCursor_GCCAsmStmt, + + DxcCursor_ObjCAtTryStmt = + 216, // Objective-C's overall \@try-\@catch-\@finally statement. + DxcCursor_ObjCAtCatchStmt = 217, // Objective-C's \@catch statement. + DxcCursor_ObjCAtFinallyStmt = 218, // Objective-C's \@finally statement. + DxcCursor_ObjCAtThrowStmt = 219, // Objective-C's \@throw statement. + DxcCursor_ObjCAtSynchronizedStmt = + 220, // Objective-C's \@synchronized statement. + DxcCursor_ObjCAutoreleasePoolStmt = + 221, // Objective-C's autorelease pool statement. + DxcCursor_ObjCForCollectionStmt = 222, // Objective-C's collection statement. + + DxcCursor_CXXCatchStmt = 223, // C++'s catch statement. + DxcCursor_CXXTryStmt = 224, // C++'s try statement. + DxcCursor_CXXForRangeStmt = 225, // C++'s for (* : *) statement. + + DxcCursor_SEHTryStmt = + 226, // Windows Structured Exception Handling's try statement. + DxcCursor_SEHExceptStmt = + 227, // Windows Structured Exception Handling's except statement. + DxcCursor_SEHFinallyStmt = + 228, // Windows Structured Exception Handling's finally statement. + + DxcCursor_MSAsmStmt = 229, // A MS inline assembly statement extension. + DxcCursor_NullStmt = 230, // The null satement ";": C99 6.8.3p3. + DxcCursor_DeclStmt = 231, // Adaptor class for mixing declarations with + // statements and expressions. + DxcCursor_OMPParallelDirective = 232, // OpenMP parallel directive. + DxcCursor_OMPSimdDirective = 233, // OpenMP SIMD directive. + DxcCursor_OMPForDirective = 234, // OpenMP for directive. + DxcCursor_OMPSectionsDirective = 235, // OpenMP sections directive. + DxcCursor_OMPSectionDirective = 236, // OpenMP section directive. + DxcCursor_OMPSingleDirective = 237, // OpenMP single directive. + DxcCursor_OMPParallelForDirective = 238, // OpenMP parallel for directive. + DxcCursor_OMPParallelSectionsDirective = + 239, // OpenMP parallel sections directive. + DxcCursor_OMPTaskDirective = 240, // OpenMP task directive. + DxcCursor_OMPMasterDirective = 241, // OpenMP master directive. + DxcCursor_OMPCriticalDirective = 242, // OpenMP critical directive. + DxcCursor_OMPTaskyieldDirective = 243, // OpenMP taskyield directive. + DxcCursor_OMPBarrierDirective = 244, // OpenMP barrier directive. + DxcCursor_OMPTaskwaitDirective = 245, // OpenMP taskwait directive. + DxcCursor_OMPFlushDirective = 246, // OpenMP flush directive. + DxcCursor_SEHLeaveStmt = + 247, // Windows Structured Exception Handling's leave statement. + DxcCursor_OMPOrderedDirective = 248, // OpenMP ordered directive. + DxcCursor_OMPAtomicDirective = 249, // OpenMP atomic directive. + DxcCursor_OMPForSimdDirective = 250, // OpenMP for SIMD directive. + DxcCursor_OMPParallelForSimdDirective = + 251, // OpenMP parallel for SIMD directive. + DxcCursor_OMPTargetDirective = 252, // OpenMP target directive. + DxcCursor_OMPTeamsDirective = 253, // OpenMP teams directive. + DxcCursor_OMPTaskgroupDirective = 254, // OpenMP taskgroup directive. + DxcCursor_OMPCancellationPointDirective = + 255, // OpenMP cancellation point directive. + DxcCursor_OMPCancelDirective = 256, // OpenMP cancel directive. + DxcCursor_LastStmt = DxcCursor_OMPCancelDirective, + + DxcCursor_TranslationUnit = + 300, // Cursor that represents the translation unit itself. + + /* Attributes */ + DxcCursor_FirstAttr = 400, + /** + * \brief An attribute whose specific kind is not exposed via this + * interface. + */ + DxcCursor_UnexposedAttr = 400, + + DxcCursor_IBActionAttr = 401, + DxcCursor_IBOutletAttr = 402, + DxcCursor_IBOutletCollectionAttr = 403, + DxcCursor_CXXFinalAttr = 404, + DxcCursor_CXXOverrideAttr = 405, + DxcCursor_AnnotateAttr = 406, + DxcCursor_AsmLabelAttr = 407, + DxcCursor_PackedAttr = 408, + DxcCursor_PureAttr = 409, + DxcCursor_ConstAttr = 410, + DxcCursor_NoDuplicateAttr = 411, + DxcCursor_CUDAConstantAttr = 412, + DxcCursor_CUDADeviceAttr = 413, + DxcCursor_CUDAGlobalAttr = 414, + DxcCursor_CUDAHostAttr = 415, + DxcCursor_CUDASharedAttr = 416, + DxcCursor_LastAttr = DxcCursor_CUDASharedAttr, + + /* Preprocessing */ + DxcCursor_PreprocessingDirective = 500, + DxcCursor_MacroDefinition = 501, + DxcCursor_MacroExpansion = 502, + DxcCursor_MacroInstantiation = DxcCursor_MacroExpansion, + DxcCursor_InclusionDirective = 503, + DxcCursor_FirstPreprocessing = DxcCursor_PreprocessingDirective, + DxcCursor_LastPreprocessing = DxcCursor_InclusionDirective, + + /* Extra Declarations */ + /** + * \brief A module import declaration. + */ + DxcCursor_ModuleImportDecl = 600, + DxcCursor_FirstExtraDecl = DxcCursor_ModuleImportDecl, + DxcCursor_LastExtraDecl = DxcCursor_ModuleImportDecl +}; + +enum DxcCursorKindFlags { + DxcCursorKind_None = 0, + DxcCursorKind_Declaration = 0x1, + DxcCursorKind_Reference = 0x2, + DxcCursorKind_Expression = 0x4, + DxcCursorKind_Statement = 0x8, + DxcCursorKind_Attribute = 0x10, + DxcCursorKind_Invalid = 0x20, + DxcCursorKind_TranslationUnit = 0x40, + DxcCursorKind_Preprocessing = 0x80, + DxcCursorKind_Unexposed = 0x100, +}; + +enum DxcCodeCompleteFlags { + DxcCodeCompleteFlags_None = 0, + DxcCodeCompleteFlags_IncludeMacros = 0x1, + DxcCodeCompleteFlags_IncludeCodePatterns = 0x2, + DxcCodeCompleteFlags_IncludeBriefComments = 0x4, +}; + +enum DxcCompletionChunkKind { + DxcCompletionChunk_Optional = 0, + DxcCompletionChunk_TypedText = 1, + DxcCompletionChunk_Text = 2, + DxcCompletionChunk_Placeholder = 3, + DxcCompletionChunk_Informative = 4, + DxcCompletionChunk_CurrentParameter = 5, + DxcCompletionChunk_LeftParen = 6, + DxcCompletionChunk_RightParen = 7, + DxcCompletionChunk_LeftBracket = 8, + DxcCompletionChunk_RightBracket = 9, + DxcCompletionChunk_LeftBrace = 10, + DxcCompletionChunk_RightBrace = 11, + DxcCompletionChunk_LeftAngle = 12, + DxcCompletionChunk_RightAngle = 13, + DxcCompletionChunk_Comma = 14, + DxcCompletionChunk_ResultType = 15, + DxcCompletionChunk_Colon = 16, + DxcCompletionChunk_SemiColon = 17, + DxcCompletionChunk_Equal = 18, + DxcCompletionChunk_HorizontalSpace = 19, + DxcCompletionChunk_VerticalSpace = 20, +}; + +struct IDxcCursor; +struct IDxcDiagnostic; +struct IDxcFile; +struct IDxcInclusion; +struct IDxcIntelliSense; +struct IDxcIndex; +struct IDxcSourceLocation; +struct IDxcSourceRange; +struct IDxcToken; +struct IDxcTranslationUnit; +struct IDxcType; +struct IDxcUnsavedFile; +struct IDxcCodeCompleteResults; +struct IDxcCompletionResult; +struct IDxcCompletionString; + +CROSS_PLATFORM_UUIDOF(IDxcCursor, "1467b985-288d-4d2a-80c1-ef89c42c40bc") +struct IDxcCursor : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetExtent(_Outptr_result_nullonfailure_ IDxcSourceRange **pRange) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetLocation(_Outptr_result_nullonfailure_ IDxcSourceLocation **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetKind(_Out_ DxcCursorKind *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetKindFlags(_Out_ DxcCursorKindFlags *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSemanticParent(_Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetLexicalParent(_Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetCursorType(_Outptr_result_nullonfailure_ IDxcType **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetNumArguments(_Out_ int *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetArgumentAt( + int index, _Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetReferencedCursor(_Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + /// For a cursor that is either a reference to or a declaration of + /// some entity, retrieve a cursor that describes the definition of that + /// entity. Some entities can be declared multiple times + /// within a translation unit, but only one of those declarations can also be + /// a definition. A cursor to the definition of this + /// entity; nullptr if there is no definition in this translation + /// unit. + virtual HRESULT STDMETHODCALLTYPE + GetDefinitionCursor(_Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + FindReferencesInFile(_In_ IDxcFile *file, unsigned skip, unsigned top, + _Out_ unsigned *pResultLength, + _Outptr_result_buffer_maybenull_(*pResultLength) + IDxcCursor ***pResult) = 0; + /// Gets the name for the entity references by the cursor, e.g. foo + /// for an 'int foo' variable. + virtual HRESULT STDMETHODCALLTYPE + GetSpelling(_Outptr_result_maybenull_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE IsEqualTo(_In_ IDxcCursor *other, + _Out_ BOOL *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE IsNull(_Out_ BOOL *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE IsDefinition(_Out_ BOOL *pResult) = 0; + /// Gets the display name for the cursor, including e.g. parameter + /// types for a function. + virtual HRESULT STDMETHODCALLTYPE GetDisplayName(_Out_ BSTR *pResult) = 0; + /// Gets the qualified name for the symbol the cursor refers + /// to. + virtual HRESULT STDMETHODCALLTYPE GetQualifiedName( + BOOL includeTemplateArgs, _Outptr_result_maybenull_ BSTR *pResult) = 0; + /// Gets a name for the cursor, applying the specified formatting + /// flags. + virtual HRESULT STDMETHODCALLTYPE + GetFormattedName(DxcCursorFormatting formatting, + _Outptr_result_maybenull_ BSTR *pResult) = 0; + /// Gets children in pResult up to top elements. + virtual HRESULT STDMETHODCALLTYPE + GetChildren(unsigned skip, unsigned top, _Out_ unsigned *pResultLength, + _Outptr_result_buffer_maybenull_(*pResultLength) + IDxcCursor ***pResult) = 0; + /// Gets the cursor following a location within a compound + /// cursor. + virtual HRESULT STDMETHODCALLTYPE + GetSnappedChild(_In_ IDxcSourceLocation *location, + _Outptr_result_maybenull_ IDxcCursor **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcDiagnostic, "4f76b234-3659-4d33-99b0-3b0db994b564") +struct IDxcDiagnostic : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + FormatDiagnostic(DxcDiagnosticDisplayOptions options, + _Outptr_result_maybenull_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSeverity(_Out_ DxcDiagnosticSeverity *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetLocation(_Outptr_result_nullonfailure_ IDxcSourceLocation **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetSpelling(_Outptr_result_maybenull_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetCategoryText(_Outptr_result_maybenull_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetNumRanges(_Out_ unsigned *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetRangeAt(unsigned index, + _Outptr_result_nullonfailure_ IDxcSourceRange **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetNumFixIts(_Out_ unsigned *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFixItAt(unsigned index, + _Outptr_result_nullonfailure_ IDxcSourceRange **pReplacementRange, + _Outptr_result_maybenull_ LPSTR *pText) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcFile, "bb2fca9e-1478-47ba-b08c-2c502ada4895") +struct IDxcFile : public IUnknown { + /// Gets the file name for this file. + virtual HRESULT STDMETHODCALLTYPE + GetName(_Outptr_result_maybenull_ LPSTR *pResult) = 0; + /// Checks whether this file is equal to the other specified + /// file. + virtual HRESULT STDMETHODCALLTYPE IsEqualTo(_In_ IDxcFile *other, + _Out_ BOOL *pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcInclusion, "0c364d65-df44-4412-888e-4e552fc5e3d6") +struct IDxcInclusion : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetIncludedFile(_Outptr_result_nullonfailure_ IDxcFile **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetStackLength(_Out_ unsigned *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetStackItem(unsigned index, + _Outptr_result_nullonfailure_ IDxcSourceLocation **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcIntelliSense, "b1f99513-46d6-4112-8169-dd0d6053f17d") +struct IDxcIntelliSense : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + CreateIndex(_Outptr_result_nullonfailure_ IDxcIndex **index) = 0; + virtual HRESULT STDMETHODCALLTYPE GetNullLocation( + _Outptr_result_nullonfailure_ IDxcSourceLocation **location) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetNullRange(_Outptr_result_nullonfailure_ IDxcSourceRange **location) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetRange(_In_ IDxcSourceLocation *start, _In_ IDxcSourceLocation *end, + _Outptr_result_nullonfailure_ IDxcSourceRange **location) = 0; + virtual HRESULT STDMETHODCALLTYPE GetDefaultDiagnosticDisplayOptions( + _Out_ DxcDiagnosticDisplayOptions *pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDefaultEditingTUOptions(_Out_ DxcTranslationUnitFlags *pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateUnsavedFile( + _In_ LPCSTR fileName, _In_ LPCSTR contents, unsigned contentLength, + _Outptr_result_nullonfailure_ IDxcUnsavedFile **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcIndex, "937824a0-7f5a-4815-9ba7-7fc0424f4173") +struct IDxcIndex : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + SetGlobalOptions(DxcGlobalOptions options) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetGlobalOptions(_Out_ DxcGlobalOptions *options) = 0; + virtual HRESULT STDMETHODCALLTYPE ParseTranslationUnit( + _In_z_ const char *source_filename, + _In_count_(num_command_line_args) const char *const *command_line_args, + int num_command_line_args, + _In_count_(num_unsaved_files) IDxcUnsavedFile **unsaved_files, + unsigned num_unsaved_files, DxcTranslationUnitFlags options, + _Out_ IDxcTranslationUnit **pTranslationUnit) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcSourceLocation, + "8e7ddf1c-d7d3-4d69-b286-85fccba1e0cf") +struct IDxcSourceLocation : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE IsEqualTo(_In_ IDxcSourceLocation *other, + _Out_ BOOL *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetSpellingLocation( + _Outptr_opt_ IDxcFile **pFile, _Out_opt_ unsigned *pLine, + _Out_opt_ unsigned *pCol, _Out_opt_ unsigned *pOffset) = 0; + virtual HRESULT STDMETHODCALLTYPE IsNull(_Out_ BOOL *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetPresumedLocation(_Outptr_opt_ LPSTR *pFilename, _Out_opt_ unsigned *pLine, + _Out_opt_ unsigned *pCol) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcSourceRange, "f1359b36-a53f-4e81-b514-b6b84122a13f") +struct IDxcSourceRange : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE IsNull(_Out_ BOOL *pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetStart(_Out_ IDxcSourceLocation **pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetEnd(_Out_ IDxcSourceLocation **pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE GetOffsets(_Out_ unsigned *startOffset, + _Out_ unsigned *endOffset) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcToken, "7f90b9ff-a275-4932-97d8-3cfd234482a2") +struct IDxcToken : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetKind(_Out_ DxcTokenKind *pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetLocation(_Out_ IDxcSourceLocation **pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetExtent(_Out_ IDxcSourceRange **pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE GetSpelling(_Out_ LPSTR *pValue) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcTranslationUnit, + "9677dee0-c0e5-46a1-8b40-3db3168be63d") +struct IDxcTranslationUnit : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetCursor(_Out_ IDxcCursor **pCursor) = 0; + virtual HRESULT STDMETHODCALLTYPE + Tokenize(_In_ IDxcSourceRange *range, + _Outptr_result_buffer_maybenull_(*pTokenCount) IDxcToken ***pTokens, + _Out_ unsigned *pTokenCount) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetLocation(_In_ IDxcFile *file, unsigned line, unsigned column, + _Outptr_result_nullonfailure_ IDxcSourceLocation **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetNumDiagnostics(_Out_ unsigned *pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDiagnostic(unsigned index, + _Outptr_result_nullonfailure_ IDxcDiagnostic **pValue) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFile(_In_ const char *name, + _Outptr_result_nullonfailure_ IDxcFile **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetFileName(_Outptr_result_maybenull_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE Reparse(_In_count_(num_unsaved_files) + IDxcUnsavedFile **unsaved_files, + unsigned num_unsaved_files) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetCursorForLocation(_In_ IDxcSourceLocation *location, + _Outptr_result_nullonfailure_ IDxcCursor **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetLocationForOffset( + _In_ IDxcFile *file, unsigned offset, + _Outptr_result_nullonfailure_ IDxcSourceLocation **pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetSkippedRanges( + _In_ IDxcFile *file, _Out_ unsigned *pResultCount, + _Outptr_result_buffer_(*pResultCount) IDxcSourceRange ***pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetDiagnosticDetails(unsigned index, DxcDiagnosticDisplayOptions options, + _Out_ unsigned *errorCode, _Out_ unsigned *errorLine, + _Out_ unsigned *errorColumn, _Out_ BSTR *errorFile, + _Out_ unsigned *errorOffset, _Out_ unsigned *errorLength, + _Out_ BSTR *errorMessage) = 0; + virtual HRESULT STDMETHODCALLTYPE GetInclusionList( + _Out_ unsigned *pResultCount, + _Outptr_result_buffer_(*pResultCount) IDxcInclusion ***pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE CodeCompleteAt( + _In_ const char *fileName, unsigned line, unsigned column, + _In_ IDxcUnsavedFile **pUnsavedFiles, unsigned numUnsavedFiles, + _In_ DxcCodeCompleteFlags options, + _Outptr_result_nullonfailure_ IDxcCodeCompleteResults **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcType, "2ec912fd-b144-4a15-ad0d-1c5439c81e46") +struct IDxcType : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetSpelling(_Outptr_result_z_ LPSTR *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE IsEqualTo(_In_ IDxcType *other, + _Out_ BOOL *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetKind(_Out_ DxcTypeKind *pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcUnsavedFile, "8ec00f98-07d0-4e60-9d7c-5a50b5b0017f") +struct IDxcUnsavedFile : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetFileName(_Outptr_result_z_ LPSTR *pFileName) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetContents(_Outptr_result_z_ LPSTR *pContents) = 0; + virtual HRESULT STDMETHODCALLTYPE GetLength(_Out_ unsigned *pLength) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCodeCompleteResults, + "1E06466A-FD8B-45F3-A78F-8A3F76EBB552") +struct IDxcCodeCompleteResults : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetNumResults(_Out_ unsigned *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetResultAt(unsigned index, + _Outptr_result_nullonfailure_ IDxcCompletionResult **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompletionResult, + "943C0588-22D0-4784-86FC-701F802AC2B6") +struct IDxcCompletionResult : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetCursorKind(_Out_ DxcCursorKind *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetCompletionString( + _Outptr_result_nullonfailure_ IDxcCompletionString **pResult) = 0; +}; + +CROSS_PLATFORM_UUIDOF(IDxcCompletionString, + "06B51E0F-A605-4C69-A110-CD6E14B58EEC") +struct IDxcCompletionString : public IUnknown { + virtual HRESULT STDMETHODCALLTYPE + GetNumCompletionChunks(_Out_ unsigned *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE GetCompletionChunkKind( + unsigned chunkNumber, _Out_ DxcCompletionChunkKind *pResult) = 0; + virtual HRESULT STDMETHODCALLTYPE + GetCompletionChunkText(unsigned chunkNumber, _Out_ LPSTR *pResult) = 0; +}; + +// Fun fact: 'extern' is required because const is by default static in C++, so +// CLSID_DxcIntelliSense is not visible externally (this is OK in C, since const +// is not by default static in C) + +#ifdef _MSC_VER +#define CLSID_SCOPE __declspec(selectany) extern +#else +#define CLSID_SCOPE +#endif + +CLSID_SCOPE const CLSID + CLSID_DxcIntelliSense = {/* 3047833c-d1c0-4b8e-9d40-102878605985 */ + 0x3047833c, + 0xd1c0, + 0x4b8e, + {0x9d, 0x40, 0x10, 0x28, 0x78, 0x60, 0x59, 0x85}}; + +#endif diff --git a/Engine/cpp/ThirdParty/DXC/include/dxc/dxcpix.h b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcpix.h new file mode 100644 index 00000000..f1ab1447 --- /dev/null +++ b/Engine/cpp/ThirdParty/DXC/include/dxc/dxcpix.h @@ -0,0 +1,206 @@ +/////////////////////////////////////////////////////////////////////////////// +// // +// dxcpix.h // +// Copyright (C) Microsoft Corporation. All rights reserved. // +// This file is distributed under the University of Illinois Open Source // +// License. See LICENSE.TXT for details. // +// // +// Provides declarations for the DirectX Compiler API with pix debugging. // +// // +/////////////////////////////////////////////////////////////////////////////// + +#ifndef __DXC_PIX__ +#define __DXC_PIX__ + +#include "dxc/dxcapi.h" +#ifdef _WIN32 +#include "objidl.h" +#endif + +struct __declspec(uuid("199d8c13-d312-4197-a2c1-07a532999727")) IDxcPixType + : public IUnknown { + virtual STDMETHODIMP GetName(_Outptr_result_z_ BSTR *Name) = 0; + + virtual STDMETHODIMP GetSizeInBits(_Out_ DWORD *GetSizeInBits) = 0; + + virtual STDMETHODIMP UnAlias(_COM_Outptr_ IDxcPixType **ppBaseType) = 0; +}; + +struct __declspec(uuid("d9df2c8b-2773-466d-9bc2-d848d8496bf6")) IDxcPixConstType + : public IDxcPixType {}; + +struct __declspec(uuid("7bfca9c0-1ed0-429c-9dc2-c75597d821d2")) + IDxcPixTypedefType : public IDxcPixType {}; + +struct __declspec(uuid("246e1652-ed2a-4ffc-a949-43bf63750ee5")) + IDxcPixScalarType : public IDxcPixType {}; + +struct __declspec(uuid("9ba0d9d3-457b-426f-8019-9f3849982aa2")) IDxcPixArrayType + : public IDxcPixType { + virtual STDMETHODIMP GetNumElements(_Out_ DWORD *ppNumElements) = 0; + + virtual STDMETHODIMP + GetIndexedType(_COM_Outptr_ IDxcPixType **ppElementType) = 0; + + virtual STDMETHODIMP + GetElementType(_COM_Outptr_ IDxcPixType **ppElementType) = 0; +}; + +struct __declspec(uuid("6c707d08-7995-4a84-bae5-e6d8291f3b78")) + IDxcPixStructField0 : public IUnknown { + virtual STDMETHODIMP GetName(_Outptr_result_z_ BSTR *Name) = 0; + + virtual STDMETHODIMP GetType(_COM_Outptr_ IDxcPixType **ppType) = 0; + + virtual STDMETHODIMP GetOffsetInBits(_Out_ DWORD *pOffsetInBits) = 0; +}; + +struct __declspec(uuid("de45597c-5869-4f97-a77b-d6650b9a16cf")) + IDxcPixStructField : public IUnknown { + virtual STDMETHODIMP GetName(_Outptr_result_z_ BSTR *Name) = 0; + + virtual STDMETHODIMP GetType(_COM_Outptr_ IDxcPixType **ppType) = 0; + + virtual STDMETHODIMP GetOffsetInBits(_Out_ DWORD *pOffsetInBits) = 0; + + virtual STDMETHODIMP GetFieldSizeInBits(_Out_ DWORD *pFieldSizeInBits) = 0; +}; + +struct __declspec(uuid("24c08c44-684b-4b1c-b41b-f8772383d074")) + IDxcPixStructType : public IDxcPixType { + virtual STDMETHODIMP GetNumFields(_Out_ DWORD *ppNumFields) = 0; + + virtual STDMETHODIMP + GetFieldByIndex(DWORD dwIndex, _COM_Outptr_ IDxcPixStructField **ppField) = 0; + + virtual STDMETHODIMP + GetFieldByName(_In_ LPCWSTR lpName, + _COM_Outptr_ IDxcPixStructField **ppField) = 0; +}; + +struct __declspec(uuid("7409f40c-dccb-41aa-bb42-1c95bbf7562f")) + IDxcPixStructType2 : public IDxcPixStructType { + virtual STDMETHODIMP GetBaseType(_COM_Outptr_ IDxcPixType **ppType) = 0; +}; + +struct __declspec(uuid("74d522f5-16c4-40cb-867b-4b4149e3db0e")) + IDxcPixDxilStorage : public IUnknown { + virtual STDMETHODIMP + AccessField(_In_ LPCWSTR Name, + _COM_Outptr_ IDxcPixDxilStorage **ppResult) = 0; + + virtual STDMETHODIMP Index(_In_ DWORD Index, + _COM_Outptr_ IDxcPixDxilStorage **ppResult) = 0; + + virtual STDMETHODIMP GetRegisterNumber(_Out_ DWORD *pRegNum) = 0; + + virtual STDMETHODIMP GetIsAlive() = 0; + + virtual STDMETHODIMP GetType(_COM_Outptr_ IDxcPixType **ppType) = 0; +}; + +struct __declspec(uuid("2f954b30-61a7-4348-95b1-2db356a75cde")) IDxcPixVariable + : public IUnknown { + virtual STDMETHODIMP GetName(_Outptr_result_z_ BSTR *Name) = 0; + + virtual STDMETHODIMP GetType(_COM_Outptr_ IDxcPixType **ppType) = 0; + + virtual STDMETHODIMP + GetStorage(_COM_Outptr_ IDxcPixDxilStorage **ppStorage) = 0; +}; + +struct __declspec(uuid("c59d302f-34a2-4fe5-9646-32ce7a52d03f")) + IDxcPixDxilLiveVariables : public IUnknown { + virtual STDMETHODIMP GetCount(_Out_ DWORD *dwSize) = 0; + + virtual STDMETHODIMP + GetVariableByIndex(_In_ DWORD Index, + _COM_Outptr_ IDxcPixVariable **ppVariable) = 0; + + virtual STDMETHODIMP + GetVariableByName(_In_ LPCWSTR Name, + _COM_Outptr_ IDxcPixVariable **ppVariable) = 0; +}; + +struct __declspec(uuid("eb71f85e-8542-44b5-87da-9d76045a1910")) + IDxcPixDxilInstructionOffsets : public IUnknown { + virtual STDMETHODIMP_(DWORD) GetCount() = 0; + + virtual STDMETHODIMP_(DWORD) GetOffsetByIndex(_In_ DWORD Index) = 0; +}; + +struct __declspec(uuid("761c833d-e7b8-4624-80f8-3a3fb4146342")) + IDxcPixDxilSourceLocations : public IUnknown { + virtual STDMETHODIMP_(DWORD) GetCount() = 0; + virtual STDMETHODIMP_(DWORD) GetLineNumberByIndex(_In_ DWORD Index) = 0; + virtual STDMETHODIMP_(DWORD) GetColumnByIndex(_In_ DWORD Index) = 0; + virtual STDMETHODIMP GetFileNameByIndex(_In_ DWORD Index, + _Outptr_result_z_ BSTR *Name) = 0; +}; + +struct __declspec(uuid("b875638e-108a-4d90-a53a-68d63773cb38")) + IDxcPixDxilDebugInfo : public IUnknown { + virtual STDMETHODIMP GetLiveVariablesAt( + _In_ DWORD InstructionOffset, + _COM_Outptr_ IDxcPixDxilLiveVariables **ppLiveVariables) = 0; + + virtual STDMETHODIMP + IsVariableInRegister(_In_ DWORD InstructionOffset, + _In_ const wchar_t *VariableName) = 0; + + virtual STDMETHODIMP + GetFunctionName(_In_ DWORD InstructionOffset, + _Outptr_result_z_ BSTR *ppFunctionName) = 0; + + virtual STDMETHODIMP GetStackDepth(_In_ DWORD InstructionOffset, + _Out_ DWORD *StackDepth) = 0; + + virtual STDMETHODIMP InstructionOffsetsFromSourceLocation( + _In_ const wchar_t *FileName, _In_ DWORD SourceLine, + _In_ DWORD SourceColumn, + _COM_Outptr_ IDxcPixDxilInstructionOffsets **ppOffsets) = 0; + + virtual STDMETHODIMP SourceLocationsFromInstructionOffset( + _In_ DWORD InstructionOffset, + _COM_Outptr_ IDxcPixDxilSourceLocations **ppSourceLocations) = 0; +}; + +struct __declspec(uuid("61b16c95-8799-4ed8-bdb0-3b6c08a141b4")) + IDxcPixCompilationInfo : public IUnknown { + virtual STDMETHODIMP + GetSourceFile(_In_ DWORD SourceFileOrdinal, + _Outptr_result_z_ BSTR *pSourceName, + _Outptr_result_z_ BSTR *pSourceContents) = 0; + virtual STDMETHODIMP GetArguments(_Outptr_result_z_ BSTR *pArguments) = 0; + virtual STDMETHODIMP + GetMacroDefinitions(_Outptr_result_z_ BSTR *pMacroDefinitions) = 0; + virtual STDMETHODIMP + GetEntryPointFile(_Outptr_result_z_ BSTR *pEntryPointFile) = 0; + virtual STDMETHODIMP GetHlslTarget(_Outptr_result_z_ BSTR *pHlslTarget) = 0; + virtual STDMETHODIMP GetEntryPoint(_Outptr_result_z_ BSTR *pEntryPoint) = 0; +}; + +struct __declspec(uuid("9c2a040d-8068-44ec-8c68-8bfef1b43789")) + IDxcPixDxilDebugInfoFactory : public IUnknown { + virtual STDMETHODIMP NewDxcPixDxilDebugInfo( + _COM_Outptr_ IDxcPixDxilDebugInfo **ppDxilDebugInfo) = 0; + virtual STDMETHODIMP NewDxcPixCompilationInfo( + _COM_Outptr_ IDxcPixCompilationInfo **ppCompilationInfo) = 0; +}; + +#ifndef CLSID_SCOPE +#ifdef _MSC_VER +#define CLSID_SCOPE __declspec(selectany) extern +#else +#define CLSID_SCOPE +#endif +#endif // !CLSID_SCOPE + +CLSID_SCOPE const CLSID CLSID_DxcPixDxilDebugger = + {/* a712b622-5af7-4c77-a965-c83ac1a5d8bc */ + 0xa712b622, + 0x5af7, + 0x4c77, + {0xa9, 0x65, 0xc8, 0x3a, 0xc1, 0xa5, 0xd8, 0xbc}}; + +#endif diff --git a/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxcompiler.so b/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxcompiler.so new file mode 100644 index 00000000..13b060b3 Binary files /dev/null and b/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxcompiler.so differ diff --git a/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxil.so b/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxil.so new file mode 100644 index 00000000..a96b1987 Binary files /dev/null and b/Engine/cpp/ThirdParty/DXC/lib/linux-x86_64/libdxil.so differ diff --git a/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxcompiler.dll b/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxcompiler.dll new file mode 100644 index 00000000..748c08a7 Binary files /dev/null and b/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxcompiler.dll differ diff --git a/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxil.dll b/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxil.dll new file mode 100644 index 00000000..b6889fc1 Binary files /dev/null and b/Engine/cpp/ThirdParty/DXC/lib/win-x64/dxil.dll differ diff --git a/Engine/cpp/ThirdParty/bgfx b/Engine/cpp/ThirdParty/bgfx deleted file mode 160000 index 8532b2c4..00000000 --- a/Engine/cpp/ThirdParty/bgfx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8532b2c45d2f4332a9ac9734b85c2ea2253cb8d5 diff --git a/Engine/cpp/ThirdParty/bimg b/Engine/cpp/ThirdParty/bimg deleted file mode 160000 index 9114b47f..00000000 --- a/Engine/cpp/ThirdParty/bimg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9114b47f532ce59cd0c6c9f8932df2c48888d4c1 diff --git a/Engine/cpp/ThirdParty/bx b/Engine/cpp/ThirdParty/bx deleted file mode 160000 index cac72f6c..00000000 --- a/Engine/cpp/ThirdParty/bx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cac72f6cfa0893393ea12692ebfacb4495f8c826 diff --git a/Engine/cpp/ThirdParty/cgltf/CMakeLists.txt b/Engine/cpp/ThirdParty/cgltf/CMakeLists.txt new file mode 100644 index 00000000..6087fcec --- /dev/null +++ b/Engine/cpp/ThirdParty/cgltf/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(cgltf INTERFACE) +target_include_directories(cgltf INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/Engine/cpp/ThirdParty/cgltf/cgltf.h b/Engine/cpp/ThirdParty/cgltf/cgltf.h new file mode 100644 index 00000000..316a11df --- /dev/null +++ b/Engine/cpp/ThirdParty/cgltf/cgltf.h @@ -0,0 +1,7240 @@ +/** + * cgltf - a single-file glTF 2.0 parser written in C99. + * + * Version: 1.15 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_parse(const cgltf_options*, const void*, + * cgltf_size, cgltf_data**)` parses both glTF and GLB data. If + * this function returns `cgltf_result_success`, you have to call + * `cgltf_free()` on the created `cgltf_data*` variable. + * Note that contents of external files for buffers and images are not + * automatically loaded. You'll need to read these files yourself using + * URIs in the `cgltf_data` structure. + * + * `cgltf_options` is the struct passed to `cgltf_parse()` to control + * parts of the parsing process. You can use it to force the file type + * and provide memory allocation as well as file operation callbacks. + * Should be zero-initialized to trigger default behavior. + * + * `cgltf_data` is the struct allocated and filled by `cgltf_parse()`. + * It generally mirrors the glTF format as described by the spec (see + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0). + * + * `void cgltf_free(cgltf_data*)` frees the allocated `cgltf_data` + * variable. + * + * `cgltf_result cgltf_load_buffers(const cgltf_options*, cgltf_data*, + * const char* gltf_path)` can be optionally called to open and read buffer + * files using the `FILE*` APIs. The `gltf_path` argument is the path to + * the original glTF file, which allows the parser to resolve the path to + * buffer files. + * + * `cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, + * cgltf_size size, const char* base64, void** out_data)` decodes + * base64-encoded data content. Used internally by `cgltf_load_buffers()`. + * This is useful when decoding data URIs in images. + * + * `cgltf_result cgltf_parse_file(const cgltf_options* options, const + * char* path, cgltf_data** out_data)` can be used to open the given + * file using `FILE*` APIs and parse the data using `cgltf_parse()`. + * + * `cgltf_result cgltf_validate(cgltf_data*)` can be used to do additional + * checks to make sure the parsed glTF data is valid. + * + * `cgltf_node_transform_local` converts the translation / rotation / scale properties of a node + * into a mat4. + * + * `cgltf_node_transform_world` calls `cgltf_node_transform_local` on every ancestor in order + * to compute the root-to-node transformation. + * + * `cgltf_accessor_unpack_floats` reads in the data from an accessor, applies sparse data (if any), + * and converts them to floating point. Assumes that `cgltf_load_buffers` has already been called. + * By passing null for the output pointer, users can find out how many floats are required in the + * output buffer. + * + * `cgltf_accessor_unpack_indices` reads in the index data from an accessor. Assumes that + * `cgltf_load_buffers` has already been called. By passing null for the output pointer, users can + * find out how many indices are required in the output buffer. Returns 0 if the accessor is + * sparse or if the output component size is less than the accessor's component size. + * + * `cgltf_num_components` is a tiny utility that tells you the dimensionality of + * a certain accessor type. This can be used before `cgltf_accessor_unpack_floats` to help allocate + * the necessary amount of memory. `cgltf_component_size` and `cgltf_calc_size` exist for + * similar purposes. + * + * `cgltf_accessor_read_float` reads a certain element from a non-sparse accessor and converts it to + * floating point, assuming that `cgltf_load_buffers` has already been called. The passed-in element + * size is the number of floats in the output buffer, which should be in the range [1, 16]. Returns + * false if the passed-in element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_uint` is similar to its floating-point counterpart, but limited to reading + * vector types and does not support matrix types. The passed-in element size is the number of uints + * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in + * element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t + * and only works with single-component data types. + * + * `cgltf_copy_extras_json` allows users to retrieve the "extras" data that can be attached to many + * glTF objects (which can be arbitrary JSON data). This is a legacy function, consider using + * cgltf_extras::data directly instead. You can parse this data using your own JSON parser + * or, if you've included the cgltf implementation using the integrated JSMN JSON parser. + */ +#ifndef CGLTF_H_INCLUDED__ +#define CGLTF_H_INCLUDED__ + +#include +#include /* For uint8_t, uint32_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef size_t cgltf_size; +typedef long long int cgltf_ssize; +typedef float cgltf_float; +typedef int cgltf_int; +typedef unsigned int cgltf_uint; +typedef int cgltf_bool; + +typedef enum cgltf_file_type +{ + cgltf_file_type_invalid, + cgltf_file_type_gltf, + cgltf_file_type_glb, + cgltf_file_type_max_enum +} cgltf_file_type; + +typedef enum cgltf_result +{ + cgltf_result_success, + cgltf_result_data_too_short, + cgltf_result_unknown_format, + cgltf_result_invalid_json, + cgltf_result_invalid_gltf, + cgltf_result_invalid_options, + cgltf_result_file_not_found, + cgltf_result_io_error, + cgltf_result_out_of_memory, + cgltf_result_legacy_gltf, + cgltf_result_max_enum +} cgltf_result; + +typedef struct cgltf_memory_options +{ + void* (*alloc_func)(void* user, cgltf_size size); + void (*free_func) (void* user, void* ptr); + void* user_data; +} cgltf_memory_options; + +typedef struct cgltf_file_options +{ + cgltf_result(*read)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data); + void (*release)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size); + void* user_data; +} cgltf_file_options; + +typedef struct cgltf_options +{ + cgltf_file_type type; /* invalid == auto detect */ + cgltf_size json_token_count; /* 0 == auto */ + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_options; + +typedef enum cgltf_buffer_view_type +{ + cgltf_buffer_view_type_invalid, + cgltf_buffer_view_type_indices, + cgltf_buffer_view_type_vertices, + cgltf_buffer_view_type_max_enum +} cgltf_buffer_view_type; + +typedef enum cgltf_attribute_type +{ + cgltf_attribute_type_invalid, + cgltf_attribute_type_position, + cgltf_attribute_type_normal, + cgltf_attribute_type_tangent, + cgltf_attribute_type_texcoord, + cgltf_attribute_type_color, + cgltf_attribute_type_joints, + cgltf_attribute_type_weights, + cgltf_attribute_type_custom, + cgltf_attribute_type_max_enum +} cgltf_attribute_type; + +typedef enum cgltf_component_type +{ + cgltf_component_type_invalid, + cgltf_component_type_r_8, /* BYTE */ + cgltf_component_type_r_8u, /* UNSIGNED_BYTE */ + cgltf_component_type_r_16, /* SHORT */ + cgltf_component_type_r_16u, /* UNSIGNED_SHORT */ + cgltf_component_type_r_32u, /* UNSIGNED_INT */ + cgltf_component_type_r_32f, /* FLOAT */ + cgltf_component_type_max_enum +} cgltf_component_type; + +typedef enum cgltf_type +{ + cgltf_type_invalid, + cgltf_type_scalar, + cgltf_type_vec2, + cgltf_type_vec3, + cgltf_type_vec4, + cgltf_type_mat2, + cgltf_type_mat3, + cgltf_type_mat4, + cgltf_type_max_enum +} cgltf_type; + +typedef enum cgltf_primitive_type +{ + cgltf_primitive_type_invalid, + cgltf_primitive_type_points, + cgltf_primitive_type_lines, + cgltf_primitive_type_line_loop, + cgltf_primitive_type_line_strip, + cgltf_primitive_type_triangles, + cgltf_primitive_type_triangle_strip, + cgltf_primitive_type_triangle_fan, + cgltf_primitive_type_max_enum +} cgltf_primitive_type; + +typedef enum cgltf_alpha_mode +{ + cgltf_alpha_mode_opaque, + cgltf_alpha_mode_mask, + cgltf_alpha_mode_blend, + cgltf_alpha_mode_max_enum +} cgltf_alpha_mode; + +typedef enum cgltf_animation_path_type { + cgltf_animation_path_type_invalid, + cgltf_animation_path_type_translation, + cgltf_animation_path_type_rotation, + cgltf_animation_path_type_scale, + cgltf_animation_path_type_weights, + cgltf_animation_path_type_max_enum +} cgltf_animation_path_type; + +typedef enum cgltf_interpolation_type { + cgltf_interpolation_type_linear, + cgltf_interpolation_type_step, + cgltf_interpolation_type_cubic_spline, + cgltf_interpolation_type_max_enum +} cgltf_interpolation_type; + +typedef enum cgltf_camera_type { + cgltf_camera_type_invalid, + cgltf_camera_type_perspective, + cgltf_camera_type_orthographic, + cgltf_camera_type_max_enum +} cgltf_camera_type; + +typedef enum cgltf_light_type { + cgltf_light_type_invalid, + cgltf_light_type_directional, + cgltf_light_type_point, + cgltf_light_type_spot, + cgltf_light_type_max_enum +} cgltf_light_type; + +typedef enum cgltf_data_free_method { + cgltf_data_free_method_none, + cgltf_data_free_method_file_release, + cgltf_data_free_method_memory_free, + cgltf_data_free_method_max_enum +} cgltf_data_free_method; + +typedef struct cgltf_extras { + cgltf_size start_offset; /* this field is deprecated and will be removed in the future; use data instead */ + cgltf_size end_offset; /* this field is deprecated and will be removed in the future; use data instead */ + + char* data; +} cgltf_extras; + +typedef struct cgltf_extension { + char* name; + char* data; +} cgltf_extension; + +typedef struct cgltf_buffer +{ + char* name; + cgltf_size size; + char* uri; + void* data; /* loaded by cgltf_load_buffers */ + cgltf_data_free_method data_free_method; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer; + +typedef enum cgltf_meshopt_compression_mode { + cgltf_meshopt_compression_mode_invalid, + cgltf_meshopt_compression_mode_attributes, + cgltf_meshopt_compression_mode_triangles, + cgltf_meshopt_compression_mode_indices, + cgltf_meshopt_compression_mode_max_enum +} cgltf_meshopt_compression_mode; + +typedef enum cgltf_meshopt_compression_filter { + cgltf_meshopt_compression_filter_none, + cgltf_meshopt_compression_filter_octahedral, + cgltf_meshopt_compression_filter_quaternion, + cgltf_meshopt_compression_filter_exponential, + cgltf_meshopt_compression_filter_color, + cgltf_meshopt_compression_filter_max_enum +} cgltf_meshopt_compression_filter; + +typedef struct cgltf_meshopt_compression +{ + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; + cgltf_size count; + cgltf_meshopt_compression_mode mode; + cgltf_meshopt_compression_filter filter; + cgltf_bool is_khr; +} cgltf_meshopt_compression; + +typedef struct cgltf_buffer_view +{ + char *name; + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; /* 0 == automatically determined by accessor */ + cgltf_buffer_view_type type; + void* data; /* overrides buffer->data if present, filled by extensions */ + cgltf_bool has_meshopt_compression; + cgltf_meshopt_compression meshopt_compression; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer_view; + +typedef struct cgltf_accessor_sparse +{ + cgltf_size count; + cgltf_buffer_view* indices_buffer_view; + cgltf_size indices_byte_offset; + cgltf_component_type indices_component_type; + cgltf_buffer_view* values_buffer_view; + cgltf_size values_byte_offset; +} cgltf_accessor_sparse; + +typedef struct cgltf_accessor +{ + char* name; + cgltf_component_type component_type; + cgltf_bool normalized; + cgltf_type type; + cgltf_size offset; + cgltf_size count; + cgltf_size stride; + cgltf_buffer_view* buffer_view; + cgltf_bool has_min; + cgltf_float min[16]; + cgltf_bool has_max; + cgltf_float max[16]; + cgltf_bool is_sparse; + cgltf_accessor_sparse sparse; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_accessor; + +typedef struct cgltf_attribute +{ + char* name; + cgltf_attribute_type type; + cgltf_int index; + cgltf_accessor* data; +} cgltf_attribute; + +typedef struct cgltf_image +{ + char* name; + char* uri; + cgltf_buffer_view* buffer_view; + char* mime_type; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_image; + +typedef enum cgltf_filter_type { + cgltf_filter_type_undefined = 0, + cgltf_filter_type_nearest = 9728, + cgltf_filter_type_linear = 9729, + cgltf_filter_type_nearest_mipmap_nearest = 9984, + cgltf_filter_type_linear_mipmap_nearest = 9985, + cgltf_filter_type_nearest_mipmap_linear = 9986, + cgltf_filter_type_linear_mipmap_linear = 9987 +} cgltf_filter_type; + +typedef enum cgltf_wrap_mode { + cgltf_wrap_mode_clamp_to_edge = 33071, + cgltf_wrap_mode_mirrored_repeat = 33648, + cgltf_wrap_mode_repeat = 10497 +} cgltf_wrap_mode; + +typedef struct cgltf_sampler +{ + char* name; + cgltf_filter_type mag_filter; + cgltf_filter_type min_filter; + cgltf_wrap_mode wrap_s; + cgltf_wrap_mode wrap_t; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_sampler; + +typedef struct cgltf_texture +{ + char* name; + cgltf_image* image; + cgltf_sampler* sampler; + cgltf_bool has_basisu; + cgltf_image* basisu_image; + cgltf_bool has_webp; + cgltf_image* webp_image; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_texture; + +typedef struct cgltf_texture_transform +{ + cgltf_float offset[2]; + cgltf_float rotation; + cgltf_float scale[2]; + cgltf_bool has_texcoord; + cgltf_int texcoord; +} cgltf_texture_transform; + +typedef struct cgltf_texture_view +{ + cgltf_texture* texture; + cgltf_int texcoord; + cgltf_float scale; /* equivalent to strength for occlusion_texture */ + cgltf_bool has_transform; + cgltf_texture_transform transform; +} cgltf_texture_view; + +typedef struct cgltf_pbr_metallic_roughness +{ + cgltf_texture_view base_color_texture; + cgltf_texture_view metallic_roughness_texture; + + cgltf_float base_color_factor[4]; + cgltf_float metallic_factor; + cgltf_float roughness_factor; +} cgltf_pbr_metallic_roughness; + +typedef struct cgltf_pbr_specular_glossiness +{ + cgltf_texture_view diffuse_texture; + cgltf_texture_view specular_glossiness_texture; + + cgltf_float diffuse_factor[4]; + cgltf_float specular_factor[3]; + cgltf_float glossiness_factor; +} cgltf_pbr_specular_glossiness; + +typedef struct cgltf_clearcoat +{ + cgltf_texture_view clearcoat_texture; + cgltf_texture_view clearcoat_roughness_texture; + cgltf_texture_view clearcoat_normal_texture; + + cgltf_float clearcoat_factor; + cgltf_float clearcoat_roughness_factor; +} cgltf_clearcoat; + +typedef struct cgltf_transmission +{ + cgltf_texture_view transmission_texture; + cgltf_float transmission_factor; +} cgltf_transmission; + +typedef struct cgltf_ior +{ + cgltf_float ior; +} cgltf_ior; + +typedef struct cgltf_specular +{ + cgltf_texture_view specular_texture; + cgltf_texture_view specular_color_texture; + cgltf_float specular_color_factor[3]; + cgltf_float specular_factor; +} cgltf_specular; + +typedef struct cgltf_volume +{ + cgltf_texture_view thickness_texture; + cgltf_float thickness_factor; + cgltf_float attenuation_color[3]; + cgltf_float attenuation_distance; +} cgltf_volume; + +typedef struct cgltf_sheen +{ + cgltf_texture_view sheen_color_texture; + cgltf_float sheen_color_factor[3]; + cgltf_texture_view sheen_roughness_texture; + cgltf_float sheen_roughness_factor; +} cgltf_sheen; + +typedef struct cgltf_emissive_strength +{ + cgltf_float emissive_strength; +} cgltf_emissive_strength; + +typedef struct cgltf_iridescence +{ + cgltf_float iridescence_factor; + cgltf_texture_view iridescence_texture; + cgltf_float iridescence_ior; + cgltf_float iridescence_thickness_min; + cgltf_float iridescence_thickness_max; + cgltf_texture_view iridescence_thickness_texture; +} cgltf_iridescence; + +typedef struct cgltf_diffuse_transmission +{ + cgltf_texture_view diffuse_transmission_texture; + cgltf_float diffuse_transmission_factor; + cgltf_float diffuse_transmission_color_factor[3]; + cgltf_texture_view diffuse_transmission_color_texture; +} cgltf_diffuse_transmission; + +typedef struct cgltf_anisotropy +{ + cgltf_float anisotropy_strength; + cgltf_float anisotropy_rotation; + cgltf_texture_view anisotropy_texture; +} cgltf_anisotropy; + +typedef struct cgltf_dispersion +{ + cgltf_float dispersion; +} cgltf_dispersion; + +typedef struct cgltf_material +{ + char* name; + cgltf_bool has_pbr_metallic_roughness; + cgltf_bool has_pbr_specular_glossiness; + cgltf_bool has_clearcoat; + cgltf_bool has_transmission; + cgltf_bool has_volume; + cgltf_bool has_ior; + cgltf_bool has_specular; + cgltf_bool has_sheen; + cgltf_bool has_emissive_strength; + cgltf_bool has_iridescence; + cgltf_bool has_diffuse_transmission; + cgltf_bool has_anisotropy; + cgltf_bool has_dispersion; + cgltf_pbr_metallic_roughness pbr_metallic_roughness; + cgltf_pbr_specular_glossiness pbr_specular_glossiness; + cgltf_clearcoat clearcoat; + cgltf_ior ior; + cgltf_specular specular; + cgltf_sheen sheen; + cgltf_transmission transmission; + cgltf_volume volume; + cgltf_emissive_strength emissive_strength; + cgltf_iridescence iridescence; + cgltf_diffuse_transmission diffuse_transmission; + cgltf_anisotropy anisotropy; + cgltf_dispersion dispersion; + cgltf_texture_view normal_texture; + cgltf_texture_view occlusion_texture; + cgltf_texture_view emissive_texture; + cgltf_float emissive_factor[3]; + cgltf_alpha_mode alpha_mode; + cgltf_float alpha_cutoff; + cgltf_bool double_sided; + cgltf_bool unlit; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_material; + +typedef struct cgltf_material_mapping +{ + cgltf_size variant; + cgltf_material* material; + cgltf_extras extras; +} cgltf_material_mapping; + +typedef struct cgltf_morph_target { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_morph_target; + +typedef struct cgltf_draco_mesh_compression { + cgltf_buffer_view* buffer_view; + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_draco_mesh_compression; + +typedef struct cgltf_mesh_gpu_instancing { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_mesh_gpu_instancing; + +typedef struct cgltf_primitive { + cgltf_primitive_type type; + cgltf_accessor* indices; + cgltf_material* material; + cgltf_attribute* attributes; + cgltf_size attributes_count; + cgltf_morph_target* targets; + cgltf_size targets_count; + cgltf_extras extras; + cgltf_bool has_draco_mesh_compression; + cgltf_draco_mesh_compression draco_mesh_compression; + cgltf_material_mapping* mappings; + cgltf_size mappings_count; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_primitive; + +typedef struct cgltf_mesh { + char* name; + cgltf_primitive* primitives; + cgltf_size primitives_count; + cgltf_float* weights; + cgltf_size weights_count; + char** target_names; + cgltf_size target_names_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_mesh; + +typedef struct cgltf_node cgltf_node; + +typedef struct cgltf_skin { + char* name; + cgltf_node** joints; + cgltf_size joints_count; + cgltf_node* skeleton; + cgltf_accessor* inverse_bind_matrices; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_skin; + +typedef struct cgltf_camera_perspective { + cgltf_bool has_aspect_ratio; + cgltf_float aspect_ratio; + cgltf_float yfov; + cgltf_bool has_zfar; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_perspective; + +typedef struct cgltf_camera_orthographic { + cgltf_float xmag; + cgltf_float ymag; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_orthographic; + +typedef struct cgltf_camera { + char* name; + cgltf_camera_type type; + union { + cgltf_camera_perspective perspective; + cgltf_camera_orthographic orthographic; + } data; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_camera; + +typedef struct cgltf_light { + char* name; + cgltf_float color[3]; + cgltf_float intensity; + cgltf_light_type type; + cgltf_float range; + cgltf_float spot_inner_cone_angle; + cgltf_float spot_outer_cone_angle; + cgltf_extras extras; +} cgltf_light; + +struct cgltf_node { + char* name; + cgltf_node* parent; + cgltf_node** children; + cgltf_size children_count; + cgltf_skin* skin; + cgltf_mesh* mesh; + cgltf_camera* camera; + cgltf_light* light; + cgltf_float* weights; + cgltf_size weights_count; + cgltf_bool has_translation; + cgltf_bool has_rotation; + cgltf_bool has_scale; + cgltf_bool has_matrix; + cgltf_float translation[3]; + cgltf_float rotation[4]; + cgltf_float scale[3]; + cgltf_float matrix[16]; + cgltf_extras extras; + cgltf_bool has_mesh_gpu_instancing; + cgltf_mesh_gpu_instancing mesh_gpu_instancing; + cgltf_size extensions_count; + cgltf_extension* extensions; +}; + +typedef struct cgltf_scene { + char* name; + cgltf_node** nodes; + cgltf_size nodes_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_scene; + +typedef struct cgltf_animation_sampler { + cgltf_accessor* input; + cgltf_accessor* output; + cgltf_interpolation_type interpolation; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_sampler; + +typedef struct cgltf_animation_channel { + cgltf_animation_sampler* sampler; + cgltf_node* target_node; + cgltf_animation_path_type target_path; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_channel; + +typedef struct cgltf_animation { + char* name; + cgltf_animation_sampler* samplers; + cgltf_size samplers_count; + cgltf_animation_channel* channels; + cgltf_size channels_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation; + +typedef struct cgltf_material_variant +{ + char* name; + cgltf_extras extras; +} cgltf_material_variant; + +typedef struct cgltf_asset { + char* copyright; + char* generator; + char* version; + char* min_version; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_asset; + +typedef struct cgltf_data +{ + cgltf_file_type file_type; + void* file_data; + cgltf_size file_size; + + cgltf_asset asset; + + cgltf_mesh* meshes; + cgltf_size meshes_count; + + cgltf_material* materials; + cgltf_size materials_count; + + cgltf_accessor* accessors; + cgltf_size accessors_count; + + cgltf_buffer_view* buffer_views; + cgltf_size buffer_views_count; + + cgltf_buffer* buffers; + cgltf_size buffers_count; + + cgltf_image* images; + cgltf_size images_count; + + cgltf_texture* textures; + cgltf_size textures_count; + + cgltf_sampler* samplers; + cgltf_size samplers_count; + + cgltf_skin* skins; + cgltf_size skins_count; + + cgltf_camera* cameras; + cgltf_size cameras_count; + + cgltf_light* lights; + cgltf_size lights_count; + + cgltf_node* nodes; + cgltf_size nodes_count; + + cgltf_scene* scenes; + cgltf_size scenes_count; + + cgltf_scene* scene; + + cgltf_animation* animations; + cgltf_size animations_count; + + cgltf_material_variant* variants; + cgltf_size variants_count; + + cgltf_extras extras; + + cgltf_size data_extensions_count; + cgltf_extension* data_extensions; + + char** extensions_used; + cgltf_size extensions_used_count; + + char** extensions_required; + cgltf_size extensions_required_count; + + const char* json; + cgltf_size json_size; + + const void* bin; + cgltf_size bin_size; + + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_data; + +cgltf_result cgltf_parse( + const cgltf_options* options, + const void* data, + cgltf_size size, + cgltf_data** out_data); + +cgltf_result cgltf_parse_file( + const cgltf_options* options, + const char* path, + cgltf_data** out_data); + +cgltf_result cgltf_load_buffers( + const cgltf_options* options, + cgltf_data* data, + const char* gltf_path); + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data); + +cgltf_size cgltf_decode_string(char* string); +cgltf_size cgltf_decode_uri(char* uri); + +cgltf_result cgltf_validate(cgltf_data* data); + +void cgltf_free(cgltf_data* data); + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix); +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix); + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view); + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index); + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size); +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size); +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index); + +cgltf_size cgltf_num_components(cgltf_type type); +cgltf_size cgltf_component_size(cgltf_component_type component_type); +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type); + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count); +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count); + +/* this function is deprecated and will be removed in the future; use cgltf_extras::data instead */ +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size); + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object); +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object); +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object); +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object); +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object); +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object); +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object); +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object); +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object); +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object); +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object); +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object); +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object); +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object); +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object); +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_IMPLEMENTATION +#endif + +#ifdef CGLTF_IMPLEMENTATION + +#include /* For assert */ +#include /* For strncpy */ +#include /* For fopen */ +#include /* For UINT_MAX etc */ +#include /* For FLT_MAX */ + +#if !defined(CGLTF_MALLOC) || !defined(CGLTF_FREE) || !defined(CGLTF_ATOI) || !defined(CGLTF_ATOF) || !defined(CGLTF_ATOLL) +#include /* For malloc, free, atoi, atof */ +#endif + +/* JSMN_PARENT_LINKS is necessary to make parsing large structures linear in input size */ +#define JSMN_PARENT_LINKS + +/* JSMN_STRICT is necessary to reject invalid JSON documents */ +#define JSMN_STRICT + +/* + * -- jsmn.h start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; +typedef struct { + jsmntype_t type; + ptrdiff_t start; + ptrdiff_t end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; +typedef struct { + size_t pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; +static void jsmn_init(jsmn_parser *parser); +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, size_t num_tokens); +/* + * -- jsmn.h end -- + */ + + +#ifndef CGLTF_CONSTS +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; +#define CGLTF_CONSTS +#endif + +#ifndef CGLTF_MALLOC +#define CGLTF_MALLOC(size) malloc(size) +#endif +#ifndef CGLTF_FREE +#define CGLTF_FREE(ptr) free(ptr) +#endif +#ifndef CGLTF_ATOI +#define CGLTF_ATOI(str) atoi(str) +#endif +#ifndef CGLTF_ATOF +#define CGLTF_ATOF(str) atof(str) +#endif +#ifndef CGLTF_ATOLL +#define CGLTF_ATOLL(str) atoll(str) +#endif +#ifndef CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_VALIDATE_ENABLE_ASSERTS 0 +#endif + +static void* cgltf_default_alloc(void* user, cgltf_size size) +{ + (void)user; + return CGLTF_MALLOC(size); +} + +static void cgltf_default_free(void* user, void* ptr) +{ + (void)user; + CGLTF_FREE(ptr); +} + +static void* cgltf_calloc(cgltf_options* options, size_t element_size, cgltf_size count) +{ + if (SIZE_MAX / element_size < count) + { + return NULL; + } + void* result = options->memory.alloc_func(options->memory.user_data, element_size * count); + if (!result) + { + return NULL; + } + memset(result, 0, element_size * count); + return result; +} + +static cgltf_result cgltf_default_file_read(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data) +{ + (void)file_options; + void* (*memory_alloc)(void*, cgltf_size) = memory_options->alloc_func ? memory_options->alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + + FILE* file = fopen(path, "rb"); + if (!file) + { + return cgltf_result_file_not_found; + } + + cgltf_size file_size = size ? *size : 0; + + if (file_size == 0) + { + fseek(file, 0, SEEK_END); + +#ifdef _MSC_VER + __int64 length = _ftelli64(file); +#else + long length = ftell(file); +#endif + + if (length < 0) + { + fclose(file); + return cgltf_result_io_error; + } + + fseek(file, 0, SEEK_SET); + file_size = (cgltf_size)length; + } + + char* file_data = (char*)memory_alloc(memory_options->user_data, file_size); + if (!file_data) + { + fclose(file); + return cgltf_result_out_of_memory; + } + + cgltf_size read_size = fread(file_data, 1, file_size, file); + + fclose(file); + + if (read_size != file_size) + { + memory_free(memory_options->user_data, file_data); + return cgltf_result_io_error; + } + + if (size) + { + *size = file_size; + } + if (data) + { + *data = file_data; + } + + return cgltf_result_success; +} + +static void cgltf_default_file_release(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size) +{ + (void)file_options; + (void)size; + void (*memfree)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + memfree(memory_options->user_data, data); +} + +static cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data); + +cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_size size, cgltf_data** out_data) +{ + if (size < GlbHeaderSize) + { + return cgltf_result_data_too_short; + } + + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_options fixed_options = *options; + if (fixed_options.memory.alloc_func == NULL) + { + fixed_options.memory.alloc_func = &cgltf_default_alloc; + } + if (fixed_options.memory.free_func == NULL) + { + fixed_options.memory.free_func = &cgltf_default_free; + } + + uint32_t tmp; + // Magic + memcpy(&tmp, data, 4); + if (tmp != GlbMagic) + { + if (fixed_options.type == cgltf_file_type_invalid) + { + fixed_options.type = cgltf_file_type_gltf; + } + else if (fixed_options.type == cgltf_file_type_glb) + { + return cgltf_result_unknown_format; + } + } + + if (fixed_options.type == cgltf_file_type_gltf) + { + cgltf_result json_result = cgltf_parse_json(&fixed_options, (const uint8_t*)data, size, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_gltf; + + return cgltf_result_success; + } + + const uint8_t* ptr = (const uint8_t*)data; + // Version + memcpy(&tmp, ptr + 4, 4); + uint32_t version = tmp; + if (version != GlbVersion) + { + return version < GlbVersion ? cgltf_result_legacy_gltf : cgltf_result_unknown_format; + } + + // Total length + memcpy(&tmp, ptr + 8, 4); + if (tmp > size) + { + return cgltf_result_data_too_short; + } + + const uint8_t* json_chunk = ptr + GlbHeaderSize; + + if (GlbHeaderSize + GlbChunkHeaderSize > size) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: length + uint32_t json_length; + memcpy(&json_length, json_chunk, 4); + if (json_length > size - GlbHeaderSize - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: magic + memcpy(&tmp, json_chunk + 4, 4); + if (tmp != GlbMagicJsonChunk) + { + return cgltf_result_unknown_format; + } + + json_chunk += GlbChunkHeaderSize; + + const void* bin = NULL; + cgltf_size bin_size = 0; + + if (GlbChunkHeaderSize <= size - GlbHeaderSize - GlbChunkHeaderSize - json_length) + { + // We can read another chunk + const uint8_t* bin_chunk = json_chunk + json_length; + + // Bin chunk: length + uint32_t bin_length; + memcpy(&bin_length, bin_chunk, 4); + if (bin_length > size - GlbHeaderSize - GlbChunkHeaderSize - json_length - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // Bin chunk: magic + memcpy(&tmp, bin_chunk + 4, 4); + if (tmp != GlbMagicBinChunk) + { + return cgltf_result_unknown_format; + } + + bin_chunk += GlbChunkHeaderSize; + + bin = bin_chunk; + bin_size = bin_length; + } + + cgltf_result json_result = cgltf_parse_json(&fixed_options, json_chunk, json_length, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_glb; + (*out_data)->bin = bin; + (*out_data)->bin_size = bin_size; + + return cgltf_result_success; +} + +cgltf_result cgltf_parse_file(const cgltf_options* options, const char* path, cgltf_data** out_data) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = options->file.release ? options->file.release : cgltf_default_file_release; + + void* file_data = NULL; + cgltf_size file_size = 0; + cgltf_result result = file_read(&options->memory, &options->file, path, &file_size, &file_data); + if (result != cgltf_result_success) + { + return result; + } + + result = cgltf_parse(options, file_data, file_size, out_data); + + if (result != cgltf_result_success) + { + file_release(&options->memory, &options->file, file_data, file_size); + return result; + } + + (*out_data)->file_data = file_data; + (*out_data)->file_size = file_size; + + return cgltf_result_success; +} + +static void cgltf_combine_paths(char* path, const char* base, const char* uri) +{ + const char* s0 = strrchr(base, '/'); + const char* s1 = strrchr(base, '\\'); + const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1; + + if (slash) + { + size_t prefix = slash - base + 1; + + strncpy(path, base, prefix); + strcpy(path + prefix, uri); + } + else + { + strcpy(path, uri); + } +} + +static cgltf_result cgltf_load_buffer_file(const cgltf_options* options, cgltf_size size, const char* uri, const char* gltf_path, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + + char* path = (char*)memory_alloc(options->memory.user_data, strlen(uri) + strlen(gltf_path) + 1); + if (!path) + { + return cgltf_result_out_of_memory; + } + + cgltf_combine_paths(path, gltf_path, uri); + + // after combining, the tail of the resulting path is a uri; decode_uri converts it into path + cgltf_decode_uri(path + strlen(path) - strlen(uri)); + + void* file_data = NULL; + cgltf_result result = file_read(&options->memory, &options->file, path, &size, &file_data); + + memory_free(options->memory.user_data, path); + + *out_data = (result == cgltf_result_success) ? file_data : NULL; + + return result; +} + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + + unsigned char* data = (unsigned char*)memory_alloc(options->memory.user_data, size); + if (!data) + { + return cgltf_result_out_of_memory; + } + + unsigned int buffer = 0; + unsigned int buffer_bits = 0; + + for (cgltf_size i = 0; i < size; ++i) + { + while (buffer_bits < 8) + { + char ch = *base64++; + + int index = + (unsigned)(ch - 'A') < 26 ? (ch - 'A') : + (unsigned)(ch - 'a') < 26 ? (ch - 'a') + 26 : + (unsigned)(ch - '0') < 10 ? (ch - '0') + 52 : + ch == '+' ? 62 : + ch == '/' ? 63 : + -1; + + if (index < 0) + { + memory_free(options->memory.user_data, data); + return cgltf_result_io_error; + } + + buffer = (buffer << 6) | index; + buffer_bits += 6; + } + + data[i] = (unsigned char)(buffer >> (buffer_bits - 8)); + buffer_bits -= 8; + } + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_unhex(char ch) +{ + return + (unsigned)(ch - '0') < 10 ? (ch - '0') : + (unsigned)(ch - 'A') < 6 ? (ch - 'A') + 10 : + (unsigned)(ch - 'a') < 6 ? (ch - 'a') + 10 : + -1; +} + +cgltf_size cgltf_decode_string(char* string) +{ + char* read = string + strcspn(string, "\\"); + if (*read == 0) + { + return read - string; + } + char* write = string; + char* last = string; + + for (;;) + { + // Copy characters since last escaped sequence + cgltf_size written = read - last; + memmove(write, last, written); + write += written; + + if (*read++ == 0) + { + break; + } + + // jsmn already checked that all escape sequences are valid + switch (*read++) + { + case '\"': *write++ = '\"'; break; + case '/': *write++ = '/'; break; + case '\\': *write++ = '\\'; break; + case 'b': *write++ = '\b'; break; + case 'f': *write++ = '\f'; break; + case 'r': *write++ = '\r'; break; + case 'n': *write++ = '\n'; break; + case 't': *write++ = '\t'; break; + case 'u': + { + // UCS-2 codepoint \uXXXX to UTF-8 + int character = 0; + for (cgltf_size i = 0; i < 4; ++i) + { + character = (character << 4) + cgltf_unhex(*read++); + } + + if (character <= 0x7F) + { + *write++ = character & 0xFF; + } + else if (character <= 0x7FF) + { + *write++ = 0xC0 | ((character >> 6) & 0xFF); + *write++ = 0x80 | (character & 0x3F); + } + else + { + *write++ = 0xE0 | ((character >> 12) & 0xFF); + *write++ = 0x80 | ((character >> 6) & 0x3F); + *write++ = 0x80 | (character & 0x3F); + } + break; + } + default: + break; + } + + last = read; + read += strcspn(read, "\\"); + } + + *write = 0; + return write - string; +} + +cgltf_size cgltf_decode_uri(char* uri) +{ + char* write = uri; + char* i = uri; + + while (*i) + { + if (*i == '%') + { + int ch1 = cgltf_unhex(i[1]); + + if (ch1 >= 0) + { + int ch2 = cgltf_unhex(i[2]); + + if (ch2 >= 0) + { + *write++ = (char)(ch1 * 16 + ch2); + i += 3; + continue; + } + } + } + + *write++ = *i++; + } + + *write = 0; + return write - uri; +} + +cgltf_result cgltf_load_buffers(const cgltf_options* options, cgltf_data* data, const char* gltf_path) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + if (data->buffers_count && data->buffers[0].data == NULL && data->buffers[0].uri == NULL && data->bin) + { + if (data->bin_size < data->buffers[0].size) + { + return cgltf_result_data_too_short; + } + + data->buffers[0].data = (void*)data->bin; + data->buffers[0].data_free_method = cgltf_data_free_method_none; + } + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + if (data->buffers[i].data) + { + continue; + } + + const char* uri = data->buffers[i].uri; + + if (uri == NULL) + { + continue; + } + + if (strncmp(uri, "data:", 5) == 0) + { + const char* comma = strchr(uri, ','); + + if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) + { + cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_memory_free; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + else if (strstr(uri, "://") == NULL && gltf_path) + { + cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_file_release; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + + return cgltf_result_success; +} + +static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_size offset, cgltf_component_type component_type, cgltf_size count) +{ + char* data = (char*)buffer_view->buffer->data + offset + buffer_view->offset; + cgltf_size bound = 0; + + switch (component_type) + { + case cgltf_component_type_r_8u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned char*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_16u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned short*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_32u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned int*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + default: + ; + } + + return bound; +} + +#if CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_ASSERT_IF(cond, result) assert(!(cond)); if (cond) return result; +#else +#define CGLTF_ASSERT_IF(cond, result) if (cond) return result; +#endif + +cgltf_result cgltf_validate(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_accessor* accessor = &data->accessors[i]; + + CGLTF_ASSERT_IF(data->accessors[i].component_type == cgltf_component_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->accessors[i].type == cgltf_type_invalid, cgltf_result_invalid_gltf); + + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + + if (accessor->buffer_view) + { + cgltf_size req_size = accessor->offset + accessor->stride * (accessor->count - 1) + element_size; + + CGLTF_ASSERT_IF(accessor->buffer_view->size < req_size, cgltf_result_data_too_short); + } + + if (accessor->is_sparse) + { + cgltf_accessor_sparse* sparse = &accessor->sparse; + + cgltf_size indices_component_size = cgltf_component_size(sparse->indices_component_type); + cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count; + cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count; + + CGLTF_ASSERT_IF(sparse->indices_buffer_view->size < indices_req_size || + sparse->values_buffer_view->size < values_req_size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(sparse->indices_component_type != cgltf_component_type_r_8u && + sparse->indices_component_type != cgltf_component_type_r_16u && + sparse->indices_component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + if (sparse->indices_buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(sparse->indices_buffer_view, sparse->indices_byte_offset, sparse->indices_component_type, sparse->count); + + CGLTF_ASSERT_IF(index_bound >= accessor->count, cgltf_result_data_too_short); + } + } + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_size req_size = data->buffer_views[i].offset + data->buffer_views[i].size; + + CGLTF_ASSERT_IF(data->buffer_views[i].buffer && data->buffer_views[i].buffer->size < req_size, cgltf_result_data_too_short); + + if (data->buffer_views[i].has_meshopt_compression) + { + cgltf_meshopt_compression* mc = &data->buffer_views[i].meshopt_compression; + + CGLTF_ASSERT_IF(mc->buffer == NULL || mc->buffer->size < mc->offset + mc->size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(data->buffer_views[i].stride && mc->stride != data->buffer_views[i].stride, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->buffer_views[i].size != mc->stride * mc->count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_invalid, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_attributes && !(mc->stride % 4 == 0 && mc->stride <= 256), cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_triangles && mc->count % 3 != 0, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->stride != 2 && mc->stride != 4, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->filter != cgltf_meshopt_compression_filter_none, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_octahedral && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_quaternion && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_color && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + if (data->meshes[i].weights) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->meshes[i].target_names) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].target_names_count, cgltf_result_invalid_gltf); + } + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].type == cgltf_primitive_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + + CGLTF_ASSERT_IF(first->count == 0, cgltf_result_invalid_gltf); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); + } + } + + cgltf_accessor* indices = data->meshes[i].primitives[j].indices; + + CGLTF_ASSERT_IF(indices && + indices->component_type != cgltf_component_type_r_8u && + indices->component_type != cgltf_component_type_r_16u && + indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(indices && indices->type != cgltf_type_scalar, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(indices && indices->stride != cgltf_component_size(indices->component_type), cgltf_result_invalid_gltf); + + if (indices && indices->buffer_view && indices->buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); + + CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + if (data->nodes[i].weights && data->nodes[i].mesh) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->nodes[i].has_mesh_gpu_instancing) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh == NULL, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->nodes[i].mesh_gpu_instancing.attributes[0].data; + + for (cgltf_size k = 0; k < data->nodes[i].mesh_gpu_instancing.attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_node* p1 = data->nodes[i].parent; + cgltf_node* p2 = p1 ? p1->parent : NULL; + + while (p1 && p2) + { + CGLTF_ASSERT_IF(p1 == p2, cgltf_result_invalid_gltf); + + p1 = p1->parent; + p2 = p2->parent ? p2->parent->parent : NULL; + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_ASSERT_IF(data->scenes[i].nodes[j]->parent, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_animation_channel* channel = &data->animations[i].channels[j]; + + if (!channel->target_node) + { + continue; + } + + cgltf_size components = 1; + + if (channel->target_path == cgltf_animation_path_type_weights) + { + CGLTF_ASSERT_IF(!channel->target_node->mesh || !channel->target_node->mesh->primitives_count, cgltf_result_invalid_gltf); + + components = channel->target_node->mesh->primitives[0].targets_count; + } + + cgltf_size values = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline ? 3 : 1; + + CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + CGLTF_ASSERT_IF(!data->variants[i].name, cgltf_result_invalid_gltf); + } + + return cgltf_result_success; +} + +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size) +{ + cgltf_size json_size = extras->end_offset - extras->start_offset; + + if (!dest) + { + if (dest_size) + { + *dest_size = json_size + 1; + return cgltf_result_success; + } + return cgltf_result_invalid_options; + } + + if (*dest_size + 1 < json_size) + { + strncpy(dest, data->json + extras->start_offset, *dest_size - 1); + dest[*dest_size - 1] = 0; + } + else + { + strncpy(dest, data->json + extras->start_offset, json_size); + dest[json_size] = 0; + } + + return cgltf_result_success; +} + +static void cgltf_free_extras(cgltf_data* data, cgltf_extras* extras) +{ + data->memory.free_func(data->memory.user_data, extras->data); +} + +static void cgltf_free_extensions(cgltf_data* data, cgltf_extension* extensions, cgltf_size extensions_count) +{ + for (cgltf_size i = 0; i < extensions_count; ++i) + { + data->memory.free_func(data->memory.user_data, extensions[i].name); + data->memory.free_func(data->memory.user_data, extensions[i].data); + } + data->memory.free_func(data->memory.user_data, extensions); +} + +void cgltf_free(cgltf_data* data) +{ + if (!data) + { + return; + } + + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = data->file.release ? data->file.release : cgltf_default_file_release; + + data->memory.free_func(data->memory.user_data, data->asset.copyright); + data->memory.free_func(data->memory.user_data, data->asset.generator); + data->memory.free_func(data->memory.user_data, data->asset.version); + data->memory.free_func(data->memory.user_data, data->asset.min_version); + + cgltf_free_extensions(data, data->asset.extensions, data->asset.extensions_count); + cgltf_free_extras(data, &data->asset.extras); + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->accessors[i].name); + + cgltf_free_extensions(data, data->accessors[i].extensions, data->accessors[i].extensions_count); + cgltf_free_extras(data, &data->accessors[i].extras); + } + data->memory.free_func(data->memory.user_data, data->accessors); + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffer_views[i].name); + data->memory.free_func(data->memory.user_data, data->buffer_views[i].data); + + cgltf_free_extensions(data, data->buffer_views[i].extensions, data->buffer_views[i].extensions_count); + cgltf_free_extras(data, &data->buffer_views[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffer_views); + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].name); + + if (data->buffers[i].data_free_method == cgltf_data_free_method_file_release) + { + file_release(&data->memory, &data->file, data->buffers[i].data, data->buffers[i].size); + } + else if (data->buffers[i].data_free_method == cgltf_data_free_method_memory_free) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].data); + } + + data->memory.free_func(data->memory.user_data, data->buffers[i].uri); + + cgltf_free_extensions(data, data->buffers[i].extensions, data->buffers[i].extensions_count); + cgltf_free_extras(data, &data->buffers[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffers); + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].name); + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes[m].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets); + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + cgltf_free_extras(data, &data->meshes[i].primitives[j].mappings[k].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].mappings); + + cgltf_free_extensions(data, data->meshes[i].primitives[j].extensions, data->meshes[i].primitives[j].extensions_count); + cgltf_free_extras(data, &data->meshes[i].primitives[j].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives); + data->memory.free_func(data->memory.user_data, data->meshes[i].weights); + + for (cgltf_size j = 0; j < data->meshes[i].target_names_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names[j]); + } + + cgltf_free_extensions(data, data->meshes[i].extensions, data->meshes[i].extensions_count); + cgltf_free_extras(data, &data->meshes[i].extras); + + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names); + } + + data->memory.free_func(data->memory.user_data, data->meshes); + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->materials[i].name); + + cgltf_free_extensions(data, data->materials[i].extensions, data->materials[i].extensions_count); + cgltf_free_extras(data, &data->materials[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->materials); + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->images[i].name); + data->memory.free_func(data->memory.user_data, data->images[i].uri); + data->memory.free_func(data->memory.user_data, data->images[i].mime_type); + + cgltf_free_extensions(data, data->images[i].extensions, data->images[i].extensions_count); + cgltf_free_extras(data, &data->images[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->images); + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->textures[i].name); + + cgltf_free_extensions(data, data->textures[i].extensions, data->textures[i].extensions_count); + cgltf_free_extras(data, &data->textures[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->textures); + + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->samplers[i].name); + + cgltf_free_extensions(data, data->samplers[i].extensions, data->samplers[i].extensions_count); + cgltf_free_extras(data, &data->samplers[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->samplers); + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->skins[i].name); + data->memory.free_func(data->memory.user_data, data->skins[i].joints); + + cgltf_free_extensions(data, data->skins[i].extensions, data->skins[i].extensions_count); + cgltf_free_extras(data, &data->skins[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->skins); + + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->cameras[i].name); + + if (data->cameras[i].type == cgltf_camera_type_perspective) + { + cgltf_free_extras(data, &data->cameras[i].data.perspective.extras); + } + else if (data->cameras[i].type == cgltf_camera_type_orthographic) + { + cgltf_free_extras(data, &data->cameras[i].data.orthographic.extras); + } + + cgltf_free_extensions(data, data->cameras[i].extensions, data->cameras[i].extensions_count); + cgltf_free_extras(data, &data->cameras[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->cameras); + + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->lights[i].name); + + cgltf_free_extras(data, &data->lights[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->lights); + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].name); + data->memory.free_func(data->memory.user_data, data->nodes[i].children); + data->memory.free_func(data->memory.user_data, data->nodes[i].weights); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size j = 0; j < data->nodes[i].mesh_gpu_instancing.attributes_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes[j].name); + } + + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes); + } + + cgltf_free_extensions(data, data->nodes[i].extensions, data->nodes[i].extensions_count); + cgltf_free_extras(data, &data->nodes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->nodes); + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->scenes[i].name); + data->memory.free_func(data->memory.user_data, data->scenes[i].nodes); + + cgltf_free_extensions(data, data->scenes[i].extensions, data->scenes[i].extensions_count); + cgltf_free_extras(data, &data->scenes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->scenes); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->animations[i].name); + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].samplers[j].extensions, data->animations[i].samplers[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].samplers[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].samplers); + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].channels[j].extensions, data->animations[i].channels[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].channels[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].channels); + + cgltf_free_extensions(data, data->animations[i].extensions, data->animations[i].extensions_count); + cgltf_free_extras(data, &data->animations[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->animations); + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->variants[i].name); + + cgltf_free_extras(data, &data->variants[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->variants); + + cgltf_free_extensions(data, data->data_extensions, data->data_extensions_count); + cgltf_free_extras(data, &data->extras); + + for (cgltf_size i = 0; i < data->extensions_used_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_used[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_used); + + for (cgltf_size i = 0; i < data->extensions_required_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_required[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_required); + + file_release(&data->memory, &data->file, data->file_data, data->file_size); + + data->memory.free_func(data->memory.user_data, data); +} + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + + if (node->has_matrix) + { + memcpy(lm, node->matrix, sizeof(float) * 16); + } + else + { + float tx = node->translation[0]; + float ty = node->translation[1]; + float tz = node->translation[2]; + + float qx = node->rotation[0]; + float qy = node->rotation[1]; + float qz = node->rotation[2]; + float qw = node->rotation[3]; + + float sx = node->scale[0]; + float sy = node->scale[1]; + float sz = node->scale[2]; + + lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx; + lm[1] = (2 * qx*qy + 2 * qz*qw) * sx; + lm[2] = (2 * qx*qz - 2 * qy*qw) * sx; + lm[3] = 0.f; + + lm[4] = (2 * qx*qy - 2 * qz*qw) * sy; + lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy; + lm[6] = (2 * qy*qz + 2 * qx*qw) * sy; + lm[7] = 0.f; + + lm[8] = (2 * qx*qz + 2 * qy*qw) * sz; + lm[9] = (2 * qy*qz - 2 * qx*qw) * sz; + lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz; + lm[11] = 0.f; + + lm[12] = tx; + lm[13] = ty; + lm[14] = tz; + lm[15] = 1.f; + } +} + +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + cgltf_node_transform_local(node, lm); + + const cgltf_node* parent = node->parent; + + while (parent) + { + float pm[16]; + cgltf_node_transform_local(parent, pm); + + for (int i = 0; i < 4; ++i) + { + float l0 = lm[i * 4 + 0]; + float l1 = lm[i * 4 + 1]; + float l2 = lm[i * 4 + 2]; + + float r0 = l0 * pm[0] + l1 * pm[4] + l2 * pm[8]; + float r1 = l0 * pm[1] + l1 * pm[5] + l2 * pm[9]; + float r2 = l0 * pm[2] + l1 * pm[6] + l2 * pm[10]; + + lm[i * 4 + 0] = r0; + lm[i * 4 + 1] = r1; + lm[i * 4 + 2] = r2; + } + + lm[12] += pm[12]; + lm[13] += pm[13]; + lm[14] += pm[14]; + + parent = parent->parent; + } +} + +static cgltf_ssize cgltf_component_read_integer(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16: + return *((const int16_t*) in); + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8: + return *((const int8_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_type component_type, cgltf_bool normalized) +{ + if (component_type == cgltf_component_type_r_32f) + { + return *((const float*) in); + } + + if (normalized) + { + switch (component_type) + { + // note: glTF spec doesn't currently define normalized conversions for 32-bit integers + case cgltf_component_type_r_16: + return *((const int16_t*) in) / (cgltf_float)32767; + case cgltf_component_type_r_16u: + return *((const uint16_t*) in) / (cgltf_float)65535; + case cgltf_component_type_r_8: + return *((const int8_t*) in) / (cgltf_float)127; + case cgltf_component_type_r_8u: + return *((const uint8_t*) in) / (cgltf_float)255; + default: + return 0; + } + } + + return (cgltf_float)cgltf_component_read_integer(in, component_type); +} + +static cgltf_bool cgltf_element_read_float(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_bool normalized, cgltf_float* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) { + return 0; + } + + // There are three special cases for component extraction, see #data-alignment in the 2.0 spec. + + cgltf_size component_size = cgltf_component_size(component_type); + + if (type == cgltf_type_mat2 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 5, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 2, component_type, normalized); + out[3] = cgltf_component_read_float(element + 4, component_type, normalized); + out[4] = cgltf_component_read_float(element + 5, component_type, normalized); + out[5] = cgltf_component_read_float(element + 6, component_type, normalized); + out[6] = cgltf_component_read_float(element + 8, component_type, normalized); + out[7] = cgltf_component_read_float(element + 9, component_type, normalized); + out[8] = cgltf_component_read_float(element + 10, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 2) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 2, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 8, component_type, normalized); + out[4] = cgltf_component_read_float(element + 10, component_type, normalized); + out[5] = cgltf_component_read_float(element + 12, component_type, normalized); + out[6] = cgltf_component_read_float(element + 16, component_type, normalized); + out[7] = cgltf_component_read_float(element + 18, component_type, normalized); + out[8] = cgltf_component_read_float(element + 20, component_type, normalized); + return 1; + } + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_float(element + component_size * i, component_type, normalized); + } + return 1; +} + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view) +{ + if (view->data) + return (const uint8_t*)view->data; + + if (!view->buffer->data) + return NULL; + + const uint8_t* result = (const uint8_t*)view->buffer->data; + result += view->offset; + return result; +} + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index) +{ + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = &prim->attributes[i]; + if (attr->type == type && attr->index == index) + return attr->data; + } + + return NULL; +} + +static const uint8_t* cgltf_find_sparse_index(const cgltf_accessor* accessor, cgltf_size needle) +{ + const cgltf_accessor_sparse* sparse = &accessor->sparse; + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* value_data = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || value_data == NULL) + return NULL; + + index_data += sparse->indices_byte_offset; + value_data += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + + cgltf_size offset = 0; + cgltf_size length = sparse->count; + + while (length) + { + cgltf_size rem = length % 2; + length /= 2; + + cgltf_size index = cgltf_component_read_index(index_data + (offset + length) * index_stride, sparse->indices_component_type); + offset += index < needle ? length + rem : 0; + } + + if (offset == sparse->count) + return NULL; + + cgltf_size index = cgltf_component_read_index(index_data + offset * index_stride, sparse->indices_component_type); + return index == needle ? value_data + offset * accessor->stride : NULL; +} + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_float)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); +} + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count) +{ + cgltf_size floats_per_element = cgltf_num_components(accessor->type); + cgltf_size available_floats = accessor->count * floats_per_element; + if (out == NULL) + { + return available_floats; + } + + float_count = available_floats < float_count ? available_floats : float_count; + cgltf_size element_count = float_count / floats_per_element; + + // First pass: convert each element in the base accessor. + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (accessor->component_type == cgltf_component_type_r_32f && accessor->stride == floats_per_element * sizeof(cgltf_float)) + { + memcpy(out, element, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + cgltf_float* dest = out; + + for (cgltf_size index = 0; index < element_count; index++, dest += floats_per_element, element += accessor->stride) + { + if (!cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, dest, floats_per_element)) + { + return 0; + } + } + } + } + + // Second pass: write out each element in the sparse accessor. + if (accessor->is_sparse) + { + const cgltf_accessor_sparse* sparse = &accessor->sparse; + + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* reader_head = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || reader_head == NULL) + { + return 0; + } + + index_data += sparse->indices_byte_offset; + reader_head += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + for (cgltf_size reader_index = 0; reader_index < sparse->count; reader_index++, index_data += index_stride, reader_head += accessor->stride) + { + size_t writer_index = cgltf_component_read_index(index_data, sparse->indices_component_type); + float* writer_head = out + writer_index * floats_per_element; + + if (!cgltf_element_read_float(reader_head, accessor->type, accessor->component_type, accessor->normalized, writer_head, floats_per_element)) + { + return 0; + } + } + } + + return element_count * floats_per_element; +} + +static cgltf_uint cgltf_component_read_uint(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_8: + return *((const int8_t*) in); + + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + + case cgltf_component_type_r_16: + return *((const int16_t*) in); + + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + + default: + return 0; + } +} + +static cgltf_bool cgltf_element_read_uint(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_uint* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) + { + return 0; + } + + // Reading integer matrices is not a valid use case + if (type == cgltf_type_mat2 || type == cgltf_type_mat3 || type == cgltf_type_mat4) + { + return 0; + } + + cgltf_size component_size = cgltf_component_size(component_type); + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_uint(element + component_size * i, component_type); + } + return 1; +} + +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_uint)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); +} + +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_component_read_index(element, accessor->component_type); + } + if (accessor->buffer_view == NULL) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; // This is an error case, but we can't communicate the error with existing interface. + } + element += accessor->offset + accessor->stride * index; + return cgltf_component_read_index(element, accessor->component_type); +} + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object) +{ + assert(object && (cgltf_size)(object - data->meshes) < data->meshes_count); + return (cgltf_size)(object - data->meshes); +} + +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object) +{ + assert(object && (cgltf_size)(object - data->materials) < data->materials_count); + return (cgltf_size)(object - data->materials); +} + +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object) +{ + assert(object && (cgltf_size)(object - data->accessors) < data->accessors_count); + return (cgltf_size)(object - data->accessors); +} + +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object) +{ + assert(object && (cgltf_size)(object - data->buffer_views) < data->buffer_views_count); + return (cgltf_size)(object - data->buffer_views); +} + +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object) +{ + assert(object && (cgltf_size)(object - data->buffers) < data->buffers_count); + return (cgltf_size)(object - data->buffers); +} + +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object) +{ + assert(object && (cgltf_size)(object - data->images) < data->images_count); + return (cgltf_size)(object - data->images); +} + +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object) +{ + assert(object && (cgltf_size)(object - data->textures) < data->textures_count); + return (cgltf_size)(object - data->textures); +} + +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object) +{ + assert(object && (cgltf_size)(object - data->samplers) < data->samplers_count); + return (cgltf_size)(object - data->samplers); +} + +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object) +{ + assert(object && (cgltf_size)(object - data->skins) < data->skins_count); + return (cgltf_size)(object - data->skins); +} + +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object) +{ + assert(object && (cgltf_size)(object - data->cameras) < data->cameras_count); + return (cgltf_size)(object - data->cameras); +} + +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object) +{ + assert(object && (cgltf_size)(object - data->lights) < data->lights_count); + return (cgltf_size)(object - data->lights); +} + +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object) +{ + assert(object && (cgltf_size)(object - data->nodes) < data->nodes_count); + return (cgltf_size)(object - data->nodes); +} + +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object) +{ + assert(object && (cgltf_size)(object - data->scenes) < data->scenes_count); + return (cgltf_size)(object - data->scenes); +} + +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object) +{ + assert(object && (cgltf_size)(object - data->animations) < data->animations_count); + return (cgltf_size)(object - data->animations); +} + +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object) +{ + assert(object && (cgltf_size)(object - animation->samplers) < animation->samplers_count); + return (cgltf_size)(object - animation->samplers); +} + +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object) +{ + assert(object && (cgltf_size)(object - animation->channels) < animation->channels_count); + return (cgltf_size)(object - animation->channels); +} + +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count) +{ + if (out == NULL) + { + return accessor->count; + } + + cgltf_size numbers_per_element = cgltf_num_components(accessor->type); + cgltf_size available_numbers = accessor->count * numbers_per_element; + + index_count = available_numbers < index_count ? available_numbers : index_count; + cgltf_size index_component_size = cgltf_component_size(accessor->component_type); + + if (accessor->is_sparse) + { + return 0; + } + if (accessor->buffer_view == NULL) + { + return 0; + } + if (index_component_size > out_component_size) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (index_component_size == out_component_size && accessor->stride == out_component_size * numbers_per_element) + { + memcpy(out, element, index_count * index_component_size); + return index_count; + } + + // Data couldn't be copied with memcpy due to stride being larger than the component size. + // OR + // The component size of the output array is larger than the component size of the index data, so index data will be padded. + switch (out_component_size) + { + case 1: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint8_t*)out)[index] = (uint8_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 2: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint16_t*)out)[index] = (uint16_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 4: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint32_t*)out)[index] = (uint32_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + default: + return 0; + } + + return index_count; +} + +#define CGLTF_ERROR_JSON -1 +#define CGLTF_ERROR_NOMEM -2 +#define CGLTF_ERROR_LEGACY -3 + +#define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_TOKTYPE_RET(tok_, type_, ret_) if ((tok_).type != (type_)) { return ret_; } +#define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */ + +#define CGLTF_PTRINDEX(type, idx) (type*)((cgltf_size)idx + 1) +#define CGLTF_PTRFIXUP(var, data, size) if (var) { if ((cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; } +#define CGLTF_PTRFIXUP_REQ(var, data, size) if (!var || (cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; + +static int cgltf_json_strcmp(jsmntok_t const* tok, const uint8_t* json_chunk, const char* str) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_STRING); + size_t const str_len = strlen(str); + size_t const name_length = (size_t)(tok->end - tok->start); + return (str_len == name_length) ? strncmp((const char*)json_chunk + tok->start, str, str_len) : 128; +} + +static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return CGLTF_ATOI(tmp); +} + +static cgltf_size cgltf_json_to_size(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE_RET(*tok, JSMN_PRIMITIVE, 0); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + long long res = CGLTF_ATOLL(tmp); + return res < 0 ? 0 : (cgltf_size)res; +} + +static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return (cgltf_float)CGLTF_ATOF(tmp); +} + +static cgltf_bool cgltf_json_to_bool(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int size = (int)(tok->end - tok->start); + return size == 4 && memcmp(json_chunk + tok->start, "true", 4) == 0; +} + +static int cgltf_skip_json(jsmntok_t const* tokens, int i) +{ + int end = i + 1; + + while (i < end) + { + switch (tokens[i].type) + { + case JSMN_OBJECT: + end += tokens[i].size * 2; + break; + + case JSMN_ARRAY: + end += tokens[i].size; + break; + + case JSMN_PRIMITIVE: + case JSMN_STRING: + break; + + default: + return -1; + } + + i++; + } + + return i; +} + +static void cgltf_fill_float_array(float* out_array, int size, float value) +{ + for (int j = 0; j < size; ++j) + { + out_array[j] = value; + } +} + +static int cgltf_parse_json_float_array(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, float* out_array, int size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + if (tokens[i].size != size) + { + return CGLTF_ERROR_JSON; + } + ++i; + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_array[j] = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + return i; +} + +static int cgltf_parse_json_string(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char** out_string) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + if (*out_string) + { + return CGLTF_ERROR_JSON; + } + int size = (int)(tokens[i].end - tokens[i].start); + char* result = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(result, (const char*)json_chunk + tokens[i].start, size); + result[size] = 0; + *out_string = result; + return i + 1; +} + +static int cgltf_parse_json_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, size_t element_size, void** out_array, cgltf_size* out_size) +{ + (void)json_chunk; + if (tokens[i].type != JSMN_ARRAY) + { + return tokens[i].type == JSMN_OBJECT ? CGLTF_ERROR_LEGACY : CGLTF_ERROR_JSON; + } + if (*out_array) + { + return CGLTF_ERROR_JSON; + } + int size = tokens[i].size; + void* result = cgltf_calloc(options, element_size, size); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + *out_array = result; + *out_size = size; + return i + 1; +} + +static int cgltf_parse_json_string_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char*** out_array, cgltf_size* out_size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(char*), (void**)out_array, out_size); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < *out_size; ++j) + { + i = cgltf_parse_json_string(options, tokens, i, json_chunk, j + (*out_array)); + if (i < 0) + { + return i; + } + } + return i; +} + +static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* out_type, int* out_index) +{ + if (*name == '_') + { + *out_type = cgltf_attribute_type_custom; + return; + } + + const char* us = strchr(name, '_'); + size_t len = us ? (size_t)(us - name) : strlen(name); + + if (len == 8 && strncmp(name, "POSITION", 8) == 0) + { + *out_type = cgltf_attribute_type_position; + } + else if (len == 6 && strncmp(name, "NORMAL", 6) == 0) + { + *out_type = cgltf_attribute_type_normal; + } + else if (len == 7 && strncmp(name, "TANGENT", 7) == 0) + { + *out_type = cgltf_attribute_type_tangent; + } + else if (len == 8 && strncmp(name, "TEXCOORD", 8) == 0) + { + *out_type = cgltf_attribute_type_texcoord; + } + else if (len == 5 && strncmp(name, "COLOR", 5) == 0) + { + *out_type = cgltf_attribute_type_color; + } + else if (len == 6 && strncmp(name, "JOINTS", 6) == 0) + { + *out_type = cgltf_attribute_type_joints; + } + else if (len == 7 && strncmp(name, "WEIGHTS", 7) == 0) + { + *out_type = cgltf_attribute_type_weights; + } + else + { + *out_type = cgltf_attribute_type_invalid; + } + + if (us && *out_type != cgltf_attribute_type_invalid) + { + *out_index = CGLTF_ATOI(us + 1); + if (*out_index < 0) + { + *out_type = cgltf_attribute_type_invalid; + *out_index = 0; + } + } +} + +static int cgltf_parse_json_attribute_list(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_attribute** out_attributes, cgltf_size* out_attributes_count) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + if (*out_attributes) + { + return CGLTF_ERROR_JSON; + } + + *out_attributes_count = tokens[i].size; + *out_attributes = (cgltf_attribute*)cgltf_calloc(options, sizeof(cgltf_attribute), *out_attributes_count); + ++i; + + if (!*out_attributes) + { + return CGLTF_ERROR_NOMEM; + } + + for (cgltf_size j = 0; j < *out_attributes_count; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + i = cgltf_parse_json_string(options, tokens, i, json_chunk, &(*out_attributes)[j].name); + if (i < 0) + { + return CGLTF_ERROR_JSON; + } + + cgltf_parse_attribute_type((*out_attributes)[j].name, &(*out_attributes)[j].type, &(*out_attributes)[j].index); + + (*out_attributes)[j].data = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + + return i; +} + +static int cgltf_parse_json_extras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extras* out_extras) +{ + if (out_extras->data) + { + return CGLTF_ERROR_JSON; + } + + /* fill deprecated fields for now, this will be removed in the future */ + out_extras->start_offset = tokens[i].start; + out_extras->end_offset = tokens[i].end; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extras->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extras->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extras->data, (const char*)json_chunk + start, size); + out_extras->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + return i; +} + +static int cgltf_parse_json_unprocessed_extension(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extension* out_extension) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + CGLTF_CHECK_TOKTYPE(tokens[i+1], JSMN_OBJECT); + if (out_extension->name) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size name_length = tokens[i].end - tokens[i].start; + out_extension->name = (char*)options->memory.alloc_func(options->memory.user_data, name_length + 1); + if (!out_extension->name) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->name, (const char*)json_chunk + tokens[i].start, name_length); + out_extension->name[name_length] = 0; + i++; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extension->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extension->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->data, (const char*)json_chunk + start, size); + out_extension->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + + return i; +} + +static int cgltf_parse_json_unprocessed_extensions(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_size* out_extensions_count, cgltf_extension** out_extensions) +{ + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(*out_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + *out_extensions_count = 0; + *out_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!*out_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int j = 0; j < extensions_size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + cgltf_size extension_index = (*out_extensions_count)++; + cgltf_extension* extension = &((*out_extensions)[extension_index]); + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, extension); + + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_draco_mesh_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_draco_mesh_compression* out_draco_mesh_compression) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_draco_mesh_compression->attributes, &out_draco_mesh_compression->attributes_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferView") == 0) + { + ++i; + out_draco_mesh_compression->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh_gpu_instancing(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh_gpu_instancing* out_mesh_gpu_instancing) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_mesh_gpu_instancing->attributes, &out_mesh_gpu_instancing->attributes_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material_mapping_data(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_mapping* out_mappings, cgltf_size* offset) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int obj_size = tokens[i].size; + ++i; + + int material = -1; + int variants_tok = -1; + int extras_tok = -1; + + for (int k = 0; k < obj_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "material") == 0) + { + ++i; + material = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + variants_tok = i+1; + CGLTF_CHECK_TOKTYPE(tokens[variants_tok], JSMN_ARRAY); + + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + extras_tok = i + 1; + i = cgltf_skip_json(tokens, extras_tok); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (material < 0 || variants_tok < 0) + { + return CGLTF_ERROR_JSON; + } + + if (out_mappings) + { + for (int k = 0; k < tokens[variants_tok].size; ++k) + { + int variant = cgltf_json_to_int(&tokens[variants_tok + 1 + k], json_chunk); + if (variant < 0) + return variant; + + out_mappings[*offset].material = CGLTF_PTRINDEX(cgltf_material, material); + out_mappings[*offset].variant = variant; + + if (extras_tok >= 0) + { + int e = cgltf_parse_json_extras(options, tokens, extras_tok, json_chunk, &out_mappings[*offset].extras); + if (e < 0) + return e; + } + + (*offset)++; + } + } + else + { + (*offset) += tokens[variants_tok].size; + } + } + + return i; +} + +static int cgltf_parse_json_material_mappings(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "mappings") == 0) + { + if (out_prim->mappings) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size mappings_offset = 0; + int k = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, NULL, &mappings_offset); + if (k < 0) + { + return k; + } + + out_prim->mappings_count = mappings_offset; + out_prim->mappings = (cgltf_material_mapping*)cgltf_calloc(options, sizeof(cgltf_material_mapping), out_prim->mappings_count); + + mappings_offset = 0; + i = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, out_prim->mappings, &mappings_offset); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static cgltf_primitive_type cgltf_json_to_primitive_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 0: + return cgltf_primitive_type_points; + case 1: + return cgltf_primitive_type_lines; + case 2: + return cgltf_primitive_type_line_loop; + case 3: + return cgltf_primitive_type_line_strip; + case 4: + return cgltf_primitive_type_triangles; + case 5: + return cgltf_primitive_type_triangle_strip; + case 6: + return cgltf_primitive_type_triangle_fan; + default: + return cgltf_primitive_type_invalid; + } +} + +static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_prim->type = cgltf_primitive_type_triangles; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + out_prim->type = cgltf_json_to_primitive_type(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + out_prim->indices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "material") == 0) + { + ++i; + out_prim->material = CGLTF_PTRINDEX(cgltf_material, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_prim->attributes, &out_prim->attributes_count); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "targets") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_morph_target), (void**)&out_prim->targets, &out_prim->targets_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_prim->targets_count; ++k) + { + i = cgltf_parse_json_attribute_list(options, tokens, i, json_chunk, &out_prim->targets[k].attributes, &out_prim->targets[k].attributes_count); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_prim->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_prim->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_prim->extensions_count = 0; + out_prim->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_prim->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_draco_mesh_compression") == 0) + { + out_prim->has_draco_mesh_compression = 1; + i = cgltf_parse_json_draco_mesh_compression(options, tokens, i + 1, json_chunk, &out_prim->draco_mesh_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + i = cgltf_parse_json_material_mappings(options, tokens, i + 1, json_chunk, out_prim); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_prim->extensions[out_prim->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh* out_mesh) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_mesh->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "primitives") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_primitive), (void**)&out_mesh->primitives, &out_mesh->primitives_count); + if (i < 0) + { + return i; + } + + for (cgltf_size prim_index = 0; prim_index < out_mesh->primitives_count; ++prim_index) + { + i = cgltf_parse_json_primitive(options, tokens, i, json_chunk, &out_mesh->primitives[prim_index]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_mesh->weights, &out_mesh->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_mesh->weights, (int)out_mesh->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + ++i; + + out_mesh->extras.start_offset = tokens[i].start; + out_mesh->extras.end_offset = tokens[i].end; + + if (tokens[i].type == JSMN_OBJECT) + { + int extras_size = tokens[i].size; + ++i; + + for (int k = 0; k < extras_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "targetNames") == 0 && tokens[i+1].type == JSMN_ARRAY) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_mesh->target_names, &out_mesh->target_names_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i); + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_mesh->extensions_count, &out_mesh->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_meshes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_mesh), (void**)&out_data->meshes, &out_data->meshes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->meshes_count; ++j) + { + i = cgltf_parse_json_mesh(options, tokens, i, json_chunk, &out_data->meshes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 5120: + return cgltf_component_type_r_8; + case 5121: + return cgltf_component_type_r_8u; + case 5122: + return cgltf_component_type_r_16; + case 5123: + return cgltf_component_type_r_16u; + case 5125: + return cgltf_component_type_r_32u; + case 5126: + return cgltf_component_type_r_32f; + default: + return cgltf_component_type_invalid; + } +} + +static int cgltf_parse_json_accessor_sparse(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_sparse->count = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int indices_size = tokens[i].size; + ++i; + + for (int k = 0; k < indices_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->indices_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->indices_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "values") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int values_size = tokens[i].size; + ++i; + + for (int k = 0; k < values_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->values_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->values_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor* out_accessor) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_accessor->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_accessor->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_accessor->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_accessor->component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "normalized") == 0) + { + ++i; + out_accessor->normalized = cgltf_json_to_bool(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_accessor->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "SCALAR") == 0) + { + out_accessor->type = cgltf_type_scalar; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC2") == 0) + { + out_accessor->type = cgltf_type_vec2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC3") == 0) + { + out_accessor->type = cgltf_type_vec3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC4") == 0) + { + out_accessor->type = cgltf_type_vec4; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT2") == 0) + { + out_accessor->type = cgltf_type_mat2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT3") == 0) + { + out_accessor->type = cgltf_type_mat3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT4") == 0) + { + out_accessor->type = cgltf_type_mat4; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "min") == 0) + { + ++i; + out_accessor->has_min = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int min_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->min, min_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "max") == 0) + { + ++i; + out_accessor->has_max = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int max_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->max, max_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0) + { + out_accessor->is_sparse = 1; + i = cgltf_parse_json_accessor_sparse(tokens, i + 1, json_chunk, &out_accessor->sparse); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_accessor->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_accessor->extensions_count, &out_accessor->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_transform* out_texture_transform) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "offset") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->offset, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "rotation") == 0) + { + ++i; + out_texture_transform->rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->scale, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_transform->has_texcoord = 1; + out_texture_transform->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view) +{ + (void)options; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_texture_view->scale = 1.0f; + cgltf_fill_float_array(out_texture_view->transform.scale, 2, 1.0f); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "index") == 0) + { + ++i; + out_texture_view->texture = CGLTF_PTRINDEX(cgltf_texture, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "strength") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int extensions_size = tokens[i].size; + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_texture_transform") == 0) + { + out_texture_view->has_transform = 1; + i = cgltf_parse_json_texture_transform(tokens, i + 1, json_chunk, &out_texture_view->transform); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_metallic_roughness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0) + { + ++i; + out_pbr->metallic_factor = + cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) + { + ++i; + out_pbr->roughness_factor = + cgltf_json_to_float(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->base_color_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->base_color_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->metallic_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_specular_glossiness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_specular_glossiness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->diffuse_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->specular_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "glossinessFactor") == 0) + { + ++i; + out_pbr->glossiness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->diffuse_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularGlossinessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->specular_glossiness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_clearcoat(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_clearcoat* out_clearcoat) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_roughness_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatNormalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_normal_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_ior(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_ior* out_ior) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_ior->ior = 1.5f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "ior") == 0) + { + ++i; + out_ior->ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_specular(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_specular* out_specular) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_specular->specular_factor = 1.0f; + cgltf_fill_float_array(out_specular->specular_color_factor, 3, 1.0f); + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + ++i; + out_specular->specular_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_specular->specular_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "specularColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_transmission* out_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionFactor") == 0) + { + ++i; + out_transmission->transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_transmission->transmission_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_volume(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_volume* out_volume) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessFactor") == 0) + { + ++i; + out_volume->thickness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_volume->thickness_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationColor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_volume->attenuation_color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationDistance") == 0) + { + ++i; + out_volume->attenuation_distance = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sheen(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sheen* out_sheen) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_sheen->sheen_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_color_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessFactor") == 0) + { + ++i; + out_sheen->sheen_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_emissive_strength(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_emissive_strength* out_emissive_strength) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_emissive_strength->emissive_strength = 1.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveStrength") == 0) + { + ++i; + out_emissive_strength->emissive_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_iridescence(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_iridescence* out_iridescence) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_iridescence->iridescence_ior = 1.3f; + out_iridescence->iridescence_thickness_min = 100.f; + out_iridescence->iridescence_thickness_max = 400.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceFactor") == 0) + { + ++i; + out_iridescence->iridescence_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceIor") == 0) + { + ++i; + out_iridescence->iridescence_ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMinimum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_min = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMaximum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_max = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_thickness_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_diffuse_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_diffuse_transmission* out_diff_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Defaults + cgltf_fill_float_array(out_diff_transmission->diffuse_transmission_color_factor, 3, 1.0f); + out_diff_transmission->diffuse_transmission_factor = 0.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionFactor") == 0) + { + ++i; + out_diff_transmission->diffuse_transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_diff_transmission->diffuse_transmission_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_anisotropy(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_anisotropy* out_anisotropy) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyStrength") == 0) + { + ++i; + out_anisotropy->anisotropy_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyRotation") == 0) + { + ++i; + out_anisotropy->anisotropy_rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_anisotropy->anisotropy_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_dispersion(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_dispersion* out_dispersion) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "dispersion") == 0) + { + ++i; + out_dispersion->dispersion = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_image->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mimeType") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->mime_type); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_image->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_image->extensions_count, &out_image->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_sampler->wrap_s = cgltf_wrap_mode_repeat; + out_sampler->wrap_t = cgltf_wrap_mode_repeat; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_sampler->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "magFilter") == 0) + { + ++i; + out_sampler->mag_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0) + { + ++i; + out_sampler->min_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0) + { + ++i; + out_sampler->wrap_s + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) + { + ++i; + out_sampler->wrap_t + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture* out_texture) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_texture->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0) + { + ++i; + out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_texture->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if (out_texture->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_texture->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_texture->extensions_count = 0; + + if (!out_texture->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_texture_basisu") == 0) + { + out_texture->has_basisu = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->basisu_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_texture_webp") == 0) + { + out_texture->has_webp = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->webp_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + cgltf_fill_float_array(out_material->pbr_metallic_roughness.base_color_factor, 4, 1.0f); + out_material->pbr_metallic_roughness.metallic_factor = 1.0f; + out_material->pbr_metallic_roughness.roughness_factor = 1.0f; + + cgltf_fill_float_array(out_material->pbr_specular_glossiness.diffuse_factor, 4, 1.0f); + cgltf_fill_float_array(out_material->pbr_specular_glossiness.specular_factor, 3, 1.0f); + out_material->pbr_specular_glossiness.glossiness_factor = 1.0f; + + cgltf_fill_float_array(out_material->volume.attenuation_color, 3, 1.0f); + out_material->volume.attenuation_distance = FLT_MAX; + + out_material->alpha_cutoff = 0.5f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "pbrMetallicRoughness") == 0) + { + out_material->has_pbr_metallic_roughness = 1; + i = cgltf_parse_json_pbr_metallic_roughness(options, tokens, i + 1, json_chunk, &out_material->pbr_metallic_roughness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "emissiveFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_material->emissive_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "normalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->normal_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "occlusionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->occlusion_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->emissive_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaMode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "OPAQUE") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_opaque; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "MASK") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_mask; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "BLEND") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_blend; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaCutoff") == 0) + { + ++i; + out_material->alpha_cutoff = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "doubleSided") == 0) + { + ++i; + out_material->double_sided = + cgltf_json_to_bool(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_material->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_material->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_material->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_material->extensions_count= 0; + + if (!out_material->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_pbrSpecularGlossiness") == 0) + { + out_material->has_pbr_specular_glossiness = 1; + i = cgltf_parse_json_pbr_specular_glossiness(options, tokens, i + 1, json_chunk, &out_material->pbr_specular_glossiness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_unlit") == 0) + { + out_material->unlit = 1; + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_clearcoat") == 0) + { + out_material->has_clearcoat = 1; + i = cgltf_parse_json_clearcoat(options, tokens, i + 1, json_chunk, &out_material->clearcoat); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_ior") == 0) + { + out_material->has_ior = 1; + i = cgltf_parse_json_ior(tokens, i + 1, json_chunk, &out_material->ior); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_specular") == 0) + { + out_material->has_specular = 1; + i = cgltf_parse_json_specular(options, tokens, i + 1, json_chunk, &out_material->specular); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_transmission") == 0) + { + out_material->has_transmission = 1; + i = cgltf_parse_json_transmission(options, tokens, i + 1, json_chunk, &out_material->transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_volume") == 0) + { + out_material->has_volume = 1; + i = cgltf_parse_json_volume(options, tokens, i + 1, json_chunk, &out_material->volume); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_sheen") == 0) + { + out_material->has_sheen = 1; + i = cgltf_parse_json_sheen(options, tokens, i + 1, json_chunk, &out_material->sheen); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_emissive_strength") == 0) + { + out_material->has_emissive_strength = 1; + i = cgltf_parse_json_emissive_strength(tokens, i + 1, json_chunk, &out_material->emissive_strength); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_iridescence") == 0) + { + out_material->has_iridescence = 1; + i = cgltf_parse_json_iridescence(options, tokens, i + 1, json_chunk, &out_material->iridescence); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_diffuse_transmission") == 0) + { + out_material->has_diffuse_transmission = 1; + i = cgltf_parse_json_diffuse_transmission(options, tokens, i + 1, json_chunk, &out_material->diffuse_transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_anisotropy") == 0) + { + out_material->has_anisotropy = 1; + i = cgltf_parse_json_anisotropy(options, tokens, i + 1, json_chunk, &out_material->anisotropy); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_dispersion") == 0) + { + out_material->has_dispersion = 1; + i = cgltf_parse_json_dispersion(tokens, i + 1, json_chunk, &out_material->dispersion); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_material->extensions[out_material->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessors(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_accessor), (void**)&out_data->accessors, &out_data->accessors_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->accessors_count; ++j) + { + i = cgltf_parse_json_accessor(options, tokens, i, json_chunk, &out_data->accessors[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_materials(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material), (void**)&out_data->materials, &out_data->materials_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->materials_count; ++j) + { + i = cgltf_parse_json_material(options, tokens, i, json_chunk, &out_data->materials[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_images(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_image), (void**)&out_data->images, &out_data->images_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->images_count; ++j) + { + i = cgltf_parse_json_image(options, tokens, i, json_chunk, &out_data->images[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_textures(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_texture), (void**)&out_data->textures, &out_data->textures_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->textures_count; ++j) + { + i = cgltf_parse_json_texture(options, tokens, i, json_chunk, &out_data->textures[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_samplers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_sampler), (void**)&out_data->samplers, &out_data->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->samplers_count; ++j) + { + i = cgltf_parse_json_sampler(options, tokens, i, json_chunk, &out_data->samplers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_meshopt_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_meshopt_compression* out_meshopt_compression) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_meshopt_compression->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_meshopt_compression->offset = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_meshopt_compression->size = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_meshopt_compression->stride = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_meshopt_compression->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "ATTRIBUTES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_attributes; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "TRIANGLES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_triangles; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "INDICES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_indices; + } + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "filter") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "NONE") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_none; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "OCTAHEDRAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_octahedral; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "QUATERNION") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_quaternion; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "EXPONENTIAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_exponential; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "COLOR") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_color; + } + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer_view* out_buffer_view) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer_view->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_buffer_view->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_buffer_view->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer_view->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_buffer_view->stride = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + int type = cgltf_json_to_int(tokens+i, json_chunk); + switch (type) + { + case 34962: + type = cgltf_buffer_view_type_vertices; + break; + case 34963: + type = cgltf_buffer_view_type_indices; + break; + default: + type = cgltf_buffer_view_type_invalid; + break; + } + out_buffer_view->type = (cgltf_buffer_view_type)type; + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer_view->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_buffer_view->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_buffer_view->extensions_count = 0; + out_buffer_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_buffer_view->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "EXT_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + out_buffer_view->meshopt_compression.is_khr = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_buffer_view->extensions[out_buffer_view->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_views(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer_view), (void**)&out_data->buffer_views, &out_data->buffer_views_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffer_views_count; ++j) + { + i = cgltf_parse_json_buffer_view(options, tokens, i, json_chunk, &out_data->buffer_views[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_buffer(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer* out_buffer) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->uri); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_buffer->extensions_count, &out_buffer->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer), (void**)&out_data->buffers, &out_data->buffers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffers_count; ++j) + { + i = cgltf_parse_json_buffer(options, tokens, i, json_chunk, &out_data->buffers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_skin(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_skin* out_skin) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_skin->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "joints") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_skin->joints, &out_skin->joints_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_skin->joints_count; ++k) + { + out_skin->joints[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skeleton") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->skeleton = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "inverseBindMatrices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->inverse_bind_matrices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_skin->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_skin->extensions_count, &out_skin->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_skins(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_skin), (void**)&out_data->skins, &out_data->skins_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->skins_count; ++j) + { + i = cgltf_parse_json_skin(options, tokens, i, json_chunk, &out_data->skins[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_camera(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_camera* out_camera) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_camera->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "perspective") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_perspective; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "aspectRatio") == 0) + { + ++i; + out_camera->data.perspective.has_aspect_ratio = 1; + out_camera->data.perspective.aspect_ratio = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "yfov") == 0) + { + ++i; + out_camera->data.perspective.yfov = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.perspective.has_zfar = 1; + out_camera->data.perspective.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.perspective.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.perspective.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "orthographic") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_orthographic; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "xmag") == 0) + { + ++i; + out_camera->data.orthographic.xmag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "ymag") == 0) + { + ++i; + out_camera->data.orthographic.ymag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.orthographic.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.orthographic.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.orthographic.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_camera->extensions_count, &out_camera->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_cameras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_camera), (void**)&out_data->cameras, &out_data->cameras_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->cameras_count; ++j) + { + i = cgltf_parse_json_camera(options, tokens, i, json_chunk, &out_data->cameras[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_light(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_light* out_light) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_light->color[0] = 1.f; + out_light->color[1] = 1.f; + out_light->color[2] = 1.f; + out_light->intensity = 1.f; + + out_light->spot_inner_cone_angle = 0.f; + out_light->spot_outer_cone_angle = 3.1415926535f / 4.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_light->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "color") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_light->color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "intensity") == 0) + { + ++i; + out_light->intensity = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "directional") == 0) + { + out_light->type = cgltf_light_type_directional; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "point") == 0) + { + out_light->type = cgltf_light_type_point; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "spot") == 0) + { + out_light->type = cgltf_light_type_spot; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "range") == 0) + { + ++i; + out_light->range = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "spot") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "innerConeAngle") == 0) + { + ++i; + out_light->spot_inner_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "outerConeAngle") == 0) + { + ++i; + out_light->spot_outer_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_light->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_lights(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_light), (void**)&out_data->lights, &out_data->lights_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->lights_count; ++j) + { + i = cgltf_parse_json_light(options, tokens, i, json_chunk, &out_data->lights[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_node(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_node* out_node) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_node->rotation[3] = 1.0f; + out_node->scale[0] = 1.0f; + out_node->scale[1] = 1.0f; + out_node->scale[2] = 1.0f; + out_node->matrix[0] = 1.0f; + out_node->matrix[5] = 1.0f; + out_node->matrix[10] = 1.0f; + out_node->matrix[15] = 1.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_node->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "children") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_node->children, &out_node->children_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_node->children_count; ++k) + { + out_node->children[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mesh") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->mesh = CGLTF_PTRINDEX(cgltf_mesh, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skin") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->skin = CGLTF_PTRINDEX(cgltf_skin, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "camera") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->camera = CGLTF_PTRINDEX(cgltf_camera, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_node->has_translation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->translation, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_node->has_rotation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->rotation, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_node->has_scale = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->scale, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "matrix") == 0) + { + out_node->has_matrix = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->matrix, 16); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_node->weights, &out_node->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_node->weights, (int)out_node->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_node->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_node->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_node->extensions_count= 0; + out_node->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_node->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "light") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->light = CGLTF_PTRINDEX(cgltf_light, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_mesh_gpu_instancing") == 0) + { + out_node->has_mesh_gpu_instancing = 1; + i = cgltf_parse_json_mesh_gpu_instancing(options, tokens, i + 1, json_chunk, &out_node->mesh_gpu_instancing); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_node->extensions[out_node->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_nodes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_node), (void**)&out_data->nodes, &out_data->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->nodes_count; ++j) + { + i = cgltf_parse_json_node(options, tokens, i, json_chunk, &out_data->nodes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_scene(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_scene* out_scene) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_scene->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_scene->nodes, &out_scene->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_scene->nodes_count; ++k) + { + out_scene->nodes[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_scene->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_scene->extensions_count, &out_scene->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_scenes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_scene), (void**)&out_data->scenes, &out_data->scenes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->scenes_count; ++j) + { + i = cgltf_parse_json_scene(options, tokens, i, json_chunk, &out_data->scenes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_animation_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "input") == 0) + { + ++i; + out_sampler->input = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "output") == 0) + { + ++i; + out_sampler->output = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "interpolation") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "LINEAR") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_linear; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "STEP") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_step; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "CUBICSPLINE") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_cubic_spline; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation_channel(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_channel* out_channel) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sampler") == 0) + { + ++i; + out_channel->sampler = CGLTF_PTRINDEX(cgltf_animation_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int target_size = tokens[i].size; + ++i; + + for (int k = 0; k < target_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "node") == 0) + { + ++i; + out_channel->target_node = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "path") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_translation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_rotation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_channel->target_path = cgltf_animation_path_type_scale; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "weights") == 0) + { + out_channel->target_path = cgltf_animation_path_type_weights; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_channel->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_channel->extensions_count, &out_channel->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation* out_animation) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_animation->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_sampler), (void**)&out_animation->samplers, &out_animation->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->samplers_count; ++k) + { + i = cgltf_parse_json_animation_sampler(options, tokens, i, json_chunk, &out_animation->samplers[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "channels") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_channel), (void**)&out_animation->channels, &out_animation->channels_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->channels_count; ++k) + { + i = cgltf_parse_json_animation_channel(options, tokens, i, json_chunk, &out_animation->channels[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_animation->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_animation->extensions_count, &out_animation->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animations(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_animation), (void**)&out_data->animations, &out_data->animations_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->animations_count; ++j) + { + i = cgltf_parse_json_animation(options, tokens, i, json_chunk, &out_data->animations[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_variant(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_variant* out_variant) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_variant->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_variant->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_variants(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material_variant), (void**)&out_data->variants, &out_data->variants_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->variants_count; ++j) + { + i = cgltf_parse_json_variant(options, tokens, i, json_chunk, &out_data->variants[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_asset(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_asset* out_asset) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "copyright") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->copyright); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "generator") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->generator); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "version") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->version); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "minVersion") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->min_version); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_asset->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_asset->extensions_count, &out_asset->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (out_asset->version && CGLTF_ATOF(out_asset->version) < 2) + { + return CGLTF_ERROR_LEGACY; + } + + return i; +} + +cgltf_size cgltf_num_components(cgltf_type type) { + switch (type) + { + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + case cgltf_type_mat2: + return 4; + case cgltf_type_mat3: + return 9; + case cgltf_type_mat4: + return 16; + case cgltf_type_invalid: + case cgltf_type_scalar: + default: + return 1; + } +} + +cgltf_size cgltf_component_size(cgltf_component_type component_type) { + switch (component_type) + { + case cgltf_component_type_r_8: + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + case cgltf_component_type_r_32f: + return 4; + case cgltf_component_type_invalid: + default: + return 0; + } +} + +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type) +{ + cgltf_size component_size = cgltf_component_size(component_type); + if (type == cgltf_type_mat2 && component_size == 1) + { + return 8 * component_size; + } + else if (type == cgltf_type_mat3 && (component_size == 1 || component_size == 2)) + { + return 12 * component_size; + } + return component_size * cgltf_num_components(type); +} + +static int cgltf_fixup_pointers(cgltf_data* out_data); + +static int cgltf_parse_json_root(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "asset") == 0) + { + i = cgltf_parse_json_asset(options, tokens, i + 1, json_chunk, &out_data->asset); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "meshes") == 0) + { + i = cgltf_parse_json_meshes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "accessors") == 0) + { + i = cgltf_parse_json_accessors(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferViews") == 0) + { + i = cgltf_parse_json_buffer_views(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "buffers") == 0) + { + i = cgltf_parse_json_buffers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "materials") == 0) + { + i = cgltf_parse_json_materials(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "images") == 0) + { + i = cgltf_parse_json_images(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "textures") == 0) + { + i = cgltf_parse_json_textures(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_samplers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "skins") == 0) + { + i = cgltf_parse_json_skins(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "cameras") == 0) + { + i = cgltf_parse_json_cameras(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_nodes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scenes") == 0) + { + i = cgltf_parse_json_scenes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scene") == 0) + { + ++i; + out_data->scene = CGLTF_PTRINDEX(cgltf_scene, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "animations") == 0) + { + i = cgltf_parse_json_animations(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_data->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_data->data_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_data->data_extensions_count = 0; + out_data->data_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_data->data_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "lights") == 0) + { + i = cgltf_parse_json_lights(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + i = cgltf_parse_json_variants(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_data->data_extensions[out_data->data_extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsUsed") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_used, &out_data->extensions_used_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsRequired") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_required, &out_data->extensions_required_count); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data) +{ + jsmn_parser parser = { 0, 0, 0 }; + + if (options->json_token_count == 0) + { + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, NULL, 0); + + if (token_count <= 0) + { + return cgltf_result_invalid_json; + } + + options->json_token_count = token_count; + } + + jsmntok_t* tokens = (jsmntok_t*)options->memory.alloc_func(options->memory.user_data, sizeof(jsmntok_t) * (options->json_token_count + 1)); + + if (!tokens) + { + return cgltf_result_out_of_memory; + } + + jsmn_init(&parser); + + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, tokens, options->json_token_count); + + if (token_count <= 0) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_invalid_json; + } + + // this makes sure that we always have an UNDEFINED token at the end of the stream + // for invalid JSON inputs this makes sure we don't perform out of bound reads of token data + tokens[token_count].type = JSMN_UNDEFINED; + + cgltf_data* data = (cgltf_data*)options->memory.alloc_func(options->memory.user_data, sizeof(cgltf_data)); + + if (!data) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_out_of_memory; + } + + memset(data, 0, sizeof(cgltf_data)); + data->memory = options->memory; + data->file = options->file; + + int i = cgltf_parse_json_root(options, tokens, 0, json_chunk, data); + + options->memory.free_func(options->memory.user_data, tokens); + + if (i < 0) + { + cgltf_free(data); + + switch (i) + { + case CGLTF_ERROR_NOMEM: return cgltf_result_out_of_memory; + case CGLTF_ERROR_LEGACY: return cgltf_result_legacy_gltf; + default: return cgltf_result_invalid_gltf; + } + } + + if (cgltf_fixup_pointers(data) < 0) + { + cgltf_free(data); + return cgltf_result_invalid_gltf; + } + + data->json = (const char*)json_chunk; + data->json_size = size; + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_fixup_pointers(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].indices, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].material, data->materials, data->materials_count); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].attributes[k].data, data->accessors, data->accessors_count); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].targets[k].attributes[m].data, data->accessors, data->accessors_count); + } + } + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.buffer_view, data->buffer_views, data->buffer_views_count); + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.attributes[m].data, data->accessors, data->accessors_count); + } + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].mappings[k].material, data->materials, data->materials_count); + } + } + } + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + CGLTF_PTRFIXUP(data->accessors[i].buffer_view, data->buffer_views, data->buffer_views_count); + + if (data->accessors[i].is_sparse) + { + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.indices_buffer_view, data->buffer_views, data->buffer_views_count); + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.values_buffer_view, data->buffer_views, data->buffer_views_count); + } + + if (data->accessors[i].buffer_view) + { + data->accessors[i].stride = data->accessors[i].buffer_view->stride; + } + + if (data->accessors[i].stride == 0) + { + data->accessors[i].stride = cgltf_calc_size(data->accessors[i].type, data->accessors[i].component_type); + } + } + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].webp_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count); + } + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + CGLTF_PTRFIXUP(data->images[i].buffer_view, data->buffer_views, data->buffer_views_count); + } + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + CGLTF_PTRFIXUP(data->materials[i].normal_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].emissive_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].occlusion_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.base_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.diffuse_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_roughness_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_normal_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].specular.specular_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].specular.specular_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].transmission.transmission_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].volume.thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].anisotropy.anisotropy_texture.texture, data->textures, data->textures_count); + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].buffer, data->buffers, data->buffers_count); + + if (data->buffer_views[i].has_meshopt_compression) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].meshopt_compression.buffer, data->buffers, data->buffers_count); + } + } + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + for (cgltf_size j = 0; j < data->skins[i].joints_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->skins[i].joints[j], data->nodes, data->nodes_count); + } + + CGLTF_PTRFIXUP(data->skins[i].skeleton, data->nodes, data->nodes_count); + CGLTF_PTRFIXUP(data->skins[i].inverse_bind_matrices, data->accessors, data->accessors_count); + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + for (cgltf_size j = 0; j < data->nodes[i].children_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].children[j], data->nodes, data->nodes_count); + + if (data->nodes[i].children[j]->parent) + { + return CGLTF_ERROR_JSON; + } + + data->nodes[i].children[j]->parent = &data->nodes[i]; + } + + CGLTF_PTRFIXUP(data->nodes[i].mesh, data->meshes, data->meshes_count); + CGLTF_PTRFIXUP(data->nodes[i].skin, data->skins, data->skins_count); + CGLTF_PTRFIXUP(data->nodes[i].camera, data->cameras, data->cameras_count); + CGLTF_PTRFIXUP(data->nodes[i].light, data->lights, data->lights_count); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size m = 0; m < data->nodes[i].mesh_gpu_instancing.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].mesh_gpu_instancing.attributes[m].data, data->accessors, data->accessors_count); + } + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->scenes[i].nodes[j], data->nodes, data->nodes_count); + + if (data->scenes[i].nodes[j]->parent) + { + return CGLTF_ERROR_JSON; + } + } + } + + CGLTF_PTRFIXUP(data->scene, data->scenes, data->scenes_count); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].input, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].output, data->accessors, data->accessors_count); + } + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].channels[j].sampler, data->animations[i].samplers, data->animations[i].samplers_count); + CGLTF_PTRFIXUP(data->animations[i].channels[j].target_node, data->nodes, data->nodes_count); + } + } + + return 0; +} + +/* + * -- jsmn.c start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + * + * Copyright (c) 2010 Serge A. Zaitsev + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + ptrdiff_t start, ptrdiff_t end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + ptrdiff_t start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + ptrdiff_t start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, size_t num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +static void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} +/* + * -- jsmn.c end -- + */ + +#endif /* #ifdef CGLTF_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2018-2021 Johannes Kuhlmann + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/Engine/cpp/ThirdParty/cgltf/cgltf_write.h b/Engine/cpp/ThirdParty/cgltf/cgltf_write.h new file mode 100644 index 00000000..4c060da7 --- /dev/null +++ b/Engine/cpp/ThirdParty/cgltf/cgltf_write.h @@ -0,0 +1,1565 @@ +/** + * cgltf_write - a single-file glTF 2.0 writer written in C99. + * + * Version: 1.15 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_WRITE_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_write_file(const cgltf_options* options, const char* + * path, const cgltf_data* data)` writes a glTF data to the given file path. + * If `options->type` is `cgltf_file_type_glb`, both JSON content and binary + * buffer of the given glTF data will be written in a GLB format. + * Otherwise, only the JSON part will be written. + * External buffers and images are not written out. `data` is not deallocated. + * + * `cgltf_size cgltf_write(const cgltf_options* options, char* buffer, + * cgltf_size size, const cgltf_data* data)` writes JSON into the given memory + * buffer. Returns the number of bytes written to `buffer`, including a null + * terminator. If buffer is null, returns the number of bytes that would have + * been written. `data` is not deallocated. + */ +#ifndef CGLTF_WRITE_H_INCLUDED__ +#define CGLTF_WRITE_H_INCLUDED__ + +#include "cgltf.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +cgltf_result cgltf_write_file(const cgltf_options* options, const char* path, const cgltf_data* data); +cgltf_size cgltf_write(const cgltf_options* options, char* buffer, cgltf_size size, const cgltf_data* data); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_WRITE_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_WRITE_IMPLEMENTATION +#endif + +#ifdef CGLTF_WRITE_IMPLEMENTATION + +#include +#include +#include +#include +#include +#include + +#define CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM (1 << 0) +#define CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT (1 << 1) +#define CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS (1 << 2) +#define CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL (1 << 3) +#define CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION (1 << 4) +#define CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT (1 << 5) +#define CGLTF_EXTENSION_FLAG_MATERIALS_IOR (1 << 6) +#define CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR (1 << 7) +#define CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION (1 << 8) +#define CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN (1 << 9) +#define CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS (1 << 10) +#define CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME (1 << 11) +#define CGLTF_EXTENSION_FLAG_TEXTURE_BASISU (1 << 12) +#define CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH (1 << 13) +#define CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING (1 << 14) +#define CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE (1 << 15) +#define CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY (1 << 16) +#define CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION (1 << 17) +#define CGLTF_EXTENSION_FLAG_TEXTURE_WEBP (1 << 18) +#define CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION (1 << 19) + +typedef struct { + char* buffer; + cgltf_size buffer_size; + cgltf_size remaining; + char* cursor; + cgltf_size tmp; + cgltf_size chars_written; + const cgltf_data* data; + int depth; + const char* indent; + int needs_comma; + uint32_t extension_flags; + uint32_t required_extension_flags; +} cgltf_write_context; + +#define CGLTF_MIN(a, b) (a < b ? a : b) + +#ifdef FLT_DECIMAL_DIG + // FLT_DECIMAL_DIG is C11 + #define CGLTF_DECIMAL_DIG (FLT_DECIMAL_DIG) +#else + #define CGLTF_DECIMAL_DIG 9 +#endif + +#define CGLTF_SPRINTF(...) { \ + assert(context->cursor || (!context->cursor && context->remaining == 0)); \ + context->tmp = snprintf ( context->cursor, context->remaining, __VA_ARGS__ ); \ + context->chars_written += context->tmp; \ + if (context->cursor) { \ + context->cursor += context->tmp; \ + context->remaining -= context->tmp; \ + } } + +#define CGLTF_SNPRINTF(length, ...) { \ + assert(context->cursor || (!context->cursor && context->remaining == 0)); \ + context->tmp = snprintf ( context->cursor, CGLTF_MIN(length + 1, context->remaining), __VA_ARGS__ ); \ + context->chars_written += length; \ + if (context->cursor) { \ + context->cursor += length; \ + context->remaining -= length; \ + } } + +#define CGLTF_WRITE_IDXPROP(label, val, start) if (val) { \ + cgltf_write_indent(context); \ + CGLTF_SPRINTF("\"%s\": %d", label, (int) (val - start)); \ + context->needs_comma = 1; } + +#define CGLTF_WRITE_IDXARRPROP(label, dim, vals, start) if (vals) { \ + cgltf_write_indent(context); \ + CGLTF_SPRINTF("\"%s\": [", label); \ + for (int i = 0; i < (int)(dim); ++i) { \ + int idx = (int) (vals[i] - start); \ + if (i != 0) CGLTF_SPRINTF(","); \ + CGLTF_SPRINTF(" %d", idx); \ + } \ + CGLTF_SPRINTF(" ]"); \ + context->needs_comma = 1; } + +#define CGLTF_WRITE_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#define CGLTF_WRITE_NORMAL_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + cgltf_write_floatprop(context, "scale", info.scale, 1.0f); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#define CGLTF_WRITE_OCCLUSION_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + cgltf_write_floatprop(context, "strength", info.scale, 1.0f); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#ifndef CGLTF_CONSTS +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; +#define CGLTF_CONSTS +#endif + +static void cgltf_write_indent(cgltf_write_context* context) +{ + if (context->needs_comma) + { + CGLTF_SPRINTF(",\n"); + context->needs_comma = 0; + } + else + { + CGLTF_SPRINTF("\n"); + } + for (int i = 0; i < context->depth; ++i) + { + CGLTF_SPRINTF("%s", context->indent); + } +} + +static void cgltf_write_line(cgltf_write_context* context, const char* line) +{ + if (line[0] == ']' || line[0] == '}') + { + --context->depth; + context->needs_comma = 0; + } + cgltf_write_indent(context); + CGLTF_SPRINTF("%s", line); + cgltf_size last = (cgltf_size)(strlen(line) - 1); + if (line[0] == ']' || line[0] == '}') + { + context->needs_comma = 1; + } + if (line[last] == '[' || line[last] == '{') + { + ++context->depth; + context->needs_comma = 0; + } +} + +static void cgltf_write_strprop(cgltf_write_context* context, const char* label, const char* val) +{ + if (val) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": \"%s\"", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_extras(cgltf_write_context* context, const cgltf_extras* extras) +{ + if (extras->data) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"extras\": %s", extras->data); + context->needs_comma = 1; + } + else + { + cgltf_size length = extras->end_offset - extras->start_offset; + if (length > 0 && context->data->json) + { + char* json_string = ((char*) context->data->json) + extras->start_offset; + cgltf_write_indent(context); + CGLTF_SPRINTF("%s", "\"extras\": "); + CGLTF_SNPRINTF(length, "%.*s", (int)(extras->end_offset - extras->start_offset), json_string); + context->needs_comma = 1; + } + } +} + +static void cgltf_write_stritem(cgltf_write_context* context, const char* item) +{ + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\"", item); + context->needs_comma = 1; +} + +static void cgltf_write_intprop(cgltf_write_context* context, const char* label, int val, int def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %d", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_sizeprop(cgltf_write_context* context, const char* label, cgltf_size val, cgltf_size def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %zu", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_floatprop(cgltf_write_context* context, const char* label, float val, float def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": ", label); + CGLTF_SPRINTF("%.*g", CGLTF_DECIMAL_DIG, val); + context->needs_comma = 1; + + if (context->cursor) + { + char *decimal_comma = strchr(context->cursor - context->tmp, ','); + if (decimal_comma) + { + *decimal_comma = '.'; + } + } + } +} + +static void cgltf_write_boolprop_optional(cgltf_write_context* context, const char* label, bool val, bool def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %s", label, val ? "true" : "false"); + context->needs_comma = 1; + } +} + +static void cgltf_write_floatarrayprop(cgltf_write_context* context, const char* label, const cgltf_float* vals, cgltf_size dim) +{ + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": [", label); + for (cgltf_size i = 0; i < dim; ++i) + { + if (i != 0) + { + CGLTF_SPRINTF(", %.*g", CGLTF_DECIMAL_DIG, vals[i]); + } + else + { + CGLTF_SPRINTF("%.*g", CGLTF_DECIMAL_DIG, vals[i]); + } + } + CGLTF_SPRINTF("]"); + context->needs_comma = 1; +} + +static bool cgltf_check_floatarray(const float* vals, int dim, float val) { + while (dim--) + { + if (vals[dim] != val) + { + return true; + } + } + return false; +} + +static int cgltf_int_from_component_type(cgltf_component_type ctype) +{ + switch (ctype) + { + case cgltf_component_type_r_8: return 5120; + case cgltf_component_type_r_8u: return 5121; + case cgltf_component_type_r_16: return 5122; + case cgltf_component_type_r_16u: return 5123; + case cgltf_component_type_r_32u: return 5125; + case cgltf_component_type_r_32f: return 5126; + default: return 0; + } +} + +static int cgltf_int_from_primitive_type(cgltf_primitive_type ctype) +{ + switch (ctype) + { + case cgltf_primitive_type_points: return 0; + case cgltf_primitive_type_lines: return 1; + case cgltf_primitive_type_line_loop: return 2; + case cgltf_primitive_type_line_strip: return 3; + case cgltf_primitive_type_triangles: return 4; + case cgltf_primitive_type_triangle_strip: return 5; + case cgltf_primitive_type_triangle_fan: return 6; + default: return -1; + } +} + +static const char* cgltf_str_from_alpha_mode(cgltf_alpha_mode alpha_mode) +{ + switch (alpha_mode) + { + case cgltf_alpha_mode_mask: return "MASK"; + case cgltf_alpha_mode_blend: return "BLEND"; + default: return NULL; + } +} + +static const char* cgltf_str_from_type(cgltf_type type) +{ + switch (type) + { + case cgltf_type_scalar: return "SCALAR"; + case cgltf_type_vec2: return "VEC2"; + case cgltf_type_vec3: return "VEC3"; + case cgltf_type_vec4: return "VEC4"; + case cgltf_type_mat2: return "MAT2"; + case cgltf_type_mat3: return "MAT3"; + case cgltf_type_mat4: return "MAT4"; + default: return NULL; + } +} + +static cgltf_size cgltf_dim_from_type(cgltf_type type) +{ + switch (type) + { + case cgltf_type_scalar: return 1; + case cgltf_type_vec2: return 2; + case cgltf_type_vec3: return 3; + case cgltf_type_vec4: return 4; + case cgltf_type_mat2: return 4; + case cgltf_type_mat3: return 9; + case cgltf_type_mat4: return 16; + default: return 0; + } +} + +static const char* cgltf_str_from_camera_type(cgltf_camera_type camera_type) +{ + switch (camera_type) + { + case cgltf_camera_type_perspective: return "perspective"; + case cgltf_camera_type_orthographic: return "orthographic"; + default: return NULL; + } +} + +static const char* cgltf_str_from_light_type(cgltf_light_type light_type) +{ + switch (light_type) + { + case cgltf_light_type_directional: return "directional"; + case cgltf_light_type_point: return "point"; + case cgltf_light_type_spot: return "spot"; + default: return NULL; + } +} + +static void cgltf_write_texture_transform(cgltf_write_context* context, const cgltf_texture_transform* transform) +{ + cgltf_write_line(context, "\"extensions\": {"); + cgltf_write_line(context, "\"KHR_texture_transform\": {"); + if (cgltf_check_floatarray(transform->offset, 2, 0.0f)) + { + cgltf_write_floatarrayprop(context, "offset", transform->offset, 2); + } + cgltf_write_floatprop(context, "rotation", transform->rotation, 0.0f); + if (cgltf_check_floatarray(transform->scale, 2, 1.0f)) + { + cgltf_write_floatarrayprop(context, "scale", transform->scale, 2); + } + if (transform->has_texcoord) + { + cgltf_write_intprop(context, "texCoord", transform->texcoord, -1); + } + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_asset(cgltf_write_context* context, const cgltf_asset* asset) +{ + cgltf_write_line(context, "\"asset\": {"); + cgltf_write_strprop(context, "copyright", asset->copyright); + cgltf_write_strprop(context, "generator", asset->generator); + cgltf_write_strprop(context, "version", asset->version); + cgltf_write_strprop(context, "min_version", asset->min_version); + cgltf_write_extras(context, &asset->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_primitive(cgltf_write_context* context, const cgltf_primitive* prim) +{ + cgltf_write_intprop(context, "mode", cgltf_int_from_primitive_type(prim->type), 4); + CGLTF_WRITE_IDXPROP("indices", prim->indices, context->data->accessors); + CGLTF_WRITE_IDXPROP("material", prim->material, context->data->materials); + cgltf_write_line(context, "\"attributes\": {"); + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = prim->attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + + if (prim->targets_count) + { + cgltf_write_line(context, "\"targets\": ["); + for (cgltf_size i = 0; i < prim->targets_count; ++i) + { + cgltf_write_line(context, "{"); + for (cgltf_size j = 0; j < prim->targets[i].attributes_count; ++j) + { + const cgltf_attribute* attr = prim->targets[i].attributes + j; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + } + cgltf_write_extras(context, &prim->extras); + + if (prim->has_draco_mesh_compression || prim->mappings_count > 0) + { + cgltf_write_line(context, "\"extensions\": {"); + + if (prim->has_draco_mesh_compression) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION; + if (prim->attributes_count == 0 || prim->indices == 0) + { + context->required_extension_flags |= CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION; + } + + cgltf_write_line(context, "\"KHR_draco_mesh_compression\": {"); + CGLTF_WRITE_IDXPROP("bufferView", prim->draco_mesh_compression.buffer_view, context->data->buffer_views); + cgltf_write_line(context, "\"attributes\": {"); + for (cgltf_size i = 0; i < prim->draco_mesh_compression.attributes_count; ++i) + { + const cgltf_attribute* attr = prim->draco_mesh_compression.attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); + } + + if (prim->mappings_count > 0) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS; + cgltf_write_line(context, "\"KHR_materials_variants\": {"); + cgltf_write_line(context, "\"mappings\": ["); + for (cgltf_size i = 0; i < prim->mappings_count; ++i) + { + const cgltf_material_mapping* map = prim->mappings + i; + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("material", map->material, context->data->materials); + + cgltf_write_indent(context); + CGLTF_SPRINTF("\"variants\": [%d]", (int)map->variant); + context->needs_comma = 1; + + cgltf_write_extras(context, &map->extras); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + cgltf_write_line(context, "}"); + } +} + +static void cgltf_write_mesh(cgltf_write_context* context, const cgltf_mesh* mesh) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", mesh->name); + + cgltf_write_line(context, "\"primitives\": ["); + for (cgltf_size i = 0; i < mesh->primitives_count; ++i) + { + cgltf_write_line(context, "{"); + cgltf_write_primitive(context, mesh->primitives + i); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + + if (mesh->weights_count > 0) + { + cgltf_write_floatarrayprop(context, "weights", mesh->weights, mesh->weights_count); + } + + cgltf_write_extras(context, &mesh->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_buffer_view(cgltf_write_context* context, const cgltf_buffer_view* view) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", view->name); + CGLTF_WRITE_IDXPROP("buffer", view->buffer, context->data->buffers); + cgltf_write_sizeprop(context, "byteLength", view->size, (cgltf_size)-1); + cgltf_write_sizeprop(context, "byteOffset", view->offset, 0); + cgltf_write_sizeprop(context, "byteStride", view->stride, 0); + // NOTE: We skip writing "target" because the spec says its usage can be inferred. + cgltf_write_extras(context, &view->extras); + cgltf_write_line(context, "}"); +} + + +static void cgltf_write_buffer(cgltf_write_context* context, const cgltf_buffer* buffer) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", buffer->name); + cgltf_write_strprop(context, "uri", buffer->uri); + cgltf_write_sizeprop(context, "byteLength", buffer->size, (cgltf_size)-1); + cgltf_write_extras(context, &buffer->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_material(cgltf_write_context* context, const cgltf_material* material) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", material->name); + if (material->alpha_mode == cgltf_alpha_mode_mask) + { + cgltf_write_floatprop(context, "alphaCutoff", material->alpha_cutoff, 0.5f); + } + cgltf_write_boolprop_optional(context, "doubleSided", (bool)material->double_sided, false); + // cgltf_write_boolprop_optional(context, "unlit", material->unlit, false); + + if (material->unlit) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT; + } + + if (material->has_pbr_specular_glossiness) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS; + } + + if (material->has_clearcoat) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT; + } + + if (material->has_transmission) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION; + } + + if (material->has_volume) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME; + } + + if (material->has_ior) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_IOR; + } + + if (material->has_specular) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR; + } + + if (material->has_sheen) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN; + } + + if (material->has_emissive_strength) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH; + } + + if (material->has_iridescence) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE; + } + + if (material->has_diffuse_transmission) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION; + } + + if (material->has_anisotropy) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY; + } + + if (material->has_dispersion) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION; + } + + if (material->has_pbr_metallic_roughness) + { + const cgltf_pbr_metallic_roughness* params = &material->pbr_metallic_roughness; + cgltf_write_line(context, "\"pbrMetallicRoughness\": {"); + CGLTF_WRITE_TEXTURE_INFO("baseColorTexture", params->base_color_texture); + CGLTF_WRITE_TEXTURE_INFO("metallicRoughnessTexture", params->metallic_roughness_texture); + cgltf_write_floatprop(context, "metallicFactor", params->metallic_factor, 1.0f); + cgltf_write_floatprop(context, "roughnessFactor", params->roughness_factor, 1.0f); + if (cgltf_check_floatarray(params->base_color_factor, 4, 1.0f)) + { + cgltf_write_floatarrayprop(context, "baseColorFactor", params->base_color_factor, 4); + } + cgltf_write_line(context, "}"); + } + + if (material->unlit || material->has_pbr_specular_glossiness || material->has_clearcoat || material->has_ior || material->has_specular || material->has_transmission || material->has_sheen || material->has_volume || material->has_emissive_strength || material->has_iridescence || material->has_anisotropy || material->has_dispersion || material->has_diffuse_transmission) + { + cgltf_write_line(context, "\"extensions\": {"); + if (material->has_clearcoat) + { + const cgltf_clearcoat* params = &material->clearcoat; + cgltf_write_line(context, "\"KHR_materials_clearcoat\": {"); + CGLTF_WRITE_TEXTURE_INFO("clearcoatTexture", params->clearcoat_texture); + CGLTF_WRITE_TEXTURE_INFO("clearcoatRoughnessTexture", params->clearcoat_roughness_texture); + CGLTF_WRITE_NORMAL_TEXTURE_INFO("clearcoatNormalTexture", params->clearcoat_normal_texture); + cgltf_write_floatprop(context, "clearcoatFactor", params->clearcoat_factor, 0.0f); + cgltf_write_floatprop(context, "clearcoatRoughnessFactor", params->clearcoat_roughness_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_ior) + { + const cgltf_ior* params = &material->ior; + cgltf_write_line(context, "\"KHR_materials_ior\": {"); + cgltf_write_floatprop(context, "ior", params->ior, 1.5f); + cgltf_write_line(context, "}"); + } + if (material->has_specular) + { + const cgltf_specular* params = &material->specular; + cgltf_write_line(context, "\"KHR_materials_specular\": {"); + CGLTF_WRITE_TEXTURE_INFO("specularTexture", params->specular_texture); + CGLTF_WRITE_TEXTURE_INFO("specularColorTexture", params->specular_color_texture); + cgltf_write_floatprop(context, "specularFactor", params->specular_factor, 1.0f); + if (cgltf_check_floatarray(params->specular_color_factor, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "specularColorFactor", params->specular_color_factor, 3); + } + cgltf_write_line(context, "}"); + } + if (material->has_transmission) + { + const cgltf_transmission* params = &material->transmission; + cgltf_write_line(context, "\"KHR_materials_transmission\": {"); + CGLTF_WRITE_TEXTURE_INFO("transmissionTexture", params->transmission_texture); + cgltf_write_floatprop(context, "transmissionFactor", params->transmission_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_volume) + { + const cgltf_volume* params = &material->volume; + cgltf_write_line(context, "\"KHR_materials_volume\": {"); + CGLTF_WRITE_TEXTURE_INFO("thicknessTexture", params->thickness_texture); + cgltf_write_floatprop(context, "thicknessFactor", params->thickness_factor, 0.0f); + if (cgltf_check_floatarray(params->attenuation_color, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "attenuationColor", params->attenuation_color, 3); + } + if (params->attenuation_distance < FLT_MAX) + { + cgltf_write_floatprop(context, "attenuationDistance", params->attenuation_distance, FLT_MAX); + } + cgltf_write_line(context, "}"); + } + if (material->has_sheen) + { + const cgltf_sheen* params = &material->sheen; + cgltf_write_line(context, "\"KHR_materials_sheen\": {"); + CGLTF_WRITE_TEXTURE_INFO("sheenColorTexture", params->sheen_color_texture); + CGLTF_WRITE_TEXTURE_INFO("sheenRoughnessTexture", params->sheen_roughness_texture); + if (cgltf_check_floatarray(params->sheen_color_factor, 3, 0.0f)) + { + cgltf_write_floatarrayprop(context, "sheenColorFactor", params->sheen_color_factor, 3); + } + cgltf_write_floatprop(context, "sheenRoughnessFactor", params->sheen_roughness_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_pbr_specular_glossiness) + { + const cgltf_pbr_specular_glossiness* params = &material->pbr_specular_glossiness; + cgltf_write_line(context, "\"KHR_materials_pbrSpecularGlossiness\": {"); + CGLTF_WRITE_TEXTURE_INFO("diffuseTexture", params->diffuse_texture); + CGLTF_WRITE_TEXTURE_INFO("specularGlossinessTexture", params->specular_glossiness_texture); + if (cgltf_check_floatarray(params->diffuse_factor, 4, 1.0f)) + { + cgltf_write_floatarrayprop(context, "diffuseFactor", params->diffuse_factor, 4); + } + if (cgltf_check_floatarray(params->specular_factor, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "specularFactor", params->specular_factor, 3); + } + cgltf_write_floatprop(context, "glossinessFactor", params->glossiness_factor, 1.0f); + cgltf_write_line(context, "}"); + } + if (material->unlit) + { + cgltf_write_line(context, "\"KHR_materials_unlit\": {}"); + } + if (material->has_emissive_strength) + { + cgltf_write_line(context, "\"KHR_materials_emissive_strength\": {"); + const cgltf_emissive_strength* params = &material->emissive_strength; + cgltf_write_floatprop(context, "emissiveStrength", params->emissive_strength, 1.f); + cgltf_write_line(context, "}"); + } + if (material->has_iridescence) + { + cgltf_write_line(context, "\"KHR_materials_iridescence\": {"); + const cgltf_iridescence* params = &material->iridescence; + cgltf_write_floatprop(context, "iridescenceFactor", params->iridescence_factor, 0.f); + CGLTF_WRITE_TEXTURE_INFO("iridescenceTexture", params->iridescence_texture); + cgltf_write_floatprop(context, "iridescenceIor", params->iridescence_ior, 1.3f); + cgltf_write_floatprop(context, "iridescenceThicknessMinimum", params->iridescence_thickness_min, 100.f); + cgltf_write_floatprop(context, "iridescenceThicknessMaximum", params->iridescence_thickness_max, 400.f); + CGLTF_WRITE_TEXTURE_INFO("iridescenceThicknessTexture", params->iridescence_thickness_texture); + cgltf_write_line(context, "}"); + } + if (material->has_diffuse_transmission) + { + const cgltf_diffuse_transmission* params = &material->diffuse_transmission; + cgltf_write_line(context, "\"KHR_materials_diffuse_transmission\": {"); + CGLTF_WRITE_TEXTURE_INFO("diffuseTransmissionTexture", params->diffuse_transmission_texture); + cgltf_write_floatprop(context, "diffuseTransmissionFactor", params->diffuse_transmission_factor, 0.f); + if (cgltf_check_floatarray(params->diffuse_transmission_color_factor, 3, 1.f)) + { + cgltf_write_floatarrayprop(context, "diffuseTransmissionColorFactor", params->diffuse_transmission_color_factor, 3); + } + CGLTF_WRITE_TEXTURE_INFO("diffuseTransmissionColorTexture", params->diffuse_transmission_color_texture); + cgltf_write_line(context, "}"); + } + if (material->has_anisotropy) + { + cgltf_write_line(context, "\"KHR_materials_anisotropy\": {"); + const cgltf_anisotropy* params = &material->anisotropy; + cgltf_write_floatprop(context, "anisotropyStrength", params->anisotropy_strength, 0.f); + cgltf_write_floatprop(context, "anisotropyRotation", params->anisotropy_rotation, 0.f); + CGLTF_WRITE_TEXTURE_INFO("anisotropyTexture", params->anisotropy_texture); + cgltf_write_line(context, "}"); + } + if (material->has_dispersion) + { + cgltf_write_line(context, "\"KHR_materials_dispersion\": {"); + const cgltf_dispersion* params = &material->dispersion; + cgltf_write_floatprop(context, "dispersion", params->dispersion, 0.f); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + + CGLTF_WRITE_NORMAL_TEXTURE_INFO("normalTexture", material->normal_texture); + CGLTF_WRITE_OCCLUSION_TEXTURE_INFO("occlusionTexture", material->occlusion_texture); + CGLTF_WRITE_TEXTURE_INFO("emissiveTexture", material->emissive_texture); + if (cgltf_check_floatarray(material->emissive_factor, 3, 0.0f)) + { + cgltf_write_floatarrayprop(context, "emissiveFactor", material->emissive_factor, 3); + } + cgltf_write_strprop(context, "alphaMode", cgltf_str_from_alpha_mode(material->alpha_mode)); + cgltf_write_extras(context, &material->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_image(cgltf_write_context* context, const cgltf_image* image) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", image->name); + cgltf_write_strprop(context, "uri", image->uri); + CGLTF_WRITE_IDXPROP("bufferView", image->buffer_view, context->data->buffer_views); + cgltf_write_strprop(context, "mimeType", image->mime_type); + cgltf_write_extras(context, &image->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_texture(cgltf_write_context* context, const cgltf_texture* texture) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", texture->name); + CGLTF_WRITE_IDXPROP("source", texture->image, context->data->images); + CGLTF_WRITE_IDXPROP("sampler", texture->sampler, context->data->samplers); + + if (texture->has_basisu || texture->has_webp) + { + cgltf_write_line(context, "\"extensions\": {"); + if (texture->has_basisu) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_BASISU; + cgltf_write_line(context, "\"KHR_texture_basisu\": {"); + CGLTF_WRITE_IDXPROP("source", texture->basisu_image, context->data->images); + cgltf_write_line(context, "}"); + } + if (texture->has_webp) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_WEBP; + cgltf_write_line(context, "\"EXT_texture_webp\": {"); + CGLTF_WRITE_IDXPROP("source", texture->webp_image, context->data->images); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &texture->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_skin(cgltf_write_context* context, const cgltf_skin* skin) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("skeleton", skin->skeleton, context->data->nodes); + CGLTF_WRITE_IDXPROP("inverseBindMatrices", skin->inverse_bind_matrices, context->data->accessors); + CGLTF_WRITE_IDXARRPROP("joints", skin->joints_count, skin->joints, context->data->nodes); + cgltf_write_strprop(context, "name", skin->name); + cgltf_write_extras(context, &skin->extras); + cgltf_write_line(context, "}"); +} + +static const char* cgltf_write_str_path_type(cgltf_animation_path_type path_type) +{ + switch (path_type) + { + case cgltf_animation_path_type_translation: + return "translation"; + case cgltf_animation_path_type_rotation: + return "rotation"; + case cgltf_animation_path_type_scale: + return "scale"; + case cgltf_animation_path_type_weights: + return "weights"; + default: + break; + } + return "invalid"; +} + +static const char* cgltf_write_str_interpolation_type(cgltf_interpolation_type interpolation_type) +{ + switch (interpolation_type) + { + case cgltf_interpolation_type_linear: + return "LINEAR"; + case cgltf_interpolation_type_step: + return "STEP"; + case cgltf_interpolation_type_cubic_spline: + return "CUBICSPLINE"; + default: + break; + } + return "invalid"; +} + +static void cgltf_write_path_type(cgltf_write_context* context, const char *label, cgltf_animation_path_type path_type) +{ + cgltf_write_strprop(context, label, cgltf_write_str_path_type(path_type)); +} + +static void cgltf_write_interpolation_type(cgltf_write_context* context, const char *label, cgltf_interpolation_type interpolation_type) +{ + cgltf_write_strprop(context, label, cgltf_write_str_interpolation_type(interpolation_type)); +} + +static void cgltf_write_animation_sampler(cgltf_write_context* context, const cgltf_animation_sampler* animation_sampler) +{ + cgltf_write_line(context, "{"); + cgltf_write_interpolation_type(context, "interpolation", animation_sampler->interpolation); + CGLTF_WRITE_IDXPROP("input", animation_sampler->input, context->data->accessors); + CGLTF_WRITE_IDXPROP("output", animation_sampler->output, context->data->accessors); + cgltf_write_extras(context, &animation_sampler->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_animation_channel(cgltf_write_context* context, const cgltf_animation* animation, const cgltf_animation_channel* animation_channel) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("sampler", animation_channel->sampler, animation->samplers); + cgltf_write_line(context, "\"target\": {"); + CGLTF_WRITE_IDXPROP("node", animation_channel->target_node, context->data->nodes); + cgltf_write_path_type(context, "path", animation_channel->target_path); + cgltf_write_line(context, "}"); + cgltf_write_extras(context, &animation_channel->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_animation(cgltf_write_context* context, const cgltf_animation* animation) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", animation->name); + + if (animation->samplers_count > 0) + { + cgltf_write_line(context, "\"samplers\": ["); + for (cgltf_size i = 0; i < animation->samplers_count; ++i) + { + cgltf_write_animation_sampler(context, animation->samplers + i); + } + cgltf_write_line(context, "]"); + } + if (animation->channels_count > 0) + { + cgltf_write_line(context, "\"channels\": ["); + for (cgltf_size i = 0; i < animation->channels_count; ++i) + { + cgltf_write_animation_channel(context, animation, animation->channels + i); + } + cgltf_write_line(context, "]"); + } + cgltf_write_extras(context, &animation->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_sampler(cgltf_write_context* context, const cgltf_sampler* sampler) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", sampler->name); + cgltf_write_intprop(context, "magFilter", sampler->mag_filter, 0); + cgltf_write_intprop(context, "minFilter", sampler->min_filter, 0); + cgltf_write_intprop(context, "wrapS", sampler->wrap_s, 10497); + cgltf_write_intprop(context, "wrapT", sampler->wrap_t, 10497); + cgltf_write_extras(context, &sampler->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_node(cgltf_write_context* context, const cgltf_node* node) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXARRPROP("children", node->children_count, node->children, context->data->nodes); + CGLTF_WRITE_IDXPROP("mesh", node->mesh, context->data->meshes); + cgltf_write_strprop(context, "name", node->name); + if (node->has_matrix) + { + cgltf_write_floatarrayprop(context, "matrix", node->matrix, 16); + } + if (node->has_translation) + { + cgltf_write_floatarrayprop(context, "translation", node->translation, 3); + } + if (node->has_rotation) + { + cgltf_write_floatarrayprop(context, "rotation", node->rotation, 4); + } + if (node->has_scale) + { + cgltf_write_floatarrayprop(context, "scale", node->scale, 3); + } + if (node->skin) + { + CGLTF_WRITE_IDXPROP("skin", node->skin, context->data->skins); + } + + bool has_extension = node->light || (node->has_mesh_gpu_instancing && node->mesh_gpu_instancing.attributes_count > 0); + if(has_extension) + cgltf_write_line(context, "\"extensions\": {"); + + if (node->light) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL; + cgltf_write_line(context, "\"KHR_lights_punctual\": {"); + CGLTF_WRITE_IDXPROP("light", node->light, context->data->lights); + cgltf_write_line(context, "}"); + } + + if (node->has_mesh_gpu_instancing && node->mesh_gpu_instancing.attributes_count > 0) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING; + context->required_extension_flags |= CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING; + + cgltf_write_line(context, "\"EXT_mesh_gpu_instancing\": {"); + { + cgltf_write_line(context, "\"attributes\": {"); + { + for (cgltf_size i = 0; i < node->mesh_gpu_instancing.attributes_count; ++i) + { + const cgltf_attribute* attr = node->mesh_gpu_instancing.attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + } + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + + if (has_extension) + cgltf_write_line(context, "}"); + + if (node->weights_count > 0) + { + cgltf_write_floatarrayprop(context, "weights", node->weights, node->weights_count); + } + + if (node->camera) + { + CGLTF_WRITE_IDXPROP("camera", node->camera, context->data->cameras); + } + + cgltf_write_extras(context, &node->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_scene(cgltf_write_context* context, const cgltf_scene* scene) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", scene->name); + CGLTF_WRITE_IDXARRPROP("nodes", scene->nodes_count, scene->nodes, context->data->nodes); + cgltf_write_extras(context, &scene->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_accessor(cgltf_write_context* context, const cgltf_accessor* accessor) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", accessor->name); + CGLTF_WRITE_IDXPROP("bufferView", accessor->buffer_view, context->data->buffer_views); + cgltf_write_intprop(context, "componentType", cgltf_int_from_component_type(accessor->component_type), 0); + cgltf_write_strprop(context, "type", cgltf_str_from_type(accessor->type)); + cgltf_size dim = cgltf_dim_from_type(accessor->type); + cgltf_write_boolprop_optional(context, "normalized", (bool)accessor->normalized, false); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->offset, 0); + cgltf_write_intprop(context, "count", (int)accessor->count, -1); + if (accessor->has_min) + { + cgltf_write_floatarrayprop(context, "min", accessor->min, dim); + } + if (accessor->has_max) + { + cgltf_write_floatarrayprop(context, "max", accessor->max, dim); + } + if (accessor->is_sparse) + { + cgltf_write_line(context, "\"sparse\": {"); + cgltf_write_intprop(context, "count", (int)accessor->sparse.count, 0); + cgltf_write_line(context, "\"indices\": {"); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->sparse.indices_byte_offset, 0); + CGLTF_WRITE_IDXPROP("bufferView", accessor->sparse.indices_buffer_view, context->data->buffer_views); + cgltf_write_intprop(context, "componentType", cgltf_int_from_component_type(accessor->sparse.indices_component_type), 0); + cgltf_write_line(context, "}"); + cgltf_write_line(context, "\"values\": {"); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->sparse.values_byte_offset, 0); + CGLTF_WRITE_IDXPROP("bufferView", accessor->sparse.values_buffer_view, context->data->buffer_views); + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &accessor->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_camera(cgltf_write_context* context, const cgltf_camera* camera) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "type", cgltf_str_from_camera_type(camera->type)); + if (camera->name) + { + cgltf_write_strprop(context, "name", camera->name); + } + + if (camera->type == cgltf_camera_type_orthographic) + { + cgltf_write_line(context, "\"orthographic\": {"); + cgltf_write_floatprop(context, "xmag", camera->data.orthographic.xmag, -1.0f); + cgltf_write_floatprop(context, "ymag", camera->data.orthographic.ymag, -1.0f); + cgltf_write_floatprop(context, "zfar", camera->data.orthographic.zfar, -1.0f); + cgltf_write_floatprop(context, "znear", camera->data.orthographic.znear, -1.0f); + cgltf_write_extras(context, &camera->data.orthographic.extras); + cgltf_write_line(context, "}"); + } + else if (camera->type == cgltf_camera_type_perspective) + { + cgltf_write_line(context, "\"perspective\": {"); + + if (camera->data.perspective.has_aspect_ratio) { + cgltf_write_floatprop(context, "aspectRatio", camera->data.perspective.aspect_ratio, -1.0f); + } + + cgltf_write_floatprop(context, "yfov", camera->data.perspective.yfov, -1.0f); + + if (camera->data.perspective.has_zfar) { + cgltf_write_floatprop(context, "zfar", camera->data.perspective.zfar, -1.0f); + } + + cgltf_write_floatprop(context, "znear", camera->data.perspective.znear, -1.0f); + cgltf_write_extras(context, &camera->data.perspective.extras); + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &camera->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_light(cgltf_write_context* context, const cgltf_light* light) +{ + context->extension_flags |= CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL; + + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "type", cgltf_str_from_light_type(light->type)); + if (light->name) + { + cgltf_write_strprop(context, "name", light->name); + } + if (cgltf_check_floatarray(light->color, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "color", light->color, 3); + } + cgltf_write_floatprop(context, "intensity", light->intensity, 1.0f); + cgltf_write_floatprop(context, "range", light->range, 0.0f); + + if (light->type == cgltf_light_type_spot) + { + cgltf_write_line(context, "\"spot\": {"); + cgltf_write_floatprop(context, "innerConeAngle", light->spot_inner_cone_angle, 0.0f); + cgltf_write_floatprop(context, "outerConeAngle", light->spot_outer_cone_angle, 3.14159265358979323846f/4.0f); + cgltf_write_line(context, "}"); + } + cgltf_write_extras( context, &light->extras ); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_variant(cgltf_write_context* context, const cgltf_material_variant* variant) +{ + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS; + + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", variant->name); + cgltf_write_extras(context, &variant->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_glb(FILE* file, const void* json_buf, const cgltf_size json_size, const void* bin_buf, const cgltf_size bin_size) +{ + char header[GlbHeaderSize]; + char chunk_header[GlbChunkHeaderSize]; + char json_pad[3] = { 0x20, 0x20, 0x20 }; + char bin_pad[3] = { 0, 0, 0 }; + + cgltf_size json_padsize = (json_size % 4 != 0) ? 4 - json_size % 4 : 0; + cgltf_size bin_padsize = (bin_size % 4 != 0) ? 4 - bin_size % 4 : 0; + cgltf_size total_size = GlbHeaderSize + GlbChunkHeaderSize + json_size + json_padsize; + if (bin_buf != NULL && bin_size > 0) { + total_size += GlbChunkHeaderSize + bin_size + bin_padsize; + } + + // Write a GLB header + memcpy(header, &GlbMagic, 4); + memcpy(header + 4, &GlbVersion, 4); + memcpy(header + 8, &total_size, 4); + fwrite(header, 1, GlbHeaderSize, file); + + // Write a JSON chunk (header & data) + uint32_t json_chunk_size = (uint32_t)(json_size + json_padsize); + memcpy(chunk_header, &json_chunk_size, 4); + memcpy(chunk_header + 4, &GlbMagicJsonChunk, 4); + fwrite(chunk_header, 1, GlbChunkHeaderSize, file); + + fwrite(json_buf, 1, json_size, file); + fwrite(json_pad, 1, json_padsize, file); + + if (bin_buf != NULL && bin_size > 0) { + // Write a binary chunk (header & data) + uint32_t bin_chunk_size = (uint32_t)(bin_size + bin_padsize); + memcpy(chunk_header, &bin_chunk_size, 4); + memcpy(chunk_header + 4, &GlbMagicBinChunk, 4); + fwrite(chunk_header, 1, GlbChunkHeaderSize, file); + + fwrite(bin_buf, 1, bin_size, file); + fwrite(bin_pad, 1, bin_padsize, file); + } +} + +cgltf_result cgltf_write_file(const cgltf_options* options, const char* path, const cgltf_data* data) +{ + cgltf_size expected = cgltf_write(options, NULL, 0, data); + char* buffer = (char*) malloc(expected); + cgltf_size actual = cgltf_write(options, buffer, expected, data); + if (expected != actual) { + fprintf(stderr, "Error: expected %zu bytes but wrote %zu bytes.\n", expected, actual); + } + FILE* file = fopen(path, "wb"); + if (!file) + { + return cgltf_result_file_not_found; + } + // Note that cgltf_write() includes a null terminator, which we omit from the file content. + if (options->type == cgltf_file_type_glb) { + cgltf_write_glb(file, buffer, actual - 1, data->bin, data->bin_size); + } else { + // Write a plain JSON file. + fwrite(buffer, actual - 1, 1, file); + } + fclose(file); + free(buffer); + return cgltf_result_success; +} + +static void cgltf_write_extensions(cgltf_write_context* context, uint32_t extension_flags) +{ + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM) { + cgltf_write_stritem(context, "KHR_texture_transform"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT) { + cgltf_write_stritem(context, "KHR_materials_unlit"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS) { + cgltf_write_stritem(context, "KHR_materials_pbrSpecularGlossiness"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL) { + cgltf_write_stritem(context, "KHR_lights_punctual"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION) { + cgltf_write_stritem(context, "KHR_draco_mesh_compression"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT) { + cgltf_write_stritem(context, "KHR_materials_clearcoat"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_IOR) { + cgltf_write_stritem(context, "KHR_materials_ior"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR) { + cgltf_write_stritem(context, "KHR_materials_specular"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION) { + cgltf_write_stritem(context, "KHR_materials_transmission"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN) { + cgltf_write_stritem(context, "KHR_materials_sheen"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS) { + cgltf_write_stritem(context, "KHR_materials_variants"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME) { + cgltf_write_stritem(context, "KHR_materials_volume"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_BASISU) { + cgltf_write_stritem(context, "KHR_texture_basisu"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_WEBP) { + cgltf_write_stritem(context, "EXT_texture_webp"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH) { + cgltf_write_stritem(context, "KHR_materials_emissive_strength"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE) { + cgltf_write_stritem(context, "KHR_materials_iridescence"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION) { + cgltf_write_stritem(context, "KHR_materials_diffuse_transmission"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY) { + cgltf_write_stritem(context, "KHR_materials_anisotropy"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING) { + cgltf_write_stritem(context, "EXT_mesh_gpu_instancing"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION) { + cgltf_write_stritem(context, "KHR_materials_dispersion"); + } +} + +cgltf_size cgltf_write(const cgltf_options* options, char* buffer, cgltf_size size, const cgltf_data* data) +{ + (void)options; + cgltf_write_context ctx; + ctx.buffer = buffer; + ctx.buffer_size = size; + ctx.remaining = size; + ctx.cursor = buffer; + ctx.chars_written = 0; + ctx.data = data; + ctx.depth = 1; + ctx.indent = " "; + ctx.needs_comma = 0; + ctx.extension_flags = 0; + ctx.required_extension_flags = 0; + + cgltf_write_context* context = &ctx; + + CGLTF_SPRINTF("{"); + + if (data->accessors_count > 0) + { + cgltf_write_line(context, "\"accessors\": ["); + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_write_accessor(context, data->accessors + i); + } + cgltf_write_line(context, "]"); + } + + cgltf_write_asset(context, &data->asset); + + if (data->buffer_views_count > 0) + { + cgltf_write_line(context, "\"bufferViews\": ["); + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_write_buffer_view(context, data->buffer_views + i); + } + cgltf_write_line(context, "]"); + } + + if (data->buffers_count > 0) + { + cgltf_write_line(context, "\"buffers\": ["); + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + cgltf_write_buffer(context, data->buffers + i); + } + cgltf_write_line(context, "]"); + } + + if (data->images_count > 0) + { + cgltf_write_line(context, "\"images\": ["); + for (cgltf_size i = 0; i < data->images_count; ++i) + { + cgltf_write_image(context, data->images + i); + } + cgltf_write_line(context, "]"); + } + + if (data->meshes_count > 0) + { + cgltf_write_line(context, "\"meshes\": ["); + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + cgltf_write_mesh(context, data->meshes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->materials_count > 0) + { + cgltf_write_line(context, "\"materials\": ["); + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + cgltf_write_material(context, data->materials + i); + } + cgltf_write_line(context, "]"); + } + + if (data->nodes_count > 0) + { + cgltf_write_line(context, "\"nodes\": ["); + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_write_node(context, data->nodes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->samplers_count > 0) + { + cgltf_write_line(context, "\"samplers\": ["); + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + cgltf_write_sampler(context, data->samplers + i); + } + cgltf_write_line(context, "]"); + } + + CGLTF_WRITE_IDXPROP("scene", data->scene, data->scenes); + + if (data->scenes_count > 0) + { + cgltf_write_line(context, "\"scenes\": ["); + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + cgltf_write_scene(context, data->scenes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->textures_count > 0) + { + cgltf_write_line(context, "\"textures\": ["); + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + cgltf_write_texture(context, data->textures + i); + } + cgltf_write_line(context, "]"); + } + + if (data->skins_count > 0) + { + cgltf_write_line(context, "\"skins\": ["); + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + cgltf_write_skin(context, data->skins + i); + } + cgltf_write_line(context, "]"); + } + + if (data->animations_count > 0) + { + cgltf_write_line(context, "\"animations\": ["); + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + cgltf_write_animation(context, data->animations + i); + } + cgltf_write_line(context, "]"); + } + + if (data->cameras_count > 0) + { + cgltf_write_line(context, "\"cameras\": ["); + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + cgltf_write_camera(context, data->cameras + i); + } + cgltf_write_line(context, "]"); + } + + if (data->lights_count > 0 || data->variants_count > 0) + { + cgltf_write_line(context, "\"extensions\": {"); + + if (data->lights_count > 0) + { + cgltf_write_line(context, "\"KHR_lights_punctual\": {"); + cgltf_write_line(context, "\"lights\": ["); + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + cgltf_write_light(context, data->lights + i); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + if (data->variants_count) + { + cgltf_write_line(context, "\"KHR_materials_variants\": {"); + cgltf_write_line(context, "\"variants\": ["); + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + cgltf_write_variant(context, data->variants + i); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + cgltf_write_line(context, "}"); + } + + if (context->extension_flags != 0) + { + cgltf_write_line(context, "\"extensionsUsed\": ["); + cgltf_write_extensions(context, context->extension_flags); + cgltf_write_line(context, "]"); + } + + if (context->required_extension_flags != 0) + { + cgltf_write_line(context, "\"extensionsRequired\": ["); + cgltf_write_extensions(context, context->required_extension_flags); + cgltf_write_line(context, "]"); + } + + cgltf_write_extras(context, &data->extras); + + CGLTF_SPRINTF("\n}\n"); + + // snprintf does not include the null terminator in its return value, so be sure to include it + // in the returned byte count. + return 1 + ctx.chars_written; +} + +#endif /* #ifdef CGLTF_WRITE_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2019-2021 Philip Rideout + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/Engine/cpp/ThirdParty/cmake/Config.cmake.in b/Engine/cpp/ThirdParty/cmake/Config.cmake.in deleted file mode 100644 index 33a3943a..00000000 --- a/Engine/cpp/ThirdParty/cmake/Config.cmake.in +++ /dev/null @@ -1,31 +0,0 @@ -@PACKAGE_INIT@ - -if(@BGFX_CMAKE_USER_SCRIPT_PRESENT@) - include("${CMAKE_CURRENT_LIST_DIR}/@BGFX_CMAKE_USER_SCRIPT_INSTALL_NAME@") -endif() -include("${CMAKE_CURRENT_LIST_DIR}/@TARGETS_EXPORT_NAME@.cmake") -get_target_property(BGFX_INCLUDE_PATH bgfx::bgfx INTERFACE_INCLUDE_DIRECTORIES) -list(GET BGFX_INCLUDE_PATH 0 BGFX_INCLUDE_PATH_1) # bgfx::bgfx exports include directory twice? -set(BGFX_SHADER_INCLUDE_PATH ${BGFX_INCLUDE_PATH_1}/bgfx) - -# If cross compiling, we need a host-compatible version of shaderc to compile shaders -macro(_bgfx_crosscompile_use_host_tool TOOL_NAME) - if(NOT TARGET bgfx::${TOOL_NAME}) - find_program( - ${TOOL_NAME}_EXECUTABLE - NAMES bgfx-${TOOL_NAME} ${TOOL_NAME} - PATHS @BGFX_ADDITIONAL_TOOL_PATHS@ /usr/bin - ) - add_executable(bgfx::${TOOL_NAME} IMPORTED) - set_target_properties(bgfx::${TOOL_NAME} PROPERTIES IMPORTED_LOCATION "${${TOOL_NAME}_EXECUTABLE}") - endif() -endmacro() - -_bgfx_crosscompile_use_host_tool(bin2c) -_bgfx_crosscompile_use_host_tool(texturec) -_bgfx_crosscompile_use_host_tool(shaderc) -_bgfx_crosscompile_use_host_tool(texturev) -_bgfx_crosscompile_use_host_tool(geometryv) - -include("${CMAKE_CURRENT_LIST_DIR}/bgfxToolUtils.cmake") -check_required_components("@PROJECT_NAME@") diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/dear-imgui.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/dear-imgui.cmake deleted file mode 100644 index baaa9fdd..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/dear-imgui.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -if(NOT DEAR_IMGUI_LIBRARIES) - file( - GLOB # - DEAR_IMGUI_SOURCES # - ${BGFX_DIR}/3rdparty/dear-imgui/*.cpp # - ${BGFX_DIR}/3rdparty/dear-imgui/*.h # - ${BGFX_DIR}/3rdparty/dear-imgui/*.inl # - ) - set(DEAR_IMGUI_INCLUDE_DIR ${BGFX_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/fcpp.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/fcpp.cmake deleted file mode 100644 index e62712cd..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/fcpp.cmake +++ /dev/null @@ -1,64 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(FCPP_DIR ${BGFX_DIR}/3rdparty/fcpp) - -file( - GLOB - FCPP_SOURCES - ${FCPP_DIR}/*.h - ${FCPP_DIR}/cpp1.c - ${FCPP_DIR}/cpp2.c - ${FCPP_DIR}/cpp3.c - ${FCPP_DIR}/cpp4.c - ${FCPP_DIR}/cpp5.c - ${FCPP_DIR}/cpp6.c - ${FCPP_DIR}/cpp6.c -) - -add_library(fcpp STATIC ${FCPP_SOURCES}) - -target_compile_definitions( - fcpp - PRIVATE "NINCLUDE=64" # - "NWORK=65536" # - "NBUFF=65536" # - "OLD_PREPROCESSOR=0" # - # "MSG_PREFIX=\"Preprocessor: \"" # -) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(fcpp PROPERTIES FOLDER "bgfx") - -target_include_directories(fcpp PUBLIC ${FCPP_DIR}) - -if(MSVC) - target_compile_options( - fcpp - PRIVATE - "/wd4055" # warning C4055: 'type cast': from data pointer 'void *' to function pointer 'void (__cdecl *)(char *,void *)' - "/wd4244" # warning C4244: '=': conversion from 'const flex_int32_t' to 'YY_CHAR', possible loss of data - "/wd4701" # warning C4701: potentially uninitialized local variable 'lower' used - "/wd4706" # warning C4706: assignment within conditional expression - ) -else() - target_compile_options( - fcpp - PRIVATE -Wno-implicit-fallthrough # - -Wno-incompatible-pointer-types # - -Wno-parentheses-equality # - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glsl-optimizer.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glsl-optimizer.cmake deleted file mode 100644 index a209a723..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glsl-optimizer.cmake +++ /dev/null @@ -1,246 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(GLSL_OPTIMIZER ${BGFX_DIR}/3rdparty/glsl-optimizer) - -file( - GLOB - GLSL_OPTIMIZER_SOURCES - ${GLSL_OPTIMIZER}/src/glsl/glcpp/glcpp.h - ${GLSL_OPTIMIZER}/src/glsl/glcpp/glcpp-lex.c - ${GLSL_OPTIMIZER}/src/glsl/glcpp/glcpp-parse.c - ${GLSL_OPTIMIZER}/src/glsl/glcpp/glcpp-parse.h - ${GLSL_OPTIMIZER}/src/glsl/glcpp/pp.c - ${GLSL_OPTIMIZER}/src/glsl/ast.h - ${GLSL_OPTIMIZER}/src/glsl/ast_array_index.cpp - ${GLSL_OPTIMIZER}/src/glsl/ast_expr.cpp - ${GLSL_OPTIMIZER}/src/glsl/ast_function.cpp - ${GLSL_OPTIMIZER}/src/glsl/ast_to_hir.cpp - ${GLSL_OPTIMIZER}/src/glsl/ast_type.cpp - ${GLSL_OPTIMIZER}/src/glsl/builtin_functions.cpp - ${GLSL_OPTIMIZER}/src/glsl/builtin_type_macros.h - ${GLSL_OPTIMIZER}/src/glsl/builtin_types.cpp - ${GLSL_OPTIMIZER}/src/glsl/builtin_variables.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_lexer.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_optimizer.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_optimizer.h - ${GLSL_OPTIMIZER}/src/glsl/glsl_parser.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_parser.h - ${GLSL_OPTIMIZER}/src/glsl/glsl_parser_extras.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_parser_extras.h - ${GLSL_OPTIMIZER}/src/glsl/glsl_symbol_table.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_symbol_table.h - ${GLSL_OPTIMIZER}/src/glsl/glsl_types.cpp - ${GLSL_OPTIMIZER}/src/glsl/glsl_types.h - ${GLSL_OPTIMIZER}/src/glsl/hir_field_selection.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir.h - ${GLSL_OPTIMIZER}/src/glsl/ir_basic_block.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_basic_block.h - ${GLSL_OPTIMIZER}/src/glsl/ir_builder.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_builder.h - ${GLSL_OPTIMIZER}/src/glsl/ir_clone.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_constant_expression.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_equals.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_expression_flattening.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_expression_flattening.h - ${GLSL_OPTIMIZER}/src/glsl/ir_function.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_function_can_inline.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_function_detect_recursion.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_function_inlining.h - ${GLSL_OPTIMIZER}/src/glsl/ir_hierarchical_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_hierarchical_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/ir_hv_accept.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_import_prototypes.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_optimization.h - ${GLSL_OPTIMIZER}/src/glsl/ir_print_glsl_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_print_glsl_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/ir_print_metal_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_print_metal_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/ir_print_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_print_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/ir_rvalue_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_rvalue_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/ir_stats.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_stats.h - ${GLSL_OPTIMIZER}/src/glsl/ir_uniform.h - ${GLSL_OPTIMIZER}/src/glsl/ir_unused_structs.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_unused_structs.h - ${GLSL_OPTIMIZER}/src/glsl/ir_validate.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_variable_refcount.cpp - ${GLSL_OPTIMIZER}/src/glsl/ir_variable_refcount.h - ${GLSL_OPTIMIZER}/src/glsl/ir_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/link_atomics.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_functions.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_interface_blocks.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_uniform_block_active_visitor.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_uniform_block_active_visitor.h - ${GLSL_OPTIMIZER}/src/glsl/link_uniform_blocks.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_uniform_initializers.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_uniforms.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_varyings.cpp - ${GLSL_OPTIMIZER}/src/glsl/link_varyings.h - ${GLSL_OPTIMIZER}/src/glsl/linker.cpp - ${GLSL_OPTIMIZER}/src/glsl/linker.h - ${GLSL_OPTIMIZER}/src/glsl/list.h - ${GLSL_OPTIMIZER}/src/glsl/loop_analysis.cpp - ${GLSL_OPTIMIZER}/src/glsl/loop_analysis.h - ${GLSL_OPTIMIZER}/src/glsl/loop_controls.cpp - ${GLSL_OPTIMIZER}/src/glsl/loop_unroll.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_clip_distance.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_discard.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_discard_flow.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_if_to_cond_assign.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_instructions.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_jumps.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_mat_op_to_vec.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_named_interface_blocks.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_noise.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_offset_array.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_output_reads.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_packed_varyings.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_packing_builtins.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_ubo_reference.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_variable_index_to_cond_assign.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_vec_index_to_cond_assign.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_vec_index_to_swizzle.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_vector.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_vector_insert.cpp - ${GLSL_OPTIMIZER}/src/glsl/lower_vertex_id.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_algebraic.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_array_splitting.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_constant_folding.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_constant_propagation.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_constant_variable.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_copy_propagation.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_copy_propagation_elements.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_cse.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_dead_builtin_variables.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_dead_builtin_varyings.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_dead_code.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_dead_code_local.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_dead_functions.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_flatten_nested_if_blocks.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_flip_matrices.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_function_inlining.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_if_simplification.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_minmax.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_noop_swizzle.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_rebalance_tree.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_redundant_jumps.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_structure_splitting.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_swizzle_swizzle.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_tree_grafting.cpp - ${GLSL_OPTIMIZER}/src/glsl/opt_vectorize.cpp - ${GLSL_OPTIMIZER}/src/glsl/program.h - ${GLSL_OPTIMIZER}/src/glsl/s_expression.cpp - ${GLSL_OPTIMIZER}/src/glsl/s_expression.h - ${GLSL_OPTIMIZER}/src/glsl/standalone_scaffolding.cpp - ${GLSL_OPTIMIZER}/src/glsl/standalone_scaffolding.h - ${GLSL_OPTIMIZER}/src/glsl/strtod.c - ${GLSL_OPTIMIZER}/src/glsl/strtod.h - ${GLSL_OPTIMIZER}/src/mesa/main/compiler.h - ${GLSL_OPTIMIZER}/src/mesa/main/config.h - ${GLSL_OPTIMIZER}/src/mesa/main/context.h - ${GLSL_OPTIMIZER}/src/mesa/main/core.h - ${GLSL_OPTIMIZER}/src/mesa/main/dd.h - ${GLSL_OPTIMIZER}/src/mesa/main/errors.h - ${GLSL_OPTIMIZER}/src/mesa/main/glheader.h - ${GLSL_OPTIMIZER}/src/mesa/main/glminimal.h - ${GLSL_OPTIMIZER}/src/mesa/main/imports.c - ${GLSL_OPTIMIZER}/src/mesa/main/imports.h - ${GLSL_OPTIMIZER}/src/mesa/main/macros.h - ${GLSL_OPTIMIZER}/src/mesa/main/mtypes.h - ${GLSL_OPTIMIZER}/src/mesa/main/simple_list.h - ${GLSL_OPTIMIZER}/src/mesa/program/hash_table.h - ${GLSL_OPTIMIZER}/src/mesa/program/prog_hash_table.c - ${GLSL_OPTIMIZER}/src/mesa/program/prog_instruction.h - ${GLSL_OPTIMIZER}/src/mesa/program/prog_parameter.h - ${GLSL_OPTIMIZER}/src/mesa/program/prog_statevars.h - ${GLSL_OPTIMIZER}/src/mesa/program/symbol_table.c - ${GLSL_OPTIMIZER}/src/mesa/program/symbol_table.h - ${GLSL_OPTIMIZER}/src/util/hash_table.c - ${GLSL_OPTIMIZER}/src/util/hash_table.h - ${GLSL_OPTIMIZER}/src/util/macros.h - ${GLSL_OPTIMIZER}/src/util/ralloc.c - ${GLSL_OPTIMIZER}/src/util/ralloc.h -) - -add_library(glsl-optimizer STATIC ${GLSL_OPTIMIZER_SOURCES}) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(glsl-optimizer PROPERTIES FOLDER "bgfx") - -target_include_directories( - glsl-optimizer - PUBLIC ${GLSL_OPTIMIZER}/include # - ${GLSL_OPTIMIZER}/src/glsl # - PRIVATE ${GLSL_OPTIMIZER}/src # - ${GLSL_OPTIMIZER}/src/mesa # - ${GLSL_OPTIMIZER}/src/mapi # -) - -if(MSVC) - target_compile_definitions( - glsl-optimizer - PRIVATE "__STDC__" # - "__STDC_VERSION__=199901L" # - "strdup=_strdup" # - "alloca=_alloca" # - "isascii=__isascii" # - ) - target_compile_options( - glsl-optimizer - PRIVATE - "/wd4100" # error C4100: '' : unreferenced formal parameter - "/wd4127" # warning C4127: conditional expression is constant - "/wd4132" # warning C4132: 'deleted_key_value': const object should be initialized - "/wd4189" # warning C4189: 'interface_type': local variable is initialized but not referenced - "/wd4204" # warning C4204: nonstandard extension used: non-constant aggregate initializer - "/wd4244" # warning C4244: '=': conversion from 'const flex_int32_t' to 'YY_CHAR', possible loss of data - "/wd4389" # warning C4389: '!=': signed/unsigned mismatch - "/wd4245" # warning C4245: 'return': conversion from 'int' to 'unsigned int', signed/unsigned mismatch - "/wd4701" # warning C4701: potentially uninitialized local variable 'lower' used - "/wd4702" # warning C4702: unreachable code - "/wd4706" # warning C4706: assignment within conditional expression - "/wd4996" # warning C4996: 'strdup': The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _strdup. - ) -else() - target_compile_options( - glsl-optimizer - PRIVATE "-fno-strict-aliasing" # glsl-optimizer has bugs if strict aliasing is used. - # - "-Wno-implicit-fallthrough" # - "-Wno-parentheses" # - "-Wno-sign-compare" # - "-Wno-unused-function" # - "-Wno-unused-parameter" # - ) -endif() - -if(XCODE) - target_compile_options( - glsl-optimizer PRIVATE # - "-Wno-deprecated-register" # - ) -endif() - -if(MINGW) - target_compile_options( - glsl-optimizer PRIVATE # - "-Wno-misleading-indentation" # - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glslang.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glslang.cmake deleted file mode 100644 index 048cc877..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/glslang.cmake +++ /dev/null @@ -1,64 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(GLSLANG ${BGFX_DIR}/3rdparty/glslang) -set(SPIRV_TOOLS ${BGFX_DIR}/3rdparty/spirv-tools) - -file( - GLOB_RECURSE - GLSLANG_SOURCES - ${GLSLANG}/glslang/*.cpp - ${GLSLANG}/glslang/*.h - # - ${GLSLANG}/hlsl/*.cpp - ${GLSLANG}/hlsl/*.h - # - ${GLSLANG}/SPIRV/*.cpp - ${GLSLANG}/SPIRV/*.h - # - ${GLSLANG}/OGLCompilersDLL/*.cpp - ${GLSLANG}/OGLCompilersDLL/*.h -) - -if(WIN32) - list(FILTER GLSLANG_SOURCES EXCLUDE REGEX "glslang/OSDependent/Unix/.*.cpp") - list(FILTER GLSLANG_SOURCES EXCLUDE REGEX "glslang/OSDependent/Unix/.*.h") -else() - list(FILTER GLSLANG_SOURCES EXCLUDE REGEX "glslang/OSDependent/Windows/.*.cpp") - list(FILTER GLSLANG_SOURCES EXCLUDE REGEX "glslang/OSDependent/Windows/.*.h") -endif() - -add_library(glslang STATIC ${GLSLANG_SOURCES}) - -target_compile_definitions( - glslang - PRIVATE # - ENABLE_OPT=1 # spriv-tools - ENABLE_HLSL=1 # -) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(glslang PROPERTIES FOLDER "bgfx") - -target_include_directories( - glslang - PUBLIC ${GLSLANG} - ${GLSLANG}/glslang/Public - ${GLSLANG}/glslang/Include - PRIVATE ${GLSLANG}/.. - ${SPIRV_TOOLS}/include - ${SPIRV_TOOLS}/source -) diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/meshoptimizer.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/meshoptimizer.cmake deleted file mode 100644 index d6283afb..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/meshoptimizer.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -if(NOT MESHOPTIMIZER_LIBRARIES) - file( - GLOB # - MESHOPTIMIZER_SOURCES # - ${BGFX_DIR}/3rdparty/meshoptimizer/src/*.cpp # - ${BGFX_DIR}/3rdparty/meshoptimizer/src/*.h # - ) - set(MESHOPTIMIZER_INCLUDE_DIR ${BGFX_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-cross.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-cross.cmake deleted file mode 100644 index 7737e445..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-cross.cmake +++ /dev/null @@ -1,60 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(SPIRV_CROSS ${BGFX_DIR}/3rdparty/spirv-cross) - -file( - GLOB - SPIRV_CROSS_SOURCES - # - ${SPIRV_CROSS}/spirv.hpp - ${SPIRV_CROSS}/spirv_cfg.cpp - ${SPIRV_CROSS}/spirv_cfg.hpp - ${SPIRV_CROSS}/spirv_common.hpp - ${SPIRV_CROSS}/spirv_cpp.cpp - ${SPIRV_CROSS}/spirv_cpp.hpp - ${SPIRV_CROSS}/spirv_cross.cpp - ${SPIRV_CROSS}/spirv_cross.hpp - ${SPIRV_CROSS}/spirv_cross_parsed_ir.cpp - ${SPIRV_CROSS}/spirv_cross_parsed_ir.hpp - ${SPIRV_CROSS}/spirv_cross_util.cpp - ${SPIRV_CROSS}/spirv_cross_util.hpp - ${SPIRV_CROSS}/spirv_glsl.cpp - ${SPIRV_CROSS}/spirv_glsl.hpp - ${SPIRV_CROSS}/spirv_hlsl.cpp - ${SPIRV_CROSS}/spirv_hlsl.hpp - ${SPIRV_CROSS}/spirv_msl.cpp - ${SPIRV_CROSS}/spirv_msl.hpp - ${SPIRV_CROSS}/spirv_parser.cpp - ${SPIRV_CROSS}/spirv_parser.hpp - ${SPIRV_CROSS}/spirv_reflect.cpp - ${SPIRV_CROSS}/spirv_reflect.hpp -) - -add_library(spirv-cross STATIC ${SPIRV_CROSS_SOURCES}) - -target_compile_definitions(spirv-cross PRIVATE SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(spirv-cross PROPERTIES FOLDER "bgfx") - -target_include_directories( - spirv-cross # - PUBLIC # - ${SPIRV_CROSS} # - PRIVATE # - ${SPIRV_CROSS}/include # -) diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-opt.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-opt.cmake deleted file mode 100644 index b1d90b91..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/spirv-opt.cmake +++ /dev/null @@ -1,159 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(SPIRV_HEADERS ${BGFX_DIR}/3rdparty/spirv-headers) -set(SPIRV_TOOLS ${BGFX_DIR}/3rdparty/spirv-tools) - -file( - GLOB - SPIRV_OPT_SOURCES - # libspirv - ${SPIRV_TOOLS}/source/opt/*.cpp - ${SPIRV_TOOLS}/source/opt/*.h - ${SPIRV_TOOLS}/source/reduce/*.cpp - ${SPIRV_TOOLS}/source/reduce/*.h - ${SPIRV_TOOLS}/source/assembly_grammar.cpp - ${SPIRV_TOOLS}/source/assembly_grammar.h - ${SPIRV_TOOLS}/source/binary.cpp - ${SPIRV_TOOLS}/source/binary.h - ${SPIRV_TOOLS}/source/cfa.h - ${SPIRV_TOOLS}/source/diagnostic.cpp - ${SPIRV_TOOLS}/source/diagnostic.h - ${SPIRV_TOOLS}/source/disassemble.cpp - ${SPIRV_TOOLS}/source/disassemble.h - ${SPIRV_TOOLS}/source/enum_set.h - ${SPIRV_TOOLS}/source/enum_string_mapping.cpp - ${SPIRV_TOOLS}/source/enum_string_mapping.h - ${SPIRV_TOOLS}/source/ext_inst.cpp - ${SPIRV_TOOLS}/source/ext_inst.h - ${SPIRV_TOOLS}/source/extensions.cpp - ${SPIRV_TOOLS}/source/extensions.h - ${SPIRV_TOOLS}/source/instruction.h - ${SPIRV_TOOLS}/source/latest_version_glsl_std_450_header.h - ${SPIRV_TOOLS}/source/latest_version_opencl_std_header.h - ${SPIRV_TOOLS}/source/latest_version_spirv_header.h - ${SPIRV_TOOLS}/source/libspirv.cpp - ${SPIRV_TOOLS}/source/macro.h - ${SPIRV_TOOLS}/source/name_mapper.cpp - ${SPIRV_TOOLS}/source/name_mapper.h - ${SPIRV_TOOLS}/source/opcode.cpp - ${SPIRV_TOOLS}/source/opcode.h - ${SPIRV_TOOLS}/source/operand.cpp - ${SPIRV_TOOLS}/source/operand.h - ${SPIRV_TOOLS}/source/parsed_operand.cpp - ${SPIRV_TOOLS}/source/parsed_operand.h - ${SPIRV_TOOLS}/source/print.cpp - ${SPIRV_TOOLS}/source/print.h - ${SPIRV_TOOLS}/source/software_version.cpp - ${SPIRV_TOOLS}/source/spirv_constant.h - ${SPIRV_TOOLS}/source/spirv_definition.h - ${SPIRV_TOOLS}/source/spirv_endian.cpp - ${SPIRV_TOOLS}/source/spirv_endian.h - ${SPIRV_TOOLS}/source/spirv_optimizer_options.cpp - ${SPIRV_TOOLS}/source/spirv_reducer_options.cpp - ${SPIRV_TOOLS}/source/spirv_target_env.cpp - ${SPIRV_TOOLS}/source/spirv_target_env.h - ${SPIRV_TOOLS}/source/spirv_validator_options.cpp - ${SPIRV_TOOLS}/source/spirv_validator_options.h - ${SPIRV_TOOLS}/source/table.cpp - ${SPIRV_TOOLS}/source/table.h - ${SPIRV_TOOLS}/source/table2.cpp - ${SPIRV_TOOLS}/source/table2.h - ${SPIRV_TOOLS}/source/text.cpp - ${SPIRV_TOOLS}/source/text.h - ${SPIRV_TOOLS}/source/text_handler.cpp - ${SPIRV_TOOLS}/source/text_handler.h - ${SPIRV_TOOLS}/source/to_string.cpp - ${SPIRV_TOOLS}/source/to_string.h - ${SPIRV_TOOLS}/source/util/bit_vector.cpp - ${SPIRV_TOOLS}/source/util/bit_vector.h - ${SPIRV_TOOLS}/source/util/bitutils.h - ${SPIRV_TOOLS}/source/util/hex_float.h - ${SPIRV_TOOLS}/source/util/parse_number.cpp - ${SPIRV_TOOLS}/source/util/parse_number.h - ${SPIRV_TOOLS}/source/util/status.h - ${SPIRV_TOOLS}/source/util/string_utils.cpp - ${SPIRV_TOOLS}/source/util/string_utils.h - ${SPIRV_TOOLS}/source/util/timer.h - ${SPIRV_TOOLS}/source/val/basic_block.cpp - ${SPIRV_TOOLS}/source/val/construct.cpp - ${SPIRV_TOOLS}/source/val/decoration.h - ${SPIRV_TOOLS}/source/val/function.cpp - ${SPIRV_TOOLS}/source/val/instruction.cpp - ${SPIRV_TOOLS}/source/val/validate.cpp - ${SPIRV_TOOLS}/source/val/validate.h - ${SPIRV_TOOLS}/source/val/validate_adjacency.cpp - ${SPIRV_TOOLS}/source/val/validate_annotation.cpp - ${SPIRV_TOOLS}/source/val/validate_arithmetics.cpp - ${SPIRV_TOOLS}/source/val/validate_atomics.cpp - ${SPIRV_TOOLS}/source/val/validate_barriers.cpp - ${SPIRV_TOOLS}/source/val/validate_bitwise.cpp - ${SPIRV_TOOLS}/source/val/validate_builtins.cpp - ${SPIRV_TOOLS}/source/val/validate_capability.cpp - ${SPIRV_TOOLS}/source/val/validate_cfg.cpp - ${SPIRV_TOOLS}/source/val/validate_composites.cpp - ${SPIRV_TOOLS}/source/val/validate_constants.cpp - ${SPIRV_TOOLS}/source/val/validate_conversion.cpp - ${SPIRV_TOOLS}/source/val/validate_debug.cpp - ${SPIRV_TOOLS}/source/val/validate_decorations.cpp - ${SPIRV_TOOLS}/source/val/validate_derivatives.cpp - ${SPIRV_TOOLS}/source/val/validate_dot_product.cpp - ${SPIRV_TOOLS}/source/val/validate_execution_limitations.cpp - ${SPIRV_TOOLS}/source/val/validate_extensions.cpp - ${SPIRV_TOOLS}/source/val/validate_function.cpp - ${SPIRV_TOOLS}/source/val/validate_graph.cpp - ${SPIRV_TOOLS}/source/val/validate_group.cpp - ${SPIRV_TOOLS}/source/val/validate_id.cpp - ${SPIRV_TOOLS}/source/val/validate_image.cpp - ${SPIRV_TOOLS}/source/val/validate_instruction.cpp - ${SPIRV_TOOLS}/source/val/validate_interfaces.cpp - ${SPIRV_TOOLS}/source/val/validate_invalid_type.cpp - ${SPIRV_TOOLS}/source/val/validate_layout.cpp - ${SPIRV_TOOLS}/source/val/validate_literals.cpp - ${SPIRV_TOOLS}/source/val/validate_logical_pointers.cpp - ${SPIRV_TOOLS}/source/val/validate_logicals.cpp - ${SPIRV_TOOLS}/source/val/validate_memory.cpp - ${SPIRV_TOOLS}/source/val/validate_memory_semantics.cpp - ${SPIRV_TOOLS}/source/val/validate_mesh_shading.cpp - ${SPIRV_TOOLS}/source/val/validate_misc.cpp - ${SPIRV_TOOLS}/source/val/validate_mode_setting.cpp - ${SPIRV_TOOLS}/source/val/validate_non_uniform.cpp - ${SPIRV_TOOLS}/source/val/validate_pipe.cpp - ${SPIRV_TOOLS}/source/val/validate_primitives.cpp - ${SPIRV_TOOLS}/source/val/validate_ray_query.cpp - ${SPIRV_TOOLS}/source/val/validate_ray_tracing.cpp - ${SPIRV_TOOLS}/source/val/validate_ray_tracing_reorder.cpp - ${SPIRV_TOOLS}/source/val/validate_scopes.cpp - ${SPIRV_TOOLS}/source/val/validate_small_type_uses.cpp - ${SPIRV_TOOLS}/source/val/validate_tensor.cpp - ${SPIRV_TOOLS}/source/val/validate_tensor_layout.cpp - ${SPIRV_TOOLS}/source/val/validate_type.cpp - ${SPIRV_TOOLS}/source/val/validation_state.cpp -) - -add_library(spirv-opt STATIC ${SPIRV_OPT_SOURCES}) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(spirv-opt PROPERTIES FOLDER "bgfx") - -target_include_directories( - spirv-opt - PUBLIC ${SPIRV_TOOLS}/include # - PRIVATE ${SPIRV_TOOLS} # - ${SPIRV_TOOLS}/include/generated # - ${SPIRV_TOOLS}/source # - ${SPIRV_HEADERS}/include # -) diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/tint.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/tint.cmake deleted file mode 100644 index eb86f57c..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/tint.cmake +++ /dev/null @@ -1,98 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -if(TARGET tint) - return() -endif() - -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -set(TINT_DIR ${BGFX_DIR}/3rdparty/dawn) -set(SPIRV_HEADERS ${BGFX_DIR}/3rdparty/spirv-headers) -set(SPIRV_TOOLS ${BGFX_DIR}/3rdparty/spirv-tools) - -file( - GLOB_RECURSE - TINT_SOURCES - ${TINT_DIR}/src/tint/utils/*.cc - ${TINT_DIR}/src/tint/utils/*.h - ${TINT_DIR}/src/tint/lang/core/*.cc - ${TINT_DIR}/src/tint/lang/core/*.h - ${TINT_DIR}/src/tint/lang/null/*.cc - ${TINT_DIR}/src/tint/lang/null/*.h - ${TINT_DIR}/src/tint/lang/spirv/*.cc - ${TINT_DIR}/src/tint/lang/spirv/*.h - ${TINT_DIR}/src/tint/lang/wgsl/*.cc - ${TINT_DIR}/src/tint/lang/wgsl/*.h - ${TINT_DIR}/src/tint/api/*.cc - ${TINT_DIR}/src/tint/api/*.h -) - -add_library(tint STATIC ${TINT_SOURCES}) - -set_target_properties(tint PROPERTIES FOLDER "bgfx") - -target_include_directories( - tint - PUBLIC ${TINT_DIR} - ${TINT_DIR}/src/tint - PRIVATE ${TINT_DIR}/third_party/protobuf/src - ${TINT_DIR}/third_party/abseil-cpp - ${SPIRV_TOOLS} - ${SPIRV_TOOLS}/include - ${SPIRV_TOOLS}/include/generated - ${SPIRV_HEADERS}/include -) - -target_compile_definitions( - tint - PRIVATE TINT_BUILD_GLSL_WRITER=0 - TINT_BUILD_HLSL_WRITER=0 - TINT_BUILD_MSL_WRITER=0 - TINT_BUILD_NULL_WRITER=0 - TINT_BUILD_SPV_READER=1 - TINT_BUILD_SPV_WRITER=0 - TINT_BUILD_WGSL_READER=0 - TINT_BUILD_WGSL_WRITER=1 - TINT_ENABLE_IR_VALIDATION=0 -) - -if(WIN32) - target_compile_definitions( - tint - PRIVATE TINT_BUILD_IS_LINUX=0 - TINT_BUILD_IS_MAC=0 - TINT_BUILD_IS_WIN=1 - ) -elseif(APPLE) - target_compile_definitions( - tint - PRIVATE TINT_BUILD_IS_LINUX=0 - TINT_BUILD_IS_MAC=1 - TINT_BUILD_IS_WIN=0 - ) -else() - target_compile_definitions( - tint - PRIVATE TINT_BUILD_IS_LINUX=1 - TINT_BUILD_IS_MAC=0 - TINT_BUILD_IS_WIN=0 - ) -endif() - -if(MSVC) - target_compile_options( - tint - PRIVATE "/Zc:preprocessor" - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/webgpu.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/webgpu.cmake deleted file mode 100644 index 1de320a1..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/3rdparty/webgpu.cmake +++ /dev/null @@ -1,40 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover - -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. - -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -if(TARGET webgpu) - return() -endif() - -file(GLOB WEBGPU_SOURCES ${BGFX_DIR}/3rdparty/webgpu/include/webgpu/*.h - # ${BGFX_DIR}/3rdparty/webgpu/webgpu_cpp.cpp # requires dawn to be installed -) - -# Library without sources is interface -#add_library( webgpu STATIC ${WEBGPU_SOURCES} ) -add_library(webgpu INTERFACE) -target_include_directories( - webgpu # PUBLIC - INTERFACE $ -) - -# These properties are not allowed on interface -# set_target_properties(webgpu PROPERTIES FOLDER "bgfx/3rdparty" PREFIX "${CMAKE_STATIC_LIBRARY_PREFIX}bgfx-") - -if(BGFX_INSTALL AND BGFX_CONFIG_RENDERER_WEBGPU) - install( - TARGETS webgpu - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - INCLUDES - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/CMakeLists.txt b/Engine/cpp/ThirdParty/cmake/bgfx/CMakeLists.txt deleted file mode 100644 index 4537e95f..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -include(bgfx.cmake) -include(3rdparty/meshoptimizer.cmake) -include(3rdparty/dear-imgui.cmake) - -if(BGFX_BUILD_TOOLS_TEXTURE) - include(texturev.cmake) -endif() -if(BGFX_BUILD_TOOLS_GEOMETRY) - include(geometryc.cmake) - include(geometryv.cmake) -endif() -if(BGFX_BUILD_TOOLS_SHADER) - include(3rdparty/spirv-opt.cmake) - include(3rdparty/spirv-cross.cmake) - include(3rdparty/glslang.cmake) - include(3rdparty/glsl-optimizer.cmake) - include(3rdparty/fcpp.cmake) - include(3rdparty/webgpu.cmake) - include(3rdparty/tint.cmake) - include(shaderc.cmake) -endif() - -include(shared.cmake) -include(examples.cmake) diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/bgfx.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/bgfx.cmake deleted file mode 100644 index 06ca401f..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/bgfx.cmake +++ /dev/null @@ -1,223 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# To prevent this warning: https://cmake.org/cmake/help/git-stage/policy/CMP0072.html -if(POLICY CMP0072) - cmake_policy(SET CMP0072 NEW) -endif() - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BGFX_DIR}) - message(SEND_ERROR "Could not load bgfx, directory does not exist. ${BGFX_DIR}") - return() -endif() - -# Grab the bgfx source files -file( - GLOB - BGFX_SOURCES - ${BGFX_DIR}/src/*.cpp - ${BGFX_DIR}/src/*.h - ${BGFX_DIR}/include/bgfx/*.h - ${BGFX_DIR}/include/bgfx/c99/*.h -) - -set(BGFX_AMALGAMATED_SOURCE ${BGFX_DIR}/src/amalgamated.cpp) - -if(BGFX_AMALGAMATED) - set(BGFX_NOBUILD ${BGFX_SOURCES}) - list(REMOVE_ITEM BGFX_NOBUILD ${BGFX_AMALGAMATED_SOURCE}) - foreach(BGFX_SRC ${BGFX_NOBUILD}) - set_source_files_properties(${BGFX_SRC} PROPERTIES HEADER_FILE_ONLY ON) - endforeach() -else() - # Do not build using amalgamated sources - set_source_files_properties(${BGFX_AMALGAMATED_SOURCE} PROPERTIES HEADER_FILE_ONLY ON) -endif() - -# Create the bgfx target -if(BGFX_LIBRARY_TYPE STREQUAL STATIC) - add_library(bgfx STATIC ${BGFX_SOURCES}) -else() - add_library(bgfx SHARED ${BGFX_SOURCES}) - target_compile_definitions(bgfx PUBLIC BGFX_SHARED_LIB_BUILD=1) -endif() - -if(BGFX_CONFIG_RENDERER_WEBGPU) - include(${CMAKE_CURRENT_LIST_DIR}/3rdparty/webgpu.cmake) - if(EMSCRIPTEN) - target_link_options(bgfx PRIVATE "-s USE_WEBGPU=1") - else() - target_link_libraries(bgfx PRIVATE webgpu) - endif() -endif() - -if(EMSCRIPTEN) - target_link_options(bgfx PUBLIC "-sMAX_WEBGL_VERSION=2") -endif() - -if(NOT ${BGFX_OPENGL_VERSION} STREQUAL "") - target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_RENDERER_OPENGL_MIN_VERSION=${BGFX_OPENGL_VERSION}) -endif() - -if(NOT ${BGFX_OPENGLES_VERSION} STREQUAL "") - target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_RENDERER_OPENGLES_MIN_VERSION=${BGFX_OPENGLES_VERSION}) -endif() - -if(NOT ${BGFX_CONFIG_DEFAULT_MAX_ENCODERS} STREQUAL "") - target_compile_definitions( - bgfx - PUBLIC - "BGFX_CONFIG_DEFAULT_MAX_ENCODERS=$,${BGFX_CONFIG_DEFAULT_MAX_ENCODERS},1>" - ) -endif() - -if(BGFX_WITH_WAYLAND) - target_compile_definitions(bgfx PRIVATE "WL_EGL_PLATFORM=1") - target_link_libraries(bgfx PRIVATE wayland-egl) -endif() - -set(BGFX_CONFIG_OPTIONS "") -list( - APPEND - BGFX_CONFIG_OPTIONS - "BGFX_CONFIG_MAX_DRAW_CALLS" - "BGFX_CONFIG_MAX_VIEWS" - "BGFX_CONFIG_MAX_FRAME_BUFFERS" - "BGFX_CONFIG_MAX_VERTEX_LAYOUTS" - "BGFX_CONFIG_MAX_VERTEX_BUFFERS" - "BGFX_CONFIG_MAX_DYNAMIC_VERTEX_BUFFERS" - "BGFX_CONFIG_MAX_INDEX_BUFFERS" - "BGFX_CONFIG_MAX_DYNAMIC_INDEX_BUFFERS" - "BGFX_CONFIG_MAX_TEXTURES" - "BGFX_CONFIG_MAX_TEXTURE_SAMPLERS" - "BGFX_CONFIG_MAX_SHADERS" - "BGFX_CONFIG_SORT_KEY_NUM_BITS_PROGRAM" -) -foreach(BGFX_CONFIG_OPTION IN LISTS BGFX_CONFIG_OPTIONS) - if(NOT ${${BGFX_CONFIG_OPTION}} STREQUAL "") - target_compile_definitions(bgfx PUBLIC "${BGFX_CONFIG_OPTION}=${${BGFX_CONFIG_OPTION}}") - endif() -endforeach() - -# Special Visual Studio Flags -if(MSVC) - target_compile_definitions(bgfx PRIVATE "_CRT_SECURE_NO_WARNINGS") -endif() - -# Add debug config required in bx headers since bx is private -target_compile_definitions( - bgfx - PUBLIC - "BX_CONFIG_DEBUG=$,$>" - "BGFX_CONFIG_DEBUG_ANNOTATION=$>,$,$>>" - "BGFX_CONFIG_MULTITHREADED=$" -) - -# directx-headers -set(DIRECTX_HEADERS) -if(UNIX - AND NOT APPLE - AND NOT EMSCRIPTEN - AND NOT ANDROID -) # Only Linux - set(DIRECTX_HEADERS - ${BGFX_DIR}/3rdparty/directx-headers/include/directx ${BGFX_DIR}/3rdparty/directx-headers/include - ${BGFX_DIR}/3rdparty/directx-headers/include/wsl/stubs - ) -elseif(WIN32) # Only Windows - set(DIRECTX_HEADERS ${BGFX_DIR}/3rdparty/directx-headers/include/directx - ${BGFX_DIR}/3rdparty/directx-headers/include - ) -endif() - -# Includes -target_include_directories( - bgfx PRIVATE ${DIRECTX_HEADERS} ${BGFX_DIR}/3rdparty ${BGFX_DIR}/3rdparty/khronos - PUBLIC $ $ -) - -# bgfx depends on bx and bimg -target_link_libraries(bgfx PRIVATE bx bimg) - -# Frameworks required on iOS, tvOS and macOS -if(${CMAKE_SYSTEM_NAME} MATCHES iOS|tvOS) - target_link_libraries( - bgfx - PUBLIC - "-framework OpenGLES -framework Metal -framework UIKit -framework CoreGraphics -framework QuartzCore -framework IOKit -framework CoreFoundation" - ) -elseif(APPLE) - find_library(COCOA_LIBRARY Cocoa) - find_library(METAL_LIBRARY Metal) - find_library(QUARTZCORE_LIBRARY QuartzCore) - find_library(IOKIT_LIBRARY IOKit) - find_library(COREFOUNDATION_LIBRARY CoreFoundation) - mark_as_advanced(COCOA_LIBRARY) - mark_as_advanced(METAL_LIBRARY) - mark_as_advanced(QUARTZCORE_LIBRARY) - mark_as_advanced(IOKIT_LIBRARY) - mark_as_advanced(COREFOUNDATION_LIBRARY) - target_link_libraries( - bgfx PUBLIC ${COCOA_LIBRARY} ${METAL_LIBRARY} ${QUARTZCORE_LIBRARY} ${IOKIT_LIBRARY} ${COREFOUNDATION_LIBRARY} - ) -endif() - -if(UNIX - AND NOT APPLE - AND NOT EMSCRIPTEN - AND NOT ANDROID -) - find_package(X11 REQUIRED) - find_package(OpenGL REQUIRED) - #The following commented libraries are linked by bx - #find_package(Threads REQUIRED) - #find_library(LIBRT_LIBRARIES rt) - #find_library(LIBDL_LIBRARIES dl) - target_link_libraries(bgfx PUBLIC ${X11_LIBRARIES} ${OPENGL_LIBRARIES}) -endif() - -# Exclude glx context on non-unix -if(NOT UNIX OR APPLE) - set_source_files_properties(${BGFX_DIR}/src/glcontext_glx.cpp PROPERTIES HEADER_FILE_ONLY ON) -endif() - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bgfx PROPERTIES FOLDER "bgfx") - -# in Xcode we need to specify these files as objective-c++ (instead of renaming to .mm) -if(XCODE) - set_source_files_properties( - ${BGFX_DIR}/src/renderer_vk.cpp - ${BGFX_DIR}/src/renderer_webgpu.cpp - PROPERTIES - LANGUAGE OBJCXX - XCODE_EXPLICIT_FILE_TYPE sourcecode.cpp.objcpp - ) -endif() - -if(BGFX_INSTALL) - install( - TARGETS bgfx - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - INCLUDES - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - ) - - install(DIRECTORY ${BGFX_DIR}/include/bgfx DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") - - # header required for shader compilation - install(FILES ${BGFX_DIR}/src/bgfx_shader.sh ${BGFX_DIR}/src/bgfx_compute.sh - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/bgfx" - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/examples.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/examples.cmake deleted file mode 100644 index a4f4f3cf..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/examples.cmake +++ /dev/null @@ -1,365 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -include(CMakeParseArguments) - -include(${CMAKE_CURRENT_LIST_DIR}/util/ConfigureDebugging.cmake) - -include(${CMAKE_CURRENT_LIST_DIR}/../bgfxToolUtils.cmake) - -function(add_bgfx_shader FILE FOLDER) - get_filename_component(FILENAME "${FILE}" NAME_WE) - string(SUBSTRING "${FILENAME}" 0 2 TYPE) - if("${TYPE}" STREQUAL "fs") - set(TYPE "FRAGMENT") - elseif("${TYPE}" STREQUAL "vs") - set(TYPE "VERTEX") - elseif("${TYPE}" STREQUAL "cs") - set(TYPE "COMPUTE") - else() - set(TYPE "") - endif() - - if(NOT "${TYPE}" STREQUAL "") - set(COMMON FILE ${FILE} ${TYPE} INCLUDES ${BGFX_DIR}/src) - set(OUTPUTS "") - set(OUTPUTS_PRETTY "") - set(OUTPUT_FILES "") - set(COMMANDS "") - - if(WIN32) - # dx11 - set(DX11_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/dx11/${FILENAME}.bin) - if(NOT "${TYPE}" STREQUAL "COMPUTE") - _bgfx_shaderc_parse( - DX11 ${COMMON} WINDOWS - PROFILE s_5_0 - O 3 - OUTPUT ${DX11_OUTPUT} - ) - else() - _bgfx_shaderc_parse( - DX11 ${COMMON} WINDOWS - PROFILE s_5_0 - O 1 - OUTPUT ${DX11_OUTPUT} - ) - endif() - list(APPEND OUTPUTS "DX11") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}DX11, ") - endif() - - if(APPLE) - # metal - set(METAL_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/metal/${FILENAME}.bin) - _bgfx_shaderc_parse(METAL ${COMMON} OSX PROFILE metal OUTPUT ${METAL_OUTPUT}) - list(APPEND OUTPUTS "METAL") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}Metal, ") - endif() - - # essl - if(NOT "${TYPE}" STREQUAL "COMPUTE") - set(ESSL_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/essl/${FILENAME}.bin) - _bgfx_shaderc_parse(ESSL ${COMMON} ANDROID PROFILE 100_es OUTPUT ${ESSL_OUTPUT}) - list(APPEND OUTPUTS "ESSL") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}ESSL, ") - endif() - - # glsl - set(GLSL_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/glsl/${FILENAME}.bin) - if(NOT "${TYPE}" STREQUAL "COMPUTE") - _bgfx_shaderc_parse(GLSL ${COMMON} LINUX PROFILE 140 OUTPUT ${GLSL_OUTPUT}) - else() - _bgfx_shaderc_parse(GLSL ${COMMON} LINUX PROFILE 430 OUTPUT ${GLSL_OUTPUT}) - endif() - list(APPEND OUTPUTS "GLSL") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}GLSL, ") - - # spirv - if(NOT "${TYPE}" STREQUAL "COMPUTE") - set(SPIRV_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/spirv/${FILENAME}.bin) - _bgfx_shaderc_parse(SPIRV ${COMMON} LINUX PROFILE spirv OUTPUT ${SPIRV_OUTPUT}) - list(APPEND OUTPUTS "SPIRV") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}SPIRV, ") - endif() - - # wgsl - set(WGSL_OUTPUT ${BGFX_DIR}/examples/runtime/shaders/wgsl/${FILENAME}.bin) - _bgfx_shaderc_parse(WGSL ${COMMON} LINUX PROFILE wgsl OUTPUT ${WGSL_OUTPUT}) - list(APPEND OUTPUTS "WGSL") - set(OUTPUTS_PRETTY "${OUTPUTS_PRETTY}WGSL") - - foreach(OUT ${OUTPUTS}) - list(APPEND OUTPUT_FILES ${${OUT}_OUTPUT}) - list(APPEND COMMANDS COMMAND "bgfx::shaderc" ${${OUT}}) - get_filename_component(OUT_DIR ${${OUT}_OUTPUT} DIRECTORY) - file(MAKE_DIRECTORY ${OUT_DIR}) - endforeach() - - file(RELATIVE_PATH PRINT_NAME ${BGFX_DIR}/examples ${FILE}) - add_custom_command( - MAIN_DEPENDENCY ${FILE} OUTPUT ${OUTPUT_FILES} ${COMMANDS} - COMMENT "Compiling shader ${PRINT_NAME} for ${OUTPUTS_PRETTY}" - ) - endif() -endfunction() - -function(add_example ARG_NAME) - # Parse arguments - cmake_parse_arguments(ARG "COMMON" "" "DIRECTORIES;SOURCES" ${ARGN}) - - # Get all source files - list(APPEND ARG_DIRECTORIES "${BGFX_DIR}/examples/${ARG_NAME}") - set(SOURCES "") - set(SHADERS "") - foreach(DIR ${ARG_DIRECTORIES}) - if(APPLE) - file(GLOB GLOB_SOURCES ${DIR}/*.mm) - list(APPEND SOURCES ${GLOB_SOURCES}) - endif() - file(GLOB GLOB_SOURCES ${DIR}/*.c ${DIR}/*.cpp ${DIR}/*.h ${DIR}/*.sc) - list(APPEND SOURCES ${GLOB_SOURCES}) - file(GLOB GLOB_SHADERS ${DIR}/*.sc) - list(APPEND SHADERS ${GLOB_SHADERS}) - endforeach() - - # Add target - if(ARG_COMMON) - add_library( - example-${ARG_NAME} STATIC EXCLUDE_FROM_ALL ${SOURCES} ${DEAR_IMGUI_SOURCES} ${MESHOPTIMIZER_SOURCES} - ) - target_include_directories( - example-${ARG_NAME} PUBLIC ${BGFX_DIR}/examples/common ${DEAR_IMGUI_INCLUDE_DIR} - ${MESHOPTIMIZER_INCLUDE_DIR} - ) - target_link_libraries( - example-${ARG_NAME} PUBLIC bgfx bx bimg bimg_decode ${DEAR_IMGUI_LIBRARIES} ${MESHOPTIMIZER_LIBRARIES} - ) - - if(BGFX_WITH_WAYLAND) - target_compile_definitions(example-${ARG_NAME} PUBLIC ENTRY_CONFIG_USE_WAYLAND=1) - endif() - - if(BGFX_WITH_GLFW) - find_package(glfw3 REQUIRED) - target_link_libraries(example-${ARG_NAME} PUBLIC glfw) - target_compile_definitions(example-${ARG_NAME} PUBLIC ENTRY_CONFIG_USE_GLFW=1) - elseif(BGFX_WITH_SDL) - find_package(SDL2 REQUIRED) - target_link_libraries(example-${ARG_NAME} PUBLIC ${SDL2_LIBRARIES}) - target_compile_definitions(example-${ARG_NAME} PUBLIC ENTRY_CONFIG_USE_SDL=1) - elseif(UNIX AND NOT APPLE AND NOT ANDROID) - target_link_libraries(example-${ARG_NAME} PUBLIC X11) - endif() - - if(ANDROID) - target_include_directories(example-${ARG_NAME} PRIVATE ${BGFX_DIR}/3rdparty/native_app_glue) - target_link_libraries(example-${ARG_NAME} INTERFACE android EGL GLESv2) - endif() - - if(BGFX_BUILD_EXAMPLES) - if(IOS OR WIN32) - # on iOS we need to build a bundle so have to copy the data rather than symlink - # and on windows we can't create symlinks - add_custom_command( - TARGET example-${ARG_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${BGFX_DIR}/examples/runtime/ - $ - ) - else() - # For everything else symlink some folders into our output directory - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/font - $/font - ) - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/images - $/images - ) - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/meshes - $/meshes - ) - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/shaders - $/shaders - ) - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/text - $/text - ) - add_custom_command( - TARGET example-${ARG_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${BGFX_DIR}/examples/runtime/textures - $/textures - ) - endif() - endif() - - else() - if(ANDROID) - add_library(example-${ARG_NAME} SHARED ${SOURCES}) - else() - add_executable(example-${ARG_NAME} WIN32 ${SOURCES}) - endif() - if(NOT BGFX_INSTALL_EXAMPLES) - set_property(TARGET example-${ARG_NAME} PROPERTY EXCLUDE_FROM_ALL ON) - endif() - target_link_libraries(example-${ARG_NAME} PUBLIC example-common) - configure_debugging(example-${ARG_NAME} WORKING_DIR ${BGFX_DIR}/examples/runtime) - if(MSVC) - set_target_properties(example-${ARG_NAME} PROPERTIES LINK_FLAGS "/ENTRY:\"mainCRTStartup\"") - endif() - if(BGFX_CUSTOM_TARGETS) - add_dependencies(examples example-${ARG_NAME}) - endif() - if(IOS) - set_target_properties( - example-${ARG_NAME} - PROPERTIES MACOSX_BUNDLE ON - MACOSX_BUNDLE_GUI_IDENTIFIER example-${ARG_NAME} - MACOSX_BUNDLE_BUNDLE_VERSION 0 - MACOSX_BUNDLE_SHORT_VERSION_STRING 0 - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" - ) - endif() - endif() - target_compile_definitions( - example-${ARG_NAME} - PRIVATE "-D_CRT_SECURE_NO_WARNINGS" # - "-D__STDC_FORMAT_MACROS" # - "-DENTRY_CONFIG_IMPLEMENT_MAIN=1" # - ) - - # Configure shaders - if(NOT ARG_COMMON - AND NOT IOS - AND NOT EMSCRIPTEN - AND NOT ANDROID - ) - foreach(SHADER ${SHADERS}) - add_bgfx_shader(${SHADER} ${ARG_NAME}) - endforeach() - source_group("Shader Files" FILES ${SHADERS}) - endif() - - if(NOT ARG_COMMON AND EMSCRIPTEN) - set_target_properties( - example-${ARG_NAME} - PROPERTIES LINK_FLAGS - "-s PRECISE_F32=1 -s TOTAL_MEMORY=268435456 -s ENVIRONMENT=web --memory-init-file 1 --emrun" - SUFFIX ".html" - ) - endif() - - # Directory name - set_target_properties(example-${ARG_NAME} PROPERTIES FOLDER "bgfx/examples") -endfunction() - -# Build all examples target -if(BGFX_CUSTOM_TARGETS) - add_custom_target(examples) - set_target_properties(examples PROPERTIES FOLDER "bgfx/examples") -endif() - -# Add common library for examples -if(BGFX_BUILD_EXAMPLE_COMMON) - add_example( - common - COMMON - DIRECTORIES - ${BGFX_DIR}/examples/common/debugdraw - ${BGFX_DIR}/examples/common/entry - ${BGFX_DIR}/examples/common/font - ${BGFX_DIR}/examples/common/imgui - ${BGFX_DIR}/examples/common/nanovg - ${BGFX_DIR}/examples/common/ps - ) -endif() - -# Only add examples if set, otherwise we still need exmaples common for tools -if(BGFX_BUILD_EXAMPLES) - # Add examples - set(BGFX_EXAMPLES - 00-helloworld - 01-cubes - 02-metaballs - 03-raymarch - 04-mesh - 05-instancing - 06-bump - 07-callback - 08-update - 09-hdr - 10-font - 11-fontsdf - 12-lod - 13-stencil - 14-shadowvolumes - 15-shadowmaps-simple - 16-shadowmaps - 17-drawstress - 18-ibl - 19-oit - 20-nanovg - # 21-deferred - 22-windows - 23-vectordisplay - 24-nbody - 25-c99 - 26-occlusion - 27-terrain - 28-wireframe - 29-debugdraw - 30-picking - 31-rsm - 32-particles - 33-pom - 34-mvs - 35-dynamic - 36-sky - # 37-gpudrivenrendering - 38-bloom - 39-assao - 40-svt - # 41-tess - 42-bunnylod - 43-denoise - 44-sss - 45-bokeh - 46-fsr - 47-pixelformats - 48-drawindirect - 49-hextile - 50-headless - 51-gpufont - ) - - foreach(EXAMPLE ${BGFX_EXAMPLES}) - add_example(${EXAMPLE}) - endforeach() - - if(BGFX_INSTALL_EXAMPLES) - install(DIRECTORY ${BGFX_DIR}/examples/runtime/ DESTINATION examples) - foreach(EXAMPLE ${BGFX_EXAMPLES}) - install(TARGETS example-${EXAMPLE} DESTINATION examples) - endforeach() - endif() -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/generated/bounds.cpp.in b/Engine/cpp/ThirdParty/cmake/bgfx/generated/bounds.cpp.in deleted file mode 100644 index 1e3029e7..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/generated/bounds.cpp.in +++ /dev/null @@ -1 +0,0 @@ -#include "@BGFX_DIR@/examples/common/bounds.cpp" diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/generated/shader.cpp.in b/Engine/cpp/ThirdParty/cmake/bgfx/generated/shader.cpp.in deleted file mode 100644 index bbfce0d1..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/generated/shader.cpp.in +++ /dev/null @@ -1,3 +0,0 @@ -#include "@BGFX_DIR@/src/shader.cpp" -#include "@BGFX_DIR@/src/shader_dxbc.cpp" -#include "@BGFX_DIR@/src/shader_spirv.cpp" diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/generated/vertexlayout.cpp.in b/Engine/cpp/ThirdParty/cmake/bgfx/generated/vertexlayout.cpp.in deleted file mode 100644 index a007bb59..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/generated/vertexlayout.cpp.in +++ /dev/null @@ -1 +0,0 @@ -#include "@BGFX_DIR@/src/vertexlayout.cpp" diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/geometryc.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/geometryc.cmake deleted file mode 100644 index e6efcd58..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/geometryc.cmake +++ /dev/null @@ -1,43 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the geometryc source files -file( - GLOB_RECURSE - GEOMETRYC_SOURCES # - ${BGFX_DIR}/tools/geometryc/*.cpp # - ${BGFX_DIR}/tools/geometryc/*.h # - # - ${MESHOPTIMIZER_SOURCES} -) -add_executable(geometryc ${GEOMETRYC_SOURCES}) - -target_include_directories(geometryc PRIVATE ${MESHOPTIMIZER_INCLUDE_DIR}) -target_link_libraries(geometryc PRIVATE bx bgfx-vertexlayout ${MESHOPTIMIZER_LIBRARIES}) -target_compile_definitions(geometryc PRIVATE "-D_CRT_SECURE_NO_WARNINGS") -set_target_properties( - geometryc PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}geometryc # -) - -if(BGFX_BUILD_TOOLS_GEOMETRY) - add_executable(bgfx::geometryc ALIAS geometryc) - if(BGFX_CUSTOM_TARGETS) - add_dependencies(tools geometryc) - endif() -endif() - -if(IOS) - set_target_properties(geometryc PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER geometryc) -endif() - -if(BGFX_INSTALL) - install(TARGETS geometryc EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/geometryv.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/geometryv.cmake deleted file mode 100644 index 764ff7c4..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/geometryv.cmake +++ /dev/null @@ -1,42 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the geometryv source files -file(GLOB_RECURSE GEOMETRYV_SOURCES # - ${BGFX_DIR}/tools/geometryv/* -) - -if(ANDROID) - add_library(geometryv SHARED ${GEOMETRYV_SOURCES}) -else() - add_executable(geometryv ${GEOMETRYV_SOURCES}) -endif() - -target_link_libraries(geometryv PRIVATE example-common) -set_target_properties( - geometryv PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}geometryv # -) - -if(BGFX_BUILD_TOOLS_GEOMETRY AND BGFX_CUSTOM_TARGETS) - add_dependencies(tools geometryv) -endif() - -if(ANDROID) - set_property(TARGET geometryv PROPERTY PREFIX "") -elseif(EMSCRIPTEN) - target_link_options(geometryv PRIVATE -sMAX_WEBGL_VERSION=2) -elseif(IOS) - set_target_properties(geometryv PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER geometryv) -endif() - -if(BGFX_INSTALL) - install(TARGETS geometryv EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/shaderc.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/shaderc.cmake deleted file mode 100644 index 7ecf472f..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/shaderc.cmake +++ /dev/null @@ -1,95 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the shaderc source files -file( - GLOB - SHADERC_SOURCES # - ${BGFX_DIR}/tools/shaderc/*.cpp # - ${BGFX_DIR}/tools/shaderc/*.h # - ${BGFX_DIR}/src/shader* # -) - -add_executable(shaderc ${SHADERC_SOURCES}) - -target_link_libraries( - shaderc - PRIVATE bx - bimg - bgfx-vertexlayout - fcpp - glslang - glsl-optimizer - spirv-opt - spirv-cross - webgpu - tint -) - -target_include_directories( - shaderc - PRIVATE ${BGFX_DIR}/3rdparty/dawn - ${BGFX_DIR}/3rdparty/dawn/src -) - -set(DXCOMPILER_RUNTIME) -if(UNIX - AND NOT APPLE - AND NOT EMSCRIPTEN - AND NOT ANDROID -) - target_include_directories( - shaderc - PRIVATE ${BGFX_DIR}/3rdparty/directx-headers/include/directx - ${BGFX_DIR}/3rdparty/directx-headers/include - ${BGFX_DIR}/3rdparty/directx-headers/include/wsl/stubs - ) - set(DXCOMPILER_RUNTIME ${BGFX_DIR}/tools/bin/linux/libdxcompiler.so) -elseif(WIN32) - set(DXCOMPILER_RUNTIME ${BGFX_DIR}/tools/bin/windows/dxcompiler.dll) -endif() - -if(BGFX_AMALGAMATED) - target_link_libraries(shaderc PRIVATE bgfx-shader) -endif() - -set_target_properties( - shaderc PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}shaderc # -) - -if(BGFX_BUILD_TOOLS_SHADER) - add_executable(bgfx::shaderc ALIAS shaderc) - if(BGFX_CUSTOM_TARGETS) - add_dependencies(tools shaderc) - endif() -endif() - -if(ANDROID) - target_link_libraries(shaderc PRIVATE log) -elseif(IOS) - set_target_properties(shaderc PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER shaderc) -endif() - -if(BGFX_INSTALL) - install(TARGETS shaderc EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() - -# DXIL compiler will be dynamically loaded at runtime - no need -# to link, just install the needed binaries alongside shaderc.exe -if(DXCOMPILER_RUNTIME) - add_custom_command( - TARGET shaderc POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${DXCOMPILER_RUNTIME} $ - ) - if(BGFX_INSTALL) - install(FILES ${DXCOMPILER_RUNTIME} DESTINATION "${CMAKE_INSTALL_BINDIR}") - endif() -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/shared.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/shared.cmake deleted file mode 100644 index b8decafb..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/shared.cmake +++ /dev/null @@ -1,29 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -add_library(bgfx-vertexlayout INTERFACE) -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/generated/vertexlayout.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/vertexlayout.cpp -) -target_sources(bgfx-vertexlayout INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/generated/vertexlayout.cpp) -target_include_directories(bgfx-vertexlayout INTERFACE ${BGFX_DIR}/include) - -add_library(bgfx-shader INTERFACE) - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/generated/shader.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/shader.cpp) -target_sources(bgfx-shader INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/generated/shader.cpp) -target_include_directories(bgfx-shader INTERFACE ${BGFX_DIR}/include) - -# Frameworks required on OS X -if(${CMAKE_SYSTEM_NAME} MATCHES Darwin) - find_library(COCOA_LIBRARY Cocoa) - mark_as_advanced(COCOA_LIBRARY) - target_link_libraries(bgfx-vertexlayout INTERFACE ${COCOA_LIBRARY}) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/texturev.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/texturev.cmake deleted file mode 100644 index a45447bd..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/texturev.cmake +++ /dev/null @@ -1,42 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the texturev source files -file(GLOB_RECURSE TEXTUREV_SOURCES # - ${BGFX_DIR}/tools/texturev/* -) - -if(ANDROID) - add_library(texturev SHARED ${TEXTUREV_SOURCES}) -else() - add_executable(texturev ${TEXTUREV_SOURCES}) -endif() - -target_link_libraries(texturev PRIVATE example-common) -set_target_properties( - texturev PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}texturev # -) - -if(BGFX_BUILD_TOOLS_TEXTURE AND BGFX_CUSTOM_TARGETS) - add_dependencies(tools texturev) -endif() - -if(ANDROID) - set_property(TARGET texturev PROPERTY PREFIX "") -elseif(EMSCRIPTEN) - target_link_options(texturev PRIVATE -sMAX_WEBGL_VERSION=2) -elseif(IOS) - set_target_properties(texturev PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER texturev) -endif() - -if(BGFX_INSTALL) - install(TARGETS texturev EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bgfx/util/ConfigureDebugging.cmake b/Engine/cpp/ThirdParty/cmake/bgfx/util/ConfigureDebugging.cmake deleted file mode 100644 index 2df4fef7..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfx/util/ConfigureDebugging.cmake +++ /dev/null @@ -1,157 +0,0 @@ -# ConfigureDebugging.cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# configure_debugging( TARGET [OPTIONS...] ) -# -# Configures the debugging settings in visual studio. -# Results in a no-op on non visual studio builds. -# Must be called in the same cmake file as the add_executable command. -# -# See OPTIONS variable in the function for supported user settings. -# See CONFIGS variable in the function for supported cmake configurations. -# See PROCESSORS variable in the function for supported architecture configurations. -# -# All variables can be set with one of the following formats: -# -# (OPTION) -# (OPTION)_(CONFIG) -# (OPTION)_(CONFIG)_(ARCH) -# (OPTION)_(ARCH) -# -# So, some examples (variables should be all caps): -# -# WORKING_DIR -# WORKING_DIR_X64 -# WORKING_DIR_RELEASE_WIN32 -# WORKING_DIR_X64 -# -# An example of a full command: -# -# configure_debugging(target COMMAND "node.exe" COMMAND_X64 "node64.exe" WORKING_DIR ${CMAKE_SOURCE_DIR} DEBUGGER_ENV "PATH=%PATH%\\;$(ProjectDir)") - -include(CMakeParseArguments) - -function(configure_debugging ARG_TARGET) - if(MSVC) - # Visual Studio Options - set(OPTIONS - WORKING_DIR - LocalDebuggerWorkingDirectory - DEBUGGER_ENV - LocalDebuggerEnvironment - COMMAND - LocalDebuggerCommand - COMMAND_ARGS - LocalDebuggerCommandArguments - ) - - # Valid Configurations - set(CONFIGS Debug Release MinSizeRel RelWithDebInfo) - - # Processors - set(PROCESSORS Win32 x64) - - # Begin hackery - if(${CMAKE_SIZEOF_VOID_P} EQUAL 8) - set(ACTIVE_PROCESSOR "x64") - else() - set(ACTIVE_PROCESSOR "Win32") - endif() - # Fix issues with semicolons, thx cmake - foreach(ARG ${ARGN}) - string(REPLACE ";" "\\\\\\\\\\\\\\;" RES "${ARG}") - list(APPEND ARGS "${RES}") - endforeach() - # Build options for cmake_parse_arguments, result is ONE_ARG variable - set(ODD ON) - foreach(OPTION ${OPTIONS}) - if(ODD) - set(ARG ${OPTION}) - list(APPEND ONE_ARG ${ARG}) - foreach(CONFIG ${CONFIGS}) - string(TOUPPER ${CONFIG} CONFIG) - list(APPEND ONE_ARG ${ARG}_${CONFIG}) - foreach(PROCESSOR ${PROCESSORS}) - string(TOUPPER ${PROCESSOR} PROCESSOR) - list(APPEND ONE_ARG ${ARG}_${CONFIG}_${PROCESSOR}) - endforeach() - endforeach() - foreach(PROCESSOR ${PROCESSORS}) - string(TOUPPER ${PROCESSOR} PROCESSOR) - list(APPEND ONE_ARG ${ARG}_${PROCESSOR}) - endforeach() - set(ODD OFF) - else() - set(ODD ON) - endif() - endforeach() - cmake_parse_arguments(ARG "" "${ONE_ARG}" "" ${ARGS}) - # Parse options, fills in all variables of format ARG_(ARG)_(CONFIG)_(PROCESSOR), for example ARG_WORKING_DIR_DEBUG_X64 - set(ODD ON) - foreach(OPTION ${OPTIONS}) - if(ODD) - set(ARG ${OPTION}) - foreach(CONFIG ${CONFIGS}) - string(TOUPPER ${CONFIG} CONFIG_CAP) - if("${ARG_${ARG}_${CONFIG_CAP}}" STREQUAL "") - set(ARG_${ARG}_${CONFIG_CAP} ${ARG_${ARG}}) - endif() - foreach(PROCESSOR ${PROCESSORS}) - string(TOUPPER ${PROCESSOR} PROCESSOR_CAP) - if("${ARG_${ARG}_${CONFIG_CAP}_${PROCESSOR_CAP}}" STREQUAL "") - if("${ARG_${ARG}_${PROCESSOR_CAP}}" STREQUAL "") - set(ARG_${ARG}_${CONFIG_CAP}_${PROCESSOR_CAP} ${ARG_${ARG}_${CONFIG_CAP}}) - else() - set(ARG_${ARG}_${CONFIG_CAP}_${PROCESSOR_CAP} ${ARG_${ARG}_${PROCESSOR_CAP}}) - endif() - endif() - if(NOT "${ARG_${ARG}_${CONFIG_CAP}_${PROCESSOR_CAP}}" STREQUAL "") - - endif() - endforeach() - endforeach() - set(ODD OFF) - else() - set(ODD ON) - endif() - endforeach() - # Create string to put in proj.vcxproj.user file - set(RESULT - "\n" - ) - foreach(CONFIG ${CONFIGS}) - string(TOUPPER ${CONFIG} CONFIG_CAPS) - foreach(PROCESSOR ${PROCESSORS}) - if("${PROCESSOR}" STREQUAL "${ACTIVE_PROCESSOR}") - string(TOUPPER ${PROCESSOR} PROCESSOR_CAPS) - set(RESULT - "${RESULT}\n " - ) - set(ODD ON) - foreach(OPTION ${OPTIONS}) - if(ODD) - set(ARG ${OPTION}) - set(ODD OFF) - else() - set(VALUE ${ARG_${ARG}_${CONFIG_CAPS}_${PROCESSOR_CAPS}}) - if(NOT "${VALUE}" STREQUAL "") - set(RESULT "${RESULT}\n <${OPTION}>${VALUE}") - endif() - set(ODD ON) - endif() - endforeach() - set(RESULT "${RESULT}\n ") - endif() - endforeach() - endforeach() - set(RESULT "${RESULT}\n") - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${ARG_TARGET}.vcxproj.user "${RESULT}") - endif() -endfunction() diff --git a/Engine/cpp/ThirdParty/cmake/bgfxToolUtils.cmake b/Engine/cpp/ThirdParty/cmake/bgfxToolUtils.cmake deleted file mode 100644 index 7f504451..00000000 --- a/Engine/cpp/ThirdParty/cmake/bgfxToolUtils.cmake +++ /dev/null @@ -1,677 +0,0 @@ -# If bgfx.cmake was compiled without tools or cross compiled without host having tools, -# then don't provide helper functions -if(TARGET bgfx::bin2c) - # _bgfx_bin2c_parse( - # INPUT_FILE filename - # OUTPUT_FILE filename - # ARRAY_NAME name - # ) - # Usage: bin2c -f -o -n - function(_bgfx_bin2c_parse ARG_OUT) - set(options "") - set(oneValueArgs INPUT_FILE;OUTPUT_FILE;ARRAY_NAME) - set(multiValueArgs "") - cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") - set(CLI "") - - # -f - if(ARG_INPUT_FILE) - list(APPEND CLI "-f" "${ARG_INPUT_FILE}") - else() - message(SEND_ERROR "Call to _bgfx_bin2c_parse() must have an INPUT_FILE") - endif() - - # -o - if(ARG_OUTPUT_FILE) - list(APPEND CLI "-o" "${ARG_OUTPUT_FILE}") - else() - message(SEND_ERROR "Call to _bgfx_bin2c_parse() must have an OUTPUT_FILE") - endif() - - # -n - if(ARG_ARRAY_NAME) - list(APPEND CLI "-n" "${ARG_ARRAY_NAME}") - else() - message(SEND_ERROR "Call to _bgfx_bin2c_parse() must have an ARRAY_NAME") - endif() - - set(${ARG_OUT} ${CLI} PARENT_SCOPE) - endfunction() - - # bgfx_compile_binary_to_header( - # INPUT_FILE filename - # OUTPUT_FILE filename - # ARRAY_NAME name - # ) - # - function(bgfx_compile_binary_to_header) - set(options "") - set(oneValueArgs INPUT_FILE;OUTPUT_FILE;ARRAY_NAME) - set(multiValueArgs "") - cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") - _bgfx_bin2c_parse( - CLI - INPUT_FILE ${ARG_INPUT_FILE} - OUTPUT_FILE ${ARG_OUTPUT_FILE} - ARRAY_NAME ${ARG_ARRAY_NAME} - ) - add_custom_command( - OUTPUT ${ARG_OUTPUT_FILE} # - COMMAND bgfx::bin2c ${CLI} # - MAIN_DEPENDENCY ${ARG_INPUT_FILE} # - ) - endfunction() -endif() - -# If bgfx.cmake was compiled without tools or cross compiled without host having tools, -# then don't provide helper functions -if(TARGET bgfx::texturec) - # _bgfx_texturec_parse( - # FILE filename - # OUTPUT filename - # [FORMAT format] - # [QUALITY default|fastest|highest] - # [MIPS] - # [MIPSKIP N] - # [NORMALMAP] - # [EQUIRECT] - # [STRIP] - # [SDF] - # [REF alpha] - # [IQA] - # [PMA] - # [LINEAR] - # [MAX max size] - # [RADIANCE model] - # [AS extension] - # ) - function(_bgfx_texturec_parse ARG_OUT) - cmake_parse_arguments( - ARG # - "MIPS;NORMALMAP;EQUIRECT;STRIP;SDF;IQA;PMA;LINEAR" # - "FILE;OUTPUT;FORMAT;QUALITY;MIPSKIP;REF;MAX;RADIANCE;AS" # - "" # - ${ARGN} # - ) - set(CLI "") - - # -f - if(ARG_FILE) - list(APPEND CLI "-f" "${ARG_FILE}") - endif() - - # -o - if(ARG_OUTPUT) - list(APPEND CLI "-o" "${ARG_OUTPUT}") - endif() - - # -t - if(ARG_FORMAT) - list(APPEND CLI "-t" "${ARG_FORMAT}") - endif() - - # -q - if(ARG_QUALITY) - list(APPEND CLI "-q" "${ARG_QUALITY}") - endif() - - # --mips - if(ARG_MIPS) - list(APPEND CLI "--mips") - endif() - - # --mipskip - if(ARG_MIPSKIP) - list(APPEND CLI "--mipskip" "${ARG_MIPSKIP}") - endif() - - # --normalmap - if(ARG_NORMALMAP) - list(APPEND CLI "--normalmap") - endif() - - # --equirect - if(ARG_EQUIRECT) - list(APPEND CLI "--equirect") - endif() - - # --strip - if(ARG_STRIP) - list(APPEND CLI "--strip") - endif() - - # --sdf - if(ARG_SDF) - list(APPEND CLI "--sdf") - endif() - - # --ref - if(ARG_REF) - list(APPEND CLI "--ref" "${ARG_REF}") - endif() - - # --iqa - if(ARG_IQA) - list(APPEND CLI "--iqa") - endif() - - # --pma - if(ARG_PMA) - list(APPEND CLI "--pma") - endif() - - # --linear - if(ARG_LINEAR) - list(APPEND CLI "--linear") - endif() - - # --max - if(ARG_MAX) - list(APPEND CLI "--max" "${ARG_MAX}") - endif() - - # --radiance - if(ARG_RADIANCE) - list(APPEND CLI "--radiance" "${ARG_RADIANCE}") - endif() - - # --as - if(ARG_AS) - list(APPEND CLI "--as" "${ARG_AS}") - endif() - - set(${ARG_OUT} ${CLI} PARENT_SCOPE) - endfunction() - - # bgfx_compile_texture( - # FILE filename - # OUTPUT filename - # [FORMAT format] - # [QUALITY default|fastest|highest] - # [MIPS] - # [MIPSKIP N] - # [NORMALMAP] - # [EQUIRECT] - # [STRIP] - # [SDF] - # [REF alpha] - # [IQA] - # [PMA] - # [LINEAR] - # [MAX max size] - # [RADIANCE model] - # [AS extension] - # ) - # - function(bgfx_compile_texture) - cmake_parse_arguments( - ARG # - "MIPS;NORMALMAP;EQUIRECT;STRIP;SDF;IQA;PMA;LINEAR" # - "FILE;OUTPUT;FORMAT;QUALITY;MIPSKIP;REF;MAX;RADIANCE;AS" # - "" # - ${ARGN} # - ) - _bgfx_texturec_parse(CLI ${ARGV}) - add_custom_command( - OUTPUT ${ARG_OUTPUT} # - COMMAND bgfx::texturec ${CLI} # - MAIN_DEPENDENCY ${ARG_FILE} # - ) - endfunction() -endif() - -# If bgfx.cmake was compiled without tools or cross compiled without host having tools, -# then don't provide helper functions -if(TARGET bgfx::geometryc) - # _bgfx_geometryc_parse( - # FILE filename - # OUTPUT filename - # [SCALE scale] - # [CCW] - # [FLIPV] - # [OBB num steps] - # [PACKNORMAL 0|1] - # [PACKUV 0|1] - # [TANGENT] - # [BARYCENTRIC] - # [COMPRESS] - # [LH_UP_Y|LH_UP_Z|RH_UP_Y|RH_UP_Z] - # ) - function(_bgfx_geometryc_parse ARG_OUT) - cmake_parse_arguments( - ARG # - "CCW;FLIPV;TANGENT;BARYCENTRIC;COMPRESS;LH_UP_Y;LH_UP_Z;RH_UP_Y;RH_UP_Z" # - "FILE;OUTPUT;SCALE;OBB;PACKNORMAL;PACKUV" # - "" # - ${ARGN} # - ) - set(CLI "") - - # -f - if(ARG_FILE) - list(APPEND CLI "-f" "${ARG_FILE}") - endif() - - # -o - if(ARG_OUTPUT) - list(APPEND CLI "-o" "${ARG_OUTPUT}") - endif() - - # -s - if(ARG_SCALE) - list(APPEND CLI "-s" "${ARG_SCALE}") - endif() - - # --cw - if(ARG_QUALITY) - list(APPEND CLI "--cw") - endif() - - # --flipv - if(ARG_FLIPV) - list(APPEND CLI "--flipv") - endif() - - # --obb - if(ARG_OBB) - list(APPEND CLI "--mipskip" "${ARG_OBB}") - endif() - - # --packnormal - if(ARG_PACKNORMAL) - list(APPEND CLI "--packnormal" "${ARG_PACKNORMAL}") - endif() - - # --packuv - if(ARG_PACKUV) - list(APPEND CLI "--packuv" "${ARG_PACKUV}") - endif() - - # --tangent - if(ARG_TANGENT) - list(APPEND CLI "--tangent") - endif() - - # --barycentric - if(ARG_BARYCENTRIC) - list(APPEND CLI "--barycentric") - endif() - - # --compress - if(ARG_REF) - list(APPEND CLI "--compress" "${ARG_COMPRESS}") - endif() - - # --lh-up+y - if(ARG_LH_UP_Y) - list(APPEND CLI "--lh-up+y") - endif() - - # --lh-up+z - if(ARG_LH_UP_Z) - list(APPEND CLI "--lh-up+z") - endif() - - # --rh-up+y - if(ARG_RH_UP_Y) - list(APPEND CLI "--rh-up+y") - endif() - - # --rh-up+z - if(ARG_RH_UP_Z) - list(APPEND CLI "--rh-up+z") - endif() - - set(${ARG_OUT} ${CLI} PARENT_SCOPE) - endfunction() - - # bgfx_compile_geometry( - # FILE filename - # OUTPUT filename - # [SCALE scale] - # [CCW] - # [FLIPV] - # [OBB num steps] - # [PACKNORMAL 0|1] - # [PACKUV 0|1] - # [TANGENT] - # [BARYCENTRIC] - # [COMPRESS] - # [LH_UP_Y|LH_UP_Z|RH_UP_Y|RH_UP_Z] - # ) - # - function(bgfx_compile_geometry) - cmake_parse_arguments( - ARG # - "CCW;FLIPV;TANGENT;BARYCENTRIC;COMPRESS;LH_UP_Y;LH_UP_Z;RH_UP_Y;RH_UP_Z" # - "FILE;OUTPUT;SCALE;OBB;PACKNORMAL;PACKUV" # - "" # - ${ARGN} # - ) - _bgfx_geometryc_parse(CLI ${ARGV}) - add_custom_command( - OUTPUT ${ARG_OUTPUT} # - COMMAND bgfx::geometryc ${CLI} # - MAIN_DEPENDENCY ${ARG_FILE} # - ) - endfunction() -endif() - -# If bgfx.cmake was compiled without tools or cross compiled without host having tools, -# then don't provide helper functions -if(TARGET bgfx::shaderc) - # _bgfx_shaderc_parse( - # FILE filename - # OUTPUT filename - # FRAGMENT|VERTEX|COMPUTE - # ANDROID|ASM_JS|IOS|LINUX|OSX|WINDOWS|ORBIS - # PROFILE profile - # [O 0|1|2|3] - # [VARYINGDEF filename] - # [BIN2C filename] - # [INCLUDES include;include] - # [DEFINES include;include] - # [DEPENDS] - # [PREPROCESS] - # [RAW] - # [VERBOSE] - # [DEBUG] - # [DISASM] - # [WERROR] - # ) - function(_bgfx_shaderc_parse ARG_OUT) - cmake_parse_arguments( - ARG - "DEPENDS;ANDROID;ASM_JS;IOS;LINUX;OSX;WINDOWS;ORBIS;PREPROCESS;RAW;FRAGMENT;VERTEX;COMPUTE;VERBOSE;DEBUG;DISASM;WERROR" - "FILE;OUTPUT;VARYINGDEF;BIN2C;PROFILE;O" - "INCLUDES;DEFINES" - ${ARGN} - ) - set(CLI "") - - # -f - if(ARG_FILE) - list(APPEND CLI "-f" "${ARG_FILE}") - else() - message(SEND_ERROR "Call to _bgfx_shaderc_parse() must have an input file path specified.") - endif() - - # -i - if(ARG_INCLUDES) - foreach(INCLUDE ${ARG_INCLUDES}) - list(APPEND CLI "-i") - list(APPEND CLI "${INCLUDE}") - endforeach() - endif() - - # -o - if(ARG_OUTPUT) - list(APPEND CLI "-o" "${ARG_OUTPUT}") - else() - message(SEND_ERROR "Call to _bgfx_shaderc_parse() must have an output file path specified.") - endif() - - # --bin2c - if(ARG_BIN2C) - list(APPEND CLI "--bin2c" "${ARG_BIN2C}") - endif() - - # --depends - if(ARG_DEPENDS) - list(APPEND CLI "--depends") - endif() - - # --platform - set(PLATFORM "") - set(PLATFORMS "ANDROID;ASM_JS;IOS;LINUX;OSX;WINDOWS;ORBIS") - foreach(P ${PLATFORMS}) - if(ARG_${P}) - if(PLATFORM) - message(SEND_ERROR "Call to _bgfx_shaderc_parse() cannot have both flags ${PLATFORM} and ${P}.") - return() - endif() - set(PLATFORM "${P}") - endif() - endforeach() - if(PLATFORM STREQUAL "") - message(SEND_ERROR "Call to _bgfx_shaderc_parse() must have a platform flag: ${PLATFORMS}") - return() - elseif(PLATFORM STREQUAL "ANDROID") - list(APPEND CLI "--platform" "android") - elseif(PLATFORM STREQUAL "ASM_JS") - list(APPEND CLI "--platform" "asm.js") - elseif(PLATFORM STREQUAL "IOS") - list(APPEND CLI "--platform" "ios") - elseif(PLATFORM STREQUAL "OSX") - list(APPEND CLI "--platform" "osx") - elseif(PLATFORM STREQUAL "LINUX") - list(APPEND CLI "--platform" "linux") - elseif(PLATFORM STREQUAL "WINDOWS") - list(APPEND CLI "--platform" "windows") - elseif(PLATFORM STREQUAL "ORBIS") - list(APPEND CLI "--platform" "orbis") - endif() - - # --preprocess - if(ARG_PREPROCESS) - list(APPEND CLI "--preprocess") - endif() - - # --define - if(ARG_DEFINES) - # Add extra escapes or CMake will expand in the final CLI - string(REPLACE ";" "\\\\\\;" DEFINES "${ARG_DEFINES}") - # Also need to quote escape for Unix shells - list(APPEND CLI "--define" "\"${DEFINES}\"") - endif() - - # --raw - if(ARG_RAW) - list(APPEND CLI "--raw") - endif() - - # --type - set(TYPE "") - set(TYPES "FRAGMENT;VERTEX;COMPUTE") - foreach(T ${TYPES}) - if(ARG_${T}) - if(TYPE) - message(SEND_ERROR "Call to _bgfx_shaderc_parse() cannot have both flags ${TYPE} and ${T}.") - return() - endif() - set(TYPE "${T}") - endif() - endforeach() - if("${TYPE}" STREQUAL "") - message(SEND_ERROR "Call to _bgfx_shaderc_parse() must have a type flag: ${TYPES}") - return() - elseif("${TYPE}" STREQUAL "FRAGMENT") - list(APPEND CLI "--type" "fragment") - elseif("${TYPE}" STREQUAL "VERTEX") - list(APPEND CLI "--type" "vertex") - elseif("${TYPE}" STREQUAL "COMPUTE") - list(APPEND CLI "--type" "compute") - endif() - - # --varyingdef - if(ARG_VARYINGDEF) - list(APPEND CLI "--varyingdef" "${ARG_VARYINGDEF}") - endif() - - # --verbose - if(ARG_VERBOSE) - list(APPEND CLI "--verbose") - endif() - - # --debug - if(ARG_DEBUG) - list(APPEND CLI "--debug") - endif() - - # --disasm - if(ARG_DISASM) - list(APPEND CLI "--disasm") - endif() - - # --profile - if(ARG_PROFILE) - list(APPEND CLI "--profile" "${ARG_PROFILE}") - else() - message(SEND_ERROR "Call to _bgfx_shaderc_parse() must have a shader profile.") - endif() - - # -O - if(ARG_O) - list(APPEND CLI "-O" "${ARG_O}") - endif() - - # --Werror - if(ARG_WERROR) - list(APPEND CLI "--Werror") - endif() - - set(${ARG_OUT} ${CLI} PARENT_SCOPE) - endfunction() - - # extensions consistent with those listed under bgfx/runtime/shaders - function(_bgfx_get_profile_path_ext PROFILE PROFILE_PATH_EXT) - string(REPLACE 100_es essl PROFILE ${PROFILE}) - string(REPLACE 300_es essl PROFILE ${PROFILE}) - string(REPLACE 120 glsl PROFILE ${PROFILE}) - string(REPLACE 430 glsl PROFILE ${PROFILE}) - string(REPLACE s_5_0 dxbc PROFILE ${PROFILE}) - string(REPLACE s_6_0 dxil PROFILE ${PROFILE}) - set(${PROFILE_PATH_EXT} ${PROFILE} PARENT_SCOPE) - endfunction() - - # extensions consistent with embedded_shader.h - function(_bgfx_get_profile_ext PROFILE PROFILE_EXT) - string(REPLACE 100_es essl PROFILE ${PROFILE}) - string(REPLACE 300_es essl PROFILE ${PROFILE}) - string(REPLACE 120 glsl PROFILE ${PROFILE}) - string(REPLACE 430 glsl PROFILE ${PROFILE}) - string(REPLACE spirv spv PROFILE ${PROFILE}) - string(REPLACE metal mtl PROFILE ${PROFILE}) - string(REPLACE s_5_0 dxbc PROFILE ${PROFILE}) - string(REPLACE s_6_0 dxil PROFILE ${PROFILE}) - set(${PROFILE_EXT} ${PROFILE} PARENT_SCOPE) - endfunction() - - # bgfx_compile_shaders( - # TYPE VERTEX|FRAGMENT|COMPUTE - # SHADERS filenames - # VARYING_DEF filename - # OUTPUT_DIR directory - # OUT_FILES_VAR variable name - # INCLUDE_DIRS directories - # DEFINES defines - # [AS_HEADERS] - # ) - # - function(bgfx_compile_shaders) - set(options AS_HEADERS) - set(oneValueArgs TYPE VARYING_DEF OUTPUT_DIR OUT_FILES_VAR) - set(multiValueArgs SHADERS INCLUDE_DIRS DEFINES) - cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") - - set(PROFILES spirv) - if(ARGS_TYPE STREQUAL "COMPUTE") - list(APPEND PROFILES 430 300_es) - else() - list(APPEND PROFILES 120 100_es) - endif() - if(BGFX_CONFIG_RENDERER_WEBGPU) - list(APPEND PROFILES wgsl) - endif() - if(IOS) - set(PLATFORM IOS) - list(APPEND PROFILES metal) - elseif(ANDROID) - set(PLATFORM ANDROID) - elseif(UNIX AND NOT APPLE) - set(PLATFORM LINUX) - elseif(EMSCRIPTEN) - set(PLATFORM ASM_JS) - elseif(APPLE) - set(PLATFORM OSX) - list(APPEND PROFILES metal) - elseif( - WIN32 - OR MINGW - OR MSYS - OR CYGWIN - ) - set(PLATFORM WINDOWS) - list(APPEND PROFILES s_5_0) - list(APPEND PROFILES s_6_0) - elseif(ORBIS) # ORBIS should be defined by a PS4 CMake toolchain - set(PLATFORM ORBIS) - list(APPEND PROFILES pssl) - else() - # pssl for Agc and Gnm renderers - # nvn for Nvn renderer - message(error "shaderc: Unsupported platform") - endif() - - set(ALL_OUTPUTS "") - foreach(SHADER_FILE ${ARGS_SHADERS}) - source_group("Shaders" FILES "${SHADER}") - get_filename_component(SHADER_FILE_BASENAME ${SHADER_FILE} NAME) - get_filename_component(SHADER_FILE_NAME_WE ${SHADER_FILE} NAME_WE) - get_filename_component(SHADER_FILE_ABSOLUTE ${SHADER_FILE} ABSOLUTE) - - # Build output targets and their commands - set(OUTPUTS "") - set(COMMANDS "") - set(MKDIR_COMMANDS "") - foreach(PROFILE ${PROFILES}) - _bgfx_get_profile_path_ext(${PROFILE} PROFILE_PATH_EXT) - _bgfx_get_profile_ext(${PROFILE} PROFILE_EXT) - if(ARGS_AS_HEADERS) - set(HEADER_PREFIX .h) - endif() - set(OUTPUT ${ARGS_OUTPUT_DIR}/${PROFILE_PATH_EXT}/${SHADER_FILE_BASENAME}.bin${HEADER_PREFIX}) - set(PLATFORM_I ${PLATFORM}) - set(BIN2C_PART "") - if(ARGS_AS_HEADERS) - set(BIN2C_PART BIN2C ${SHADER_FILE_NAME_WE}_${PROFILE_EXT}) - endif() - _bgfx_shaderc_parse( - CLI # - ${BIN2C_PART} # - ${ARGS_TYPE} ${PLATFORM_I} WERROR "$<$:DEBUG>" - FILE ${SHADER_FILE_ABSOLUTE} - OUTPUT ${OUTPUT} - PROFILE ${PROFILE} - O "$:0,3>" - VARYINGDEF ${ARGS_VARYING_DEF} - INCLUDES ${BGFX_SHADER_INCLUDE_PATH} ${ARGS_INCLUDE_DIRS} - DEFINES ${ARGS_DEFINES} - ) - list(APPEND OUTPUTS ${OUTPUT}) - list(APPEND ALL_OUTPUTS ${OUTPUT}) - list( - APPEND - MKDIR_COMMANDS - COMMAND - ${CMAKE_COMMAND} - -E - make_directory - ${ARGS_OUTPUT_DIR}/${PROFILE_PATH_EXT} - ) - list(APPEND COMMANDS COMMAND bgfx::shaderc ${CLI}) - endforeach() - - add_custom_command( - OUTPUT ${OUTPUTS} - COMMAND ${MKDIR_COMMANDS} ${COMMANDS} - MAIN_DEPENDENCY ${SHADER_FILE_ABSOLUTE} - DEPENDS ${ARGS_VARYING_DEF} - ) - endforeach() - - if(DEFINED ARGS_OUT_FILES_VAR) - set(${ARGS_OUT_FILES_VAR} ${ALL_OUTPUTS} PARENT_SCOPE) - endif() - endfunction() -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/astc_encoder.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/astc_encoder.cmake deleted file mode 100644 index a2155954..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/astc_encoder.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT ASTC_ENCODER_LIBRARIES) - file( - GLOB_RECURSE # - ASTC_ENCODER_SOURCES # - ${BIMG_DIR}/3rdparty/astc-encoder/source/*.cpp # - ${BIMG_DIR}/3rdparty/astc-encoder/source/*.h # - ) - set(ASTC_ENCODER_INCLUDE_DIR ${BIMG_DIR}/3rdparty/astc-encoder/include) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/edtaa3.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/edtaa3.cmake deleted file mode 100644 index 8b243972..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/edtaa3.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT EDTAA3_LIBRARIES) - file( - GLOB_RECURSE # - EDTAA3_SOURCES # - ${BIMG_DIR}/3rdparty/edtaa3/**.cpp # - ${BIMG_DIR}/3rdparty/edtaa3/**.h # - ) - set(EDTAA3_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc1.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc1.cmake deleted file mode 100644 index f16eba33..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc1.cmake +++ /dev/null @@ -1,22 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT ETC1_LIBRARIES) - file(GLOB_RECURSE ETC1_SOURCES ${BIMG_DIR}/3rdparty/etc1/**.cpp # - ${BIMG_DIR}/3rdparty/etc1/**.hpp # - ) - set(ETC1_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc2.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc2.cmake deleted file mode 100644 index 41bed011..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/etc2.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT ETC2_LIBRARIES) - file( - GLOB_RECURSE # - ETC2_SOURCES # - ${BIMG_DIR}/3rdparty/etc2/**.cpp # - ${BIMG_DIR}/3rdparty/etc2/**.hpp # - ) - set(ETC2_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/iqa.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/iqa.cmake deleted file mode 100644 index 4e1c40a5..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/iqa.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT IQA_LIBRARIES) - file( - GLOB_RECURSE # - IQA_SOURCES # - ${BIMG_DIR}/3rdparty/iqa/include/**.h # - ${BIMG_DIR}/3rdparty/iqa/source/**.c # - ) - set(IQA_INCLUDE_DIR ${BIMG_DIR}/3rdparty/iqa/include) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/libsquish.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/libsquish.cmake deleted file mode 100644 index 290df63c..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/libsquish.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT LIBSQUISH_LIBRARIES) - file( - GLOB_RECURSE # - LIBSQUISH_SOURCES # - ${BIMG_DIR}/3rdparty/libsquish/**.cpp # - ${BIMG_DIR}/3rdparty/libsquish/**.h # - ) - set(LIBSQUISH_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/loadpng.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/loadpng.cmake deleted file mode 100644 index 5f99436c..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/loadpng.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT LOADPNG_LIBRARIES) - file( - GLOB_RECURSE # - LOADPNG_SOURCES # - ${BIMG_DIR}/3rdparty/lodepng/lodepng.cpp # - ${BIMG_DIR}/3rdparty/lodepng/lodepng.h # - ) - set_source_files_properties(${BIMG_DIR}/3rdparty/lodepng/lodepng.cpp PROPERTIES HEADER_FILE_ONLY ON) - set(LOADPNG_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/miniz.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/miniz.cmake deleted file mode 100644 index 20d60f1f..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/miniz.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT MINIZ_LIBRARIES) - file(GLOB_RECURSE # - MINIZ_SOURCES # - ${BIMG_DIR}/3rdparty/tinyexr/deps/miniz/miniz.* # - ) - set(MINIZ_INCLUDE_DIR ${BIMG_DIR}/3rdparty/tinyexr/deps) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/nvtt.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/nvtt.cmake deleted file mode 100644 index 26a0b765..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/nvtt.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT NVTT_LIBRARIES) - file( - GLOB_RECURSE # - NVTT_SOURCES # - ${BIMG_DIR}/3rdparty/nvtt/**.cpp # - ${BIMG_DIR}/3rdparty/nvtt/**.h # - ) - set(NVTT_INCLUDE_DIR ${BIMG_DIR}/3rdparty/nvtt) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/pvrtc.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/pvrtc.cmake deleted file mode 100644 index c8974ed3..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/pvrtc.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT PVRTC_LIBRARIES) - file( - GLOB_RECURSE # - PVRTC_SOURCES # - ${BIMG_DIR}/3rdparty/pvrtc/**.cpp # - ${BIMG_DIR}/3rdparty/pvrtc/**.h # - ) - set(PVRTC_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/tinyexr.cmake b/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/tinyexr.cmake deleted file mode 100644 index f3cbc55f..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/3rdparty/tinyexr.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -if(NOT TINYEXR_LIBRARIES) - file(GLOB_RECURSE # - TINYEXR_SOURCES # - ${BIMG_DIR}/3rdparty/tinyexr/**.h # - ) - set(TINYEXR_INCLUDE_DIR ${BIMG_DIR}/3rdparty) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/CMakeLists.txt b/Engine/cpp/ThirdParty/cmake/bimg/CMakeLists.txt deleted file mode 100644 index 200b29bd..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -include(3rdparty/loadpng.cmake) -include(3rdparty/libsquish.cmake) -include(3rdparty/astc_encoder.cmake) -include(3rdparty/edtaa3.cmake) -include(3rdparty/etc1.cmake) -include(3rdparty/etc2.cmake) -include(3rdparty/nvtt.cmake) -include(3rdparty/pvrtc.cmake) -include(3rdparty/tinyexr.cmake) -include(3rdparty/iqa.cmake) -include(3rdparty/miniz.cmake) -include(bimg.cmake) -include(bimg_decode.cmake) -include(bimg_encode.cmake) - -if(BGFX_BUILD_TOOLS_TEXTURE) - include(texturec.cmake) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/bimg.cmake b/Engine/cpp/ThirdParty/cmake/bimg/bimg.cmake deleted file mode 100644 index 6f52fe18..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/bimg.cmake +++ /dev/null @@ -1,57 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg, directory does not exist. ${BIMG_DIR}") - return() -endif() - -file( - GLOB_RECURSE - BIMG_SOURCES - ${BIMG_DIR}/include/* # - ${BIMG_DIR}/src/image.* # - ${BIMG_DIR}/src/image_gnf.cpp # - # - ${ASTC_ENCODER_SOURCES} - ${MINIZ_SOURCES} -) - -add_library(bimg STATIC ${BIMG_SOURCES}) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bimg PROPERTIES FOLDER "bgfx") - -target_include_directories( - bimg PUBLIC $$ - PRIVATE ${ASTC_ENCODER_INCLUDE_DIR} # - ${MINIZ_INCLUDE_DIR} # -) - -target_link_libraries( - bimg - PUBLIC bx # - ${ASTC_ENCODER_LIBRARIES} # - ${MINIZ_LIBRARIES} # -) - -if(BGFX_INSTALL) - install( - TARGETS bimg - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - INCLUDES - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - ) - install(DIRECTORY ${BIMG_DIR}/include/bimg DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/bimg_decode.cmake b/Engine/cpp/ThirdParty/cmake/bimg/bimg_decode.cmake deleted file mode 100644 index a511e8f0..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/bimg_decode.cmake +++ /dev/null @@ -1,57 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg_decode, directory does not exist. ${BIMG_DIR}") - return() -endif() - -file( - GLOB_RECURSE - BIMG_DECODE_SOURCES # - ${BIMG_DIR}/include/* # - ${BIMG_DIR}/src/image_decode.* # - # - ${LOADPNG_SOURCES} # - ${MINIZ_SOURCES} # -) - -add_library(bimg_decode STATIC ${BIMG_DECODE_SOURCES}) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bimg_decode PROPERTIES FOLDER "bgfx") -target_include_directories( - bimg_decode - PUBLIC $ $ - PRIVATE ${LOADPNG_INCLUDE_DIR} # - ${MINIZ_INCLUDE_DIR} # - ${TINYEXR_INCLUDE_DIR} # -) - -target_link_libraries( - bimg_decode - PUBLIC bx # - ${LOADPNG_LIBRARIES} # - ${MINIZ_LIBRARIES} # - ${TINYEXR_LIBRARIES} # -) - -if(BGFX_INSTALL AND NOT BGFX_LIBRARY_TYPE MATCHES "SHARED") - install( - TARGETS bimg_decode - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - INCLUDES - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/bimg_encode.cmake b/Engine/cpp/ThirdParty/cmake/bimg/bimg_encode.cmake deleted file mode 100644 index 82d9fe09..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/bimg_encode.cmake +++ /dev/null @@ -1,99 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BIMG_DIR}) - message(SEND_ERROR "Could not load bimg_encode, directory does not exist. ${BIMG_DIR}") - return() -endif() - -add_library(bimg_encode STATIC) - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bimg_encode PROPERTIES FOLDER "bgfx") - -target_include_directories( - bimg_encode - PUBLIC $ $ - PRIVATE ${LIBSQUISH_INCLUDE_DIR} # - ${ASTC_ENCODER_INCLUDE_DIR} # - ${EDTAA3_INCLUDE_DIR} # - ${ETC1_INCLUDE_DIR} # - ${ETC2_INCLUDE_DIR} # - ${NVTT_INCLUDE_DIR} # - ${PVRTC_INCLUDE_DIR} # - ${TINYEXR_INCLUDE_DIR} # - ${IQA_INCLUDE_DIR} # - ${MINIZ_INCLUDE_DIR} # -) - -file( - GLOB_RECURSE - BIMG_ENCODE_SOURCES - ${BIMG_DIR}/include/* # - ${BIMG_DIR}/src/image_encode.* # - ${BIMG_DIR}/src/image_cubemap_filter.* # - ${LIBSQUISH_SOURCES} # - ${EDTAA3_SOURCES} # - ${ETC1_SOURCES} # - ${ETC2_SOURCES} # - ${NVTT_SOURCES} # - ${PVRTC_SOURCES} # - ${TINYEXR_SOURCES} - ${IQA_SOURCES} # -) - -target_sources(bimg_encode PRIVATE ${BIMG_ENCODE_SOURCES}) - -target_link_libraries( - bimg_encode - PUBLIC bx # - ${LIBSQUISH_LIBRARIES} # - ${ASTC_ENCODER_LIBRARIES} # - ${EDTAA3_LIBRARIES} # - ${ETC1_LIBRARIES} # - ${ETC2_LIBRARIES} # - ${NVTT_LIBRARIES} # - ${PVRTC_LIBRARIES} # - ${TINYEXR_LIBRARIES} # - ${IQA_LIBRARIES} # -) - -include(CheckCXXCompilerFlag) -foreach(flag "-Wno-implicit-fallthrough" "-Wno-shadow" "-Wno-shift-negative-value" "-Wno-undef") - check_cxx_compiler_flag(${flag} flag_supported) - if(flag_supported) - target_compile_options(bimg_encode PRIVATE ${flag}) - endif() -endforeach() - -foreach(flag "-Wno-class-memaccess" "-Wno-deprecated-copy") - check_cxx_compiler_flag(${flag} flag_supported) - if(flag_supported) - foreach(file ${BIMG_ENCODE_SOURCES}) - get_source_file_property(lang ${file} LANGUAGE) - if(lang STREQUAL "CXX") - set_source_files_properties(${file} PROPERTIES COMPILE_OPTIONS ${flag}) - endif() - endforeach() - endif() -endforeach() - -if(BGFX_INSTALL AND NOT BGFX_LIBRARY_TYPE MATCHES "SHARED") - install( - TARGETS bimg_encode - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - INCLUDES - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - ) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bimg/texturec.cmake b/Engine/cpp/ThirdParty/cmake/bimg/texturec.cmake deleted file mode 100644 index b6f7cac0..00000000 --- a/Engine/cpp/ThirdParty/cmake/bimg/texturec.cmake +++ /dev/null @@ -1,40 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the texturec source files -file(GLOB_RECURSE TEXTUREC_SOURCES # - ${BIMG_DIR}/tools/texturec/*.cpp # - ${BIMG_DIR}/tools/texturec/*.h # -) - -add_executable(texturec ${TEXTUREC_SOURCES}) - -target_link_libraries(texturec PRIVATE bimg_decode bimg_encode bimg) -set_target_properties( - texturec PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}texturec # -) - -if(BGFX_BUILD_TOOLS_TEXTURE) - add_executable(bgfx::texturec ALIAS texturec) - if(BGFX_CUSTOM_TARGETS) - add_dependencies(tools texturec) - endif() -endif() - -if(ANDROID) - target_link_libraries(texturec PRIVATE log) -elseif(IOS) - set_target_properties(texturec PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER texturec) -endif() - -if(BGFX_INSTALL) - install(TARGETS texturec EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bx/CMakeLists.txt b/Engine/cpp/ThirdParty/cmake/bx/CMakeLists.txt deleted file mode 100644 index 2ed7e282..00000000 --- a/Engine/cpp/ThirdParty/cmake/bx/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -include(bx.cmake) - -if(BGFX_BUILD_TOOLS_BIN2C) - include(bin2c.cmake) -endif() - -if(BGFX_BUILD_TESTS) - file( - GLOB - BX_TEST_SOURCES # - ${BX_DIR}/3rdparty/catch/catch_amalgamated.cpp - ${BX_DIR}/tests/*_test.cpp # - ${BX_DIR}/tests/*.h # - ${BX_DIR}/tests/dbg.* # - ) - add_executable(bx_test ${BX_TEST_SOURCES}) - target_compile_definitions(bx_test PRIVATE CATCH_AMALGAMATED_CUSTOM_MAIN) - target_link_libraries(bx_test PRIVATE bx) - add_test(NAME bx.test COMMAND bx_test) - - file( - GLOB - BX_BENCH_SOURCES # - ${BX_DIR}/tests/*_bench.cpp # - ${BX_DIR}/tests/*_bench.h # - ${BX_DIR}/tests/dbg.* # - ) - add_executable(bx_bench ${BX_BENCH_SOURCES}) - target_link_libraries(bx_bench PRIVATE bx) - add_test(NAME bx.bench COMMAND bx_bench) -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bx/bin2c.cmake b/Engine/cpp/ThirdParty/cmake/bx/bin2c.cmake deleted file mode 100644 index 3c361594..00000000 --- a/Engine/cpp/ThirdParty/cmake/bx/bin2c.cmake +++ /dev/null @@ -1,40 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Grab the bin2c source files -file(GLOB_RECURSE BIN2C_SOURCES # - ${BX_DIR}/tools/bin2c/*.cpp # - ${BX_DIR}/tools/bin2c/*.h # -) - -add_executable(bin2c ${BIN2C_SOURCES}) - -target_link_libraries(bin2c PRIVATE bx) -set_target_properties( - bin2c PROPERTIES FOLDER "bgfx/tools" # - OUTPUT_NAME ${BGFX_TOOLS_PREFIX}bin2c # -) - -if(BGFX_BUILD_TOOLS_BIN2C) - add_executable(bgfx::bin2c ALIAS bin2c) - if(BGFX_CUSTOM_TARGETS) - add_dependencies(tools bin2c) - endif() -endif() - -if(ANDROID) - target_link_libraries(bin2c PRIVATE log) -elseif(IOS) - set_target_properties(bin2c PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_GUI_IDENTIFIER bin2c) -endif() - -if(BGFX_INSTALL) - install(TARGETS bin2c EXPORT "${TARGETS_EXPORT_NAME}" DESTINATION "${CMAKE_INSTALL_BINDIR}") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/bx/bx.cmake b/Engine/cpp/ThirdParty/cmake/bx/bx.cmake deleted file mode 100644 index bbca9e51..00000000 --- a/Engine/cpp/ThirdParty/cmake/bx/bx.cmake +++ /dev/null @@ -1,143 +0,0 @@ -# bgfx.cmake - bgfx building in cmake -# Written in 2017 by Joshua Brookover -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along with -# this software. If not, see . - -# Ensure the directory exists -if(NOT IS_DIRECTORY ${BX_DIR}) - message(SEND_ERROR "Could not load bx, directory does not exist. ${BX_DIR}") - return() -endif() - -include(GNUInstallDirs) - -# Grab the bx source files -file( - GLOB_RECURSE - BX_SOURCES - ${BX_DIR}/include/*.h # - ${BX_DIR}/include/**.inl # - ${BX_DIR}/src/*.cpp # - ${BX_DIR}/scripts/*.natvis # -) - -if(BX_AMALGAMATED) - list(APPEND BX_NOBUILD "${BX_DIR}/src/allocator.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/bounds.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/bx.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/commandline.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/crtnone.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/debug.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/dtoa.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/easing.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/file.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/filepath.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/hash.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/math.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/mutex.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/os.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/process.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/semaphore.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/settings.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/sort.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/string.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/thread.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/timer.cpp") - list(APPEND BX_NOBUILD "${BX_DIR}/src/url.cpp") -else() - file(GLOB_RECURSE BX_NOBUILD "${BX_DIR}/src/amalgamated.*") -endif() - -# Exclude files from the build but keep them in project -foreach(BX_SRC ${BX_NOBUILD}) - set_source_files_properties(${BX_SRC} PROPERTIES HEADER_FILE_ONLY ON) -endforeach() - -add_library(bx STATIC ${BX_SOURCES}) - -if(MSVC) - target_compile_options(bx PRIVATE /EHs-c-) - target_compile_definitions(bx PRIVATE _HAS_EXCEPTIONS=0) -endif() - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bx PROPERTIES FOLDER "bgfx") - -# Build system specific configurations -if(MINGW) - set(BX_COMPAT_PLATFORM mingw) -elseif(WIN32) - set(BX_COMPAT_PLATFORM msvc) -elseif(APPLE) # APPLE is technically UNIX... ORDERING MATTERS! - set(BX_COMPAT_PLATFORM osx) -elseif(UNIX) - set(BX_COMPAT_PLATFORM linux) -endif() - -# Add include directory of bx -target_include_directories( - bx - PUBLIC $ # - $ # - $ # - $ # - $ # -) - -# All configurations -target_compile_definitions(bx PUBLIC "BX_CONFIG_DEBUG=$,1,$>") -target_compile_definitions(bx PUBLIC "__STDC_LIMIT_MACROS") -target_compile_definitions(bx PUBLIC "__STDC_FORMAT_MACROS") -target_compile_definitions(bx PUBLIC "__STDC_CONSTANT_MACROS") - -target_compile_features(bx PUBLIC cxx_std_14) -# (note: see bx\scripts\toolchain.lua for equivalent compiler flag) -target_compile_options(bx PUBLIC $<$:/Zc:__cplusplus /Zc:preprocessor>) - -# Link against psapi on Windows -if(WIN32) - target_link_libraries(bx PUBLIC psapi) -endif() - -# Additional dependencies on Unix -if(ANDROID) - # For __android_log_write - find_library(LOG_LIBRARY log) - mark_as_advanced(LOG_LIBRARY) - target_link_libraries(bx PUBLIC ${LOG_LIBRARY}) -elseif(APPLE) - find_library(FOUNDATION_LIBRARY Foundation) - mark_as_advanced(FOUNDATION_LIBRARY) - target_link_libraries(bx PUBLIC ${FOUNDATION_LIBRARY}) -elseif(UNIX) - # Threads - find_package(Threads) - target_link_libraries(bx ${CMAKE_THREAD_LIBS_INIT} dl) - - # Real time (for clock_gettime) - target_link_libraries(bx rt) -endif() - -# Put in a "bgfx" folder in Visual Studio -set_target_properties(bx PROPERTIES FOLDER "bgfx") - -if(BGFX_INSTALL) - install( - TARGETS bx - EXPORT "${TARGETS_EXPORT_NAME}" - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" - ) - # We will make sure tinystl and compat are not installed in /usr/include - install(DIRECTORY "${BX_DIR}/include/bx" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") - install(DIRECTORY "${BX_DIR}/include/compat/${BX_COMPAT_PLATFORM}" - DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/bx/compat" - ) - install(DIRECTORY "${BX_DIR}/include/tinystl" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/bx") -endif() diff --git a/Engine/cpp/ThirdParty/cmake/version.cmake b/Engine/cpp/ThirdParty/cmake/version.cmake deleted file mode 100644 index 43d1573e..00000000 --- a/Engine/cpp/ThirdParty/cmake/version.cmake +++ /dev/null @@ -1,40 +0,0 @@ -# bgfx versioning scheme: -# bgfx 1.104.7082 -# ^ ^^^ ^^^^ -# | | +--- Commit number (https://github.com/bkaradzic/bgfx / git rev-list --count HEAD) -# | +------- API version (from https://github.com/bkaradzic/bgfx/blob/master/scripts/bgfx.idl#L4) -# +--------- Major revision (always 1) -# -# BGFX_API_VERSION generated from https://github.com/bkaradzic/bgfx/blob/master/scripts/bgfx.idl#L4 -# bgfx/src/version.h: -# BGFX_REV_NUMBER -# BGFX_REV_SHA1 - -find_package(Git QUIET) - -execute_process( - COMMAND "${GIT_EXECUTABLE}" -C bgfx log --pretty=format:'%h' -n 1 - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REV - ERROR_QUIET -) - -execute_process( - COMMAND "${GIT_EXECUTABLE}" -C bgfx rev-list --count HEAD - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REV_COUNT - OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET -) - -# read version(100) from bgfx.idl -file(READ "${BGFX_DIR}/scripts/bgfx.idl" BGFX_IDL) -string(REGEX MATCH "version\\(([^\)]+)\\)" BGFX_API_VERSION ${BGFX_IDL}) -set(BGFX_API_VERSION ${CMAKE_MATCH_1}) -set(BGFX_REV_NUMBER ${GIT_REV_COUNT}) -set(BGFX_REV ${GIT_REV}) - -# set project specific versions -set(PROJECT_VERSION 1.${BGFX_API_VERSION}.${BGFX_REV_NUMBER}) -set(PROJECT_VERSION_MAJOR 1) -set(PROJECT_VERSION_MINOR ${BGFX_API_VERSION}) -set(PROJECT_VERSION_PATCH ${BGFX_REV_NUMBER}) diff --git a/Engine/cpp/ThirdParty/stb/stb_image.h b/Engine/cpp/ThirdParty/stb/stb_image.h index d4b5c87d..9eedabed 100644 --- a/Engine/cpp/ThirdParty/stb/stb_image.h +++ b/Engine/cpp/ThirdParty/stb/stb_image.h @@ -7985,4 +7985,4 @@ AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ -*/ \ No newline at end of file +*/ diff --git a/Engine/cpp/ThirdParty/stb/stb_image_write.h b/Engine/cpp/ThirdParty/stb/stb_image_write.h new file mode 100644 index 00000000..e4b32ed1 --- /dev/null +++ b/Engine/cpp/ThirdParty/stb/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/Engine/cpp/ThirdParty/stb/stb_truetype.h b/Engine/cpp/ThirdParty/stb/stb_truetype.h new file mode 100644 index 00000000..90a5c2e2 --- /dev/null +++ b/Engine/cpp/ThirdParty/stb/stb_truetype.h @@ -0,0 +1,5079 @@ +// stb_truetype.h - v1.26 - public domain +// authored from 2009-2021 by Sean Barrett / RAD Game Tools +// +// ======================================================================= +// +// NO SECURITY GUARANTEE -- DO NOT USE THIS ON UNTRUSTED FONT FILES +// +// This library does no range checking of the offsets found in the file, +// meaning an attacker can use it to read arbitrary memory. +// +// ======================================================================= +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// render glyphs to one-channel SDF bitmaps (signed-distance field/function) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// Dougall Johnson: OpenType / Type 2 font handling +// Daniel Ribeiro Maciel: basic GPOS-based kerning +// +// Misc other: +// Ryan Gordon +// Simon Glass +// github:IntellectualKitty +// Imanol Celaya +// Daniel Ribeiro Maciel +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket Fabian "ryg" Giesen github:NiLuJe +// Cass Everitt Martins Mozeiko github:aloucks +// stoiko (Haemimont Games) Cap Petschulat github:oyvindjam +// Brian Hook Omar Cornut github:vassvik +// Walter van Niftrik Ryan Griege +// David Gow Peter LaValle +// David Given Sergey Popov +// Ivan-Assen Ivanov Giumo X. Clanjor +// Anthony Pesch Higor Euripedes +// Johan Duparc Thomas Fields +// Hou Qiming Derek Vinyard +// Rob Loach Cort Stratton +// Kenney Phillis Jr. Brian Costabile +// Ken Voskuil (kaesve) Yakov Galka +// +// VERSION HISTORY +// +// 1.26 (2021-08-28) fix broken rasterizer +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) GPOS kerning, STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// See end of file for license information. +// +// USAGE +// +// Include this file in whatever places need to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversampling() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- indexing for TTC font collections +// stbtt_GetNumberOfFonts() -- number of fonts for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetFontVMetricsOS2() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// DETAILED USAGE: +// +// Scale: +// Select how high you want the font to be, in points or pixels. +// Call ScaleForPixelHeight or ScaleForMappingEmToPixels to compute +// a scale factor SF that will be used by all other functions. +// +// Baseline: +// You need to select a y-coordinate that is the baseline of where +// your text will appear. Call GetFontBoundingBox to get the baseline-relative +// bounding box for all characters. SF*-y0 will be the distance in pixels +// that the worst-case character could extend above the baseline, so if +// you want the top edge of characters to appear at the top of the +// screen where y=0, then you would set the baseline to SF*-y0. +// +// Current point: +// Set the current point where the first character will appear. The +// first character could extend left of the current point; this is font +// dependent. You can either choose a current point that is the leftmost +// point and hope, or add some padding, or check the bounding box or +// left-side-bearing of the first character to be displayed and set +// the current point based on that. +// +// Displaying a character: +// Compute the bounding box of the character. It will contain signed values +// relative to . I.e. if it returns x0,y0,x1,y1, +// then the character should be displayed in the rectangle from +// to = 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype, e.g. if you don't +//// link with the C runtime library. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // e.g. #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + #ifndef STBTT_sqrt + #include + #define STBTT_sqrt(x) sqrt(x) + #define STBTT_pow(x,y) pow(x,y) + #endif + + #ifndef STBTT_fmod + #include + #define STBTT_fmod(x,y) fmod(x,y) + #endif + + #ifndef STBTT_cos + #include + #define STBTT_cos(x) cos(x) + #define STBTT_acos(x) acos(x) + #endif + + #ifndef STBTT_fabs + #include + #define STBTT_fabs(x) fabs(x) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) ((void)(u),malloc(x)) + #define STBTT_free(x,u) ((void)(u),free(x)) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else +#define STBTT_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// private structure +typedef struct +{ + unsigned char *data; + int cursor; + int size; +} stbtt__buf; + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap); +// Query the font vertical metrics without having to create a font first. + + +////////////////////////////////////////////////////////////////////////////// +// +// NEW TEXTURE BAKING API +// +// This provides options for packing multiple fonts into one atlas, not +// perfectly but better than nothing. + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; + float xoff2,yoff2; +} stbtt_packedchar; + +typedef struct stbtt_pack_context stbtt_pack_context; +typedef struct stbtt_fontinfo stbtt_fontinfo; +#ifndef STB_RECT_PACK_VERSION +typedef struct stbrp_rect stbrp_rect; +#endif + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); +// Initializes a packing context stored in the passed-in stbtt_pack_context. +// Future calls using this context will pack characters into the bitmap passed +// in here: a 1-channel bitmap that is width * height. stride_in_bytes is +// the distance from one row to the next (or 0 to mean they are packed tightly +// together). "padding" is the amount of padding to leave between each +// character (normally you want '1' for bitmaps you'll use as textures with +// bilinear filtering). +// +// Returns 0 on failure, 1 on success. + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); +// Cleans up the packing context and frees all memory. + +#define STBTT_POINT_SIZE(x) (-(x)) + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); +// Creates character bitmaps from the font_index'th font found in fontdata (use +// font_index=0 if you don't know what that is). It creates num_chars_in_range +// bitmaps for characters with unicode values starting at first_unicode_char_in_range +// and increasing. Data for how to render them is stored in chardata_for_range; +// pass these to stbtt_GetPackedQuad to get back renderable quads. +// +// font_size is the full height of the character from ascender to descender, +// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed +// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() +// and pass that result as 'font_size': +// ..., 20 , ... // font max minus min y is 20 pixels tall +// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + +typedef struct +{ + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally +} stbtt_pack_range; + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); +// Creates character bitmaps from multiple ranges of characters stored in +// ranges. This will usually create a better-packed bitmap than multiple +// calls to stbtt_PackFontRange. Note that you can call this multiple +// times within a single PackBegin/PackEnd. + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); +// Oversampling a font increases the quality by allowing higher-quality subpixel +// positioning, and is especially valuable at smaller text sizes. +// +// This function sets the amount of oversampling for all following calls to +// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given +// pack context. The default (no oversampling) is achieved by h_oversample=1 +// and v_oversample=1. The total number of pixels required is +// h_oversample*v_oversample larger than the default; for example, 2x2 +// oversampling requires 4x the storage of 1x1. For best results, render +// oversampled textures with bilinear filtering. Look at the readme in +// stb/tests/oversample for information about oversampled fonts +// +// To use with PackFontRangesGather etc., you must set it before calls +// call to PackFontRangesGatherRects. + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip); +// If skip != 0, this tells stb_truetype to skip any codepoints for which +// there is no corresponding glyph. If skip=0, which is the default, then +// codepoints without a glyph recived the font's "missing character" glyph, +// typically an empty box by convention. + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +// Calling these functions in sequence is roughly equivalent to calling +// stbtt_PackFontRanges(). If you more control over the packing of multiple +// fonts, or if you want to pack custom data into a font texture, take a look +// at the source to of stbtt_PackFontRanges() and create a custom version +// using these functions, e.g. call GatherRects multiple times, +// building up a single array of rects, then call PackRects once, +// then call RenderIntoRects repeatedly. This may result in a +// better packing than calling PackFontRanges multiple times +// (or it may not). + +// this is an opaque structure that you shouldn't mess with which holds +// all the context needed from PackBegin to PackEnd. +struct stbtt_pack_context { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + int skip_missing; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data); +// This function will determine the number of fonts in a font file. TrueType +// collection (.ttc) files may contain multiple fonts, while TrueType font +// (.ttf) files only contain one font. The number of fonts can be used for +// indexing with the previous function where the index is between zero and one +// less than the total fonts. If an error occurs, -1 is returned. + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. + +// The following structure is defined publicly so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern,gpos,svg; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph + + stbtt__buf cff; // cff font data + stbtt__buf charstrings; // the charstring index + stbtt__buf gsubrs; // global charstring subroutines index + stbtt__buf subrs; // private charstring subroutines index + stbtt__buf fontdicts; // array of font dicts + stbtt__buf fdselect; // map from glyph to fontdict +}; + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. +// Returns 0 if the character codepoint is not defined in the font. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap); +// analogous to GetFontVMetrics, but returns the "typographic" values from the OS/2 +// table (specific to MS/Windows TTF files). +// +// Returns 1 on success (table present), 0 on failure. + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + +typedef struct stbtt_kerningentry +{ + int glyph1; // use stbtt_FindGlyphIndex + int glyph2; + int advance; +} stbtt_kerningentry; + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info); +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length); +// Retrieves a complete list of all of the kerning pairs provided by the font +// stbtt_GetKerningTable never writes more than table_length entries and returns how many entries it did write. +// The table will be sorted by (a.glyph1 == b.glyph1)?(a.glyph2 < b.glyph2):(a.glyph1 < b.glyph1) + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve, + STBTT_vcubic + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy,cx1,cy1; + unsigned char type,padding; + } stbtt_vertex; +#endif + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates +// +// The shape is a series of contours. Each one starts with +// a STBTT_moveto, then consists of a series of mixed +// STBTT_lineto and STBTT_curveto segments. A lineto +// draws a line from previous endpoint to its x,y; a curveto +// draws a quadratic bezier from previous endpoint to +// its x,y, using cx,cy as the bezier control point. + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +STBTT_DEF unsigned char *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl); +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg); +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg); +// fills svg with the character's SVG data. +// returns data size or 0 if SVG not found. + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint); +// same as stbtt_MakeCodepointBitmapSubpixel, but prefiltering +// is performed (see stbtt_PackSetOversampling) + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int glyph); +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +// rasterize a shape with quadratic beziers into a bitmap +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + +////////////////////////////////////////////////////////////////////////////// +// +// Signed Distance Function (or Field) rendering + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata); +// frees the SDF bitmap allocated below + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +// These functions compute a discretized SDF field for a single character, suitable for storing +// in a single-channel texture, sampling with bilinear filtering, and testing against +// larger than some threshold to produce scalable fonts. +// info -- the font +// scale -- controls the size of the resulting SDF bitmap, same as it would be creating a regular bitmap +// glyph/codepoint -- the character to generate the SDF for +// padding -- extra "pixels" around the character which are filled with the distance to the character (not 0), +// which allows effects like bit outlines +// onedge_value -- value 0-255 to test the SDF against to reconstruct the character (i.e. the isocontour of the character) +// pixel_dist_scale -- what value the SDF should increase by when moving one SDF "pixel" away from the edge (on the 0..255 scale) +// if positive, > onedge_value is inside; if negative, < onedge_value is inside +// width,height -- output height & width of the SDF bitmap (including padding) +// xoff,yoff -- output origin of the character +// return value -- a 2D array of bytes 0..255, width*height in size +// +// pixel_dist_scale & onedge_value are a scale & bias that allows you to make +// optimal use of the limited 0..255 for your application, trading off precision +// and special effects. SDF values outside the range 0..255 are clamped to 0..255. +// +// Example: +// scale = stbtt_ScaleForPixelHeight(22) +// padding = 5 +// onedge_value = 180 +// pixel_dist_scale = 180/5.0 = 36.0 +// +// This will create an SDF bitmap in which the character is about 22 pixels +// high but the whole bitmap is about 22+5+5=32 pixels high. To produce a filled +// shape, sample the SDF at each pixel and fill the pixel if the SDF value +// is greater than or equal to 180/255. (You'll actually want to antialias, +// which is beyond the scope of this example.) Additionally, you can compute +// offset outlines (e.g. to stroke the character border inside & outside, +// or only outside). For example, to fill outside the character up to 3 SDF +// pixels, you would compare against (180-36.0*3)/255 = 72/255. The above +// choice of variables maps a range from 5 pixels outside the shape to +// 2 pixels inside the shape to 0..255; this is intended primarily for apply +// outside effects only (the interior range is needed to allow proper +// antialiasing of the font at *smaller* sizes) +// +// The function computes the SDF analytically at each SDF pixel, not by e.g. +// building a higher-res bitmap and approximating it. In theory the quality +// should be as high as possible for an SDF of this size & representation, but +// unclear if this is true in practice (perhaps building a higher-res bitmap +// and computing from that can allow drop-out prevention). +// +// The algorithm has not been optimized at all, so expect it to be slow +// if computing lots of characters or very large sizes. + + + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#endif + +#if STBTT_MAX_OVERSAMPLE > 255 +#error "STBTT_MAX_OVERSAMPLE cannot be > 255" +#endif + +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +#ifdef _MSC_VER +#define STBTT__NOTUSED(v) (void)(v) +#else +#define STBTT__NOTUSED(v) (void)sizeof(v) +#endif + +////////////////////////////////////////////////////////////////////////// +// +// stbtt__buf helpers to parse data from file +// + +static stbtt_uint8 stbtt__buf_get8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor++]; +} + +static stbtt_uint8 stbtt__buf_peek8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor]; +} + +static void stbtt__buf_seek(stbtt__buf *b, int o) +{ + STBTT_assert(!(o > b->size || o < 0)); + b->cursor = (o > b->size || o < 0) ? b->size : o; +} + +static void stbtt__buf_skip(stbtt__buf *b, int o) +{ + stbtt__buf_seek(b, b->cursor + o); +} + +static stbtt_uint32 stbtt__buf_get(stbtt__buf *b, int n) +{ + stbtt_uint32 v = 0; + int i; + STBTT_assert(n >= 1 && n <= 4); + for (i = 0; i < n; i++) + v = (v << 8) | stbtt__buf_get8(b); + return v; +} + +static stbtt__buf stbtt__new_buf(const void *p, size_t size) +{ + stbtt__buf r; + STBTT_assert(size < 0x40000000); + r.data = (stbtt_uint8*) p; + r.size = (int) size; + r.cursor = 0; + return r; +} + +#define stbtt__buf_get16(b) stbtt__buf_get((b), 2) +#define stbtt__buf_get32(b) stbtt__buf_get((b), 4) + +static stbtt__buf stbtt__buf_range(const stbtt__buf *b, int o, int s) +{ + stbtt__buf r = stbtt__new_buf(NULL, 0); + if (o < 0 || s < 0 || o > b->size || s > b->size - o) return r; + r.data = b->data + o; + r.size = s; + return r; +} + +static stbtt__buf stbtt__cff_get_index(stbtt__buf *b) +{ + int count, start, offsize; + start = b->cursor; + count = stbtt__buf_get16(b); + if (count) { + offsize = stbtt__buf_get8(b); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(b, offsize * count); + stbtt__buf_skip(b, stbtt__buf_get(b, offsize) - 1); + } + return stbtt__buf_range(b, start, b->cursor - start); +} + +static stbtt_uint32 stbtt__cff_int(stbtt__buf *b) +{ + int b0 = stbtt__buf_get8(b); + if (b0 >= 32 && b0 <= 246) return b0 - 139; + else if (b0 >= 247 && b0 <= 250) return (b0 - 247)*256 + stbtt__buf_get8(b) + 108; + else if (b0 >= 251 && b0 <= 254) return -(b0 - 251)*256 - stbtt__buf_get8(b) - 108; + else if (b0 == 28) return stbtt__buf_get16(b); + else if (b0 == 29) return stbtt__buf_get32(b); + STBTT_assert(0); + return 0; +} + +static void stbtt__cff_skip_operand(stbtt__buf *b) { + int v, b0 = stbtt__buf_peek8(b); + STBTT_assert(b0 >= 28); + if (b0 == 30) { + stbtt__buf_skip(b, 1); + while (b->cursor < b->size) { + v = stbtt__buf_get8(b); + if ((v & 0xF) == 0xF || (v >> 4) == 0xF) + break; + } + } else { + stbtt__cff_int(b); + } +} + +static stbtt__buf stbtt__dict_get(stbtt__buf *b, int key) +{ + stbtt__buf_seek(b, 0); + while (b->cursor < b->size) { + int start = b->cursor, end, op; + while (stbtt__buf_peek8(b) >= 28) + stbtt__cff_skip_operand(b); + end = b->cursor; + op = stbtt__buf_get8(b); + if (op == 12) op = stbtt__buf_get8(b) | 0x100; + if (op == key) return stbtt__buf_range(b, start, end-start); + } + return stbtt__buf_range(b, 0, 0); +} + +static void stbtt__dict_get_ints(stbtt__buf *b, int key, int outcount, stbtt_uint32 *out) +{ + int i; + stbtt__buf operands = stbtt__dict_get(b, key); + for (i = 0; i < outcount && operands.cursor < operands.size; i++) + out[i] = stbtt__cff_int(&operands); +} + +static int stbtt__cff_index_count(stbtt__buf *b) +{ + stbtt__buf_seek(b, 0); + return stbtt__buf_get16(b); +} + +static stbtt__buf stbtt__cff_index_get(stbtt__buf b, int i) +{ + int count, offsize, start, end; + stbtt__buf_seek(&b, 0); + count = stbtt__buf_get16(&b); + offsize = stbtt__buf_get8(&b); + STBTT_assert(i >= 0 && i < count); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(&b, i*offsize); + start = stbtt__buf_get(&b, offsize); + end = stbtt__buf_get(&b, offsize); + return stbtt__buf_range(&b, 2+(count+1)*offsize+start, end - start); +} + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +static stbtt_uint16 ttUSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_int16 ttSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_uint32 ttULONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } +static stbtt_int32 ttLONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + if (stbtt_tag(font, "true")) return 1; // Apple specification for TrueType fonts + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +static int stbtt_GetFontOffsetForIndex_internal(unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*4); + } + } + return -1; +} + +static int stbtt_GetNumberOfFonts_internal(unsigned char *font_collection) +{ + // if it's just a font, there's only one valid font + if (stbtt__isfont(font_collection)) + return 1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + return ttLONG(font_collection+8); + } + } + return 0; +} + +static stbtt__buf stbtt__get_subrs(stbtt__buf cff, stbtt__buf fontdict) +{ + stbtt_uint32 subrsoff = 0, private_loc[2] = { 0, 0 }; + stbtt__buf pdict; + stbtt__dict_get_ints(&fontdict, 18, 2, private_loc); + if (!private_loc[1] || !private_loc[0]) return stbtt__new_buf(NULL, 0); + pdict = stbtt__buf_range(&cff, private_loc[1], private_loc[0]); + stbtt__dict_get_ints(&pdict, 19, 1, &subrsoff); + if (!subrsoff) return stbtt__new_buf(NULL, 0); + stbtt__buf_seek(&cff, private_loc[1]+subrsoff); + return stbtt__cff_get_index(&cff); +} + +// since most people won't use this, find this table the first time it's needed +static int stbtt__get_svg(stbtt_fontinfo *info) +{ + stbtt_uint32 t; + if (info->svg < 0) { + t = stbtt__find_table(info->data, info->fontstart, "SVG "); + if (t) { + stbtt_uint32 offset = ttULONG(info->data + t + 2); + info->svg = t + offset; + } else { + info->svg = 0; + } + } + return info->svg; +} + +static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, int fontstart) +{ + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + info->cff = stbtt__new_buf(NULL, 0); + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + info->gpos = stbtt__find_table(data, fontstart, "GPOS"); // not required + + if (!cmap || !info->head || !info->hhea || !info->hmtx) + return 0; + if (info->glyf) { + // required for truetype + if (!info->loca) return 0; + } else { + // initialization for CFF / Type2 fonts (OTF) + stbtt__buf b, topdict, topdictidx; + stbtt_uint32 cstype = 2, charstrings = 0, fdarrayoff = 0, fdselectoff = 0; + stbtt_uint32 cff; + + cff = stbtt__find_table(data, fontstart, "CFF "); + if (!cff) return 0; + + info->fontdicts = stbtt__new_buf(NULL, 0); + info->fdselect = stbtt__new_buf(NULL, 0); + + // @TODO this should use size from table (not 512MB) + info->cff = stbtt__new_buf(data+cff, 512*1024*1024); + b = info->cff; + + // read the header + stbtt__buf_skip(&b, 2); + stbtt__buf_seek(&b, stbtt__buf_get8(&b)); // hdrsize + + // @TODO the name INDEX could list multiple fonts, + // but we just use the first one. + stbtt__cff_get_index(&b); // name INDEX + topdictidx = stbtt__cff_get_index(&b); + topdict = stbtt__cff_index_get(topdictidx, 0); + stbtt__cff_get_index(&b); // string INDEX + info->gsubrs = stbtt__cff_get_index(&b); + + stbtt__dict_get_ints(&topdict, 17, 1, &charstrings); + stbtt__dict_get_ints(&topdict, 0x100 | 6, 1, &cstype); + stbtt__dict_get_ints(&topdict, 0x100 | 36, 1, &fdarrayoff); + stbtt__dict_get_ints(&topdict, 0x100 | 37, 1, &fdselectoff); + info->subrs = stbtt__get_subrs(b, topdict); + + // we only support Type 2 charstrings + if (cstype != 2) return 0; + if (charstrings == 0) return 0; + + if (fdarrayoff) { + // looks like a CID font + if (!fdselectoff) return 0; + stbtt__buf_seek(&b, fdarrayoff); + info->fontdicts = stbtt__cff_get_index(&b); + info->fdselect = stbtt__buf_range(&b, fdselectoff, b.size-fdselectoff); + } + + stbtt__buf_seek(&b, charstrings); + info->charstrings = stbtt__cff_get_index(&b); + } + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + info->svg = -1; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start, last; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + last = ttUSHORT(data + endCount + 2*item); + if (unicode_codepoint < start || unicode_codepoint > last) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + STBTT_assert(!info->cff.size); + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); + +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + if (info->cff.size) { + stbtt__GetGlyphInfoT2(info, glyph_index, x0, y0, x1, y1); + } else { + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + } + return 1; +} + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g; + if (info->cff.size) + return stbtt__GetGlyphInfoT2(info, glyph_index, NULL, NULL, NULL, NULL) == 0; + g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours < 0) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0 && vertices) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); + STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +typedef struct +{ + int bounds; + int started; + float first_x, first_y; + float x, y; + stbtt_int32 min_x, max_x, min_y, max_y; + + stbtt_vertex *pvertices; + int num_vertices; +} stbtt__csctx; + +#define STBTT__CSCTX_INIT(bounds) {bounds,0, 0,0, 0,0, 0,0,0,0, NULL, 0} + +static void stbtt__track_vertex(stbtt__csctx *c, stbtt_int32 x, stbtt_int32 y) +{ + if (x > c->max_x || !c->started) c->max_x = x; + if (y > c->max_y || !c->started) c->max_y = y; + if (x < c->min_x || !c->started) c->min_x = x; + if (y < c->min_y || !c->started) c->min_y = y; + c->started = 1; +} + +static void stbtt__csctx_v(stbtt__csctx *c, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy, stbtt_int32 cx1, stbtt_int32 cy1) +{ + if (c->bounds) { + stbtt__track_vertex(c, x, y); + if (type == STBTT_vcubic) { + stbtt__track_vertex(c, cx, cy); + stbtt__track_vertex(c, cx1, cy1); + } + } else { + stbtt_setvertex(&c->pvertices[c->num_vertices], type, x, y, cx, cy); + c->pvertices[c->num_vertices].cx1 = (stbtt_int16) cx1; + c->pvertices[c->num_vertices].cy1 = (stbtt_int16) cy1; + } + c->num_vertices++; +} + +static void stbtt__csctx_close_shape(stbtt__csctx *ctx) +{ + if (ctx->first_x != ctx->x || ctx->first_y != ctx->y) + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->first_x, (int)ctx->first_y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rmove_to(stbtt__csctx *ctx, float dx, float dy) +{ + stbtt__csctx_close_shape(ctx); + ctx->first_x = ctx->x = ctx->x + dx; + ctx->first_y = ctx->y = ctx->y + dy; + stbtt__csctx_v(ctx, STBTT_vmove, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rline_to(stbtt__csctx *ctx, float dx, float dy) +{ + ctx->x += dx; + ctx->y += dy; + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rccurve_to(stbtt__csctx *ctx, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) +{ + float cx1 = ctx->x + dx1; + float cy1 = ctx->y + dy1; + float cx2 = cx1 + dx2; + float cy2 = cy1 + dy2; + ctx->x = cx2 + dx3; + ctx->y = cy2 + dy3; + stbtt__csctx_v(ctx, STBTT_vcubic, (int)ctx->x, (int)ctx->y, (int)cx1, (int)cy1, (int)cx2, (int)cy2); +} + +static stbtt__buf stbtt__get_subr(stbtt__buf idx, int n) +{ + int count = stbtt__cff_index_count(&idx); + int bias = 107; + if (count >= 33900) + bias = 32768; + else if (count >= 1240) + bias = 1131; + n += bias; + if (n < 0 || n >= count) + return stbtt__new_buf(NULL, 0); + return stbtt__cff_index_get(idx, n); +} + +static stbtt__buf stbtt__cid_get_glyph_subrs(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt__buf fdselect = info->fdselect; + int nranges, start, end, v, fmt, fdselector = -1, i; + + stbtt__buf_seek(&fdselect, 0); + fmt = stbtt__buf_get8(&fdselect); + if (fmt == 0) { + // untested + stbtt__buf_skip(&fdselect, glyph_index); + fdselector = stbtt__buf_get8(&fdselect); + } else if (fmt == 3) { + nranges = stbtt__buf_get16(&fdselect); + start = stbtt__buf_get16(&fdselect); + for (i = 0; i < nranges; i++) { + v = stbtt__buf_get8(&fdselect); + end = stbtt__buf_get16(&fdselect); + if (glyph_index >= start && glyph_index < end) { + fdselector = v; + break; + } + start = end; + } + } + if (fdselector == -1) stbtt__new_buf(NULL, 0); + return stbtt__get_subrs(info->cff, stbtt__cff_index_get(info->fontdicts, fdselector)); +} + +static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, stbtt__csctx *c) +{ + int in_header = 1, maskbits = 0, subr_stack_height = 0, sp = 0, v, i, b0; + int has_subrs = 0, clear_stack; + float s[48]; + stbtt__buf subr_stack[10], subrs = info->subrs, b; + float f; + +#define STBTT__CSERR(s) (0) + + // this currently ignores the initial width value, which isn't needed if we have hmtx + b = stbtt__cff_index_get(info->charstrings, glyph_index); + while (b.cursor < b.size) { + i = 0; + clear_stack = 1; + b0 = stbtt__buf_get8(&b); + switch (b0) { + // @TODO implement hinting + case 0x13: // hintmask + case 0x14: // cntrmask + if (in_header) + maskbits += (sp / 2); // implicit "vstem" + in_header = 0; + stbtt__buf_skip(&b, (maskbits + 7) / 8); + break; + + case 0x01: // hstem + case 0x03: // vstem + case 0x12: // hstemhm + case 0x17: // vstemhm + maskbits += (sp / 2); + break; + + case 0x15: // rmoveto + in_header = 0; + if (sp < 2) return STBTT__CSERR("rmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-2], s[sp-1]); + break; + case 0x04: // vmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("vmoveto stack"); + stbtt__csctx_rmove_to(c, 0, s[sp-1]); + break; + case 0x16: // hmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("hmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-1], 0); + break; + + case 0x05: // rlineto + if (sp < 2) return STBTT__CSERR("rlineto stack"); + for (; i + 1 < sp; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + // hlineto/vlineto and vhcurveto/hvcurveto alternate horizontal and vertical + // starting from a different place. + + case 0x07: // vlineto + if (sp < 1) return STBTT__CSERR("vlineto stack"); + goto vlineto; + case 0x06: // hlineto + if (sp < 1) return STBTT__CSERR("hlineto stack"); + for (;;) { + if (i >= sp) break; + stbtt__csctx_rline_to(c, s[i], 0); + i++; + vlineto: + if (i >= sp) break; + stbtt__csctx_rline_to(c, 0, s[i]); + i++; + } + break; + + case 0x1F: // hvcurveto + if (sp < 4) return STBTT__CSERR("hvcurveto stack"); + goto hvcurveto; + case 0x1E: // vhcurveto + if (sp < 4) return STBTT__CSERR("vhcurveto stack"); + for (;;) { + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], (sp - i == 5) ? s[i + 4] : 0.0f); + i += 4; + hvcurveto: + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], (sp - i == 5) ? s[i+4] : 0.0f, s[i+3]); + i += 4; + } + break; + + case 0x08: // rrcurveto + if (sp < 6) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x18: // rcurveline + if (sp < 8) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp - 2; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + if (i + 1 >= sp) return STBTT__CSERR("rcurveline stack"); + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + case 0x19: // rlinecurve + if (sp < 8) return STBTT__CSERR("rlinecurve stack"); + for (; i + 1 < sp - 6; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + if (i + 5 >= sp) return STBTT__CSERR("rlinecurve stack"); + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x1A: // vvcurveto + case 0x1B: // hhcurveto + if (sp < 4) return STBTT__CSERR("(vv|hh)curveto stack"); + f = 0.0; + if (sp & 1) { f = s[i]; i++; } + for (; i + 3 < sp; i += 4) { + if (b0 == 0x1B) + stbtt__csctx_rccurve_to(c, s[i], f, s[i+1], s[i+2], s[i+3], 0.0); + else + stbtt__csctx_rccurve_to(c, f, s[i], s[i+1], s[i+2], 0.0, s[i+3]); + f = 0.0; + } + break; + + case 0x0A: // callsubr + if (!has_subrs) { + if (info->fdselect.size) + subrs = stbtt__cid_get_glyph_subrs(info, glyph_index); + has_subrs = 1; + } + // FALLTHROUGH + case 0x1D: // callgsubr + if (sp < 1) return STBTT__CSERR("call(g|)subr stack"); + v = (int) s[--sp]; + if (subr_stack_height >= 10) return STBTT__CSERR("recursion limit"); + subr_stack[subr_stack_height++] = b; + b = stbtt__get_subr(b0 == 0x0A ? subrs : info->gsubrs, v); + if (b.size == 0) return STBTT__CSERR("subr not found"); + b.cursor = 0; + clear_stack = 0; + break; + + case 0x0B: // return + if (subr_stack_height <= 0) return STBTT__CSERR("return outside subr"); + b = subr_stack[--subr_stack_height]; + clear_stack = 0; + break; + + case 0x0E: // endchar + stbtt__csctx_close_shape(c); + return 1; + + case 0x0C: { // two-byte escape + float dx1, dx2, dx3, dx4, dx5, dx6, dy1, dy2, dy3, dy4, dy5, dy6; + float dx, dy; + int b1 = stbtt__buf_get8(&b); + switch (b1) { + // @TODO These "flex" implementations ignore the flex-depth and resolution, + // and always draw beziers. + case 0x22: // hflex + if (sp < 7) return STBTT__CSERR("hflex stack"); + dx1 = s[0]; + dx2 = s[1]; + dy2 = s[2]; + dx3 = s[3]; + dx4 = s[4]; + dx5 = s[5]; + dx6 = s[6]; + stbtt__csctx_rccurve_to(c, dx1, 0, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, -dy2, dx6, 0); + break; + + case 0x23: // flex + if (sp < 13) return STBTT__CSERR("flex stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = s[10]; + dy6 = s[11]; + //fd is s[12] + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + case 0x24: // hflex1 + if (sp < 9) return STBTT__CSERR("hflex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dx4 = s[5]; + dx5 = s[6]; + dy5 = s[7]; + dx6 = s[8]; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, dy5, dx6, -(dy1+dy2+dy5)); + break; + + case 0x25: // flex1 + if (sp < 11) return STBTT__CSERR("flex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = dy6 = s[10]; + dx = dx1+dx2+dx3+dx4+dx5; + dy = dy1+dy2+dy3+dy4+dy5; + if (STBTT_fabs(dx) > STBTT_fabs(dy)) + dy6 = -dy; + else + dx6 = -dx; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + default: + return STBTT__CSERR("unimplemented"); + } + } break; + + default: + if (b0 != 255 && b0 != 28 && b0 < 32) + return STBTT__CSERR("reserved operator"); + + // push immediate + if (b0 == 255) { + f = (float)(stbtt_int32)stbtt__buf_get32(&b) / 0x10000; + } else { + stbtt__buf_skip(&b, -1); + f = (float)(stbtt_int16)stbtt__cff_int(&b); + } + if (sp >= 48) return STBTT__CSERR("push stack overflow"); + s[sp++] = f; + clear_stack = 0; + break; + } + if (clear_stack) sp = 0; + } + return STBTT__CSERR("no endchar"); + +#undef STBTT__CSERR +} + +static int stbtt__GetGlyphShapeT2(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + // runs the charstring twice, once to count and once to output (to avoid realloc) + stbtt__csctx count_ctx = STBTT__CSCTX_INIT(1); + stbtt__csctx output_ctx = STBTT__CSCTX_INIT(0); + if (stbtt__run_charstring(info, glyph_index, &count_ctx)) { + *pvertices = (stbtt_vertex*)STBTT_malloc(count_ctx.num_vertices*sizeof(stbtt_vertex), info->userdata); + output_ctx.pvertices = *pvertices; + if (stbtt__run_charstring(info, glyph_index, &output_ctx)) { + STBTT_assert(output_ctx.num_vertices == count_ctx.num_vertices); + return output_ctx.num_vertices; + } + } + *pvertices = NULL; + return 0; +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + stbtt__csctx c = STBTT__CSCTX_INIT(1); + int r = stbtt__run_charstring(info, glyph_index, &c); + if (x0) *x0 = r ? c.min_x : 0; + if (y0) *y0 = r ? c.min_y : 0; + if (x1) *x1 = r ? c.max_x : 0; + if (y1) *y1 = r ? c.max_y : 0; + return r ? c.num_vertices : 0; +} + +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + if (!info->cff.size) + return stbtt__GetGlyphShapeTT(info, glyph_index, pvertices); + else + return stbtt__GetGlyphShapeT2(info, glyph_index, pvertices); +} + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info) +{ + stbtt_uint8 *data = info->data + info->kern; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + return ttUSHORT(data+10); +} + +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length) +{ + stbtt_uint8 *data = info->data + info->kern; + int k, length; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + length = ttUSHORT(data+10); + if (table_length < length) + length = table_length; + + for (k = 0; k < length; k++) + { + table[k].glyph1 = ttUSHORT(data+18+(k*6)); + table[k].glyph2 = ttUSHORT(data+20+(k*6)); + table[k].advance = ttSHORT(data+22+(k*6)); + } + + return length; +} + +static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) +{ + stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); + switch (coverageFormat) { + case 1: { + stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); + + // Binary search. + stbtt_int32 l=0, r=glyphCount-1, m; + int straw, needle=glyph; + while (l <= r) { + stbtt_uint8 *glyphArray = coverageTable + 4; + stbtt_uint16 glyphID; + m = (l + r) >> 1; + glyphID = ttUSHORT(glyphArray + 2 * m); + straw = glyphID; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + return m; + } + } + break; + } + + case 2: { + stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); + stbtt_uint8 *rangeArray = coverageTable + 4; + + // Binary search. + stbtt_int32 l=0, r=rangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *rangeRecord; + m = (l + r) >> 1; + rangeRecord = rangeArray + 6 * m; + strawStart = ttUSHORT(rangeRecord); + strawEnd = ttUSHORT(rangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else { + stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); + return startCoverageIndex + glyph - strawStart; + } + } + break; + } + + default: return -1; // unsupported + } + + return -1; +} + +static stbtt_int32 stbtt__GetGlyphClass(stbtt_uint8 *classDefTable, int glyph) +{ + stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); + switch (classDefFormat) + { + case 1: { + stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); + stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); + stbtt_uint8 *classDef1ValueArray = classDefTable + 6; + + if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) + return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + break; + } + + case 2: { + stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); + stbtt_uint8 *classRangeRecords = classDefTable + 4; + + // Binary search. + stbtt_int32 l=0, r=classRangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *classRangeRecord; + m = (l + r) >> 1; + classRangeRecord = classRangeRecords + 6 * m; + strawStart = ttUSHORT(classRangeRecord); + strawEnd = ttUSHORT(classRangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else + return (stbtt_int32)ttUSHORT(classRangeRecord + 4); + } + break; + } + + default: + return -1; // Unsupported definition type, return an error. + } + + // "All glyphs not assigned to a class fall into class 0". (OpenType spec) + return 0; +} + +// Define to STBTT_assert(x) if you want to break on unimplemented formats. +#define STBTT_GPOS_TODO_assert(x) + +static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint16 lookupListOffset; + stbtt_uint8 *lookupList; + stbtt_uint16 lookupCount; + stbtt_uint8 *data; + stbtt_int32 i, sti; + + if (!info->gpos) return 0; + + data = info->data + info->gpos; + + if (ttUSHORT(data+0) != 1) return 0; // Major version 1 + if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 + + lookupListOffset = ttUSHORT(data+8); + lookupList = data + lookupListOffset; + lookupCount = ttUSHORT(lookupList); + + for (i=0; i= pairSetCount) return 0; + + needle=glyph2; + r=pairValueCount-1; + l=0; + + // Binary search. + while (l <= r) { + stbtt_uint16 secondGlyph; + stbtt_uint8 *pairValue; + m = (l + r) >> 1; + pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; + secondGlyph = ttUSHORT(pairValue); + straw = secondGlyph; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + stbtt_int16 xAdvance = ttSHORT(pairValue + 2); + return xAdvance; + } + } + } else + return 0; + break; + } + + case 2: { + stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); + stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + if (valueFormat1 == 4 && valueFormat2 == 0) { // Support more formats? + stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); + stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); + int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); + int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); + + stbtt_uint16 class1Count = ttUSHORT(table + 12); + stbtt_uint16 class2Count = ttUSHORT(table + 14); + stbtt_uint8 *class1Records, *class2Records; + stbtt_int16 xAdvance; + + if (glyph1class < 0 || glyph1class >= class1Count) return 0; // malformed + if (glyph2class < 0 || glyph2class >= class2Count) return 0; // malformed + + class1Records = table + 16; + class2Records = class1Records + 2 * (glyph1class * class2Count); + xAdvance = ttSHORT(class2Records + 2 * glyph2class); + return xAdvance; + } else + return 0; + break; + } + + default: + return 0; // Unsupported position format + } + } + } + + return 0; +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int g2) +{ + int xAdvance = 0; + + if (info->gpos) + xAdvance += stbtt__GetGlyphGPOSInfoAdvance(info, g1, g2); + else if (info->kern) + xAdvance += stbtt__GetGlyphKernInfoAdvance(info, g1, g2); + + return xAdvance; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern && !info->gpos) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap) +{ + int tab = stbtt__find_table(info->data, info->fontstart, "OS/2"); + if (!tab) + return 0; + if (typoAscent ) *typoAscent = ttSHORT(info->data+tab + 68); + if (typoDescent) *typoDescent = ttSHORT(info->data+tab + 70); + if (typoLineGap) *typoLineGap = ttSHORT(info->data+tab + 72); + return 1; +} + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +STBTT_DEF stbtt_uint8 *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl) +{ + int i; + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc_list = data + stbtt__get_svg((stbtt_fontinfo *) info); + + int numEntries = ttUSHORT(svg_doc_list); + stbtt_uint8 *svg_docs = svg_doc_list + 2; + + for(i=0; i= ttUSHORT(svg_doc)) && (gl <= ttUSHORT(svg_doc + 2))) + return svg_doc; + } + return 0; +} + +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg) +{ + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc; + + if (info->svg == 0) + return 0; + + svg_doc = stbtt_FindSVGDoc(info, gl); + if (svg_doc != NULL) { + *svg = (char *) data + info->svg + ttULONG(svg_doc + 4); + return ttULONG(svg_doc + 8); + } else { + return 0; + } +} + +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg) +{ + return stbtt_GetGlyphSVG(info, stbtt_FindGlyphIndex(info, unicode_codepoint), svg); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { + // e.g. space character + if (ix0) *ix0 = 0; + if (iy0) *iy0 = 0; + if (ix1) *ix1 = 0; + if (iy1) *iy1 = 0; + } else { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); + if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); + if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); + } +} + +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + struct stbtt__hheap_chunk *next; +} stbtt__hheap_chunk; + +typedef struct stbtt__hheap +{ + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; +} stbtt__hheap; + +static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) +{ + if (hh->first_free) { + void *p = hh->first_free; + hh->first_free = * (void **) p; + return p; + } else { + if (hh->num_remaining_in_head_chunk == 0) { + int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + sizeof(stbtt__hheap_chunk) + size * hh->num_remaining_in_head_chunk; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *p) +{ + *(void **) p = hh->first_free; + hh->first_free = p; +} + +static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) +{ + stbtt__hheap_chunk *c = hh->head; + while (c) { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + + +typedef struct stbtt__active_edge +{ + struct stbtt__active_edge *next; + #if STBTT_RASTERIZER_VERSION==1 + int x,dx; + float ey; + int direction; + #elif STBTT_RASTERIZER_VERSION==2 + float fx,fdx,fdy; + float direction; + float sy; + float ey; + #else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" + #endif +} stbtt__active_edge; + +#if STBTT_RASTERIZER_VERSION == 1 +#define STBTT_FIXSHIFT 10 +#define STBTT_FIX (1 << STBTT_FIXSHIFT) +#define STBTT_FIXMASK (STBTT_FIX-1) + +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +#elif STBTT_RASTERIZER_VERSION == 2 +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + //STBTT_assert(e->y0 <= start_point); + if (!z) return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#if STBTT_RASTERIZER_VERSION == 1 +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->direction; + } else { + int x1 = e->x; w += e->direction; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +#elif STBTT_RASTERIZER_VERSION == 2 + +// the edge passed in here does not cross the vertical line at x or the vertical line at x+1 +// (i.e. it has already been clipped to those) +static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) +{ + if (y0 == y1) return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) return; + if (y1 < e->sy) return; + if (y0 < e->sy) { + x0 += (x1-x0) * (e->sy - y0) / (y1-y0); + y0 = e->sy; + } + if (y1 > e->ey) { + x1 += (x1-x0) * (e->ey - y1) / (y1-y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x+1); + else if (x0 == x+1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x+1) + STBTT_assert(x1 >= x+1); + else + STBTT_assert(x1 >= x && x1 <= x+1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1-y0); + else if (x0 >= x+1 && x1 >= x+1) + ; + else { + STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); + scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position + } +} + +static float stbtt__sized_trapezoid_area(float height, float top_width, float bottom_width) +{ + STBTT_assert(top_width >= 0); + STBTT_assert(bottom_width >= 0); + return (top_width + bottom_width) / 2.0f * height; +} + +static float stbtt__position_trapezoid_area(float height, float tx0, float tx1, float bx0, float bx1) +{ + return stbtt__sized_trapezoid_area(height, tx1 - tx0, bx1 - bx0); +} + +static float stbtt__sized_triangle_area(float height, float width) +{ + return height * width / 2; +} + +static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) +{ + float y_bottom = y_top+1; + + while (e) { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) { + float x0 = e->fx; + if (x0 < len) { + if (x0 >= 0) { + stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); + stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); + } else { + stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); + } + } + } else { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0,sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } else { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } else { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = (sy1 - sy0) * e->direction; + STBTT_assert(x >= 0 && x < len); + scanline[x] += stbtt__position_trapezoid_area(height, x_top, x+1.0f, x_bottom, x+1.0f); + scanline_fill[x] += height; // everything right of this pixel is filled + } else { + int x,x1,x2; + float y_crossing, y_final, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + } + STBTT_assert(dy >= 0); + STBTT_assert(dx >= 0); + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = y_top + dy * (x1+1 - x0); + + // compute intersection with y axis at x2 + y_final = y_top + dy * (x2 - x0); + + // x1 x_top x2 x_bottom + // y_top +------|-----+------------+------------+--------|---+------------+ + // | | | | | | + // | | | | | | + // sy0 | Txxxxx|............|............|............|............| + // y_crossing | *xxxxx.......|............|............|............| + // | | xxxxx..|............|............|............| + // | | /- xx*xxxx........|............|............| + // | | dy < | xxxxxx..|............|............| + // y_final | | \- | xx*xxx.........|............| + // sy1 | | | | xxxxxB...|............| + // | | | | | | + // | | | | | | + // y_bottom +------------+------------+------------+------------+------------+ + // + // goal is to measure the area covered by '.' in each pixel + + // if x2 is right at the right edge of x1, y_crossing can blow up, github #1057 + // @TODO: maybe test against sy1 rather than y_bottom? + if (y_crossing > y_bottom) + y_crossing = y_bottom; + + sign = e->direction; + + // area of the rectangle covered from sy0..y_crossing + area = sign * (y_crossing-sy0); + + // area of the triangle (x_top,sy0), (x1+1,sy0), (x1+1,y_crossing) + scanline[x1] += stbtt__sized_triangle_area(area, x1+1 - x_top); + + // check if final y_crossing is blown up; no test case for this + if (y_final > y_bottom) { + y_final = y_bottom; + dy = (y_final - y_crossing ) / (x2 - (x1+1)); // if denom=0, y_final = y_crossing, so y_final <= y_bottom + } + + // in second pixel, area covered by line segment found in first pixel + // is always a rectangle 1 wide * the height of that line segment; this + // is exactly what the variable 'area' stores. it also gets a contribution + // from the line segment within it. the THIRD pixel will get the first + // pixel's rectangle contribution, the second pixel's rectangle contribution, + // and its own contribution. the 'own contribution' is the same in every pixel except + // the leftmost and rightmost, a trapezoid that slides down in each pixel. + // the second pixel's contribution to the third pixel will be the + // rectangle 1 wide times the height change in the second pixel, which is dy. + + step = sign * dy * 1; // dy is dy/dx, change in y for every 1 change in x, + // which multiplied by 1-pixel-width is how much pixel area changes for each step in x + // so the area advances by 'step' every time + + for (x = x1+1; x < x2; ++x) { + scanline[x] += area + step/2; // area of trapezoid is 1*step/2 + area += step; + } + STBTT_assert(STBTT_fabs(area) <= 1.01f); // accumulated error from area += step unless we round step down + STBTT_assert(sy1 > y_final-0.01f); + + // area covered in the last pixel is the rectangle from all the pixels to the left, + // plus the trapezoid filled by the line segment in this pixel all the way to the right edge + scanline[x2] += area + sign * stbtt__position_trapezoid_area(sy1-y_final, (float) x2, x2+1.0f, x_bottom, x2+1.0f); + + // the rest of the line is filled based on the total height of the line segment in this pixel + scanline_fill[x2] += sign * (sy1-sy0); + } + } else { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + // note though that this does happen some of the time because + // x_top and x_bottom can be extrapolated at the top & bottom of + // the shape and actually lie outside the bounding box + int x; + for (x=0; x < len; ++x) { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clearly-defined pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x+1); + float x3 = xb; + float y3 = y_bottom; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + float y1 = (x - x0) / dx + y_top; + float y2 = (x+1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) { // three segments descending down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x1 && x0 > x2) { // three segments descending down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else { // one segment + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); + } + } + } + } + e = e->next; + } +} + +// directly AA rasterize edges w/o supersampling +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0, i; + float scanline_data[129], *scanline, *scanline2; + + STBTT__NOTUSED(vsubsample); + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y_top) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) { + if (e->y0 != e->y1) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) { + if (j == 0 && off_y != 0) { + if (z->ey < scan_y_top) { + // this can happen due to subpixel positioning and some kind of fp rounding error i think + z->ey = scan_y_top; + } + } + STBTT_assert(z->ey >= scan_y_top); // if we get really unlucky a tiny bit of an edge can be out of bounds + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i=0; i < result->w; ++i) { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k)*255 + 0.5f; + m = (int) k; + if (m > 255) m = 255; + result->pixels[j*result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + +static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) +{ + int i,j; + for (i=1; i < n; ++i) { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) { + stbtt__edge *b = &p[j-1]; + int c = STBTT__COMPARE(a,b); + if (!c) break; + p[j] = p[j-1]; + --j; + } + if (i != j) + p[j] = t; + } +} + +static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) +{ + /* threshold for transitioning to insertion sort */ + while (n > 12) { + stbtt__edge t; + int c01,c12,c,m,i,j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0],&p[m]); + c12 = STBTT__COMPARE(&p[m],&p[n-1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0],&p[n-1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n-1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i=1; + j=n-1; + for(;;) { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;;++i) { + if (!STBTT__COMPARE(&p[i], &p[0])) break; + } + for (;;--j) { + if (!STBTT__COMPARE(&p[0], &p[j])) break; + } + /* make sure we haven't crossed */ + if (i >= j) break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n-i)) { + stbtt__sort_edges_quicksort(p,j); + p = p+i; + n = n-i; + } else { + stbtt__sort_edges_quicksort(p+i, n-i); + n = j; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; +#if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +#elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +#else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +static void stbtt__tesselate_cubic(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float objspace_flatness_squared, int n) +{ + // @TODO this "flatness" calculation is just made-up nonsense that seems to work well enough + float dx0 = x1-x0; + float dy0 = y1-y0; + float dx1 = x2-x1; + float dy1 = y2-y1; + float dx2 = x3-x2; + float dy2 = y3-y2; + float dx = x3-x0; + float dy = y3-y0; + float longlen = (float) (STBTT_sqrt(dx0*dx0+dy0*dy0)+STBTT_sqrt(dx1*dx1+dy1*dy1)+STBTT_sqrt(dx2*dx2+dy2*dy2)); + float shortlen = (float) STBTT_sqrt(dx*dx+dy*dy); + float flatness_squared = longlen*longlen-shortlen*shortlen; + + if (n > 16) // 65536 segments on one curve better be enough! + return; + + if (flatness_squared > objspace_flatness_squared) { + float x01 = (x0+x1)/2; + float y01 = (y0+y1)/2; + float x12 = (x1+x2)/2; + float y12 = (y1+y2)/2; + float x23 = (x2+x3)/2; + float y23 = (y2+y3)/2; + + float xa = (x01+x12)/2; + float ya = (y01+y12)/2; + float xb = (x12+x23)/2; + float yb = (y12+y23)/2; + + float mx = (xa+xb)/2; + float my = (ya+yb)/2; + + stbtt__tesselate_cubic(points, num_points, x0,y0, x01,y01, xa,ya, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_cubic(points, num_points, mx,my, xb,yb, x23,y23, x3,y3, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x3,y3); + *num_points = *num_points+1; + } +} + +// returns number of contours +static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + case STBTT_vcubic: + stbtt__tesselate_cubic(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].cx1, vertices[i].cy1, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count = 0; + int *winding_lengths = NULL; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixelPrefilter(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, oversample_x, oversample_y, sub_x, sub_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +static int stbtt_BakeFontBitmap_internal(unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y+gh+1 > bottom_y) + bottom_y = y+gh+1; + } + return bottom_y; +} + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// rectangle packing replacement routines if you don't have stb_rect_pack.h +// + +#ifndef STB_RECT_PACK_VERSION + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + stbrp_coord x,y; + int id,w,h,was_packed; +}; + +static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) +{ + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); +} + +static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) +{ + int i; + for (i=0; i < num_rects; ++i) { + if (con->x + rects[i].w > con->width) { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for ( ; i < num_rects; ++i) + rects[i].was_packed = 0; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) +{ + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + + if (context == NULL || nodes == NULL) { + if (context != NULL) STBTT_free(context, alloc_context); + if (nodes != NULL) STBTT_free(nodes , alloc_context); + return 0; + } + + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; + spc->skip_missing = 0; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +{ + STBTT_free(spc->nodes , spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); +} + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) +{ + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; +} + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip) +{ + spc->skip_missing = skip; +} + +#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) + +static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < h; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } +} + +static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < w; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) { + STBTT_assert(pixels[i*stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + if (!oversample) + return 0.0f; + + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float)-(oversample - 1) / (2.0f * (float)oversample); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k; + int missing_glyph_added = 0; + + k=0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j=0; j < ranges[i].num_chars; ++j) { + int x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + if (glyph == 0 && (spc->skip_missing || missing_glyph_added)) { + rects[k].w = rects[k].h = 0; + } else { + stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + &x0,&y0,&x1,&y1); + rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); + rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + if (glyph == 0) + missing_glyph_added = 1; + } + ++k; + } + } + + return k; +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int prefilter_x, int prefilter_y, float *sub_x, float *sub_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, + output, + out_w - (prefilter_x - 1), + out_h - (prefilter_y - 1), + out_stride, + scale_x, + scale_y, + shift_x, + shift_y, + glyph); + + if (prefilter_x > 1) + stbtt__h_prefilter(output, out_w, out_h, out_stride, prefilter_x); + + if (prefilter_y > 1) + stbtt__v_prefilter(output, out_w, out_h, out_stride, prefilter_y); + + *sub_x = stbtt__oversample_shift(prefilter_x); + *sub_y = stbtt__oversample_shift(prefilter_y); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k, missing_glyph = -1, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h,recip_v,sub_x,sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j=0; j < ranges[i].num_chars; ++j) { + stbrp_rect *r = &rects[k]; + if (r->was_packed && r->w != 0 && r->h != 0) { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0,&y0,&x1,&y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w - spc->h_oversample+1, + r->h - spc->v_oversample+1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + + if (glyph == 0) + missing_glyph = j; + } else if (spc->skip_missing) { + return_value = 0; + } else if (r->was_packed && r->w == 0 && r->h == 0 && missing_glyph >= 0) { + ranges[i].chardata_for_range[j] = ranges[i].chardata_for_range[missing_glyph]; + } else { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; +} + +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) +{ + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); +} + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) +{ + stbtt_fontinfo info; + int i,j,n, return_value = 1; + //stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i=0; i < num_ranges; ++i) + for (j=0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; + + n = 0; + for (i=0; i < num_ranges; ++i) + n += ranges[i].num_chars; + + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; + + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + + stbtt_PackFontRangesPackRects(spc, rects, n); + + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; +} + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) +{ + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); +} + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap) +{ + int i_ascent, i_descent, i_lineGap; + float scale; + stbtt_fontinfo info; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata, index)); + scale = size > 0 ? stbtt_ScaleForPixelHeight(&info, size) : stbtt_ScaleForMappingEmToPixels(&info, -size); + stbtt_GetFontVMetrics(&info, &i_ascent, &i_descent, &i_lineGap); + *ascent = (float) i_ascent * scale; + *descent = (float) i_descent * scale; + *lineGap = (float) i_lineGap * scale; +} + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) +{ + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } else { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// sdf computation +// + +#define STBTT_min(a,b) ((a) < (b) ? (a) : (b)) +#define STBTT_max(a,b) ((a) < (b) ? (b) : (a)) + +static int stbtt__ray_intersect_bezier(float orig[2], float ray[2], float q0[2], float q1[2], float q2[2], float hits[2][2]) +{ + float q0perp = q0[1]*ray[0] - q0[0]*ray[1]; + float q1perp = q1[1]*ray[0] - q1[0]*ray[1]; + float q2perp = q2[1]*ray[0] - q2[0]*ray[1]; + float roperp = orig[1]*ray[0] - orig[0]*ray[1]; + + float a = q0perp - 2*q1perp + q2perp; + float b = q1perp - q0perp; + float c = q0perp - roperp; + + float s0 = 0., s1 = 0.; + int num_s = 0; + + if (a != 0.0) { + float discr = b*b - a*c; + if (discr > 0.0) { + float rcpna = -1 / a; + float d = (float) STBTT_sqrt(discr); + s0 = (b+d) * rcpna; + s1 = (b-d) * rcpna; + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + if (d > 0.0 && s1 >= 0.0 && s1 <= 1.0) { + if (num_s == 0) s0 = s1; + ++num_s; + } + } + } else { + // 2*b*s + c = 0 + // s = -c / (2*b) + s0 = c / (-2 * b); + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + } + + if (num_s == 0) + return 0; + else { + float rcp_len2 = 1 / (ray[0]*ray[0] + ray[1]*ray[1]); + float rayn_x = ray[0] * rcp_len2, rayn_y = ray[1] * rcp_len2; + + float q0d = q0[0]*rayn_x + q0[1]*rayn_y; + float q1d = q1[0]*rayn_x + q1[1]*rayn_y; + float q2d = q2[0]*rayn_x + q2[1]*rayn_y; + float rod = orig[0]*rayn_x + orig[1]*rayn_y; + + float q10d = q1d - q0d; + float q20d = q2d - q0d; + float q0rd = q0d - rod; + + hits[0][0] = q0rd + s0*(2.0f - 2.0f*s0)*q10d + s0*s0*q20d; + hits[0][1] = a*s0+b; + + if (num_s > 1) { + hits[1][0] = q0rd + s1*(2.0f - 2.0f*s1)*q10d + s1*s1*q20d; + hits[1][1] = a*s1+b; + return 2; + } else { + return 1; + } + } +} + +static int equal(float *a, float *b) +{ + return (a[0] == b[0] && a[1] == b[1]); +} + +static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex *verts) +{ + int i; + float orig[2], ray[2] = { 1, 0 }; + float y_frac; + int winding = 0; + + // make sure y never passes through a vertex of the shape + y_frac = (float) STBTT_fmod(y, 1.0f); + if (y_frac < 0.01f) + y += 0.01f; + else if (y_frac > 0.99f) + y -= 0.01f; + + orig[0] = x; + orig[1] = y; + + // test a ray from (-infinity,y) to (x,y) + for (i=0; i < nverts; ++i) { + if (verts[i].type == STBTT_vline) { + int x0 = (int) verts[i-1].x, y0 = (int) verts[i-1].y; + int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } + if (verts[i].type == STBTT_vcurve) { + int x0 = (int) verts[i-1].x , y0 = (int) verts[i-1].y ; + int x1 = (int) verts[i ].cx, y1 = (int) verts[i ].cy; + int x2 = (int) verts[i ].x , y2 = (int) verts[i ].y ; + int ax = STBTT_min(x0,STBTT_min(x1,x2)), ay = STBTT_min(y0,STBTT_min(y1,y2)); + int by = STBTT_max(y0,STBTT_max(y1,y2)); + if (y > ay && y < by && x > ax) { + float q0[2],q1[2],q2[2]; + float hits[2][2]; + q0[0] = (float)x0; + q0[1] = (float)y0; + q1[0] = (float)x1; + q1[1] = (float)y1; + q2[0] = (float)x2; + q2[1] = (float)y2; + if (equal(q0,q1) || equal(q1,q2)) { + x0 = (int)verts[i-1].x; + y0 = (int)verts[i-1].y; + x1 = (int)verts[i ].x; + y1 = (int)verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } else { + int num_hits = stbtt__ray_intersect_bezier(orig, ray, q0, q1, q2, hits); + if (num_hits >= 1) + if (hits[0][0] < 0) + winding += (hits[0][1] < 0 ? -1 : 1); + if (num_hits >= 2) + if (hits[1][0] < 0) + winding += (hits[1][1] < 0 ? -1 : 1); + } + } + } + } + return winding; +} + +static float stbtt__cuberoot( float x ) +{ + if (x<0) + return -(float) STBTT_pow(-x,1.0f/3.0f); + else + return (float) STBTT_pow( x,1.0f/3.0f); +} + +// x^3 + a*x^2 + b*x + c = 0 +static int stbtt__solve_cubic(float a, float b, float c, float* r) +{ + float s = -a / 3; + float p = b - a*a / 3; + float q = a * (2*a*a - 9*b) / 27 + c; + float p3 = p*p*p; + float d = q*q + 4*p3 / 27; + if (d >= 0) { + float z = (float) STBTT_sqrt(d); + float u = (-q + z) / 2; + float v = (-q - z) / 2; + u = stbtt__cuberoot(u); + v = stbtt__cuberoot(v); + r[0] = s + u + v; + return 1; + } else { + float u = (float) STBTT_sqrt(-p/3); + float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative + float m = (float) STBTT_cos(v); + float n = (float) STBTT_cos(v-3.141592/2)*1.732050808f; + r[0] = s + u * 2 * m; + r[1] = s - u * (m + n); + r[2] = s - u * (m - n); + + //STBTT_assert( STBTT_fabs(((r[0]+a)*r[0]+b)*r[0]+c) < 0.05f); // these asserts may not be safe at all scales, though they're in bezier t parameter units so maybe? + //STBTT_assert( STBTT_fabs(((r[1]+a)*r[1]+b)*r[1]+c) < 0.05f); + //STBTT_assert( STBTT_fabs(((r[2]+a)*r[2]+b)*r[2]+c) < 0.05f); + return 3; + } +} + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + float scale_x = scale, scale_y = scale; + int ix0,iy0,ix1,iy1; + int w,h; + unsigned char *data; + + if (scale == 0) return NULL; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale, scale, 0.0f,0.0f, &ix0,&iy0,&ix1,&iy1); + + // if empty, return NULL + if (ix0 == ix1 || iy0 == iy1) + return NULL; + + ix0 -= padding; + iy0 -= padding; + ix1 += padding; + iy1 += padding; + + w = (ix1 - ix0); + h = (iy1 - iy0); + + if (width ) *width = w; + if (height) *height = h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + // invert for y-downwards bitmaps + scale_y = -scale_y; + + { + // distance from singular values (in the same units as the pixel grid) + const float eps = 1./1024, eps2 = eps*eps; + int x,y,i,j; + float *precompute; + stbtt_vertex *verts; + int num_verts = stbtt_GetGlyphShape(info, glyph, &verts); + data = (unsigned char *) STBTT_malloc(w * h, info->userdata); + precompute = (float *) STBTT_malloc(num_verts * sizeof(float), info->userdata); + + for (i=0,j=num_verts-1; i < num_verts; j=i++) { + if (verts[i].type == STBTT_vline) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; + float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); + precompute[i] = (dist < eps) ? 0.0f : 1.0f / dist; + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; + float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; + float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float len2 = bx*bx + by*by; + if (len2 >= eps2) + precompute[i] = 1.0f / len2; + else + precompute[i] = 0.0f; + } else + precompute[i] = 0.0f; + } + + for (y=iy0; y < iy1; ++y) { + for (x=ix0; x < ix1; ++x) { + float val; + float min_dist = 999999.0f; + float sx = (float) x + 0.5f; + float sy = (float) y + 0.5f; + float x_gspace = (sx / scale_x); + float y_gspace = (sy / scale_y); + + int winding = stbtt__compute_crossings_x(x_gspace, y_gspace, num_verts, verts); // @OPTIMIZE: this could just be a rasterization, but needs to be line vs. non-tesselated curves so a new path + + for (i=0; i < num_verts; ++i) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + + if (verts[i].type == STBTT_vline && precompute[i] != 0.0f) { + float x1 = verts[i-1].x*scale_x, y1 = verts[i-1].y*scale_y; + + float dist,dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + // coarse culling against bbox + //if (sx > STBTT_min(x0,x1)-min_dist && sx < STBTT_max(x0,x1)+min_dist && + // sy > STBTT_min(y0,y1)-min_dist && sy < STBTT_max(y0,y1)+min_dist) + dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; + STBTT_assert(i != 0); + if (dist < min_dist) { + // check position along line + // x' = x0 + t*(x1-x0), y' = y0 + t*(y1-y0) + // minimize (x'-sx)*(x'-sx)+(y'-sy)*(y'-sy) + float dx = x1-x0, dy = y1-y0; + float px = x0-sx, py = y0-sy; + // minimize (px+t*dx)^2 + (py+t*dy)^2 = px*px + 2*px*dx*t + t^2*dx*dx + py*py + 2*py*dy*t + t^2*dy*dy + // derivative: 2*px*dx + 2*py*dy + (2*dx*dx+2*dy*dy)*t, set to 0 and solve + float t = -(px*dx + py*dy) / (dx*dx + dy*dy); + if (t >= 0.0f && t <= 1.0f) + min_dist = dist; + } + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[i-1].x *scale_x, y2 = verts[i-1].y *scale_y; + float x1 = verts[i ].cx*scale_x, y1 = verts[i ].cy*scale_y; + float box_x0 = STBTT_min(STBTT_min(x0,x1),x2); + float box_y0 = STBTT_min(STBTT_min(y0,y1),y2); + float box_x1 = STBTT_max(STBTT_max(x0,x1),x2); + float box_y1 = STBTT_max(STBTT_max(y0,y1),y2); + // coarse culling against bbox to avoid computing cubic unnecessarily + if (sx > box_x0-min_dist && sx < box_x1+min_dist && sy > box_y0-min_dist && sy < box_y1+min_dist) { + int num=0; + float ax = x1-x0, ay = y1-y0; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float mx = x0 - sx, my = y0 - sy; + float res[3] = {0.f,0.f,0.f}; + float px,py,t,it,dist2; + float a_inv = precompute[i]; + if (a_inv == 0.0) { // if a_inv is 0, it's 2nd degree so use quadratic formula + float a = 3*(ax*bx + ay*by); + float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); + float c = mx*ax+my*ay; + if (STBTT_fabs(a) < eps2) { // if a is 0, it's linear + if (STBTT_fabs(b) >= eps2) { + res[num++] = -c/b; + } + } else { + float discriminant = b*b - 4*a*c; + if (discriminant < 0) + num = 0; + else { + float root = (float) STBTT_sqrt(discriminant); + res[0] = (-b - root)/(2*a); + res[1] = (-b + root)/(2*a); + num = 2; // don't bother distinguishing 1-solution case, as code below will still work + } + } + } else { + float b = 3*(ax*bx + ay*by) * a_inv; // could precompute this as it doesn't depend on sample point + float c = (2*(ax*ax + ay*ay) + (mx*bx+my*by)) * a_inv; + float d = (mx*ax+my*ay) * a_inv; + num = stbtt__solve_cubic(b, c, d, res); + } + dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + if (num >= 1 && res[0] >= 0.0f && res[0] <= 1.0f) { + t = res[0], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 2 && res[1] >= 0.0f && res[1] <= 1.0f) { + t = res[1], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 3 && res[2] >= 0.0f && res[2] <= 1.0f) { + t = res[2], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + } + } + } + if (winding == 0) + min_dist = -min_dist; // if outside the shape, value is negative + val = onedge_value + pixel_dist_scale * min_dist; + if (val < 0) + val = 0; + else if (val > 255) + val = 255; + data[(y-iy0)*w+(x-ix0)] = (unsigned char) val; + } + } + STBTT_free(precompute, info->userdata); + STBTT_free(verts, info->userdata); + } + return data; +} + +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphSDF(info, scale, stbtt_FindGlyphIndex(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8); + stbtt_int32 off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + slen = ttUSHORT(fc+loc+12+8); + off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian_internal((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +static int stbtt_FindMatchingFont_internal(unsigned char *font_collection, char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, + float pixel_height, unsigned char *pixels, int pw, int ph, + int first_char, int num_chars, stbtt_bakedchar *chardata) +{ + return stbtt_BakeFontBitmap_internal((unsigned char *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata); +} + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) +{ + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); +} + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) +{ + return stbtt_GetNumberOfFonts_internal((unsigned char *) data); +} + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset) +{ + return stbtt_InitFont_internal(info, (unsigned char *) data, offset); +} + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags) +{ + return stbtt_FindMatchingFont_internal((unsigned char *) fontdata, (char *) name, flags); +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return stbtt_CompareUTF8toUTF16_bigendian_internal((char *) s1, len1, (char *) s2, len2); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) OpenType GPOS kerning (horizontal only), STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/Engine/cpp/ThirdParty/ufbx/CMakeLists.txt b/Engine/cpp/ThirdParty/ufbx/CMakeLists.txt new file mode 100644 index 00000000..11b63b45 --- /dev/null +++ b/Engine/cpp/ThirdParty/ufbx/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(ufbx INTERFACE) +target_include_directories(ufbx INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/Engine/cpp/ThirdParty/ufbx/ufbx.c b/Engine/cpp/ThirdParty/ufbx/ufbx.c new file mode 100644 index 00000000..bf5f7ada --- /dev/null +++ b/Engine/cpp/ThirdParty/ufbx/ufbx.c @@ -0,0 +1,33101 @@ +#ifndef UFBX_UFBX_C_INCLUDED +#define UFBX_UFBX_C_INCLUDED + +#if defined(UFBX_HEADER_PATH) + #include UFBX_HEADER_PATH +#else + #include "ufbx.h" +#endif + +// -- User configuration + +// User configuration: +// UFBX_REGRESSION Enable regression mode for development +// UFBX_UBSAN Explicitly enable undefined behavior sanitizer workarounds +// UFBX_NO_UNALIGNED_LOADS Do not use unaligned loads even when they are supported +// UFBX_USE_UNALIGNED_LOADS Forcibly use unaligned loads on unknown platforms +// UFBX_USE_SSE Explicitly enable SSE2 support (for x86) +// UFBX_HAS_FTELLO Allow ufbx to use `ftello()` to measure file size +// UFBX_WASM_32BIT Optimize WASM for 32-bit architectures +// UFBX_TRACE Log calls of `ufbxi_check()` for tracing execution +// UFBX_LITTLE_ENDIAN=0/1 Explicitly define little/big endian architecture +// UFBX_PATH_SEPARATOR='' Specify default platform path separator +// UFBX_NO_SSE Do not try to include SSE + +// Dependencies: +// UFBX_NO_MALLOC Disable default malloc/realloc/free +// UFBX_NO_STDIO Disable stdio FILE API +// UFBX_EXTERNAL_MALLOC Link to external ufbx_malloc() interface +// UFBX_EXTERNAL_STDIO Link to external ufbx_stdio_() interface +// UFBX_EXTERNAL_MATH Link to external interface +// UFBX_EXTERNAL_STRING Link to external interface + +// Freestanding: +// UFBX_MATH_PREFIX='ufbx_' Prefix for external functions used +// UFBX_STRING_PREFIX='ufbx_' Prefix for external functions used +// UFBX_NO_LIBC Do not include libc (implies UFBX_EXTERNAL_MATH/STRING/MALLOC/STDIO by default) +// UFBX_NO_LIBC_TYPES Do not include any libc headers, you must define all types in + +// Mostly internal for debugging: +// UFBX_STATIC_ANALYSIS Enable static analysis augmentation +// UFBX_DEBUG_BINARY_SEARCH Force using binary search for debugging +// UFBX_EXTENSIVE_THREADING Use threads for small inputs +// UFBX_MAXIMUM_ALIGNMENT Maximum alignment used for allocation + +#if defined(UFBX_CONFIG_SOURCE) + #include UFBX_CONFIG_SOURCE +#endif + +// -- Configuration + +#define UFBXI_MAX_NON_ARRAY_VALUES 8 +#define UFBXI_MAX_NODE_DEPTH 32 +#define UFBXI_MAX_XML_DEPTH 32 +#define UFBXI_MAX_SKIP_SIZE 0x40000000 +#define UFBXI_MAP_MAX_SCAN 32 +#define UFBXI_KD_FAST_DEPTH 6 +#define UFBXI_HUGE_MAX_SCAN 16 +#define UFBXI_MIN_FILE_FORMAT_LOOKAHEAD 32 +#define UFBXI_FACE_GROUP_HASH_BITS 8 +#define UFBXI_MIN_THREADED_DEFLATE_BYTES 256 +#define UFBXI_MIN_THREADED_ASCII_VALUES 64 +#define UFBXI_GEOMETRY_CACHE_BUFFER_SIZE 512 + +#ifndef UFBXI_MAX_NURBS_ORDER +#define UFBXI_MAX_NURBS_ORDER 128 +#endif + +// By default enough to have squares be non-denormal +#ifndef UFBX_EPSILON +#define UFBX_EPSILON (sizeof(ufbx_real) == sizeof(float) ? \ + (ufbx_real)1.0842021795674597e-19f : (ufbx_real)1.4916681462400413e-154) +#endif + +// -- Feature exclusion + +#if !defined(UFBX_MINIMAL) + #if !defined(UFBX_NO_SUBDIVISION) + #define UFBXI_FEATURE_SUBDIVISION 1 + #endif + #if !defined(UFBX_NO_TESSELLATION) + #define UFBXI_FEATURE_TESSELLATION 1 + #endif + #if !defined(UFBX_NO_GEOMETRY_CACHE) + #define UFBXI_FEATURE_GEOMETRY_CACHE 1 + #endif + #if !defined(UFBX_NO_SCENE_EVALUATION) + #define UFBXI_FEATURE_SCENE_EVALUATION 1 + #endif + #if !defined(UFBX_NO_SKINNING_EVALUATION) + #define UFBXI_FEATURE_SKINNING_EVALUATION 1 + #endif + #if !defined(UFBX_NO_ANIMATION_BAKING) + #define UFBXI_FEATURE_ANIMATION_BAKING 1 + #endif + #if !defined(UFBX_NO_TRIANGULATION) + #define UFBXI_FEATURE_TRIANGULATION 1 + #endif + #if !defined(UFBX_NO_INDEX_GENERATION) + #define UFBXI_FEATURE_INDEX_GENERATION 1 + #endif + #if !defined(UFBX_NO_FORMAT_OBJ) + #define UFBXI_FEATURE_FORMAT_OBJ 1 + #endif +#endif + +#if defined(UFBX_DEV) + #if !defined(UFBX_NO_ERROR_STACK) + #define UFBXI_FEATURE_ERROR_STACK 1 + #endif +#endif + +#if !defined(UFBXI_FEATURE_SUBDIVISION) && defined(UFBX_ENABLE_SUBDIVISION) + #define UFBXI_FEATURE_SUBDIVISION 1 +#endif +#if !defined(UFBXI_FEATURE_TESSELLATION) && defined(UFBX_ENABLE_TESSELLATION) + #define UFBXI_FEATURE_TESSELLATION 1 +#endif +#if !defined(UFBXI_FEATURE_GEOMETRY_CACHE) && defined(UFBX_ENABLE_GEOMETRY_CACHE) + #define UFBXI_FEATURE_GEOMETRY_CACHE 1 +#endif +#if !defined(UFBXI_FEATURE_SCENE_EVALUATION) && defined(UFBX_ENABLE_SCENE_EVALUATION) + #define UFBXI_FEATURE_SCENE_EVALUATION 1 +#endif +#if !defined(UFBXI_FEATURE_SKINNING_EVALUATION) && defined(UFBX_ENABLE_SKINNING_EVALUATION) + #define UFBXI_FEATURE_SKINNING_EVALUATION 1 +#endif +#if !defined(UFBXI_FEATURE_ANIMATION_BAKING) && defined(UFBX_ENABLE_ANIMATION_BAKING) + #define UFBXI_FEATURE_ANIMATION_BAKING 1 +#endif +#if !defined(UFBXI_FEATURE_TRIANGULATION) && defined(UFBX_ENABLE_TRIANGULATION) + #define UFBXI_FEATURE_TRIANGULATION 1 +#endif +#if !defined(UFBXI_FEATURE_INDEX_GENERATION) && defined(UFBX_ENABLE_INDEX_GENERATION) + #define UFBXI_FEATURE_INDEX_GENERATION 1 +#endif +#if !defined(UFBXI_FEATURE_FORMAT_OBJ) && defined(UFBX_ENABLE_FORMAT_OBJ) + #define UFBXI_FEATURE_FORMAT_OBJ 1 +#endif +#if !defined(UFBXI_FEATURE_ERROR_STACK) && defined(UFBX_ENABLE_ERROR_STACK) + #define UFBXI_FEATURE_ERROR_STACK 1 +#endif + +#if !defined(UFBXI_FEATURE_SUBDIVISION) + #define UFBXI_FEATURE_SUBDIVISION 0 +#endif +#if !defined(UFBXI_FEATURE_TESSELLATION) + #define UFBXI_FEATURE_TESSELLATION 0 +#endif +#if !defined(UFBXI_FEATURE_GEOMETRY_CACHE) + #define UFBXI_FEATURE_GEOMETRY_CACHE 0 +#endif +#if !defined(UFBXI_FEATURE_SCENE_EVALUATION) + #define UFBXI_FEATURE_SCENE_EVALUATION 0 +#endif +#if !defined(UFBXI_FEATURE_SKINNING_EVALUATION) + #define UFBXI_FEATURE_SKINNING_EVALUATION 0 +#endif +#if !defined(UFBXI_FEATURE_ANIMATION_BAKING) + #define UFBXI_FEATURE_ANIMATION_BAKING 0 +#endif +#if !defined(UFBXI_FEATURE_TRIANGULATION) + #define UFBXI_FEATURE_TRIANGULATION 0 +#endif +#if !defined(UFBXI_FEATURE_INDEX_GENERATION) + #define UFBXI_FEATURE_INDEX_GENERATION 0 +#endif +#if !defined(UFBXI_FEATURE_FORMAT_OBJ) + #define UFBXI_FEATURE_FORMAT_OBJ 0 +#endif +#if !defined(UFBXI_FEATURE_ERROR_STACK) + #define UFBXI_FEATURE_ERROR_STACK 0 +#endif + +// Derived features + +#if UFBXI_FEATURE_GEOMETRY_CACHE + #define UFBXI_FEATURE_XML 1 +#else + #define UFBXI_FEATURE_XML 0 +#endif + +#if UFBXI_FEATURE_TRIANGULATION + #define UFBXI_FEATURE_KD 1 +#else + #define UFBXI_FEATURE_KD 0 +#endif + +#if !UFBXI_FEATURE_SUBDIVISION || !UFBXI_FEATURE_TESSELLATION || !UFBXI_FEATURE_GEOMETRY_CACHE || !UFBXI_FEATURE_SCENE_EVALUATION || !UFBXI_FEATURE_SKINNING_EVALUATION || !UFBXI_FEATURE_ANIMATION_BAKING || !UFBXI_FEATURE_TRIANGULATION || !UFBXI_FEATURE_INDEX_GENERATION || !UFBXI_FEATURE_XML || !UFBXI_FEATURE_KD || !UFBXI_FEATURE_FORMAT_OBJ + #define UFBXI_PARTIAL_FEATURES 1 +#endif + +// -- Headers + +// Legacy mapping +#if !defined(UFBX_EXTERNAL_MATH) && defined(UFBX_NO_MATH_H) + #define UFBX_EXTERNAL_MATH +#endif + +#if !defined(UFBX_NO_LIBC_TYPES) + #include +#endif + +#if !defined(UFBX_NO_LIBC) + #if !defined(UFBX_NO_FLOAT_H) + #include + #endif + #if !defined(UFBX_EXTERNAL_MATH) + #include + #endif + #if !defined(UFBX_EXTERNAL_STRING) + #include + #endif + #if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO) + #include + #endif + #if !defined(UFBX_NO_MALLOC) && !defined(UFBX_EXTERNAL_MALLOC) + #include + #endif +#else + #if !defined(UFBX_EXTERNAL_MATH) && !defined(UFBX_NO_EXTERNAL_MATH) + #define UFBX_EXTERNAL_MATH + #endif + #if !defined(UFBX_EXTERNAL_STRING) && !defined(UFBX_NO_EXTERNAL_STRING) + #define UFBX_EXTERNAL_STRING + #endif + #if !defined(UFBX_EXTERNAL_MALLOC) && !defined(UFBX_NO_EXTERNAL_MALLOC) && !defined(UFBX_NO_MALLOC) + #define UFBX_EXTERNAL_MALLOC + #endif + #if !defined(UFBX_EXTERNAL_STDIO) && !defined(UFBX_NO_EXTERNAL_STDIO) && !defined(UFBX_NO_STDIO) + #define UFBX_EXTERNAL_STDIO + #endif +#endif + +#if defined(UFBX_EXTERNAL_STRING) && !defined(UFBX_STRING_PREFIX) + #define UFBX_STRING_PREFIX ufbx_ +#endif + +#if !defined(UFBX_EXTERNAL_MATH) + #if !defined(UFBX_MATH_PREFIX) + #define UFBX_MATH_PREFIX + #endif +#endif + +#define ufbxi_pre_cat2(a, b) a##b +#define ufbxi_pre_cat(a, b) ufbxi_pre_cat2(a, b) + +// -- External functions + +#ifndef ufbx_extern_abi + #if defined(UFBX_STATIC) + #define ufbx_extern_abi static + #else + #define ufbx_extern_abi + #endif +#endif + +#if defined(UFBX_MATH_PREFIX) + #define ufbxi_math_fn(name) ufbxi_pre_cat(UFBX_MATH_PREFIX, name) + #define ufbx_sqrt ufbxi_math_fn(sqrt) + #define ufbx_fabs ufbxi_math_fn(fabs) + #define ufbx_pow ufbxi_math_fn(pow) + #define ufbx_sin ufbxi_math_fn(sin) + #define ufbx_cos ufbxi_math_fn(cos) + #define ufbx_tan ufbxi_math_fn(tan) + #define ufbx_asin ufbxi_math_fn(asin) + #define ufbx_acos ufbxi_math_fn(acos) + #define ufbx_atan ufbxi_math_fn(atan) + #define ufbx_atan2 ufbxi_math_fn(atan2) + #define ufbx_copysign ufbxi_math_fn(copysign) + #define ufbx_fmin ufbxi_math_fn(fmin) + #define ufbx_fmax ufbxi_math_fn(fmax) + #define ufbx_nextafter ufbxi_math_fn(nextafter) + #define ufbx_rint ufbxi_math_fn(rint) + #define ufbx_floor ufbxi_math_fn(floor) + #define ufbx_ceil ufbxi_math_fn(ceil) + #define ufbx_isnan ufbxi_math_fn(isnan) +#endif + +#if !defined(UFBX_NO_EXTERNAL_DEFINES) + +#if defined(__cplusplus) +extern "C" { +#endif + +#if defined(UFBX_EXTERNAL_MATH) + ufbx_extern_abi double ufbx_sqrt(double x); + ufbx_extern_abi double ufbx_sin(double x); + ufbx_extern_abi double ufbx_cos(double x); + ufbx_extern_abi double ufbx_tan(double x); + ufbx_extern_abi double ufbx_asin(double x); + ufbx_extern_abi double ufbx_acos(double x); + ufbx_extern_abi double ufbx_atan(double x); + ufbx_extern_abi double ufbx_atan2(double y, double x); + ufbx_extern_abi double ufbx_pow(double x, double y); + ufbx_extern_abi double ufbx_fmin(double a, double b); + ufbx_extern_abi double ufbx_fmax(double a, double b); + ufbx_extern_abi double ufbx_fabs(double x); + ufbx_extern_abi double ufbx_copysign(double x, double y); + ufbx_extern_abi double ufbx_nextafter(double x, double y); + ufbx_extern_abi double ufbx_rint(double x); + ufbx_extern_abi double ufbx_floor(double x); + ufbx_extern_abi double ufbx_ceil(double x); + ufbx_extern_abi int ufbx_isnan(double x); +#endif + +#if defined(UFBX_EXTERNAL_STRING) + ufbx_extern_abi size_t ufbx_strlen(const char *str); + ufbx_extern_abi void *ufbx_memcpy(void *dst, const void *src, size_t count); + ufbx_extern_abi void *ufbx_memmove(void *dst, const void *src, size_t count); + ufbx_extern_abi void *ufbx_memset(void *dst, int ch, size_t count); + ufbx_extern_abi const void *ufbx_memchr(const void *ptr, int value, size_t count); + ufbx_extern_abi int ufbx_memcmp(const void *a, const void *b, size_t count); + ufbx_extern_abi int ufbx_strcmp(const char *a, const char *b); + ufbx_extern_abi int ufbx_strncmp(const char *a, const char *b, size_t count); +#endif + +#if defined(UFBX_EXTERNAL_MALLOC) + ufbx_extern_abi void *ufbx_malloc(size_t size); + ufbx_extern_abi void *ufbx_realloc(void *ptr, size_t old_size, size_t new_size); + ufbx_extern_abi void ufbx_free(void *ptr, size_t old_size); +#endif + +#if defined(UFBX_EXTERNAL_STDIO) + ufbx_extern_abi void *ufbx_stdio_open(const char *path, size_t path_len); + ufbx_extern_abi size_t ufbx_stdio_read(void *file, void *data, size_t size); + ufbx_extern_abi bool ufbx_stdio_skip(void *file, size_t size); + ufbx_extern_abi uint64_t ufbx_stdio_size(void *file); + ufbx_extern_abi void ufbx_stdio_close(void *file); +#endif + +#if defined(__cplusplus) +} +#endif + +#endif + +#if !defined(UFBX_INFINITY) + #if defined(INFINITY) + #define UFBX_INFINITY INFINITY + #else + #define UFBX_INFINITY (1e+300 * 1e+300) + #endif +#endif +#if !defined(UFBX_NAN) + #if defined(NAN) + #define UFBX_NAN NAN + #else + #define UFBX_NAN (UFBX_INFINITY * 0.0f) + #endif +#endif +#if !defined(UFBX_FLT_EPSILON) + #if defined(FLT_EPSILON) + #define UFBX_FLT_EPSILON FLT_EPSILON + #else + #define UFBX_FLT_EPSILON 1.192092896e-07f + #endif +#endif +#if !defined(UFBX_FLT_EVAL_METHOD) + #if defined(FLT_EVAL_METHOD) + #define UFBX_FLT_EVAL_METHOD FLT_EVAL_METHOD + #elif defined(__FLT_EVAL_METHOD__) + #define UFBX_FLT_EVAL_METHOD __FLT_EVAL_METHOD__ + #elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + #define UFBX_FLT_EVAL_METHOD 0 + #else + #define UFBX_FLT_EVAL_METHOD -1 + #endif +#endif + +#if defined(ufbx_malloc) || defined(ufbx_realloc) || defined(ufbx_free) + // User provided allocators + #if !defined(ufbx_malloc) || !defined(ufbx_realloc) || !defined(ufbx_free) + #error Inconsistent custom global allocator + #endif +#elif defined(UFBX_NO_MALLOC) + #define ufbx_malloc(size) ((void)(size), (void)NULL) + #define ufbx_realloc(ptr, old_size, new_size) ((void)(ptr), (void)(old_size), (void)(new_size), (void)NULL) + #define ufbx_free(ptr, old_size) ((void)(ptr), (void)(old_size)) +#elif defined(UFBX_EXTERNAL_MALLOC) + // Nop +#else + #define ufbx_malloc(size) malloc((size)) + #define ufbx_realloc(ptr, old_size, new_size) realloc((ptr), (new_size)) + #define ufbx_free(ptr, old_size) free((ptr)) +#endif + +#if !defined(ufbx_panic_handler) + static void ufbxi_panic_handler(const char *message) + { + (void)message; + #if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO) + fprintf(stderr, "ufbx panic: %s\n", message); + #endif + ufbx_assert(false && "ufbx panic: See stderr for more information"); + } + #define ufbx_panic_handler ufbxi_panic_handler +#endif + +// -- Platform + +#if defined(_MSC_VER) + #define UFBXI_MSC_VER _MSC_VER +#else + #define UFBXI_MSC_VER 0 +#endif + +#if defined(__GNUC__) + #define UFBXI_GNUC __GNUC__ + #define UFBXI_GNUC_VERSION ufbx_pack_version(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else + #define UFBXI_GNUC 0 + #define UFBXI_GNUC_VERSION 0 +#endif + +#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + #define ufbxi_noinline __declspec(noinline) + #define ufbxi_forceinline __forceinline + #define ufbxi_restrict __restrict + #if defined(_Check_return_) + #define ufbxi_nodiscard _Check_return_ + #else + #define ufbxi_nodiscard + #endif + #define ufbxi_unused + #define ufbxi_unlikely(cond) (cond) +#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__)) + #define ufbxi_noinline __attribute__((noinline)) + #define ufbxi_forceinline inline __attribute__((always_inline)) + #define ufbxi_restrict __restrict + #define ufbxi_nodiscard __attribute__((warn_unused_result)) + #define ufbxi_unused __attribute__((unused)) + #define ufbxi_unlikely(cond) __builtin_expect((cond), 0) +#else + #define ufbxi_noinline + #define ufbxi_forceinline + #define ufbxi_nodiscard + #define ufbxi_restrict + #define ufbxi_unused + #define ufbxi_unlikely(cond) (cond) +#endif + +#if !defined(UFBX_STANDARD_C) && defined(__clang__) + #define ufbxi_nounroll _Pragma("clang loop unroll(disable)") _Pragma("clang loop vectorize(disable)") +#elif !defined(UFBX_STANDARD_C) && UFBXI_GNUC >= 8 + #define ufbxi_nounroll _Pragma("GCC unroll 0") +#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + #define ufbxi_nounroll __pragma(loop(no_vector)) +#else + #define ufbxi_nounroll +#endif + +#if defined(__GNUC__) && !defined(__clang__) + #define ufbxi_ignore(cond) (void)!(cond) +#else + #define ufbxi_ignore(cond) (void)(cond) +#endif + +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable: 4061) // enumerator 'ENUM' in switch of enum 'enum' is not explicitly handled by a case label + #pragma warning(disable: 4200) // nonstandard extension used: zero-sized array in struct/union + #pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union + #pragma warning(disable: 4210) // nonstandard extension used: function given file scope + #pragma warning(disable: 4127) // conditional expression is constant + #pragma warning(disable: 4706) // assignment within conditional expression + #pragma warning(disable: 4789) // buffer 'type_and_name' of size 8 bytes will be overrun; 16 bytes will be written starting at offset 0 + #pragma warning(disable: 4820) // type': 'N' bytes padding added after data member 'member' + #if defined(UFBX_STANDARD_C) + #pragma warning(disable: 4996) // 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. + #endif + #if defined(UFBXI_PARTIAL_FEATURES) + #pragma warning(disable: 4100) // 'name': unreferenced formal parameter + #pragma warning(disable: 4505) // 'func': unreferenced function with internal linkage has been removed + #endif +#endif + +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wmissing-field-initializers" + #pragma clang diagnostic ignored "-Wmissing-braces" + #pragma clang diagnostic ignored "-Wdouble-promotion" + #pragma clang diagnostic ignored "-Wpedantic" + #pragma clang diagnostic ignored "-Wcast-qual" + #pragma clang diagnostic ignored "-Wcast-align" + #pragma clang diagnostic ignored "-Wcovered-switch-default" + #pragma clang diagnostic ignored "-Wpadded" + #pragma clang diagnostic ignored "-Wswitch-enum" + #pragma clang diagnostic ignored "-Wfloat-equal" + #pragma clang diagnostic ignored "-Wformat-nonliteral" + #if __has_warning("-Watomic-implicit-seq-cst") + #pragma clang diagnostic ignored "-Watomic-implicit-seq-cst" + #endif + #if defined(UFBX_STANDARD_C) + #pragma clang diagnostic ignored "-Wunused-function" + #endif + #if defined(UFBXI_PARTIAL_FEATURES) + #pragma clang diagnostic ignored "-Wunused-function" + #pragma clang diagnostic ignored "-Wunused-parameter" + #endif + #if defined(__cplusplus) + #pragma clang diagnostic ignored "-Wold-style-cast" + #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" + #else + #pragma clang diagnostic ignored "-Wdeclaration-after-statement" + #pragma clang diagnostic ignored "-Wbad-function-cast" + #endif +#endif + +#if defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wmissing-field-initializers" + #pragma GCC diagnostic ignored "-Wmissing-braces" + #pragma GCC diagnostic ignored "-Wdouble-promotion" + #pragma GCC diagnostic ignored "-Wpedantic" + #pragma GCC diagnostic ignored "-Wcast-qual" + #pragma GCC diagnostic ignored "-Wcast-align" + #pragma GCC diagnostic ignored "-Wpadded" + #pragma GCC diagnostic ignored "-Wswitch-enum" + #pragma GCC diagnostic ignored "-Wfloat-equal" + #pragma GCC diagnostic ignored "-Wformat-nonliteral" + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(UFBX_STANDARD_C) + #pragma GCC diagnostic ignored "-Wunused-function" + #endif + #if defined(UFBXI_PARTIAL_FEATURES) + #pragma GCC diagnostic ignored "-Wunused-function" + #pragma GCC diagnostic ignored "-Wunused-parameter" + #endif + #if defined(__cplusplus) + #pragma GCC diagnostic ignored "-Wold-style-cast" + #pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" + #else + #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" + #pragma GCC diagnostic ignored "-Wbad-function-cast" + #if __GNUC__ >= 5 + #pragma GCC diagnostic ignored "-Wc90-c99-compat" + #pragma GCC diagnostic ignored "-Wc99-c11-compat" + #endif + #endif + // MSC isnan() definition triggers this error on MinGW GCC + #if defined(__MINGW32__) + #pragma GCC diagnostic ignored "-Wfloat-conversion" + #endif + // `-Warray-bounds` results in warnings if UBsan is enabled and pre-GCC-14 has no way of detecting it.. + // The workaround for this uses an assert, but if the user has both UBsan and asserts disabled we need to silence the warning. + #if (UFBXI_GNUC_VERSION >= ufbx_pack_version(4, 3, 0) && UFBXI_GNUC_VERSION < ufbx_pack_version(14, 0, 0)) || defined(NDEBUG) + #pragma GCC diagnostic ignored "-Warray-bounds" + #endif +#endif + +#if !defined(ufbx_static_assert) + #if defined(__cplusplus) && __cplusplus >= 201103 + #define ufbx_static_assert(desc, cond) static_assert(cond, #desc ": " #cond) + #else + #define ufbx_static_assert(desc, cond) typedef char ufbxi_static_assert_##desc[(cond)?1:-1] + #endif +#endif + +#if defined(__has_feature) + #if __has_feature(undefined_behavior_sanitizer) && !defined(UFBX_UBSAN) + #define UFBX_UBSAN 1 + #endif +#endif + +#if defined(__SANITIZE_UNDEFINED__) && !defined(UFBX_UBSAN) + #define UFBX_UBSAN 1 +#endif + +// Don't use unaligned loads with UB-sanitizer +#if defined(UFBX_UBSAN) && !defined(UFBX_NO_UNALIGNED_LOADS) + #define UFBX_NO_UNALIGNED_LOADS +#endif + +#if defined(__clang_analyzer__) && !defined(UFBX_STATIC_ANALYSIS) + #define UFBX_STATIC_ANALYSIS 1 +#endif + +#if defined(UFBX_STATIC_ANALYSIS) + bool ufbxi_analysis_opaque; + #define ufbxi_maybe_null(ptr) (ufbxi_analysis_opaque ? (ptr) : NULL) + #define ufbxi_analysis_assert(cond) ufbx_assert(cond) +#else + #define ufbxi_maybe_null(ptr) (ptr) + #define ufbxi_analysis_assert(cond) (void)0 +#endif + +#if defined(UFBX_STATIC_ANALYSIS) || defined(UFBX_UBSAN) + #define ufbxi_maybe_uninit(cond, value, def) ((cond) ? (value) : (def)) +#else + #define ufbxi_maybe_uninit(cond, value, def) (value) +#endif + +#if !defined(ufbxi_trace) + #if defined(UFBX_TRACE) + #define ufbxi_trace(desc) (fprintf(stderr, "ufbx trace: %s:%d: %s\n", __FILE__, __LINE__, #desc), fflush(stderr), desc) + #else + #define ufbxi_trace(desc) (desc) + #endif +#endif + +#ifndef UFBX_PATH_SEPARATOR + #if defined(_WIN32) + #define UFBX_PATH_SEPARATOR '\\' + #else + #define UFBX_PATH_SEPARATOR '/' + #endif +#endif + +#if !defined(UFBX_STANDARD_C) && defined(_POSIX_C_SOURCE) + #if _POSIX_C_SOURCE >= 200112l + #ifndef UFBX_HAS_FTELLO + #define UFBX_HAS_FTELLO + #endif + #endif +#endif + +#if !defined(UFBX_STANDARD_C) && ((defined(_MSC_VER) && defined(_M_X64) && !defined(_M_ARM64EC)) || ((defined(__GNUC__) || defined(__clang__)) && defined(__x86_64__))) + #define UFBXI_ARCH_X64 1 +#else + #define UFBXI_ARCH_X64 0 +#endif + +#if defined(UFBX_USE_SSE) || (!defined(UFBX_STANDARD_C) && !defined(UFBX_NO_SSE) && UFBXI_ARCH_X64) + #define UFBXI_HAS_SSE 1 + #include + #include +#else + #define UFBXI_HAS_SSE 0 +#endif + +// -- Atomic counter + +#define UFBXI_THREAD_SAFE 1 + +#if defined(__cplusplus) + #define ufbxi_extern_c extern "C" +#else + #define ufbxi_extern_c +#endif + +#if !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)) + typedef size_t ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_inc(ptr) __sync_fetch_and_add((ptr), 1) + #define ufbxi_atomic_counter_dec(ptr) __sync_fetch_and_sub((ptr), 1) + #define ufbxi_atomic_counter_load(ptr) __sync_fetch_and_add((ptr), 0) // TODO: Proper atomic load +#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + #if defined(_M_X64) || defined(_M_ARM64) + ufbxi_extern_c __int64 _InterlockedIncrement64(__int64 volatile * lpAddend); + ufbxi_extern_c __int64 _InterlockedDecrement64(__int64 volatile * lpAddend); + ufbxi_extern_c __int64 _InterlockedExchangeAdd64(__int64 volatile * lpAddend, __int64 Value); + typedef volatile __int64 ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_inc(ptr) ((size_t)_InterlockedIncrement64(ptr) - 1) + #define ufbxi_atomic_counter_dec(ptr) ((size_t)_InterlockedDecrement64(ptr) + 1) + #define ufbxi_atomic_counter_load(ptr) ((size_t)_InterlockedExchangeAdd64((ptr), 0)) + #else + ufbxi_extern_c long __cdecl _InterlockedIncrement(long volatile * lpAddend); + ufbxi_extern_c long __cdecl _InterlockedDecrement(long volatile * lpAddend); + ufbxi_extern_c long __cdecl _InterlockedExchangeAdd(long volatile * lpAddend, long Value); + typedef volatile long ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_inc(ptr) ((size_t)_InterlockedIncrement(ptr) - 1) + #define ufbxi_atomic_counter_dec(ptr) ((size_t)_InterlockedDecrement(ptr) + 1) + #define ufbxi_atomic_counter_load(ptr) ((size_t)_InterlockedExchangeAdd((ptr), 0)) + #endif +#elif !defined(UFBX_STANDARD_C) && defined(__TINYC__) + #if defined(__x86_64__) || defined(_AMD64_) + static size_t ufbxi_tcc_atomic_add(volatile size_t *dst, size_t value) { + __asm__ __volatile__("lock; xaddq %0, %1;" : "+r" (value), "=m" (*dst) : "m" (dst)); + return value; + } + #elif defined(__i386__) || defined(_X86_) + static size_t ufbxi_tcc_atomic_add(volatile size_t *dst, size_t value) { + __asm__ __volatile__("lock; xaddl %0, %1;" : "+r" (value), "=m" (*dst) : "m" (dst)); + return value; + } + #else + #error Unexpected TCC architecture + #endif + typedef volatile size_t ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_inc(ptr) ufbxi_tcc_atomic_add((ptr), 1) + #define ufbxi_atomic_counter_dec(ptr) ufbxi_tcc_atomic_add((ptr), SIZE_MAX) + #define ufbxi_atomic_counter_load(ptr) ufbxi_tcc_atomic_add((ptr), 0) +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #include + #include + typedef struct { alignas(std::atomic_size_t) char data[sizeof(std::atomic_size_t)]; } ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (new (&(ptr)->data) std::atomic_size_t(0)) + #define ufbxi_atomic_counter_free(ptr) (((std::atomic_size_t*)(ptr)->data)->~atomic()) + #define ufbxi_atomic_counter_inc(ptr) ((std::atomic_size_t*)(ptr)->data)->fetch_add(1) + #define ufbxi_atomic_counter_dec(ptr) ((std::atomic_size_t*)(ptr)->data)->fetch_sub(1) + #define ufbxi_atomic_counter_load(ptr) ((std::atomic_size_t*)(ptr)->data)->load(std::memory_order_acquire) +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && !defined(__STDC_NO_ATOMICS__) + #include + typedef volatile atomic_size_t ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) atomic_init(ptr, 0) + #define ufbxi_atomic_counter_free(ptr) (void)(ptr) + #define ufbxi_atomic_counter_inc(ptr) atomic_fetch_add((ptr), 1) + #define ufbxi_atomic_counter_dec(ptr) atomic_fetch_sub((ptr), 1) + #define ufbxi_atomic_counter_load(ptr) atomic_load_explicit((ptr), memory_order_acquire) +#else + typedef volatile size_t ufbxi_atomic_counter; + #define ufbxi_atomic_counter_init(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_free(ptr) (*(ptr) = 0) + #define ufbxi_atomic_counter_inc(ptr) ((*(ptr))++) + #define ufbxi_atomic_counter_dec(ptr) ((*(ptr))--) + #define ufbxi_atomic_counter_load(ptr) (*(ptr)) + #undef UFBXI_THREAD_SAFE + #define UFBXI_THREAD_SAFE 0 +#endif + +// ^^ No references to before this point ^^ +// vv No more includes past this point vv + +#if defined(UFBX_STRING_PREFIX) + #define ufbxi_string_fn(name) ufbxi_pre_cat(UFBX_STRING_PREFIX, name) + #define strlen ufbxi_string_fn(strlen) + #define memcpy ufbxi_string_fn(memcpy) + #define memmove ufbxi_string_fn(memmove) + #define memset ufbxi_string_fn(memset) + #define memchr ufbxi_string_fn(memchr) + #define memcmp ufbxi_string_fn(memcmp) + #define strcmp ufbxi_string_fn(strcmp) + #define strncmp ufbxi_string_fn(strncmp) +#endif + +#if !defined(UFBX_LITTLE_ENDIAN) + #if !defined(UFBX_STANDARD_C) && (defined(_M_IX86) || defined(__i386__) || defined(_M_X64) || defined(__x86_64__) || defined(_M_ARM64) || defined(__aarch64__) || defined(__wasm__) || defined(__EMSCRIPTEN__)) + #define UFBX_LITTLE_ENDIAN 1 + #else + #define UFBX_LITTLE_ENDIAN 0 + #endif +#endif + +// Unaligned little-endian load functions +// On platforms that support unaligned access natively (x86, x64, ARM64) just use normal loads, +// with unaligned attributes, otherwise do manual byte-wise load. + +#define ufbxi_read_u8(ptr) (*(const uint8_t*)(ptr)) + +// Detect support for `__attribute__((aligned(1)))` +#if !defined(UFBX_STANDARD_C) && (defined(__clang__) && defined(__APPLE__)) + // Apple overrides Clang versioning, 5.0 here maps to 3.3 + #if __clang_major__ >= 5 + #define UFBXI_HAS_ATTRIBUTE_ALIGNED 1 + #endif +#elif !defined(UFBX_STANDARD_C) && defined(__clang__) + #if (__clang_major__ >= 4) || (__clang_major__ == 3 && __clang_minor__ >= 3) + #define UFBXI_HAS_ATTRIBUTE_ALIGNED 1 + #endif +#elif !defined(UFBX_STANDARD_C) && defined(__GNUC__) + #if __GNUC__ >= 5 + #define UFBXI_HAS_ATTRIBUTE_ALIGNED 1 + #endif +#endif + +#if defined(UFBXI_HAS_ATTRIBUTE_ALIGNED) + #define UFBXI_HAS_UNALIGNED 1 + #define UFBXI_HAS_ALIASING 1 + #define ufbxi_unaligned + typedef uint16_t __attribute__((aligned(1))) ufbxi_unaligned_u16; + typedef uint32_t __attribute__((aligned(1))) ufbxi_unaligned_u32; + typedef uint64_t __attribute__((aligned(1))) ufbxi_unaligned_u64; + typedef float __attribute__((aligned(1))) ufbxi_unaligned_f32; + typedef double __attribute__((aligned(1))) ufbxi_unaligned_f64; + typedef uint32_t __attribute__((may_alias)) ufbxi_aliasing_u32; +#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + #define UFBXI_HAS_UNALIGNED 1 + #if defined(_M_IX86) + // MSVC seems to assume all pointers are unaligned for x86 + #define ufbxi_unaligned + #else + #define ufbxi_unaligned __unaligned + #endif + typedef uint16_t ufbxi_unaligned_u16; + typedef uint32_t ufbxi_unaligned_u32; + typedef uint64_t ufbxi_unaligned_u64; + typedef float ufbxi_unaligned_f32; + typedef double ufbxi_unaligned_f64; + // MSVC doesn't have aliasing types in theory, but it works in practice.. + #define UFBXI_HAS_ALIASING 1 + typedef uint32_t ufbxi_aliasing_u32; +#endif + +#if (defined(UFBXI_HAS_UNALIGNED) && UFBX_LITTLE_ENDIAN && !defined(UFBX_NO_UNALIGNED_LOADS)) || defined(UFBX_USE_UNALIGNED_LOADS) + #define ufbxi_read_u16(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u16*)(ptr)) + #define ufbxi_read_u32(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u32*)(ptr)) + #define ufbxi_read_u64(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_u64*)(ptr)) + #define ufbxi_read_f32(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_f32*)(ptr)) + #define ufbxi_read_f64(ptr) (*(const ufbxi_unaligned ufbxi_unaligned_f64*)(ptr)) +#else + static ufbxi_forceinline uint16_t ufbxi_read_u16(const void *ptr) { + const char *p = (const char*)ptr; + return (uint16_t)( + (unsigned)(uint8_t)p[0] << 0u | + (unsigned)(uint8_t)p[1] << 8u ); + } + static ufbxi_forceinline uint32_t ufbxi_read_u32(const void *ptr) { + const char *p = (const char*)ptr; + return (uint32_t)( + (unsigned)(uint8_t)p[0] << 0u | + (unsigned)(uint8_t)p[1] << 8u | + (unsigned)(uint8_t)p[2] << 16u | + (unsigned)(uint8_t)p[3] << 24u ); + } + static ufbxi_forceinline uint64_t ufbxi_read_u64(const void *ptr) { + const char *p = (const char*)ptr; + return (uint64_t)( + (uint64_t)(uint8_t)p[0] << 0u | + (uint64_t)(uint8_t)p[1] << 8u | + (uint64_t)(uint8_t)p[2] << 16u | + (uint64_t)(uint8_t)p[3] << 24u | + (uint64_t)(uint8_t)p[4] << 32u | + (uint64_t)(uint8_t)p[5] << 40u | + (uint64_t)(uint8_t)p[6] << 48u | + (uint64_t)(uint8_t)p[7] << 56u ); + } + static ufbxi_forceinline float ufbxi_read_f32(const void *ptr) { + uint32_t u = ufbxi_read_u32(ptr); + float f; + memcpy(&f, &u, 4); + return f; + } + static ufbxi_forceinline double ufbxi_read_f64(const void *ptr) { + uint64_t u = ufbxi_read_u64(ptr); + double f; + memcpy(&f, &u, 8); + return f; + } +#endif + +#define ufbxi_read_i8(ptr) (int8_t)(ufbxi_read_u8(ptr)) +#define ufbxi_read_i16(ptr) (int16_t)(ufbxi_read_u16(ptr)) +#define ufbxi_read_i32(ptr) (int32_t)(ufbxi_read_u32(ptr)) +#define ufbxi_read_i64(ptr) (int64_t)(ufbxi_read_u64(ptr)) + +ufbx_static_assert(sizeof_bool, sizeof(bool) == 1); +ufbx_static_assert(sizeof_char, sizeof(char) == 1); +ufbx_static_assert(sizeof_i8, sizeof(int8_t) == 1); +ufbx_static_assert(sizeof_i16, sizeof(int16_t) == 2); +ufbx_static_assert(sizeof_i32, sizeof(int32_t) == 4); +ufbx_static_assert(sizeof_i64, sizeof(int64_t) == 8); +ufbx_static_assert(sizeof_u8, sizeof(uint8_t) == 1); +ufbx_static_assert(sizeof_u16, sizeof(uint16_t) == 2); +ufbx_static_assert(sizeof_u32, sizeof(uint32_t) == 4); +ufbx_static_assert(sizeof_u64, sizeof(uint64_t) == 8); +ufbx_static_assert(sizeof_f32, sizeof(float) == 4); +ufbx_static_assert(sizeof_f64, sizeof(double) == 8); + +// -- Alignment + +#ifndef UFBX_MAXIMUM_ALIGNMENT +enum { UFBX_MAXIMUM_ALIGNMENT = sizeof(void*) > 8 ? sizeof(void*) : 8 }; +#endif + +#if !defined(UFBXI_UINTPTR_SIZE) && !defined(__CHERI__) // CHERI lies about UINTPTR_MAX + #if UINTPTR_MAX == UINT64_MAX + #define UFBXI_UINTPTR_SIZE 8 + #elif UINTPTR_MAX == UINT32_MAX + #define UFBXI_UINTPTR_SIZE 4 + #endif +#endif +#if defined(UFBXI_UINTPTR_SIZE) + ufbx_static_assert(pointer_size, UFBXI_UINTPTR_SIZE == sizeof(uintptr_t)); +#else + #define UFBXI_UINTPTR_SIZE 0 +#endif + +// -- Version + +#define UFBX_SOURCE_VERSION ufbx_pack_version(0, 22, 0) +ufbx_abi_data_def const uint32_t ufbx_source_version = UFBX_SOURCE_VERSION; + +ufbx_static_assert(source_header_version, UFBX_SOURCE_VERSION/1000u == UFBX_HEADER_VERSION/1000u); + +// -- Fast copy + +#if UFBXI_HAS_SSE + #define ufbxi_copy_16_bytes(dst, src) _mm_storeu_si128((__m128i*)(dst), _mm_loadu_si128((const __m128i*)(src))) +#elif defined(UFBXI_HAS_UNALIGNED) + #define ufbxi_copy_16_bytes(dst, src) do { \ + ufbxi_unaligned ufbxi_unaligned_u64 *mi_dst = (ufbxi_unaligned ufbxi_unaligned_u64 *)(dst); \ + const ufbxi_unaligned ufbxi_unaligned_u64 *mi_src = (const ufbxi_unaligned ufbxi_unaligned_u64 *)src; \ + mi_dst[0] = mi_src[0]; \ + mi_dst[1] = mi_src[1]; \ + } while (0) +#else + #define ufbxi_copy_16_bytes(dst, src) memcpy((dst), (src), 16) +#endif + + +// -- Large fast integer + +#if !defined(UFBX_STANDARD_C) && (defined(__wasm__) || defined(__EMSCRIPTEN__)) && !defined(UFBX_WASM_32BIT) + typedef uint64_t ufbxi_fast_uint; +#else + typedef size_t ufbxi_fast_uint; +#endif + +// -- Wrapping right shift + +#if !defined(UFBX_UBSAN) && UFBXI_ARCH_X64 + #define ufbxi_wrap_shr64(a, b) ((a) >> (b)) +#else + #define ufbxi_wrap_shr64(a, b) ((a) >> ((b) & 63)) +#endif + +// -- Bit manipulation + +#if !defined(UFBX_STANDARD_C) && defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + ufbxi_extern_c unsigned char _BitScanReverse(unsigned long * _Index, unsigned long _Mask); + ufbxi_extern_c unsigned char _BitScanReverse64(unsigned long * _Index, unsigned __int64 _Mask); + static ufbxi_forceinline ufbxi_unused uint32_t ufbxi_lzcnt32(uint32_t v) { + unsigned long index; + _BitScanReverse(&index, (unsigned long)v); + return 31 - (uint32_t)index; + } + static ufbxi_forceinline ufbxi_unused uint32_t ufbxi_lzcnt64(uint64_t v) { + unsigned long index; + #if defined(_M_X64) + _BitScanReverse64(&index, (unsigned __int64)v); + #else + uint32_t hi = (uint32_t)(v >> 32u); + uint32_t hi_nonzero = hi != 0 ? 1 : 0; + uint32_t part = hi_nonzero ? hi : (uint32_t)v; + _BitScanReverse(&index, (unsigned long)part); + index += hi_nonzero * 32u; + #endif + return 63 - (uint32_t)index; + } +#elif !defined(UFBX_STANDARD_C) && (defined(__GNUC__) || defined(__clang__)) + #define ufbxi_lzcnt32(v) ((uint32_t)__builtin_clz((unsigned)(v))) + #define ufbxi_lzcnt64(v) ((uint32_t)__builtin_clzll((unsigned long long)(v))) +#else + // DeBrujin table lookup + static const uint8_t ufbxi_lzcnt32_table[] = { + 31, 22, 30, 21, 18, 10, 29, 2, 20, 17, 15, 13, 9, 6, 28, 1, 23, 19, 11, 3, 16, 14, 7, 24, 12, 4, 8, 25, 5, 26, 27, 0, + }; + static const uint8_t ufbxi_lzcnt64_table[] = { + 63, 16, 62, 7, 15, 36, 61, 3, 6, 14, 22, 26, 35, 47, 60, 2, 9, 5, 28, 11, 13, 21, 42, + 19, 25, 31, 34, 40, 46, 52, 59, 1, 17, 8, 37, 4, 23, 27, 48, 10, 29, 12, 43, 20, 32, 41, + 53, 18, 38, 24, 49, 30, 44, 33, 54, 39, 50, 45, 55, 51, 56, 57, 58, 0, + }; + static ufbxi_noinline ufbxi_unused uint32_t ufbxi_lzcnt32(uint32_t v) { + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + return ufbxi_lzcnt32_table[(v * 0x07c4acddu) >> 27]; + } + static ufbxi_noinline ufbxi_unused uint32_t ufbxi_lzcnt64(uint64_t v) { + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v |= v >> 32; + return ufbxi_lzcnt64_table[(v * UINT64_C(0x03f79d71b4cb0a89)) >> 58]; + } +#endif + +// -- Bit conversion + +#if defined(__cplusplus) + #define ufbxi_bit_cast(m_dst_type, m_dst, m_src_type, m_src) memcpy(&(m_dst), &(m_src), sizeof(m_dst_type)) +#else + #define ufbxi_bit_cast(m_dst_type, m_dst, m_src_type, m_src) do { \ + union { m_dst_type mi_dst; m_src_type mi_src; } mi_union; \ + mi_union.mi_src = (m_src); (m_dst) = mi_union.mi_dst; } while (0) +#endif + +// -- Pointer alignment + +#if !defined(UFBX_STANDARD_C) && defined(__GNUC__) && defined(__has_builtin) + #if __has_builtin(__builtin_is_aligned) + #define ufbxi_is_aligned(m_ptr, m_align) __builtin_is_aligned((m_ptr), (m_align)) + #define ufbxi_is_aligned_mask(m_ptr, m_align) __builtin_is_aligned((m_ptr), (m_align) + 1) + #endif +#endif +#ifndef ufbxi_is_aligned + #define ufbxi_is_aligned(m_ptr, m_align) (((uintptr_t)(m_ptr) & ((m_align) - 1)) == 0) + #define ufbxi_is_aligned_mask(m_ptr, m_align) (((uintptr_t)(m_ptr) & (m_align)) == 0) +#endif + +// -- Debug + +#if defined(UFBX_DEBUG_BINARY_SEARCH) || defined(UFBX_REGRESSION) + #define ufbxi_clamp_linear_threshold(v) (2) +#else + #define ufbxi_clamp_linear_threshold(v) (v) +#endif + +#if defined(UFBX_REGRESSION) + #undef UFBXI_MAX_SKIP_SIZE + #define UFBXI_MAX_SKIP_SIZE 128 + + #undef UFBXI_MAP_MAX_SCAN + #define UFBXI_MAP_MAX_SCAN 2 + + #undef UFBXI_KD_FAST_DEPTH + #define UFBXI_KD_FAST_DEPTH 2 + + #undef UFBXI_FACE_GROUP_HASH_BITS + #define UFBXI_FACE_GROUP_HASH_BITS 2 +#endif + +#if defined(UFBX_REGRESSION) || defined(UFBX_EXTENSIVE_THREADING) + #undef UFBXI_MIN_THREADED_DEFLATE_BYTES + #define UFBXI_MIN_THREADED_DEFLATE_BYTES 2 + + #undef UFBXI_MIN_THREADED_ASCII_VALUES + #define UFBXI_MIN_THREADED_ASCII_VALUES 2 +#endif + +#if defined(UFBX_REGRESSION) + #define ufbxi_regression_assert(cond) ufbx_assert(cond) +#else + #define ufbxi_regression_assert(cond) (void)0 +#endif + +#if defined(UFBX_REGRESSION) || defined(UFBX_DEV) || defined(UFBX_UBSAN) + #define ufbxi_dev_assert(cond) ufbx_assert(cond) +#else + #define ufbxi_dev_assert(cond) (void)0 +#endif + +#define ufbxi_unreachable(reason) do { ufbx_assert(0 && reason); } while (0) + +#if defined(UFBX_REGRESSION) + #define UFBXI_IS_REGRESSION 1 +#else + #define UFBXI_IS_REGRESSION 0 +#endif + +#if defined(_MSC_VER) + #define ufbxi_thread_local __declspec(thread) +#elif defined(__GNUC__) || defined(__clang__) + #define ufbxi_thread_local __thread +#elif UFBXI_HAS_CPP11 + #define ufbxi_thread_local thread_local +#elif UFBX_STDC >= 201112L + #define ufbxi_thread_local _Thread_local +#endif + +#if defined(UFBXI_ANALYSIS_RECURSIVE) + #define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params) UFBXI_RECURSIVE_FUNCTION(m_name, m_max_depth); + #define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params) UFBXI_RECURSIVE_FUNCTION(m_name, m_max_depth); +#elif UFBXI_IS_REGRESSION && defined(ufbxi_thread_local) + #define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params) \ + { \ + m_ret m_name##_rec m_params; \ + static ufbxi_thread_local unsigned ufbxi_recursion_depth; \ + ufbx_assert(ufbxi_recursion_depth < m_max_depth); \ + ++ufbxi_recursion_depth; \ + m_ret ret = m_name##_rec m_args; \ + --ufbxi_recursion_depth; \ + return ret; \ + } \ + m_ret m_name##_rec m_params + #define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params) \ + { \ + void m_name##_rec m_params; \ + static ufbxi_thread_local unsigned ufbxi_recursion_depth; \ + ufbx_assert(ufbxi_recursion_depth < m_max_depth); \ + ++ufbxi_recursion_depth; \ + m_name##_rec m_args; \ + --ufbxi_recursion_depth; \ + } \ + void m_name##_rec m_params +#else + #define ufbxi_recursive_function(m_ret, m_name, m_args, m_max_depth, m_params) + #define ufbxi_recursive_function_void(m_name, m_args, m_max_depth, m_params) +#endif + +// -- Utility + +#if defined(UFBX_UBSAN) + static void ufbxi_assert_zero(size_t offset) { (void)offset; ufbx_assert(offset == 0); } + #define ufbxi_add_ptr(ptr, offset) ((ptr) ? (ptr) + (offset) : (ufbxi_assert_zero((size_t)(offset)), (ptr))) + #define ufbxi_sub_ptr(ptr, offset) ((ptr) ? (ptr) - (offset) : (ufbxi_assert_zero((size_t)(offset)), (ptr))) +#else + #define ufbxi_add_ptr(ptr, offset) ((ptr) + (offset)) + #define ufbxi_sub_ptr(ptr, offset) ((ptr) - (offset)) +#endif + +#define ufbxi_arraycount(arr) (sizeof(arr) / sizeof(*(arr))) +#define ufbxi_for(m_type, m_name, m_begin, m_num) for (m_type *m_name = m_begin, *m_name##_end = ufbxi_add_ptr(m_name, m_num); m_name != m_name##_end; m_name++) +#define ufbxi_for_ptr(m_type, m_name, m_begin, m_num) for (m_type **m_name = m_begin, **m_name##_end = ufbxi_add_ptr(m_name, m_num); m_name != m_name##_end; m_name++) + +// WARNING: Evaluates `m_list` twice! +#define ufbxi_for_list(m_type, m_name, m_list) for (m_type *m_name = (m_list).data, *m_name##_end = ufbxi_add_ptr(m_name, (m_list).count); m_name != m_name##_end; m_name++) +#define ufbxi_for_ptr_list(m_type, m_name, m_list) for (m_type **m_name = (m_list).data, **m_name##_end = ufbxi_add_ptr(m_name, (m_list).count); m_name != m_name##_end; m_name++) + +#define ufbxi_string_literal(str) { str, sizeof(str) - 1 } + +static ufbxi_forceinline uint32_t ufbxi_min32(uint32_t a, uint32_t b) { return a < b ? a : b; } +static ufbxi_forceinline uint32_t ufbxi_max32(uint32_t a, uint32_t b) { return a < b ? b : a; } +static ufbxi_forceinline uint64_t ufbxi_min64(uint64_t a, uint64_t b) { return a < b ? a : b; } +static ufbxi_forceinline uint64_t ufbxi_max64(uint64_t a, uint64_t b) { return a < b ? b : a; } +static ufbxi_forceinline size_t ufbxi_min_sz(size_t a, size_t b) { return a < b ? a : b; } +static ufbxi_forceinline size_t ufbxi_max_sz(size_t a, size_t b) { return a < b ? b : a; } +static ufbxi_forceinline ufbx_real ufbxi_min_real(ufbx_real a, ufbx_real b) { return a < b ? a : b; } +static ufbxi_forceinline ufbx_real ufbxi_max_real(ufbx_real a, ufbx_real b) { return a < b ? b : a; } + +static ufbxi_forceinline int32_t ufbxi_f64_to_i32(double value) +{ + if (ufbx_fabs(value) <= (double)INT32_MAX) { + return (int32_t)value; + } else { + return value >= 0.0 ? INT32_MAX : INT32_MIN; + } +} + +static ufbxi_forceinline int64_t ufbxi_f64_to_i64(double value) +{ + if (ufbx_fabs(value) <= (double)INT64_MAX) { + return (int64_t)value; + } else { + return value >= 0.0 ? INT64_MAX : INT64_MIN; + } +} + +#if defined(UFBX_REGRESSION) + static size_t ufbxi_to_size(ptrdiff_t delta) { + ufbx_assert(delta >= 0); + return (size_t)delta; + } +#else + #define ufbxi_to_size(delta) ((size_t)(delta)) +#endif + +// Stable sort array `m_type m_data[m_size]` using the predicate `m_cmp_lambda(a, b)` +// `m_linear_size` is a hint for how large blocks handle initially do with insertion sort +// `m_tmp` must be a memory buffer with at least the same size and alignment as `m_data` +#define ufbxi_macro_stable_sort(m_type, m_linear_size, m_data, m_tmp, m_size, m_cmp_lambda) do { \ + typedef m_type mi_type; \ + mi_type *mi_src = (mi_type*)(m_tmp); \ + mi_type *mi_data = m_data, *mi_dst = mi_data; \ + size_t mi_block_size = ufbxi_clamp_linear_threshold(m_linear_size), mi_size = m_size; \ + /* Insertion sort in `m_linear_size` blocks */ \ + for (size_t mi_base = 0; mi_base < mi_size; mi_base += mi_block_size) { \ + size_t mi_i_end = mi_base + mi_block_size; \ + if (mi_i_end > mi_size) mi_i_end = mi_size; \ + for (size_t mi_i = mi_base + 1; mi_i < mi_i_end; mi_i++) { \ + size_t mi_j = mi_i; \ + mi_src[0] = mi_dst[mi_i]; \ + for (; mi_j != mi_base; --mi_j) { \ + mi_type *a = &mi_src[0], *b = &mi_dst[mi_j - 1]; \ + if (!( m_cmp_lambda )) break; \ + mi_dst[mi_j] = mi_dst[mi_j - 1]; \ + } \ + mi_dst[mi_j] = mi_src[0]; \ + } \ + } \ + /* Merge sort ping-ponging between `m_data` and `m_tmp` */ \ + for (; mi_block_size < mi_size; mi_block_size *= 2) { \ + mi_type *mi_swap = mi_dst; mi_dst = mi_src; mi_src = mi_swap; \ + for (size_t mi_base = 0; mi_base < mi_size; mi_base += mi_block_size * 2) { \ + size_t mi_i = mi_base, mi_i_end = mi_base + mi_block_size; \ + size_t mi_j = mi_i_end, mi_j_end = mi_j + mi_block_size; \ + size_t mi_k = mi_base; \ + if (mi_i_end > mi_size) mi_i_end = mi_size; \ + if (mi_j_end > mi_size) mi_j_end = mi_size; \ + while ((mi_i < mi_i_end) & (mi_j < mi_j_end)) { \ + mi_type *a = &mi_src[mi_j], *b = &mi_src[mi_i]; \ + if ( m_cmp_lambda ) { \ + mi_dst[mi_k] = *a; mi_j++; \ + } else { \ + mi_dst[mi_k] = *b; mi_i++; \ + } \ + mi_k++; \ + } \ + while (mi_i < mi_i_end) mi_dst[mi_k++] = mi_src[mi_i++]; \ + while (mi_j < mi_j_end) mi_dst[mi_k++] = mi_src[mi_j++]; \ + } \ + } \ + /* Copy the result to `m_data` if we ended up in `m_tmp` */ \ + if (mi_dst != mi_data) memcpy((void*)mi_data, mi_dst, sizeof(mi_type) * mi_size); \ + } while (0) + +#define ufbxi_macro_lower_bound_eq(m_type, m_linear_size, m_result_ptr, m_data, m_begin, m_size, m_cmp_lambda, m_eq_lambda) do { \ + typedef m_type mi_type; \ + const mi_type *mi_data = (m_data); \ + size_t mi_lo = m_begin, mi_hi = m_size, mi_linear_size = ufbxi_clamp_linear_threshold(m_linear_size); \ + ufbx_assert(mi_linear_size > 1); \ + /* Binary search until we get down to `m_linear_size` elements */ \ + while (mi_hi - mi_lo > mi_linear_size) { \ + size_t mi_mid = mi_lo + (mi_hi - mi_lo) / 2; \ + const mi_type *a = &mi_data[mi_mid]; \ + if ( m_cmp_lambda ) { mi_lo = mi_mid + 1; } else { mi_hi = mi_mid + 1; } \ + } \ + /* Linearly scan until we find the edge */ \ + for (; mi_lo < mi_hi; mi_lo++) { \ + const mi_type *a = &mi_data[mi_lo]; \ + if ( m_eq_lambda ) { *(m_result_ptr) = mi_lo; break; } \ + } \ + } while (0) + +#define ufbxi_macro_upper_bound_eq(m_type, m_linear_size, m_result_ptr, m_data, m_begin, m_size, m_eq_lambda) do { \ + typedef m_type mi_type; \ + const mi_type *mi_data = (m_data); \ + size_t mi_lo = m_begin, mi_hi = m_size, mi_linear_size = ufbxi_clamp_linear_threshold(m_linear_size); \ + ufbx_assert(mi_linear_size > 1); \ + /* Linearly scan with galloping */ \ + for (size_t mi_step = 1; mi_step < 100 && mi_hi - mi_lo > mi_step; mi_step *= 2) { \ + const mi_type *a = &mi_data[mi_lo + mi_step]; \ + if (!( m_eq_lambda )) { mi_hi = mi_lo + mi_step; break; } \ + mi_lo += mi_step; \ + } \ + /* Binary search until we get down to `m_linear_size` elements */ \ + while (mi_hi - mi_lo > mi_linear_size) { \ + size_t mi_mid = mi_lo + (mi_hi - mi_lo) / 2; \ + const mi_type *a = &mi_data[mi_mid]; \ + if ( m_eq_lambda ) { mi_lo = mi_mid + 1; } else { mi_hi = mi_mid + 1; } \ + } \ + /* Linearly scan until we find the edge */ \ + for (; mi_lo < mi_hi; mi_lo++) { \ + const mi_type *a = &mi_data[mi_lo]; \ + if (!( m_eq_lambda )) break; \ + } \ + *(m_result_ptr) = mi_lo; \ + } while (0) + +typedef bool ufbxi_less_fn(void *user, const void *a, const void *b); + +static ufbxi_noinline void ufbxi_stable_sort(size_t stride, size_t linear_size, void *in_data, void *in_tmp, size_t size, ufbxi_less_fn *less_fn, void *less_user) +{ + (void)linear_size; + + char *src = (char*)in_tmp; + char *data = (char*)in_data, *dst = (char*)data; + size_t block_size = ufbxi_clamp_linear_threshold(linear_size); + /* Insertion sort in `linear_size` blocks */ + for (size_t base = 0; base < size; base += block_size) { + size_t i_end = base + block_size; + if (i_end > size) i_end = size; + for (size_t i = base + 1; i < i_end; i++) { + + { + char *a = dst + i * stride, *b = dst + (i - 1) * stride; + if (!less_fn(less_user, a, b)) continue; + } + + size_t j = i - 1; + memcpy(src, dst + i * stride, stride); + memcpy(dst + i * stride, dst + j * stride, stride); + for (; j != base; --j) { + char *a = src, *b = dst + (j - 1) * stride; + if (!less_fn(less_user, a, b)) break; + memcpy(dst + j * stride, dst + (j - 1) * stride, stride); + } + memcpy(dst + j * stride, src, stride); + } + } + /* Merge sort ping-ponging between `data` and `tmp` */ + for (; block_size < size; block_size *= 2) { + char *swap = dst; dst = src; src = swap; + for (size_t base = 0; base < size; base += block_size * 2) { + size_t i = base, i_end = base + block_size; + size_t j = i_end, j_end = j + block_size; + size_t k = base; + if (i_end > size) i_end = size; + if (j_end > size) j_end = size; + while ((i < i_end) & (j < j_end)) { + char *a = src + j * stride, *b = src + i * stride; + if (less_fn(less_user, a, b)) { + memcpy(dst + k * stride, a, stride); + j++; + } else { + memcpy(dst + k * stride, b, stride); + i++; + } + k++; + } + + memcpy(dst + k * stride, src + i * stride, (i_end - i) * stride); + if (j < j_end) { + memcpy(dst + (k + (i_end - i)) * stride, src + j * stride, (j_end - j) * stride); + } + } + } + /* Copy the result to `data` if we ended up in `tmp` */ + if (dst != data) memcpy((void*)data, dst, size * stride); +} + +static ufbxi_forceinline void ufbxi_swap(void *a, void *b, size_t size) +{ +#if UFBXI_HAS_ALIASING && !defined(__CHERI__) // CHERI needs to copy pointer metadata tag bits.. + ufbxi_dev_assert(size % 4 == 0 && (uintptr_t)a % 4 == 0 && (uintptr_t)b % 4 == 0); + char *ca = (char*)a, *cb = (char*)b; + for (size_t i = 0; i < size; i += 4, ca += 4, cb += 4) { + ufbxi_aliasing_u32 *ua = (ufbxi_aliasing_u32*)ca, *ub = (ufbxi_aliasing_u32*)cb; + uint32_t va = *ua, vb = *ub; + *ua = vb; + *ub = va; + } +#else + union { + void *align_ptr; + uintptr_t align_uptr; + uint64_t align_u64; + char data[256]; + } tmp; + ufbxi_dev_assert(size <= sizeof(tmp)); + memcpy(tmp.data, a, size); + memcpy(a, b, size); + memcpy(b, tmp.data, size); +#endif +} + +static ufbxi_noinline void ufbxi_unstable_sort(void *in_data, size_t size, size_t stride, ufbxi_less_fn *less_fn, void *less_user) +{ + if (size <= 1) return; + + char *data = (char*)in_data; + size_t start = (size - 1) >> 1; + size_t end = size - 1; + for (;;) { + size_t root = start; + size_t child; + while ((child = root*2 + 1) <= end) { + size_t next = less_fn(less_user, data + child * stride, data + root * stride) ? root : child; + if (child + 1 <= end && less_fn(less_user, data + next * stride, data + (child + 1) * stride)) { + next = child + 1; + } + if (next == root) break; + ufbxi_swap(data + root * stride, data + next * stride, stride); + root = next; + } + + if (start > 0) { + start--; + } else if (end > 0) { + ufbxi_swap(data + end * stride, data, stride); + end--; + } else { + break; + } + } +} + +// -- Float parsing + +#define UFBXI_BIGINT_LIMB_BITS 32 +#define UFBXI_BIGINT_ACCUM_BITS (UFBXI_BIGINT_LIMB_BITS * 2) +#define UFBXI_BIGINT_LIMB_MAX (ufbxi_bigint_limb)(((ufbxi_bigint_accum)1 << UFBXI_BIGINT_LIMB_BITS) - 1) +typedef uint32_t ufbxi_bigint_limb; +typedef uint64_t ufbxi_bigint_accum; + +typedef struct { + ufbxi_bigint_limb *limbs; + uint32_t capacity; + uint32_t length; +} ufbxi_bigint; + +static ufbxi_bigint ufbxi_bigint_make(ufbxi_bigint_limb *limbs, size_t capacity) +{ + ufbxi_bigint bi = { limbs, (uint32_t)capacity }; + return bi; +} + +#define ufbxi_bigint_array(arr) ufbxi_bigint_make((arr), sizeof(arr) / sizeof(*(arr))) + +static const uint64_t ufbxi_pow5_tab[] = { + UINT64_C(0x1), UINT64_C(0x5), UINT64_C(0x19), UINT64_C(0x7d), UINT64_C(0x271), UINT64_C(0xc35), UINT64_C(0x3d09), UINT64_C(0x1312d), UINT64_C(0x5f5e1), + UINT64_C(0x1dcd65), UINT64_C(0x9502f9), UINT64_C(0x2e90edd), UINT64_C(0xe8d4a51), UINT64_C(0x48c27395), UINT64_C(0x16bcc41e9), UINT64_C(0x71afd498d), + UINT64_C(0x2386f26fc1), UINT64_C(0xb1a2bc2ec5), UINT64_C(0x3782dace9d9), UINT64_C(0x1158e460913d), UINT64_C(0x56bc75e2d631), UINT64_C(0x1b1ae4d6e2ef5), + UINT64_C(0x878678326eac9), UINT64_C(0x2a5a058fc295ed), UINT64_C(0xd3c21bcecceda1), UINT64_C(0x422ca8b0a00a425), UINT64_C(0x14adf4b7320334b9), UINT64_C(0x6765c793fa10079d), +}; + +static const double ufbxi_pow10_tab_f64[] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, +}; + +static ufbxi_noinline void ufbxi_bigint_mad(ufbxi_bigint *bigint, ufbxi_bigint_accum multiplicand, ufbxi_bigint_accum addend) +{ + ufbxi_dev_assert((multiplicand | addend) >> (UFBXI_BIGINT_ACCUM_BITS - 1) == 0); + ufbxi_bigint b = *bigint; + ufbxi_bigint_limb m_lo = (ufbxi_bigint_limb)multiplicand; + ufbxi_bigint_limb m_hi = (ufbxi_bigint_limb)(multiplicand >> UFBXI_BIGINT_LIMB_BITS); + ufbxi_bigint_accum carry = addend; + for (uint32_t i = 0; i < b.length; i++) { + ufbxi_bigint_accum limb = (ufbxi_bigint_accum)b.limbs[i]; + ufbxi_bigint_accum lo = limb * m_lo + (carry & UFBXI_BIGINT_LIMB_MAX); + ufbxi_bigint_accum hi = limb * m_hi; + b.limbs[i] = (ufbxi_bigint_limb)lo; + carry = (carry >> 32u) + (lo >> 32u) + hi; + } + while (carry) { + b.limbs[b.length++] = (ufbxi_bigint_limb)carry; + ufbxi_dev_assert(b.length < b.capacity); + carry >>= 32u; + } + bigint->length = b.length; +} + +static ufbxi_noinline bool ufbxi_bigint_div(ufbxi_bigint *q, ufbxi_bigint *u, ufbxi_bigint *v) +{ + int32_t n = (int32_t)v->length; + int32_t m = (int32_t)u->length - n; + ufbxi_bigint_limb v_hi = v->limbs[v->length - 1]; + ufbxi_bigint_limb *un = u->limbs, *vn = v->limbs; + ufbxi_dev_assert(n >= 2 && m >= 1 && v_hi >> (UFBXI_BIGINT_LIMB_BITS - 1) != 0 && un[n+m - 1] >> (UFBXI_BIGINT_LIMB_BITS - 1) == 0); + un[n + m] = 0; + q->length = 0; + for (int32_t j = m - 1; j >= 0; j--) { + ufbxi_bigint_accum u_hi = ((ufbxi_bigint_accum)un[n+j] << UFBXI_BIGINT_LIMB_BITS) | un[n+j-1]; + ufbxi_bigint_accum t, qhat = u_hi / v_hi, rhat = u_hi % v_hi; + while (qhat >> UFBXI_BIGINT_LIMB_BITS != 0 || qhat*vn[n-2] > ((rhat<> UFBXI_BIGINT_LIMB_BITS != 0) break; + } + ufbxi_bigint_limb carry = 0; + for (int32_t i = 0; i < n; i++) { + ufbxi_bigint_accum p = qhat * vn[i]; + t = (ufbxi_bigint_accum)un[i+j] - carry - (ufbxi_bigint_limb)p; + un[i+j] = (ufbxi_bigint_limb)t; + carry = (ufbxi_bigint_limb)((p >> UFBXI_BIGINT_LIMB_BITS) - (t >> UFBXI_BIGINT_LIMB_BITS)); + } + t = (ufbxi_bigint_accum)un[j+n] - carry; + un[j+n] = (ufbxi_bigint_limb)t; + if (t >> UFBXI_BIGINT_LIMB_BITS != 0) { + qhat -= 1; + carry = 0; + for (int32_t i = 0; i < n; i++) { + t = (ufbxi_bigint_accum)un[i+j] + vn[i] + carry; + un[i+j] = (ufbxi_bigint_limb)t; + carry = (ufbxi_bigint_limb)(t >> UFBXI_BIGINT_LIMB_BITS); + } + un[j+n] += carry; + } + q->limbs[j] = (ufbxi_bigint_limb)qhat; + if (qhat && !q->length) { + ufbxi_dev_assert(j + 1 < (int32_t)q->capacity); + q->length = (uint32_t)(j + 1); + } + } + for (int32_t i = 0; i < n; i++) { + if (un[i]) return true; + } + return false; +} + +static void ufbxi_bigint_mul_pow5(ufbxi_bigint *b, uint32_t power) +{ + for (; power > 27; power -= 27) { + ufbxi_bigint_mad(b, ufbxi_pow5_tab[27], 0); + } + ufbxi_bigint_mad(b, ufbxi_pow5_tab[power], 0); +} + +static ufbxi_noinline void ufbxi_bigint_shift_left(ufbxi_bigint *bigint, uint32_t amount) +{ + uint32_t words = amount / UFBXI_BIGINT_LIMB_BITS, bits = amount % UFBXI_BIGINT_LIMB_BITS; + ufbxi_bigint b = *bigint; + ufbxi_dev_assert(b.length + words + 1 < b.capacity && b.capacity >= 4); + uint32_t bits_down = UFBXI_BIGINT_LIMB_BITS - bits - 1; + bigint->length += words + (b.limbs[b.length - 1] >> 1 >> bits_down != 0 ? 1 : 0); + b.limbs[b.length] = 0; + if (b.length <= 3 && words <= 3) { + ufbxi_bigint_limb l0 = b.limbs[0]; + ufbxi_bigint_limb l1 = ufbxi_maybe_uninit(b.length >= 1, b.limbs[1], ~0u); + ufbxi_bigint_limb l2 = ufbxi_maybe_uninit(b.length >= 2, b.limbs[2], ~0u); + b.limbs[0] = 0; + b.limbs[1] = 0; + b.limbs[2] = 0; + b.limbs[words + 0] = l0 << bits; + b.limbs[words + 1] = (l1 << bits) | (l0 >> 1 >> bits_down); + b.limbs[words + 2] = (l2 << bits) | (l1 >> 1 >> bits_down); + b.limbs[words + 3] = (l2 >> 1 >> bits_down); + } else { + for (uint32_t i = b.length + 1; i-- > 1; ) { + b.limbs[i + words] = (b.limbs[i] << bits) | (b.limbs[i - 1] >> 1 >> bits_down); + } + b.limbs[words] = b.limbs[0] << bits; + for (uint32_t i = 0; i < words; i++) { + b.limbs[i] = 0; + } + } +} + +static ufbxi_bigint_limb ufbxi_bigint_top_limb(const ufbxi_bigint b, uint32_t index) { + return index < b.length ? b.limbs[b.length - 1 - index] : 0; +} + +static ufbxi_noinline uint64_t ufbxi_bigint_extract_high(const ufbxi_bigint b, int32_t *p_exponent, bool *p_tail) +{ + ufbxi_dev_assert(b.length != 0); + uint64_t result = 0; + const uint32_t limb_count = 64 / UFBXI_BIGINT_LIMB_BITS; + for (uint32_t i = 0; i < limb_count; i++) { + result = (result << UFBXI_BIGINT_LIMB_BITS) | ufbxi_bigint_top_limb(b, i); + } + uint32_t shift = ufbxi_lzcnt64(result); + result <<= shift; + ufbxi_bigint_limb lo = ufbxi_bigint_top_limb(b, limb_count); + if (shift > 0) { + result |= lo >> (UFBXI_BIGINT_LIMB_BITS - shift); + } + *p_tail |= (ufbxi_bigint_limb)(lo << shift) != 0; + for (uint32_t i = limb_count + 1; i < b.length; i++) { + *p_tail |= ufbxi_bigint_top_limb(b, i) != 0; + } + *p_exponent += (int32_t)(b.length * UFBXI_BIGINT_LIMB_BITS - shift - 1); + return result; +} + +static uint64_t ufbxi_shift_right_round(uint64_t value, uint32_t shift, bool tail) +{ + if (shift == 0) return value; + if (shift > 64) return 0; + uint64_t result = value >> (shift - 1); + uint64_t tail_mask = (UINT64_C(1) << (shift - 1)) - 1; + + bool r_odd = (result & 0x2) != 0; + bool r_round = (result & 0x1) != 0; + bool r_tail = tail || (value & tail_mask) != 0; + uint64_t round_bit = (r_round && (r_odd || r_tail)) ? 1u : 0u; + + return (result >> 1u) + round_bit; +} + +typedef enum { + UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH = 0x1, + UFBXI_PARSE_DOUBLE_AS_BINARY32 = 0x2, +} ufbxi_parse_double_flag; + +static bool ufbxi_scan_ignorecase(const char *p, const char *end, const char *fmt) +{ + for (const char *f = fmt; *f; f++, p++) { + if (p >= end) return false; + if ((*p | 0x20) != *f) return false; + } + return true; +} + +static ufbxi_noinline bool ufbxi_parse_inf_nan(double *p_result, const char *str, size_t max_length, char **p_end) +{ + bool negative = false; + const char *p = str, *end = p + max_length; + if (p != end && (*p == '+' || *p == '-')) { + negative = *p++ == '-'; + } + + uint32_t top_bits = 0; + if (end - p >= 3 && (p[0] >= '0' && p[0] <= '9') && p[1] == '.' && p[2] == '#') { + // Legacy MSVC 1.#NAN + p += 3; + if (ufbxi_scan_ignorecase(p, end, "inf")) { + p += 3; + top_bits = 0x7ff0; + } else if (ufbxi_scan_ignorecase(p, end, "nan") || ufbxi_scan_ignorecase(p, end, "ind")) { + p += 3; + top_bits = 0x7ff8; + } else { + return false; + } + while (p != end && *p >= '0' && *p <= '9') { + p++; + } + } else { + // Standard + if (ufbxi_scan_ignorecase(p, end, "nan")) { + p += 3; + top_bits = 0x7ff8; + if (p != end && *p == '(') { + p++; + while (p != end && *p != ')') { + char c = *p; + if (!((c>='0'&&c<='9') || (c>='a'&&c<='z') || (c>='A'&&c<='Z'))) { + return false; + } + p++; + } + if (p == end) return false; + p++; + } + } else if (ufbxi_scan_ignorecase(p, end, "inf")) { + p += ufbxi_scan_ignorecase(p + 3, end, "inity") ? 8 : 3; + top_bits = 0x7ff0; + } + } + + *p_end = (char*)p; + top_bits |= negative ? 0x8000 : 0; + uint64_t bits = (uint64_t)top_bits << 48; + double result; + ufbxi_bit_cast(double, result, uint64_t, bits); + *p_result = result; + return true; +} + +static ufbxi_noinline double ufbxi_parse_double(const char *str, size_t max_length, char **p_end, uint32_t flags) +{ + const uint32_t max_limbs = 14; + + ufbxi_bigint_limb mantissa_limbs[42], divisor_limbs[42], quotient_limbs[42]; + ufbxi_bigint big_mantissa = ufbxi_bigint_array(mantissa_limbs); + ufbxi_bigint big_quotient = ufbxi_bigint_array(quotient_limbs); + int32_t dec_exponent = 0, has_dot = 0; + bool negative = false, tail = false, digits_valid = true; + uint64_t digits = 0; + uint32_t num_digits = 0; + + const char *p = str, *end = p + max_length; + if (p != end && (*p == '+' || *p == '-')) { + negative = *p++ == '-'; + } + while (p != end) { + char c = *p; + if (c >= '0' && c <= '9') { + if (big_mantissa.length < max_limbs) { + digits = digits * 10 + (uint64_t)(c - '0'); + num_digits++; + if (num_digits >= 18) { + ufbxi_dev_assert(num_digits < ufbxi_arraycount(ufbxi_pow5_tab)); + ufbxi_bigint_mad(&big_mantissa, ufbxi_pow5_tab[num_digits] << num_digits, digits); + digits = 0; + num_digits = 0; + digits_valid = false; + } + dec_exponent -= has_dot; + } else { + dec_exponent += 1 - has_dot; + } + p++; + } else if (c == '.' && !has_dot) { + has_dot = true; + p++; + } else { + break; + } + } + if (p != end && (*p == 'e' || *p == 'E')) { + p++; + bool exp_negative = false; + if (p != end && (*p == '+' || *p == '-')) { + exp_negative = *p == '-'; + p++; + } + int32_t exp = 0; + while (p != end) { + char c = *p; + if (c >= '0' && c <= '9') { + p++; + exp = exp * 10 + (c - '0'); + if (exp >= 10000) break; + } else { + break; + } + } + dec_exponent += exp_negative ? -exp : exp; + } + + if (p != end) { + char c = *p; + if (c == '#' || c == 'i' || c == 'I' || c == 'n' || c == 'N') { + double result; + if (ufbxi_parse_inf_nan(&result, str, max_length, p_end)) { + return result; + } + } + } + + *p_end = (char*)p; + + // Both power of 10 and integer are exactly representable as doubles + // Powers of 10 are factored as 2*5, and 2^N can be always exactly represented. + if ((flags & UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH) != 0 && big_mantissa.length == 0 && dec_exponent >= -22 && dec_exponent <= 22 && (digits >> 53) == 0) { + double value; + if (dec_exponent < 0) { + value = (double)digits / ufbxi_pow10_tab_f64[-dec_exponent]; + } else { + value = (double)digits * ufbxi_pow10_tab_f64[dec_exponent]; + } + return negative ? -value : value; + } + + if (big_mantissa.length == 0) { + big_mantissa.limbs[0] = (ufbxi_bigint_limb)digits; + big_mantissa.limbs[1] = (ufbxi_bigint_limb)(digits >> 32u); + big_mantissa.length = (digits >> 32u) ? 2 : digits ? 1 : 0; + if (big_mantissa.length == 0) return negative ? -0.0 : 0.0; + } else { + ufbxi_dev_assert(num_digits < ufbxi_arraycount(ufbxi_pow5_tab)); + ufbxi_bigint_mad(&big_mantissa, ufbxi_pow5_tab[num_digits] << num_digits, digits); + } + + uint32_t enc_sign_shift = 63; + uint32_t enc_mantissa_bits = 53; + int32_t enc_max_exponent = 1023; + if (flags & UFBXI_PARSE_DOUBLE_AS_BINARY32) { + enc_sign_shift = 31; + enc_mantissa_bits = 24; + enc_max_exponent = 127; + } + + int32_t exponent = 0; + if (dec_exponent < 0) { + if (dec_exponent + (int32_t)big_mantissa.length * 10 <= -325) return negative ? -0.0 : 0.0; + + ufbxi_bigint big_divisor = ufbxi_bigint_array(divisor_limbs); + uint32_t pow5 = (uint32_t)-dec_exponent; + uint32_t initial_pow5 = pow5 <= 27 ? pow5 : 27; + uint64_t pow5_value = ufbxi_pow5_tab[initial_pow5]; + pow5 -= initial_pow5; + exponent += dec_exponent; + + if (pow5 == 0 && digits_valid && digits >> 63 == 0) { + uint32_t divisor_zeros = ufbxi_lzcnt64(pow5_value); + uint64_t mantissa_zeros = ufbxi_lzcnt64(digits) - 1; + uint64_t divisor_bits = pow5_value << divisor_zeros; + uint64_t mantissa_bits = digits << mantissa_zeros; + big_divisor.limbs[0] = (ufbxi_bigint_limb)divisor_bits; + big_divisor.limbs[1] = (ufbxi_bigint_limb)(divisor_bits >> 32u); + big_divisor.length = 2; + big_mantissa.limbs[0] = 0; + big_mantissa.limbs[1] = 0; + big_mantissa.limbs[2] = (ufbxi_bigint_limb)mantissa_bits; + big_mantissa.limbs[3] = (ufbxi_bigint_limb)(mantissa_bits >> 32u); + big_mantissa.length = 4; + exponent += (int32_t)divisor_zeros - (int32_t)mantissa_zeros - 64; + } else { + big_divisor.limbs[0] = (ufbxi_bigint_limb)pow5_value; + big_divisor.limbs[1] = (ufbxi_bigint_limb)(pow5_value >> 32u); + big_divisor.length = (pow5_value >> 32u) != 0 ? 2 : 1; + if (pow5 > 0) { + ufbxi_bigint_mul_pow5(&big_divisor, pow5); + } + + uint32_t divisor_zeros = ufbxi_lzcnt32(big_divisor.limbs[big_divisor.length - 1]); + if (big_divisor.length == 1) divisor_zeros += UFBXI_BIGINT_LIMB_BITS; + ufbxi_bigint_shift_left(&big_divisor, divisor_zeros); + uint32_t divisor_bits = big_divisor.length * UFBXI_BIGINT_LIMB_BITS; + + uint32_t mantissa_zeros = ufbxi_lzcnt32(big_mantissa.limbs[big_mantissa.length - 1]); + uint32_t mantissa_bits = big_mantissa.length * UFBXI_BIGINT_LIMB_BITS - mantissa_zeros; + uint32_t mantissa_min_bits = divisor_bits + enc_mantissa_bits + 2; + uint32_t mantissa_shift = mantissa_bits < mantissa_min_bits ? mantissa_min_bits - mantissa_bits : 0; + // Align mantissa to never have a high bit, this means we can skip the first digit during division. + mantissa_shift += ((mantissa_shift - mantissa_zeros) & (UFBXI_BIGINT_LIMB_BITS - 1)) == 0 ? 1 : 0; + if (mantissa_shift > 0) { + ufbxi_bigint_shift_left(&big_mantissa, mantissa_shift); + } + exponent += (int32_t)divisor_zeros - (int32_t)mantissa_shift; + } + + tail = ufbxi_bigint_div(&big_quotient, &big_mantissa, &big_divisor); + big_mantissa = big_quotient; + } else if (dec_exponent > 0) { + if (dec_exponent + (int32_t)(big_mantissa.length - 1) * 9 >= 310) return negative ? -UFBX_INFINITY : UFBX_INFINITY; + + exponent += dec_exponent; + ufbxi_bigint_mul_pow5(&big_mantissa, (uint32_t)dec_exponent); + } + + uint64_t mantissa = ufbxi_bigint_extract_high(big_mantissa, &exponent, &tail); + uint64_t sign_bit = (uint64_t)(negative ? 1u : 0u) << enc_sign_shift; + + uint32_t mantissa_shift = 64 - enc_mantissa_bits; + if (exponent > enc_max_exponent) { + return negative ? -UFBX_INFINITY : UFBX_INFINITY; + } else if (exponent <= -enc_max_exponent) { + mantissa_shift += (uint32_t)(-enc_max_exponent + 1 - exponent); + exponent = -enc_max_exponent + 1; + } + + mantissa = ufbxi_shift_right_round(mantissa, mantissa_shift, tail); + if (mantissa == 0) return negative ? -0.0 : 0.0; + + uint64_t bits = mantissa; + bits += (uint64_t)(exponent + enc_max_exponent - 1) << (enc_mantissa_bits - 1); + bits |= sign_bit; + + if (flags & UFBXI_PARSE_DOUBLE_AS_BINARY32) { + uint32_t bits_lo = (uint32_t)bits; + float result; + ufbxi_bit_cast(float, result, uint32_t, bits_lo); + return result; + } else { + double result; + ufbxi_bit_cast(double, result, uint64_t, bits); + return result; + } +} + +static ufbxi_noinline uint32_t ufbxi_parse_double_init_flags(void) +{ + // We require evaluation in double precision, either for doubles (0) or always (1) + // and rounding to nearest, which we can check for with `1 + eps == 1 - eps`. + #if UFBX_FLT_EVAL_METHOD == 0 || UFBX_FLT_EVAL_METHOD == 1 + static volatile double ufbxi_volatile_eps = 2.2250738585072014e-308; + if (1.0 + ufbxi_volatile_eps == 1.0 - ufbxi_volatile_eps) return UFBXI_PARSE_DOUBLE_ALLOW_FAST_PATH; + #endif + + return 0; +} + +static ufbxi_forceinline int64_t ufbxi_parse_int64(const char *str, char **end) +{ + uint64_t abs_val = 0; + bool negative = *str == '-'; + bool positive = *str == '+'; + + size_t init_len = (negative | positive) ? 1 : 0; + size_t len = init_len; + for (; len < 30; len++) { + char c = str[len]; + if (!(c >= '0' && c <= '9')) break; + abs_val = 10 * abs_val + (uint64_t)(c - '0'); + } + if (len == 30 || len == init_len) { + *end = NULL; + return 0; + } + + // TODO: Wrap/clamp? + *end = (char*)str + len; + return negative ? (int64_t)(0 - abs_val) : (int64_t)abs_val; +} + +static ufbxi_noinline uint32_t ufbxi_parse_uint32_radix(const char *str, uint32_t radix) +{ + uint32_t value = 0; + for (const char *p = str; ; p++) { + char c = *p; + if (c >= '0' && c <= '9') { + value = value * radix + (uint32_t)(c - '0'); + } else if (radix == 16 && (c >= 'a' && c <= 'f')) { + value = value * radix + (uint32_t)(c + (10 - 'a')); + } else if (radix == 16 && (c >= 'A' && c <= 'F')) { + value = value * radix + (uint32_t)(c + (10 - 'A')); + } else { + break; + } + } + return value; +} + +// -- DEFLATE implementation + +#if !defined(ufbx_inflate) + +// Lookup data: [0:5] extra bits [5:8] flags [16:32] base value +// Generated by `misc/deflate_lut.py` +static const uint32_t ufbxi_deflate_length_lut[] = { + 0x00000020, 0x00030040, 0x00040040, 0x00050040, 0x00060040, 0x00070040, 0x00080040, 0x00090040, + 0x000a0040, 0x000b0041, 0x000d0041, 0x000f0041, 0x00110041, 0x00130042, 0x00170042, 0x001b0042, + 0x001f0042, 0x00230043, 0x002b0043, 0x00330043, 0x003b0043, 0x00430044, 0x00530044, 0x00630044, + 0x00730044, 0x00830045, 0x00a30045, 0x00c30045, 0x00e30045, 0x01020040, 0x00010020, 0x00010020, +}; +static const uint32_t ufbxi_deflate_dist_lut[] = { + 0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050001, 0x00070001, 0x00090002, 0x000d0002, + 0x00110003, 0x00190003, 0x00210004, 0x00310004, 0x00410005, 0x00610005, 0x00810006, 0x00c10006, + 0x01010007, 0x01810007, 0x02010008, 0x03010008, 0x04010009, 0x06010009, 0x0801000a, 0x0c01000a, + 0x1001000b, 0x1801000b, 0x2001000c, 0x3001000c, 0x4001000d, 0x6001000d, 0x00010020, 0x00010020, +}; + +static const uint8_t ufbxi_deflate_code_length_permutation[] = { + 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15, +}; + +#define UFBXI_INFLATE_FAST_MIN_IN 8 +#define UFBXI_INFLATE_FAST_MIN_OUT 2 + +#define UFBXI_HUFF_MAX_BITS 16 +#define UFBXI_HUFF_MAX_VALUE 288 +#define UFBXI_HUFF_FAST_BITS 10 +#define UFBXI_HUFF_FAST_SIZE (1 << UFBXI_HUFF_FAST_BITS) +#define UFBXI_HUFF_FAST_MASK (UFBXI_HUFF_FAST_SIZE - 1) +#define UFBXI_HUFF_MAX_LONG_BITS 5 +#define UFBXI_HUFF_MAX_LONG_SYMS 380 + +#define UFBXI_HUFF_CODELEN_FAST_BITS 8 +#define UFBXI_HUFF_CODELEN_FAST_MASK ((1<0] Base offset (halved) to `long_sym[]` +// [8:16] code_prefix // [fast_sym, fast=0, extra_mask=0] First 8-bits of the code (reverse of the lookup) +// +// (*1) Not allowing `end` if `fast` serves a dual purpose: It allows us to omit a check for the end symbol in the +// fast path and allows using the symbol as a 64-bit shift amount (x64/ARM64/WASM have native modulo 64 shifts). +// +// Valid bit patterns, all other patterns are forbidden (`sorted_to_sym[]` contains same patterns as `long_sym[]`): +// +// tree b e m f v +// +// lit_length.fast_sym[] N 0 0 1 L // Short N bit code (no extra allowed) for literal byte L +// lit_length.fast_sym[] N 0 1 1 I // Short N bit code (huff+extra bits) for length index I +// lit_length.fast_sym[] M 0 0 0 X // Long code at `lit_length.long_sym[X*2 + ((bits>>FAST_BITS) & M)]` +// lit_length.fast_sym[] 0 0 0 0 R // Extra long code with prefix R, use `lit_length.sorted_to_sym[]` to resolve (*1) +// lit_length.fast_sym[] N 1 0 0 0 // Short N bit code for end-of-block (256) symbol +// lit_length.fast_sym[] 0 1 0 0 1 // Invalid lit_length code +// +// lit_length.long_sym[] N 0 0 0 L // Long N bit code (no extra allowed) for literal byte L +// lit_length.long_sym[] N 0 1 0 I // Long N bit code (huff+extra bits) for length index I +// lit_length.long_sym[] N 1 0 0 0 // Long N bit code for end-of-block (256) symbol +// lit_length.long_sym[] 0 1 0 0 1 // Invalid lit_length code +// +// dist.fast_sym[] N 0 0 1 L // Short N bit code (huff+extra bits) for distance index I +// dist.fast_sym[] M 0 0 0 X // Long code at `dist.long_sym[X*2 + ((bits>>FAST_BITS) & M)]` +// dist.fast_sym[] 0 0 0 0 R // Extra long code with prefix R, use `dist.sorted_to_sym[]` to resolve (*1) +// dist.fast_sym[] N 1 0 0 1 // Unused symbol 30-31 or invalid distance code +// +// dist.long_sym[] N 0 0 0 I // Long N bit code (huff+extra bits) for distance index I +// dist.long_sym[] N 1 0 0 1 // Unused symbol 30-31 or invalid distance code +// +// code_length.fast_sym[] N 0 0 1 B // Short N bit code (huff only, extra handled explicitly) for symbol bit count B +// code_length.fast_sym[] M 0 0 0 X // Long code at `dist.long_sym[X*2 + ((bits>>FAST_BITS) & M)]` +// code_length.fast_sym[] 0 0 0 0 R // Extra long code with prefix R, use `code_length.sorted_to_sym[]` to resolve (*1) +// +// code_length.long_sym[] N 0 0 0 B // Long N bit code (huff only, extra handled explicitly) for symbol bit count B +// +// (*1) Never necessary if `fast_bits >= 10` due to `long_sym[]` covering all possible codes, +// +typedef uint16_t ufbxi_huff_sym; + +#define ufbxi_huff_sym_total_bits(sym) ((uint32_t)(sym) & 0x1f) +#define ufbxi_huff_sym_long_mask(sym) ((uint32_t)(sym) & 0x1f) +#define ufbxi_huff_sym_long_offset(sym) ((uint32_t)(sym) >> 7u) +#define ufbxi_huff_sym_value(sym) ((uint32_t)(sym) >> 8u) + +enum { + UFBXI_HUFF_SYM_END = 0x20, + UFBXI_HUFF_SYM_MATCH = 0x40, + UFBXI_HUFF_SYM_FAST = 0x80, +}; + +#define UFBXI_HUFF_ERROR_SYM ((ufbxi_huff_sym)0x0120) // Error symbol, END (value 1) +#define UFBXI_HUFF_UNINITIALIZED_SYM ((ufbxi_huff_sym)0x0220) // Uninitialized symbol for regression, END (value 2) + +typedef struct { + ufbxi_huff_sym fast_sym[UFBXI_HUFF_FAST_SIZE]; // < Lookup from N bytes to symbol information + ufbxi_huff_sym long_sym[UFBXI_HUFF_MAX_LONG_SYMS]; // < Fast long symbol lookup + ufbxi_huff_sym sorted_to_sym[UFBXI_HUFF_MAX_VALUE]; // < Symbol information per sorted index + + uint32_t extra_shift_base[UFBXI_HUFF_MAX_EXTRA_SYMS]; // < [0:6] shift [16:32] base value + uint16_t extra_mask[UFBXI_HUFF_MAX_EXTRA_SYMS]; // < Mask for extra bits + + uint16_t past_max_code[UFBXI_HUFF_MAX_BITS]; // < One past maximum code value per bit length + int16_t code_to_sorted[UFBXI_HUFF_MAX_BITS]; // < Code to sorted symbol index per bit length + uint32_t num_symbols; + + uint32_t end_of_block_bits; +} ufbxi_huff_tree; + +typedef struct { + union { + struct { + ufbxi_huff_tree lit_length; + ufbxi_huff_tree dist; + }; + ufbxi_huff_tree trees[2]; + }; + uint32_t fast_bits; +} ufbxi_trees; + +typedef struct { + bool initialized; + ufbxi_trees static_trees; +} ufbxi_inflate_retain_imp; + +ufbx_static_assert(inflate_retain_size, sizeof(ufbxi_inflate_retain_imp) <= sizeof(ufbx_inflate_retain)); + +typedef struct { + ufbxi_bit_stream stream; + uint32_t fast_bits; + + char *out_begin; + char *out_ptr; + char *out_end; +} ufbxi_deflate_context; + +static ufbxi_forceinline uint32_t +ufbxi_bit_reverse(uint32_t mask, uint32_t num_bits) +{ + ufbxi_dev_assert(num_bits <= 16); + uint32_t x = mask; + x = (((x & 0xaaaa) >> 1) | ((x & 0x5555) << 1)); + x = (((x & 0xcccc) >> 2) | ((x & 0x3333) << 2)); + x = (((x & 0xf0f0) >> 4) | ((x & 0x0f0f) << 4)); + x = (((x & 0xff00) >> 8) | ((x & 0x00ff) << 8)); + return x >> (16 - num_bits); +} + +static ufbxi_noinline const char * +ufbxi_bit_chunk_refill(ufbxi_bit_stream *s, const char *ptr) +{ + // Copy any left-over data to the beginning of `buffer` + size_t left = ufbxi_to_size(s->chunk_real_end - ptr); + ufbxi_dev_assert(left < 64); + if (left > 0) memmove(s->buffer, ptr, left); + + s->num_read_before_chunk += ufbxi_to_size(ptr - s->chunk_begin); + + // Read more user data if the user supplied a `read_fn()`, otherwise + // we assume the initial data chunk is the whole input buffer. + if (s->read_fn && !s->cancelled) { + size_t to_read = ufbxi_min_sz(s->input_left, s->buffer_size - left); + if (to_read > 0) { + size_t num_read = s->read_fn(s->read_user, s->buffer + left, to_read); + // TODO: IO error, should unify with (currently broken) cancel logic + if (num_read > to_read) num_read = 0; + ufbxi_dev_assert(s->input_left >= num_read); + s->input_left -= num_read; + left += num_read; + } + } + + // Pad the rest with zeros + if (left < 64) { + memset(s->buffer + left, 0, 64 - left); + left = 64; + } + + s->chunk_begin = s->buffer; + s->chunk_ptr = s->buffer; + s->chunk_end = s->buffer + left - 8; + s->chunk_real_end = s->buffer + left; + return s->buffer; +} + +static ufbxi_noinline void ufbxi_bit_stream_init(ufbxi_bit_stream *s, const ufbx_inflate_input *input) +{ + size_t data_size = input->data_size; + if (data_size > input->total_size) { + data_size = input->total_size; + } + + s->read_fn = input->read_fn; + s->read_user = input->read_user; + s->progress_cb = input->progress_cb; + s->chunk_begin = (const char*)input->data; + s->chunk_ptr = (const char*)input->data; + s->chunk_end = ufbxi_add_ptr((const char*)input->data, ufbxi_max_sz(8, data_size) - 8); + s->chunk_real_end = ufbxi_add_ptr((const char*)input->data, data_size); + s->input_left = input->total_size - data_size; + + // Use the user buffer if it's large enough, otherwise `local_buffer` + if (input->buffer_size > sizeof(s->local_buffer)) { + s->buffer = (char*)input->buffer; + s->buffer_size = input->buffer_size; + } else { + s->buffer = s->local_buffer; + s->buffer_size = sizeof(s->local_buffer); + } + s->num_read_before_chunk = 0; + s->progress_bias = input->progress_size_before; + s->progress_total = input->total_size + input->progress_size_before + input->progress_size_after; + if (!s->progress_cb.fn || input->progress_interval_hint >= SIZE_MAX) { + s->progress_interval = SIZE_MAX; + } else if (input->progress_interval_hint > 0) { + s->progress_interval = (size_t)input->progress_interval_hint; + } else { + s->progress_interval = 0x4000; + } + s->cancelled = false; + + // Clear the initial bit buffer + s->bits = 0; + s->left = 0; + + // If the initial data buffer is not large enough to be read directly + // from refill the chunk once. + if (data_size < 64) { + ufbxi_bit_chunk_refill(s, s->chunk_begin); + } + + if (s->progress_cb.fn && ufbxi_to_size(s->chunk_end - s->chunk_ptr) > s->progress_interval + 8) { + s->chunk_yield = s->chunk_ptr + s->progress_interval; + } else { + s->chunk_yield = s->chunk_end; + } +} + +static ufbxi_noinline const char * +ufbxi_bit_yield(ufbxi_bit_stream *s, const char *ptr) +{ + if (ptr > s->chunk_end) { + ptr = ufbxi_bit_chunk_refill(s, ptr); + } + + if (s->progress_cb.fn) { + size_t num_read = s->num_read_before_chunk + ufbxi_to_size(ptr - s->chunk_begin); + + ufbx_progress progress = { s->progress_bias + num_read, s->progress_total }; + uint32_t result = (uint32_t)s->progress_cb.fn(s->progress_cb.user, &progress); + ufbx_assert(result == UFBX_PROGRESS_CONTINUE || result == UFBX_PROGRESS_CANCEL); + if (result == UFBX_PROGRESS_CANCEL) { + s->cancelled = true; + ptr = s->local_buffer; + s->buffer = s->local_buffer; + s->buffer_size = sizeof(s->local_buffer); + s->chunk_begin = ptr; + s->chunk_ptr = ptr; + s->chunk_end = ptr + sizeof(s->local_buffer) - 8; + s->chunk_real_end = ptr + sizeof(s->local_buffer); + memset(s->local_buffer, 0, sizeof(s->local_buffer)); + } + } + + if (s->progress_cb.fn && ufbxi_to_size(s->chunk_end - ptr) > s->progress_interval + 8) { + s->chunk_yield = ptr + s->progress_interval; + } else { + s->chunk_yield = s->chunk_end; + } + + return ptr; +} + +static ufbxi_forceinline void +ufbxi_bit_refill(uint64_t *p_bits, size_t *p_left, const char **p_data, ufbxi_bit_stream *s) +{ + if (*p_data > s->chunk_yield) { + *p_data = ufbxi_bit_yield(s, *p_data); + if (s->cancelled) { + // Force an end-of-block symbol when cancelled so we don't need an + // extra branch in the chunk decoding loop. + *p_bits = s->cancel_bits; + } + } + + // See https://fgiesen.wordpress.com/2018/02/20/reading-bits-in-far-too-many-ways-part-2/ + // variant 4. This branchless refill guarantees [56,63] bits to be valid in `*p_bits`. + ufbxi_regression_assert(*p_left <= 64); \ + *p_bits |= ufbxi_read_u64(*p_data) << *p_left; + *p_data += (63 - *p_left) >> 3; + *p_left |= 56; +} + +// See `ufbxi_bit_refill()` +#define ufbxi_macro_bit_refill_fast(m_bits, m_left, m_data, m_refill_bits) do { \ + ufbxi_regression_assert(m_left <= 64); \ + m_bits |= m_refill_bits << m_left; \ + m_data += (63 - m_left) >> 3; \ + m_left |= 56; \ + } while (0) + +static ufbxi_noinline int +ufbxi_bit_copy_bytes(void *dst, ufbxi_bit_stream *s, size_t len) +{ + ufbx_assert(s->left % 8 == 0); + char *ptr = (char*)dst; + + // Copy the buffered bits first + while (len > 0 && s->left > 0) { + *ptr++ = (char)(uint8_t)s->bits; + len -= 1; + s->bits >>= 8; + s->left -= 8; + } + + // Copied fully from buffer + if (len == 0) { + return 1; + } + + // We need to clear the top bits as there may be data + // read ahead past `s->left` in some cases + s->bits = 0; + + // Copy the current chunk + size_t chunk_left = ufbxi_to_size(s->chunk_real_end - s->chunk_ptr); + if (chunk_left >= len) { + memcpy(ptr, s->chunk_ptr, len); + s->chunk_ptr += len; + return 1; + } else { + memcpy(ptr, s->chunk_ptr, chunk_left); + s->chunk_ptr += chunk_left; + ptr += chunk_left; + len -= chunk_left; + } + + // Read extra bytes from user + if (len > s->input_left) return 0; + size_t num_read = 0; + if (s->read_fn) { + num_read = s->read_fn(s->read_user, ptr, len); + s->input_left -= num_read; + } + return num_read == len; +} + +// 0: Success +// -1: Overfull +// -2: Underfull +static ufbxi_noinline ptrdiff_t +ufbxi_huff_build_imp(ufbxi_huff_tree *tree, uint8_t *sym_bits, uint32_t sym_count, const uint32_t *sym_extra, uint32_t sym_extra_offset, uint32_t fast_bits, uint32_t *bits_counts) +{ + uint32_t fast_mask = (1u << fast_bits) - 1; + + ufbx_assert(sym_count <= UFBXI_HUFF_MAX_VALUE); + tree->num_symbols = sym_count; + + uint32_t nonzero_sym_count = sym_count - bits_counts[0]; + + uint32_t total_syms[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit + uint32_t first_code[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit + + tree->code_to_sorted[0] = INT16_MAX; + tree->past_max_code[0] = 0; + total_syms[0] = 0; + + // Clear to uninitialized symbols + #if defined(UFBX_REGRESSION) + { + for (size_t i = 0; i < UFBXI_HUFF_FAST_SIZE; i++) { + tree->fast_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM; + } + for (size_t i = 0; i < UFBXI_HUFF_MAX_VALUE; i++) { + tree->sorted_to_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM; + } + for (size_t i = 0; i < UFBXI_HUFF_MAX_LONG_SYMS; i++) { + tree->long_sym[i] = UFBXI_HUFF_UNINITIALIZED_SYM; + } + } + #endif + + uint32_t last_valid_prefix = 0; + + // Resolve the maximum code per bit length and ensure that the tree is not + // overfull or underfull. + { + int num_codes_left = 1; + uint32_t code = 0; + uint32_t prev_count = 0; + uint32_t long_offset = 0; + for (uint32_t bits = 1; bits < UFBXI_HUFF_MAX_BITS; bits++) { + uint32_t count = bits_counts[bits]; + code = (code + prev_count) << 1; + first_code[bits] = code; + tree->past_max_code[bits] = (uint16_t)(code + count); + + uint32_t prev_syms = total_syms[bits - 1]; + total_syms[bits] = prev_syms + count; + + // Each bit level doubles the amount of codes and potentially removes some + num_codes_left = (num_codes_left << 1) - (int32_t)count; + if (num_codes_left < 0) { + return -1; + } + + if (count > 0 && bits > fast_bits && bits - fast_bits <= UFBXI_HUFF_MAX_LONG_BITS) { + uint32_t shift = bits - fast_bits; + uint32_t last_inclusive = num_codes_left == 0 ? (1u<> shift; + uint32_t last_prefix = (code + count + last_inclusive) >> shift; + uint32_t mask = (1u << shift) - 1u; + uint32_t half_step = 1u << (shift - 1u); + for (uint32_t prefix = first_prefix; prefix < last_prefix; prefix++) { + uint32_t rev_prefix = ufbxi_bit_reverse(prefix, fast_bits); + tree->fast_sym[rev_prefix] = (ufbxi_huff_sym)(mask | (long_offset << 8)); + long_offset += half_step; + } + + last_valid_prefix = last_prefix; + } + + if (count > 0) { + tree->code_to_sorted[bits] = (int16_t)((int)prev_syms - (int)code); + } else { + tree->code_to_sorted[bits] = INT16_MAX; + } + prev_count = count; + } + + // All codes should be used if there's more than one symbol, if there's only one symbol there should be + // only a single 1-bit code. + if (nonzero_sym_count > 1 && num_codes_left != 0) { + return -2; + } else if (nonzero_sym_count == 1 && total_syms[1] != 1) { + return -2; + } + + // We should always have enough space for long symbols as we support up to 5 (UFBXI_HUFF_MAX_LONG_BITS) + // bits and the largest tree has 286 symbols. For each bit we may waste at most 2^bits slots (conservative) + // and in the end we may waste 2^5 slots giving us `286+2+4+8+16+32+32 = 380` (UFBXI_HUFF_MAX_LONG_SYMS) + ufbx_assert(long_offset <= UFBXI_HUFF_MAX_LONG_SYMS); + } + + tree->end_of_block_bits = 0; + uint32_t num_extra = 0; + tree->extra_shift_base[0] = 0; + tree->extra_mask[0] = 0; + + // Fill `fast_sym[]` with error symbols if necessary, we don't need to do this if we have two or more symbols + // as the tree is guaranteed to be full, which means we will populate the whole `fast_sym[]` + if (nonzero_sym_count <= 1) { + for (uint32_t i = 0; i <= fast_mask; i++) { + tree->fast_sym[i] = UFBXI_HUFF_ERROR_SYM; + } + } + + // Generate per-length sorted-to-symbol and fast lookup tables + uint32_t bits_index[UFBXI_HUFF_MAX_BITS] = { 0 }; + for (uint32_t i = 0; i < sym_count; i++) { + uint32_t bits = sym_bits[i]; + if (bits == 0) continue; + + uint32_t sym = i << 8 | bits; + if (i >= sym_extra_offset) { + uint32_t extra = sym_extra[i - sym_extra_offset]; + sym += extra; + + // Store length/distance codes with extra values in a table. + // TODO: This is unnecessary for small values + if ((extra & 0xffff001f) != 0 && (extra & 0x20) == 0) { + uint32_t ix = ++num_extra; + tree->extra_shift_base[ix] = (extra & 0xffff0000) | bits; + tree->extra_mask[ix] = (uint16_t)((1u << (extra & 0x1f)) - 1); + sym = (sym & 0xff) | ix << 8; + } + + } + + uint32_t index = bits_index[bits]++; + uint32_t sorted = total_syms[bits - 1] + index; + tree->sorted_to_sym[sorted] = (ufbxi_huff_sym)sym; + + // Reverse the code and fill all fast lookups with the reversed prefix + uint32_t code = first_code[bits] + index; + uint32_t rev_code = ufbxi_bit_reverse(code, bits); + + if (bits <= fast_bits) { + uint32_t fast_sym = sym; + // The `end` and `fast` flags are mutually exclusive + if ((fast_sym & UFBXI_HUFF_SYM_END) == 0) { + fast_sym |= UFBXI_HUFF_SYM_FAST; + } + uint32_t hi_max = 1u << (fast_bits - bits); + for (uint32_t hi = 0; hi < hi_max; hi++) { + ufbxi_regression_assert(nonzero_sym_count <= 1 || tree->fast_sym[rev_code | hi << bits] == UFBXI_HUFF_UNINITIALIZED_SYM); + tree->fast_sym[rev_code | hi << bits] = (ufbxi_huff_sym)fast_sym; + } + } else if (bits <= fast_bits + UFBXI_HUFF_MAX_LONG_BITS && (code >> (bits - fast_bits)) < last_valid_prefix) { + uint32_t fast_sym = tree->fast_sym[rev_code & fast_mask]; + ufbxi_regression_assert(fast_sym != UFBXI_HUFF_UNINITIALIZED_SYM); + uint32_t long_bits = 0; + + uint32_t long_mask = fast_sym; + while (long_bits < UFBXI_HUFF_MAX_LONG_BITS && (long_mask & 1) != 0) { + long_mask >>= 1; + long_bits += 1; + } + ufbxi_dev_assert(long_bits >= 1); + + uint32_t long_base = fast_sym >> 7u; // aka (fast_sym >> 8) * 2 + uint32_t lo_bits = bits - fast_bits; + uint32_t hi_max = 1u << (long_bits - lo_bits); + uint32_t rev_suffix = rev_code >> fast_bits; + for (uint32_t hi = 0; hi < hi_max; hi++) { + ufbxi_regression_assert(tree->long_sym[long_base + (rev_suffix | hi << lo_bits)] == UFBXI_HUFF_UNINITIALIZED_SYM); + tree->long_sym[long_base + (rev_suffix | hi << lo_bits)] = (ufbxi_huff_sym)sym; + } + } else { + uint32_t fast_sym = (code >> (bits - fast_bits)) << 8; + ufbxi_regression_assert( + tree->fast_sym[rev_code & fast_mask] == UFBXI_HUFF_UNINITIALIZED_SYM || + tree->fast_sym[rev_code & fast_mask] == (ufbxi_huff_sym)fast_sym); + tree->fast_sym[rev_code & fast_mask] = (ufbxi_huff_sym)fast_sym; + } + + // Make sure the end-of-block symbol goes through the slow path + // Also store the end-of-block code so we can interrupt decoding + if (i == 256) { + tree->end_of_block_bits = rev_code; + } + } + + // Make sure all `fast_sym[]` are filled with an initialized value. + #if defined(UFBX_REGRESSION) + { + for (size_t i = 0; i < UFBXI_HUFF_FAST_SIZE; i++) { + if (i <= fast_mask) { + ufbx_assert(tree->fast_sym[i] != UFBXI_HUFF_UNINITIALIZED_SYM); + } else { + ufbx_assert(tree->fast_sym[i] == UFBXI_HUFF_UNINITIALIZED_SYM); + } + } + for (size_t i = 0; i < nonzero_sym_count; i++) { + ufbx_assert(tree->sorted_to_sym[i] != UFBXI_HUFF_UNINITIALIZED_SYM); + } + } + #endif + + return 0; +} + +// 0: Success +// -1: Overfull +// -2: Underfull +static ufbxi_noinline ptrdiff_t +ufbxi_huff_build(ufbxi_huff_tree *tree, uint8_t *sym_bits, uint32_t sym_count, const uint32_t *sym_extra, uint32_t sym_extra_offset, uint32_t fast_bits) +{ + // Count the number of codes per bit length + // `bits_counts[0]` contains the number of non-used symbols + uint32_t bits_counts[UFBXI_HUFF_MAX_BITS]; // ufbxi_uninit + memset(bits_counts, 0, sizeof(bits_counts)); + for (uint32_t i = 0; i < sym_count; i++) { + uint32_t bits = sym_bits[i]; + ufbx_assert(bits < UFBXI_HUFF_MAX_BITS); + bits_counts[bits]++; + } + + return ufbxi_huff_build_imp(tree, sym_bits, sym_count, sym_extra, sym_extra_offset, fast_bits, bits_counts); +} + +static ufbxi_forceinline ufbxi_huff_sym +ufbxi_huff_decode_bits(const ufbxi_huff_tree *tree, uint64_t bits, uint32_t fast_bits, uint32_t fast_mask) +{ + ufbxi_huff_sym sym = tree->fast_sym[bits & fast_mask]; + ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM); + + if ((sym & (UFBXI_HUFF_SYM_FAST|UFBXI_HUFF_SYM_END)) != 0) { + return sym; + } + + uint32_t tail = (uint32_t)(bits >> fast_bits); + uint32_t long_mask = ufbxi_huff_sym_long_mask(sym); + if (long_mask) { + sym = tree->long_sym[ufbxi_huff_sym_long_offset(sym) + (tail & long_mask)]; + ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM); + return sym; + } + + ufbxi_dev_assert(fast_bits <= 8); + + uint32_t code = ufbxi_huff_sym_value(sym); + uint32_t num_bits = fast_bits; + for (;;) { + code = code << 1 | (tail & 1); + tail >>= 1; + num_bits++; + + ufbxi_regression_assert(num_bits < UFBXI_HUFF_MAX_BITS); + if (code < tree->past_max_code[num_bits]) { + sym = tree->sorted_to_sym[(int32_t)code + (int32_t)tree->code_to_sorted[num_bits]]; + ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM); + return sym; + } + } +} + +static ufbxi_noinline void ufbxi_init_static_huff(ufbxi_trees *trees, const ufbx_inflate_input *input) +{ + ptrdiff_t err = 0; + + // Override `fast_bits` if necessary, this must always be valid as it's checked in the beginning of `ufbx_inflate()`. + if (input && input->internal_fast_bits != 0) { + trees->fast_bits = (uint32_t)input->internal_fast_bits; + ufbx_assert(!(trees->fast_bits < 1 || trees->fast_bits == 9 || trees->fast_bits > 10)); + } else { + trees->fast_bits = UFBXI_HUFF_FAST_BITS; + } + + // 0-143: 8 bits, 144-255: 9 bits, 256-279: 7 bits, 280-287: 8 bits + uint8_t lit_length_bits[288]; // ufbxi_uninit + memset(lit_length_bits + 0, 8, 144 - 0); + memset(lit_length_bits + 144, 9, 256 - 144); + memset(lit_length_bits + 256, 7, 280 - 256); + memset(lit_length_bits + 280, 8, 288 - 280); + err |= ufbxi_huff_build(&trees->lit_length, lit_length_bits, sizeof(lit_length_bits), ufbxi_deflate_length_lut, 256, trees->fast_bits); + + // "Distance codes 0-31 are represented by (fixed-length) 5-bit codes" + uint8_t dist_bits[32]; // ufbxi_uninit + memset(dist_bits + 0, 5, 32 - 0); + err |= ufbxi_huff_build(&trees->dist, dist_bits, sizeof(dist_bits), ufbxi_deflate_dist_lut, 0, trees->fast_bits); + + // Building the static trees cannot fail as we use pre-defined code lengths. + ufbxi_ignore(err); + ufbx_assert(err == 0); +} + +static ufbxi_noinline ptrdiff_t ufbxi_decode_dynamic_huff_bits(ufbxi_deflate_context *dc, const ufbxi_huff_tree *huff_code_length, uint8_t *code_lengths, uint32_t num_symbols) +{ + uint64_t bits = dc->stream.bits; + size_t left = dc->stream.left; + const char *data = dc->stream.chunk_ptr; + + uint32_t symbol_index = 0; + uint8_t prev = 0; + while (symbol_index < num_symbols) { + ufbxi_bit_refill(&bits, &left, &data, &dc->stream); + if (dc->stream.cancelled) return -28; + + ufbxi_huff_sym sym = ufbxi_huff_decode_bits(huff_code_length, bits, UFBXI_HUFF_CODELEN_FAST_BITS, UFBXI_HUFF_CODELEN_FAST_MASK); + ufbxi_regression_assert(sym != UFBXI_HUFF_UNINITIALIZED_SYM); + if (sym == UFBXI_HUFF_ERROR_SYM) return -21; + + uint32_t inst = ufbxi_huff_sym_value(sym); + uint32_t sym_len = ufbxi_huff_sym_total_bits(sym); + + bits >>= sym_len; + left -= sym_len; + + if (inst <= 15) { + // "0 - 15: Represent code lengths of 0 - 15" + prev = (uint8_t)inst; + code_lengths[symbol_index++] = (uint8_t)inst; + } else if (inst == 16) { + // "16: Copy the previous code length 3 - 6 times. The next 2 bits indicate repeat length." + uint32_t num = 3 + ((uint32_t)bits & 0x3); + bits >>= 2; + left -= 2; + if (symbol_index + num > num_symbols) return -18; + memset(code_lengths + symbol_index, prev, num); + symbol_index += num; + } else if (inst == 17) { + // "17: Repeat a code length of 0 for 3 - 10 times. (3 bits of length)" + uint32_t num = 3 + ((uint32_t)bits & 0x7); + bits >>= 3; + left -= 3; + if (symbol_index + num > num_symbols) return -19; + memset(code_lengths + symbol_index, 0, num); + symbol_index += num; + prev = 0; + } else if (inst == 18) { + // "18: Repeat a code length of 0 for 11 - 138 times (7 bits of length)" + uint32_t num = 11 + ((uint32_t)bits & 0x7f); + bits >>= 7; + left -= 7; + if (symbol_index + num > num_symbols) return -20; + memset(code_lengths + symbol_index, 0, num); + symbol_index += num; + prev = 0; + } else { + return -6; + } + } + + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + + return 0; +} + +static ufbxi_noinline ptrdiff_t +ufbxi_init_dynamic_huff(ufbxi_deflate_context *dc, ufbxi_trees *trees) +{ + uint64_t bits = dc->stream.bits; + size_t left = dc->stream.left; + const char *data = dc->stream.chunk_ptr; + ufbxi_bit_refill(&bits, &left, &data, &dc->stream); + if (dc->stream.cancelled) return -28; + + trees->fast_bits = dc->fast_bits; + + // The header contains the number of Huffman codes in each of the three trees. + uint32_t num_lit_lengths = 257 + (uint32_t)(bits & 0x1f); + uint32_t num_dists = 1 + (uint32_t)(bits >> 5 & 0x1f); + uint32_t num_code_lengths = 4 + (uint32_t)(bits >> 10 & 0xf); + bits >>= 14; + left -= 14; + + uint8_t code_lengths[UFBXI_HUFF_MAX_COMBINED_SYMS]; // ufbxi_uninit + + // Code lengths for the "code length" Huffman tree are represented literally + // 3 bits in order of: 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 up to + // `num_code_lengths`, rest of the code lengths are 0 (unused) + memset(code_lengths, 0, UFBXI_HUFF_CODELEN_SYMS); + + for (size_t len_i = 0; len_i < num_code_lengths; len_i++) { + if (len_i == 14) { + ufbxi_bit_refill(&bits, &left, &data, &dc->stream); + if (dc->stream.cancelled) return -28; + } + code_lengths[ufbxi_deflate_code_length_permutation[len_i]] = (uint32_t)bits & 0x7; + bits >>= 3; + left -= 3; + } + + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + + ufbxi_huff_tree huff_code_length; // ufbxi_uninit + ptrdiff_t err; // ufbxi_uninit + + // Build the temporary "code length" Huffman tree used to encode the actual + // trees used to compress the data. Use that to build the literal/length and + // distance trees. + err = ufbxi_huff_build(&huff_code_length, code_lengths, UFBXI_HUFF_CODELEN_SYMS, NULL, INT32_MAX, UFBXI_HUFF_CODELEN_FAST_BITS); + if (err) return -14 + 1 + err; + + err = ufbxi_decode_dynamic_huff_bits(dc, &huff_code_length, code_lengths, num_lit_lengths + num_dists); + if (err) return err; + + err = ufbxi_huff_build(&trees->lit_length, code_lengths, num_lit_lengths, ufbxi_deflate_length_lut, 256, dc->fast_bits); + if (err) return err == -7 ? -28 : -16 + 1 + err; + + err = ufbxi_huff_build(&trees->dist, code_lengths + num_lit_lengths, num_dists, ufbxi_deflate_dist_lut, 0, dc->fast_bits); + if (err) return err == -7 ? -28 : -22 + 1 + err; + + return 0; +} + +static ufbxi_noinline uint32_t ufbxi_adler32(const void *data, size_t size) +{ + ufbxi_fast_uint a = 1, b = 0; + const char *p = (const char*)data; + + // Adler-32 consists of two running sums modulo 65521. As an optimization + // we can accumulate N sums before applying the modulo, where N depends on + // the size of the type holding the sum. + const ufbxi_fast_uint num_before_wrap = sizeof(ufbxi_fast_uint) == 8 ? 380368439u : 5552u; + + ufbxi_fast_uint size_left = size; + while (size_left > 0) { + ufbxi_fast_uint num = size_left <= num_before_wrap ? size_left : num_before_wrap; + size_left -= num; + const char *end = p + num; + + // Align to 16 bytes + while (p != end && !ufbxi_is_aligned(p, 16)) { + a += (ufbxi_fast_uint)(uint8_t)p[0]; b += a; + p++; + } + +#if UFBXI_HAS_SSE + static const uint16_t factors[2][8] = { + { 16, 15, 14, 13, 12, 11, 10, 9, }, + { 8, 7, 6, 5, 4, 3, 2, 1, }, + }; + + const __m128i zero = _mm_setzero_si128(); + const __m128i factor_1 = _mm_set1_epi16(1); + const __m128i factor_16 = _mm_set1_epi16(16); + const __m128i factor_lo = _mm_loadu_si128((const __m128i*)factors[0]); + const __m128i factor_hi = _mm_loadu_si128((const __m128i*)factors[1]); + + for (;;) { + size_t chunk_size = ufbxi_min_sz(ufbxi_to_size(end - p), 5803) & ~(size_t)0xff; + if (chunk_size == 0) break; + const char *chunk_end = p + chunk_size; + + __m128i s1 = zero; + __m128i s2 = zero; + + while (p != chunk_end) { + __m128i s1_lo = zero, s1_hi = zero; + __m128i tmp_lo = zero, tmp_hi = zero; + + ufbxi_nounroll for (size_t i = 0; i < 256; i += 32) { + __m128i d0 = _mm_load_si128((const __m128i*)(p + i + 0)); + __m128i d1 = _mm_load_si128((const __m128i*)(p + i + 16)); + + tmp_lo = _mm_add_epi16(tmp_lo, s1_lo); + tmp_hi = _mm_add_epi16(tmp_hi, s1_hi); + s1_lo = _mm_add_epi16(s1_lo, _mm_unpacklo_epi8(d0, zero)); + s1_hi = _mm_add_epi16(s1_hi, _mm_unpackhi_epi8(d0, zero)); + + tmp_lo = _mm_add_epi16(tmp_lo, s1_lo); + tmp_hi = _mm_add_epi16(tmp_hi, s1_hi); + s1_lo = _mm_add_epi16(s1_lo, _mm_unpacklo_epi8(d1, zero)); + s1_hi = _mm_add_epi16(s1_hi, _mm_unpackhi_epi8(d1, zero)); + } + + s2 = _mm_add_epi32(s2, _mm_slli_epi32(s1, 8)); + s1 = _mm_add_epi32(s1, _mm_madd_epi16(s1_lo, factor_1)); + s1 = _mm_add_epi32(s1, _mm_madd_epi16(s1_hi, factor_1)); + + s2 = _mm_add_epi32(s2, _mm_madd_epi16(tmp_lo, factor_16)); + s2 = _mm_add_epi32(s2, _mm_madd_epi16(tmp_hi, factor_16)); + s2 = _mm_add_epi32(s2, _mm_madd_epi16(s1_lo, factor_lo)); + s2 = _mm_add_epi32(s2, _mm_madd_epi16(s1_hi, factor_hi)); + + p += 256; + } + + s1 = _mm_add_epi32(s1, _mm_shuffle_epi32(s1, _MM_SHUFFLE(2,3,0,1))); + s2 = _mm_add_epi32(s2, _mm_shuffle_epi32(s2, _MM_SHUFFLE(2,3,0,1))); + s1 = _mm_add_epi32(s1, _mm_shuffle_epi32(s1, _MM_SHUFFLE(1,0,3,2))); + s2 = _mm_add_epi32(s2, _mm_shuffle_epi32(s2, _MM_SHUFFLE(1,0,3,2))); + + b += chunk_size * a; + a += (uint32_t)_mm_cvtsi128_si32(s1); + b += (uint32_t)_mm_cvtsi128_si32(s2); + } +#elif UFBX_LITTLE_ENDIAN + for (;;) { + size_t chunk_size = ufbxi_min_sz(ufbxi_to_size(end - p), 256*8/4) & ~(size_t)0xf; + if (chunk_size == 0) break; + const char *chunk_end = p + chunk_size; + + uint64_t s1_lo = 0, s1_hi = 0; + uint64_t tmp, s2 = 0; + uint64_t mask8 = UINT64_C(0x00ff00ff00ff00ff); + uint64_t mask16 = UINT64_C(0x0000ffff0000ffff); + + while (p != chunk_end) { + uint64_t d0 = *(const uint64_t*)p; + uint64_t d1 = *(const uint64_t*)(p + 8); + + tmp = s1_lo + s1_hi; + s1_lo += d0 & mask8; + s1_hi += (d0 >> 8) & mask8; + + tmp += s1_lo + s1_hi; + s1_lo += d1 & mask8; + s1_hi += (d1 >> 8) & mask8; + + s2 += (tmp & mask16) + ((tmp >> 16) & mask16); + p += 16; + } + + uint64_t s1 = s1_lo + s1_hi; + s1 = (s1 & mask16) + ((s1 >> 16u) & mask16); + ufbxi_fast_uint s1_sum = (ufbxi_fast_uint)(s1 + (s1 >> 32u)); + + ufbxi_fast_uint s2_sum = (ufbxi_fast_uint)(s2 + (s2 >> 32u)) * 8; + s2_sum += ((ufbxi_fast_uint)(s1_lo >> 0) & 0xffff) * 8; + s2_sum += ((ufbxi_fast_uint)(s1_hi >> 0) & 0xffff) * 7; + s2_sum += ((ufbxi_fast_uint)(s1_lo >> 16) & 0xffff) * 6; + s2_sum += ((ufbxi_fast_uint)(s1_hi >> 16) & 0xffff) * 5; + s2_sum += ((ufbxi_fast_uint)(s1_lo >> 32) & 0xffff) * 4; + s2_sum += ((ufbxi_fast_uint)(s1_hi >> 32) & 0xffff) * 3; + s2_sum += ((ufbxi_fast_uint)(s1_lo >> 48) & 0xffff) * 2; + s2_sum += ((ufbxi_fast_uint)(s1_hi >> 48) & 0xffff) * 1; + + b += chunk_size * a; + a += s1_sum & 0xffffffffu; + b += s2_sum & 0xffffffffu; + } +#endif + + while (p != end) { + a += (size_t)(uint8_t)p[0]; b += a; + p++; + } + + a %= 65521u; + b %= 65521u; + } + + return (uint32_t)((b << 16) | (a & 0xffff)); +} + +static ufbxi_noinline int +ufbxi_inflate_block_slow(ufbxi_deflate_context *dc, ufbxi_trees *trees, size_t max_symbols) +{ + char *out_ptr = dc->out_ptr; + char *const out_begin = dc->out_begin; + char *const out_end = dc->out_end; + + uint32_t fast_bits = trees->fast_bits; + uint32_t fast_mask = (1u << fast_bits) - 1; + + uint64_t bits = dc->stream.bits; + size_t left = dc->stream.left; + const char *data = dc->stream.chunk_ptr; + + for (;;) { + if (max_symbols-- == 0) break; + + ufbxi_bit_refill(&bits, &left, &data, &dc->stream); + uint64_t sym_bits = bits; + + ufbxi_huff_sym sym0 = ufbxi_huff_decode_bits(&trees->lit_length, bits, fast_bits, fast_mask); + ufbxi_regression_assert(sym0 != UFBXI_HUFF_UNINITIALIZED_SYM); + + uint32_t sym0_bits = ufbxi_huff_sym_total_bits(sym0); + + bits >>= sym0_bits; + left -= sym0_bits; + if (sym0 & UFBXI_HUFF_SYM_END) { + if (ufbxi_huff_sym_value(sym0) != 0) return -13; + + dc->out_ptr = out_ptr; + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + return 0; + } else if ((sym0 & UFBXI_HUFF_SYM_MATCH) == 0) { + if (out_ptr == out_end) return -10; + *out_ptr++ = (char)ufbxi_huff_sym_value(sym0); + continue; + } + + uint32_t sym0_value = ufbxi_huff_sym_value(sym0); + uint32_t len_shift_base = trees->lit_length.extra_shift_base[sym0_value]; + uint16_t len_mask = trees->lit_length.extra_mask[sym0_value]; + uint32_t length = (len_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym_bits, len_shift_base) & len_mask); + + ufbxi_huff_sym sym1 = ufbxi_huff_decode_bits(&trees->dist, bits, fast_bits, fast_mask); + ufbxi_regression_assert(sym1 != UFBXI_HUFF_UNINITIALIZED_SYM); + if (sym1 & UFBXI_HUFF_SYM_END) return -11; + + uint32_t sym1_bits = ufbxi_huff_sym_total_bits(sym1); + + bits >>= sym1_bits; + left -= sym1_bits; + + uint32_t sym1_value = ufbxi_huff_sym_value(sym1); + uint32_t dist_shift_base = trees->dist.extra_shift_base[sym1_value]; + uint16_t dist_mask = trees->dist.extra_mask[sym1_value]; + uint32_t distance = (dist_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym_bits, dist_shift_base + sym0) & dist_mask); + + // Bounds checking + size_t out_space = ufbxi_to_size(out_end - out_ptr); + if ((ptrdiff_t)distance > out_ptr - out_begin || length > out_space) { + return -12; + } + + // Copy the match + const char *src = out_ptr - distance; + char *dst = out_ptr; + char *end = dst + length; + out_ptr += length; + + if (out_space >= length + 16) { + uint32_t min_dist = length < 16 ? length : 16; + if (distance >= min_dist) { + ufbxi_copy_16_bytes(dst, src); + while (length > 16) { + src += 16; + dst += 16; + length -= 16; + ufbxi_copy_16_bytes(dst, src); + } + } else { + while (dst != end) { + *dst++ = *src++; + } + } + } else { + while (dst != end) { + *dst++ = *src++; + } + } + } + + dc->out_ptr = out_ptr; + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + return 1; +} + +ufbx_static_assert(inflate_huff_fast_bits, UFBXI_HUFF_FAST_BITS <= 11); // `fast lit, fast len, slow dist` in 56 bits +ufbx_static_assert(inflate_huff_long_bits, UFBXI_HUFF_FAST_BITS + UFBXI_HUFF_MAX_LONG_BITS >= 15); // Largest code fits in a single long lookup + +// Optimized version of `ufbxi_inflate_block_slow()`. +// Has a lot of assumptions (see asserts) and does not call _any_ (even forceinlined) functions. +static ufbxi_noinline int +ufbxi_inflate_block_fast(ufbxi_deflate_context *dc, ufbxi_trees *trees) +{ + ufbxi_dev_assert(!dc->stream.cancelled); + ufbxi_dev_assert(trees->fast_bits == UFBXI_HUFF_FAST_BITS); + ufbxi_dev_assert(dc->stream.chunk_yield - dc->stream.chunk_ptr >= UFBXI_INFLATE_FAST_MIN_IN); + ufbxi_dev_assert(dc->out_end - dc->out_ptr >= UFBXI_INFLATE_FAST_MIN_OUT); + + char *out_ptr = dc->out_ptr; + char *const out_begin = dc->out_begin; + char *const out_end = dc->out_end - UFBXI_INFLATE_FAST_MIN_OUT; + + const ufbxi_huff_tree *tree_lit_length = &trees->lit_length; + const ufbxi_huff_tree *tree_dist = &trees->dist; + + uint64_t bits = dc->stream.bits; + size_t left = dc->stream.left; + const char *data = dc->stream.chunk_ptr; + const char *data_end = dc->stream.chunk_yield - UFBXI_INFLATE_FAST_MIN_IN; + + uint64_t sym01_bits; + ufbxi_huff_sym sym0, sym1; + uint64_t refill_bits = ufbxi_read_u64(data); + + #define ufbxi_fast_inflate_refill_and_decode() do { \ + ufbxi_macro_bit_refill_fast(bits, left, data, refill_bits); \ + sym01_bits = bits; \ + sym0 = tree_lit_length->fast_sym[sym01_bits & UFBXI_HUFF_FAST_MASK]; \ + sym1 = ((sym0 & UFBXI_HUFF_SYM_MATCH) ? tree_dist : tree_lit_length)->fast_sym[ufbxi_wrap_shr64(sym01_bits, sym0) & UFBXI_HUFF_FAST_MASK]; \ + refill_bits = ufbxi_read_u64(data); \ + } while (0) + + #define ufbxi_fast_inflate_should_continue() \ + (((data_end - data) | (out_end - out_ptr)) >= 0) + + ufbxi_fast_inflate_refill_and_decode(); + + for (;;) { + if ((sym0 & sym1) & UFBXI_HUFF_SYM_FAST) { + bits = ufbxi_wrap_shr64(sym01_bits, sym0 + sym1); + left -= (sym0 + sym1) & 0x3f; + + if (((sym0 | sym1) & UFBXI_HUFF_SYM_MATCH) == 0) { + // Literal, Literal + // -> Output the two literals and loop back to start. + + out_ptr[0] = (char)ufbxi_huff_sym_value(sym0); + out_ptr[1] = (char)ufbxi_huff_sym_value(sym1); + out_ptr += 2; + + ufbxi_fast_inflate_refill_and_decode(); + if (ufbxi_fast_inflate_should_continue()) continue; + break; + + } else if ((sym0 & UFBXI_HUFF_SYM_MATCH) == 0) { + // Literal, Match, (Distance) + // -> Output a single literal, decode the missing distance and fall through to match. + + out_ptr[0] = (char)ufbxi_huff_sym_value(sym0); + out_ptr += 1; + + sym01_bits = ufbxi_wrap_shr64(sym01_bits, sym0); + + // This must fit as literals never have extra bits and the match length is fast so: + // 10 (lit) + 10 (len code) + 5 (len extra) + 15 (dist code) + 13 (dist extra) = 53 <= 56 + sym0 = sym1; + sym1 = tree_dist->fast_sym[bits & UFBXI_HUFF_FAST_MASK]; + + if ((sym1 & UFBXI_HUFF_SYM_FAST) == 0) { + // Slow sym1 + if (sym1 & UFBXI_HUFF_SYM_END) return -11; + uint32_t tail = (uint32_t)(bits >> UFBXI_HUFF_FAST_BITS); + uint32_t long_mask = ufbxi_huff_sym_long_mask(sym1); + sym1 = tree_dist->long_sym[ufbxi_huff_sym_long_offset(sym1) + (tail & long_mask)]; + if (sym1 & UFBXI_HUFF_SYM_END) return -11; + } + + bits = ufbxi_wrap_shr64(bits, sym1); + left -= sym1 & 0x3f; + } else { + // Match, Distance + // -> Fall through to match copy. + } + + } else { + if ((sym0 & (UFBXI_HUFF_SYM_FAST|UFBXI_HUFF_SYM_END)) == 0) { + // Slow sym0 + uint32_t tail = (uint32_t)(sym01_bits >> UFBXI_HUFF_FAST_BITS); + uint32_t long_mask = ufbxi_huff_sym_long_mask(sym0); + sym0 = tree_lit_length->long_sym[ufbxi_huff_sym_long_offset(sym0) + (tail & long_mask)]; + } + + uint32_t sym0_bits = ufbxi_huff_sym_total_bits(sym0); + bits >>= sym0_bits; + left -= sym0_bits; + + if (sym0 & UFBXI_HUFF_SYM_END) { + if (ufbxi_huff_sym_value(sym0) != 0) return -13; + dc->out_ptr = out_ptr; + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + return 0; + } + + if (sym0 & UFBXI_HUFF_SYM_MATCH) { + sym1 = tree_dist->fast_sym[bits & UFBXI_HUFF_FAST_MASK]; + + if ((sym1 & UFBXI_HUFF_SYM_FAST) == 0) { + // Slow sym1 + if (sym1 & UFBXI_HUFF_SYM_END) return -11; + uint32_t tail = (uint32_t)(bits >> UFBXI_HUFF_FAST_BITS); + uint32_t long_mask = ufbxi_huff_sym_long_mask(sym1); + sym1 = tree_dist->long_sym[ufbxi_huff_sym_long_offset(sym1) + (tail & long_mask)]; + if (sym1 & UFBXI_HUFF_SYM_END) return -11; + } + + bits = ufbxi_wrap_shr64(bits, sym1); + left -= sym1 & 0x3f; + } else { + *out_ptr++ = (char)ufbxi_huff_sym_value(sym0); + + ufbxi_fast_inflate_refill_and_decode(); + if (ufbxi_fast_inflate_should_continue()) continue; + break; + } + } + + uint32_t sym0_value = ufbxi_huff_sym_value(sym0); + uint32_t len_shift_base = trees->lit_length.extra_shift_base[sym0_value]; + uint16_t len_mask = trees->lit_length.extra_mask[sym0_value]; + uint32_t length = (len_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym01_bits, len_shift_base) & len_mask); + + uint32_t sym1_value = ufbxi_huff_sym_value(sym1); + uint32_t dist_shift_base = trees->dist.extra_shift_base[sym1_value]; + uint16_t dist_mask = trees->dist.extra_mask[sym1_value]; + uint32_t distance = (dist_shift_base >> 16) + (uint32_t)(ufbxi_wrap_shr64(sym01_bits, dist_shift_base + sym0) & dist_mask); + + ufbxi_fast_inflate_refill_and_decode(); + + // Bounds checking: We don't actually handle the error here, just bail out to the slow implementation + ptrdiff_t dst_space = out_end - out_ptr - (ptrdiff_t)length + UFBXI_INFLATE_FAST_MIN_OUT; + ptrdiff_t src_space = out_ptr - out_begin - (ptrdiff_t)distance; + if ((dst_space | src_space) < 0) { + return -12; + } + + const char *src = out_ptr - distance; + char *dst = out_ptr; + char *end = dst + length; + out_ptr += length; + + // Copy the match + + uint32_t min_dist = length < 16 ? length : 16; + if (distance >= min_dist && dst_space >= 16) { + ufbxi_copy_16_bytes(dst, src); + while (length > 16) { + src += 16; + dst += 16; + length -= 16; + ufbxi_copy_16_bytes(dst, src); + } + } else { + while (dst != end) { + *dst++ = *src++; + } + } + + if (ufbxi_fast_inflate_should_continue()) continue; + break; + } + + dc->out_ptr = out_ptr; + dc->stream.bits = bits; + dc->stream.left = left; + dc->stream.chunk_ptr = data; + return 1; + + #undef ufbxi_fast_inflate_refill_and_decode + #undef ufbxi_fast_inflate_should_continue +} + +static void ufbxi_inflate_init_retain(ufbx_inflate_retain *retain) +{ + ufbxi_inflate_retain_imp *ret_imp = (ufbxi_inflate_retain_imp*)retain; + if (!ret_imp->initialized) { + ufbxi_init_static_huff(&ret_imp->static_trees, NULL); + ret_imp->initialized = true; + } +} + +// TODO: Error codes should have a quick test if the destination buffer overflowed +// Returns actual number of decompressed bytes or negative error: +// -1: Bad compression method (ZLIB header) +// -2: Requires dictionary (ZLIB header) +// -3: Bad FCHECK (ZLIB header) +// -4: Bad NLEN (Uncompressed LEN != ~NLEN) +// -5: Uncompressed source overflow +// -6: Uncompressed destination overflow +// -7: Bad block type +// -8: Truncated checksum (deprecated, reported as -9) +// -9: Checksum mismatch +// -10: Literal destination overflow +// -11: Bad distance code or distance of (30..31) +// -12: Match out of bounds +// -13: Bad lit/length code +// -14: Codelen Huffman Overfull +// -15: Codelen Huffman Underfull +// -16: Litlen Huffman Overfull +// -17: Litlen Huffman Underfull +// -18: Repeat 16 overflow +// -19: Repeat 17 overflow +// -20: Repeat 18 overflow +// -21: Bad codelen code +// -22: Distance Huffman: Overfull +// -23: Distance Huffman: Underfull +// -28: Cancelled +// -29: Invalid ufbx_inflate_input.internal_fast_bits value +// -30: Bad window size (ZLIB header) +ufbxi_extern_c ptrdiff_t ufbx_inflate(void *dst, size_t dst_size, const ufbx_inflate_input *input, ufbx_inflate_retain *retain) +{ + ufbxi_inflate_retain_imp *ret_imp = (ufbxi_inflate_retain_imp*)retain; + + ptrdiff_t err; + ufbxi_deflate_context dc; + ufbxi_bit_stream_init(&dc.stream, input); + dc.out_begin = (char*)dst; + dc.out_ptr = (char*)dst; + dc.out_end = (char*)dst + dst_size; + if (input->internal_fast_bits != 0) { + dc.fast_bits = (uint32_t)input->internal_fast_bits; + if (dc.fast_bits < 1 || dc.fast_bits == 9 || dc.fast_bits > 10) return -29; + } else { + // TODO: Profile this + dc.fast_bits = input->total_size > 2048 ? 10 : 8; + } + + uint64_t bits = dc.stream.bits; + size_t left = dc.stream.left; + const char *data = dc.stream.chunk_ptr; + + ufbxi_bit_refill(&bits, &left, &data, &dc.stream); + if (dc.stream.cancelled) return -28; + + // Zlib header + if (!input->no_header) { + size_t cmf = (size_t)(bits & 0xff); + size_t flg = (size_t)(bits >> 8) & 0xff; + bits >>= 16; + left -= 16; + + if ((cmf & 0xf) != 0x8) return -1; + if ((flg & 0x20) != 0) return -2; + if ((cmf << 8 | flg) % 31u != 0) return -3; + if ((cmf >> 4) > 7) return -30; + } + + for (;;) { + ufbxi_bit_refill(&bits, &left, &data, &dc.stream); + if (dc.stream.cancelled) return -28; + + // Block header: [0:1] BFINAL [1:3] BTYPE + size_t header = (size_t)bits & 0x7; + bits >>= 3; + left -= 3; + + size_t type = header >> 1; + if (type == 0) { + + // Round up to the next byte + size_t align_bits = left & 0x7; + bits >>= align_bits; + left -= align_bits; + + size_t len = (size_t)(bits & 0xffff); + size_t nlen = (size_t)((bits >> 16) & 0xffff); + if ((len ^ nlen) != 0xffff) return -4; + if (dc.out_end - dc.out_ptr < (ptrdiff_t)len) return -6; + bits >>= 32; + left -= 32; + + dc.stream.bits = bits; + dc.stream.left = left; + dc.stream.chunk_ptr = data; + + // Copy `len` bytes of literal data + if (!ufbxi_bit_copy_bytes(dc.out_ptr, &dc.stream, len)) return -5; + + dc.out_ptr += len; + + } else if (type <= 2) { + + dc.stream.bits = bits; + dc.stream.left = left; + dc.stream.chunk_ptr = data; + + ufbxi_trees tree_data; // ufbxi_uninit + ufbxi_trees *trees; // ufbxi_uninit + if (type == 1) { + // Static Huffman: Initialize the trees once and cache them in `retain`. + if (!ret_imp->initialized) { + ufbxi_init_static_huff(&ret_imp->static_trees, input); + ret_imp->initialized = true; + } + trees = &ret_imp->static_trees; + } else { + // Dynamic Huffman + err = ufbxi_init_dynamic_huff(&dc, &tree_data); + if (err) return err; + trees = &tree_data; + } + + for (;;) { + bool fast_viable = trees->fast_bits == UFBXI_HUFF_FAST_BITS && dc.out_end - dc.out_ptr >= UFBXI_INFLATE_FAST_MIN_OUT; + + // `ufbxi_inflate_block_fast()` needs a bit more upfront setup, see asserts on top of the function + if (fast_viable && dc.stream.chunk_yield - dc.stream.chunk_ptr >= UFBXI_INFLATE_FAST_MIN_IN) { + err = ufbxi_inflate_block_fast(&dc, trees); + } else { + err = ufbxi_inflate_block_slow(&dc, trees, fast_viable ? 32 : SIZE_MAX); + } + + if (err < 0) return err; + + // `ufbxi_inflate_block()` returns normally on cancel so check it here + if (dc.stream.cancelled) return -28; + + if (err == 0) break; + } + + } else { + // 0b11 - reserved (error) + return -7; + } + + bits = dc.stream.bits; + left = dc.stream.left; + data = dc.stream.chunk_ptr; + + // BFINAL: End of stream + if (header & 1) break; + } + + // Check Adler-32 + { + // Round up to the next byte + size_t align_bits = left & 0x7; + bits >>= align_bits; + left -= align_bits; + ufbxi_bit_refill(&bits, &left, &data, &dc.stream); + if (dc.stream.cancelled) return -28; + + if (!input->no_checksum) { + uint32_t ref = (uint32_t)bits; + ref = (ref>>24) | ((ref>>8)&0xff00) | ((ref<<8)&0xff0000) | (ref<<24); + + uint32_t checksum = ufbxi_adler32(dc.out_begin, ufbxi_to_size(dc.out_ptr - dc.out_begin)); + if (ref != checksum) { + return -9; + } + } + } + + return dc.out_ptr - dc.out_begin; +} + +#endif // !defined(ufbx_inflate) + +// -- Printf + +typedef struct { + char *dst; + size_t length; + size_t pos; +} ufbxi_print_buffer; + +#define UFBXI_PRINT_UNSIGNED 0x1 +#define UFBXI_PRINT_STRING 0x2 +#define UFBXI_PRINT_SIZE_T 0x10 + +static void ufbxi_print_append(ufbxi_print_buffer *buf, size_t min_width, size_t max_width, const char *str) +{ + size_t width = 0; + for (width = 0; width < max_width; width++) { + if (!str[width]) break; + } + size_t pad = min_width > width ? min_width - width : 0; + for (size_t i = 0; i < pad; i++) { + if (buf->pos < buf->length) buf->dst[buf->pos++] = ' '; + } + for (size_t i = 0; i < width; i++) { + if (buf->pos < buf->length) buf->dst[buf->pos++] = str[i]; + } +} + +static char *ufbxi_print_format_int(char *buffer, uint64_t value) +{ + *--buffer = '\0'; + do { + uint32_t digit = (uint32_t)(value % 10); + value = value / 10; + *--buffer = (char)('0' + digit); + } while (value > 0); + return buffer; +} + +static void ufbxi_vprint(ufbxi_print_buffer *buf, const char *fmt, va_list args) +{ + char buffer[96]; // ufbxi_uninit + for (const char *p = fmt; *p;) { + if (*p == '%' && *++p != '%') { + size_t min_width = 0, max_width = SIZE_MAX; + if (*p == '*') { + p++; + min_width = (size_t)va_arg(args, int); + } + if (*p == '.') { + ufbxi_dev_assert(p[1] == '*'); + p += 2; + max_width = (size_t)va_arg(args, int); + } + uint32_t flags = 0; + switch (*p) { + case 'z': p++; flags |= UFBXI_PRINT_SIZE_T; break; + default: break; + } + switch (*p++) { + case 'u': flags |= UFBXI_PRINT_UNSIGNED; break; + case 's': flags |= UFBXI_PRINT_STRING; break; + default: break; + } + if (flags & UFBXI_PRINT_STRING) { + const char *str = va_arg(args, const char*); + ufbxi_print_append(buf, min_width, max_width, str); + } else if (flags & UFBXI_PRINT_UNSIGNED) { + uint64_t value = (flags & UFBXI_PRINT_SIZE_T) != 0 ? (uint64_t)va_arg(args, size_t) : (uint64_t)va_arg(args, uint32_t); + char *str = ufbxi_print_format_int(buffer + sizeof(buffer), value); + ufbxi_print_append(buf, min_width, max_width, str); + } else { + ufbxi_unreachable("Bad printf format"); + } + } else { + if (buf->pos < buf->length) buf->dst[buf->pos++] = *p; + p++; + } + } + if (buf->length && buf->dst) { + size_t end = buf->pos <= buf->length - 1 ? buf->pos : buf->length - 1; + buf->dst[end] = '\0'; + } +} + +// -- Errors + +static const char ufbxi_empty_char[1] = { '\0' }; + +static ufbxi_noinline int ufbxi_vsnprintf(char *buf, size_t buf_size, const char *fmt, va_list args) +{ + ufbxi_print_buffer buffer = { buf, buf_size }; + ufbxi_vprint(&buffer, fmt, args); + return (int)ufbxi_min_sz(buffer.pos, buf_size - 1); +} + +static ufbxi_noinline int ufbxi_snprintf(char *buf, size_t buf_size, const char *fmt, ...) +{ + va_list args; // ufbxi_uninit + va_start(args, fmt); + int result = ufbxi_vsnprintf(buf, buf_size, fmt, args); + va_end(args); + return result; +} + +static ufbxi_noinline void ufbxi_panicf_imp(ufbx_panic *panic, const char *fmt, ...) +{ + if (panic && panic->did_panic) return; + + va_list args; // ufbxi_uninit + + if (panic) { + va_start(args, fmt); + panic->did_panic = true; + panic->message_length = (size_t)ufbxi_vsnprintf(panic->message, sizeof(panic->message), fmt, args); + va_end(args); + } else { + va_start(args, fmt); + char message[UFBX_PANIC_MESSAGE_LENGTH]; + ufbxi_vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + ufbx_panic_handler(message); + } +} + +#define ufbxi_panicf(panic, cond, ...) \ + ((cond) ? false : (ufbxi_panicf_imp((panic), __VA_ARGS__), true)) + +// Prefix the error condition with $Description\0 for a human readable description +#define ufbxi_error_msg(cond, msg) "$" msg "\0" cond + +static ufbxi_noinline int ufbxi_fail_imp_err(ufbx_error *err, const char *cond, const char *func, uint32_t line) +{ + if (cond && cond[0] == '$') { + if (!err->description.data) { + err->description.data = cond + 1; + err->description.length = strlen(err->description.data); + } + +#if UFBXI_FEATURE_ERROR_STACK + // Skip the description part if adding to a stack + cond = cond + strlen(cond) + 1; +#endif + } + + // NOTE: This is the base function all fails boil down to, place a breakpoint here to + // break at the first error +#if UFBXI_FEATURE_ERROR_STACK + ufbx_assert(cond); + ufbx_assert(func); + if (err->stack_size < UFBX_ERROR_STACK_MAX_DEPTH) { + ufbx_error_frame *frame = &err->stack[err->stack_size++]; + frame->description.data = cond; + frame->description.length = strlen(cond); + frame->function.data = func; + frame->function.length = strlen(func); + frame->source_line = line; + } +#else + ufbxi_ignore(func); + ufbxi_ignore(line); +#endif + + return 0; +} + +ufbxi_nodiscard static ufbxi_noinline size_t ufbxi_utf8_valid_length(const char *str, size_t length) +{ + size_t index = 0; + while (index < length) { + uint8_t c = (uint8_t)str[index]; + size_t left = length - index; + + if ((c & 0x80) == 0) { + if (c != 0) { + index += 1; + continue; + } + } else if ((c & 0xe0) == 0xc0 && left >= 2) { + uint8_t t0 = (uint8_t)str[index + 1]; + uint32_t code = (uint32_t)c << 8 | (uint32_t)t0; + if ((code & 0xc0) == 0x80 && code >= 0xc280) { + index += 2; + continue; + } + } else if ((c & 0xf0) == 0xe0 && left >= 3) { + uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2]; + uint32_t code = (uint32_t)c << 16 | (uint32_t)t0 << 8 | (uint32_t)t1; + if ((code & 0xc0c0) == 0x8080 && code >= 0xe0a080 && (code < 0xeda080 || code >= 0xee8080)) { + index += 3; + continue; + } + } else if ((c & 0xf8) == 0xf0 && left >= 4) { + uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2], t2 = (uint8_t)str[index + 3]; + uint32_t code = (uint32_t)c << 24 | (uint32_t)t0 << 16 | (uint32_t)t1 << 8 | (uint32_t)t2; + if ((code & 0xc0c0c0) == 0x808080 && code >= 0xf0908080u && code <= 0xf48fbfbfu) { + index += 4; + continue; + } + } + + break; + } + + ufbx_assert(index <= length); + return index; +} + +static ufbxi_noinline void ufbxi_clean_string_utf8(char *str, size_t length) +{ + size_t pos = 0; + for (;;) { + pos += ufbxi_utf8_valid_length(str + pos, length); + if (pos == length) break; + str[pos++] = '?'; + } +} + +static ufbxi_noinline void ufbxi_set_err_info(ufbx_error *err, const char *data, size_t length) +{ + if (!err) return; + + if (length == SIZE_MAX) length = strlen(data); + size_t to_copy = ufbxi_min_sz(sizeof(err->info) - 1, length); + memcpy(err->info, data, to_copy); + err->info[to_copy] = '\0'; + err->info_length = to_copy; + ufbxi_clean_string_utf8(err->info, err->info_length); +} + +static ufbxi_noinline void ufbxi_fmt_err_info(ufbx_error *err, const char *fmt, ...) +{ + if (!err) return; + + va_list args; // ufbxi_uninit + va_start(args, fmt); + err->info_length = (size_t)ufbxi_vsnprintf(err->info, sizeof(err->info), fmt, args); + va_end(args); + ufbxi_clean_string_utf8(err->info, err->info_length); +} + +static ufbxi_noinline void ufbxi_clear_error(ufbx_error *err) +{ + if (!err) return; + + err->type = UFBX_ERROR_NONE; + err->description.data = ufbxi_empty_char; + err->description.length = 0; + err->stack_size = 0; + err->info[0] = '\0'; + err->info_length = 0; +} + +#if UFBXI_FEATURE_ERROR_STACK + #define ufbxi_function __FUNCTION__ + #define ufbxi_line __LINE__ + #define ufbxi_cond_str(cond) #cond +#else + #define ufbxi_function NULL + #define ufbxi_line 0 + #define ufbxi_cond_str(cond) "" +#endif + +#if UFBXI_FEATURE_ERROR_STACK + #define ufbxi_fail_err_no_msg(err, cond, func, line) ufbxi_fail_imp_err((err), (cond), (func), (line)) +#else + static ufbxi_noinline int ufbxi_fail_imp_err_no_stack(ufbx_error *err) { return ufbxi_fail_imp_err(err, NULL, NULL, 0); } + #define ufbxi_fail_err_no_msg(err, cond, func, line) ufbxi_fail_imp_err_no_stack((err)) +#endif + +#define ufbxi_check_err(err, cond) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_err_no_msg((err), ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return 0; } } while (0) +#define ufbxi_check_return_err(err, cond, ret) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_err_no_msg((err), ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return ret; } } while (0) +#define ufbxi_fail_err(err, desc) return ufbxi_fail_err_no_msg(err, desc, ufbxi_function, ufbxi_line) + +#define ufbxi_check_err_msg(err, cond, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return 0; } } while (0) +#define ufbxi_check_return_err_msg(err, cond, ret, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp_err((err), ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return ret; } } while (0) +#define ufbxi_fail_err_msg(err, desc, msg) return ufbxi_fail_imp_err(err, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line) +#define ufbxi_report_err_msg(err, desc, msg) (void)ufbxi_fail_imp_err(err, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line) + +static ufbxi_noinline void ufbxi_fix_error_type(ufbx_error *error, const char *default_desc, ufbx_error *p_error) +{ + const char *desc = error->description.data; + if (!desc) desc = default_desc; + error->type = UFBX_ERROR_UNKNOWN; + if (!strcmp(desc, "Out of memory")) { + error->type = UFBX_ERROR_OUT_OF_MEMORY; + } else if (!strcmp(desc, "Memory limit exceeded")) { + error->type = UFBX_ERROR_MEMORY_LIMIT; + } else if (!strcmp(desc, "Allocation limit exceeded")) { + error->type = UFBX_ERROR_ALLOCATION_LIMIT; + } else if (!strcmp(desc, "Truncated file")) { + error->type = UFBX_ERROR_TRUNCATED_FILE; + } else if (!strcmp(desc, "IO error")) { + error->type = UFBX_ERROR_IO; + } else if (!strcmp(desc, "Cancelled")) { + error->type = UFBX_ERROR_CANCELLED; + } else if (!strcmp(desc, "Unrecognized file format")) { + error->type = UFBX_ERROR_UNRECOGNIZED_FILE_FORMAT; + } else if (!strcmp(desc, "File not found")) { + error->type = UFBX_ERROR_FILE_NOT_FOUND; + } else if (!strcmp(desc, "Empty file")) { + error->type = UFBX_ERROR_EMPTY_FILE; + } else if (!strcmp(desc, "External file not found")) { + error->type = UFBX_ERROR_EXTERNAL_FILE_NOT_FOUND; + } else if (!strcmp(desc, "Uninitialized options")) { + error->type = UFBX_ERROR_UNINITIALIZED_OPTIONS; + } else if (!strcmp(desc, "Zero vertex size")) { + error->type = UFBX_ERROR_ZERO_VERTEX_SIZE; + } else if (!strcmp(desc, "Truncated vertex stream")) { + error->type = UFBX_ERROR_TRUNCATED_VERTEX_STREAM; + } else if (!strcmp(desc, "Invalid UTF-8")) { + error->type = UFBX_ERROR_INVALID_UTF8; + } else if (!strcmp(desc, "Feature disabled")) { + error->type = UFBX_ERROR_FEATURE_DISABLED; + } else if (!strcmp(desc, "Bad NURBS geometry")) { + error->type = UFBX_ERROR_BAD_NURBS; + } else if (!strcmp(desc, "Bad index")) { + error->type = UFBX_ERROR_BAD_INDEX; + } else if (!strcmp(desc, "Node depth limit exceeded")) { + error->type = UFBX_ERROR_NODE_DEPTH_LIMIT; + } else if (!strcmp(desc, "Threaded ASCII parse error")) { + error->type = UFBX_ERROR_THREADED_ASCII_PARSE; + } else if (!strcmp(desc, "Unsafe options")) { + error->type = UFBX_ERROR_UNSAFE_OPTIONS; + } else if (!strcmp(desc, "Duplicate override")) { + error->type = UFBX_ERROR_DUPLICATE_OVERRIDE; + } + error->description.data = desc; + error->description.length = strlen(desc); + if (p_error) { + memcpy(p_error, error, sizeof(ufbx_error)); + } +} + +// -- Allocator + +// Returned for zero size allocations, place in the constant data +// to catch writes to bad allocations. +#if defined(UFBX_REGRESSION) +static const char ufbxi_zero_size_buffer[4096] = { 0 }; +#else +static const char ufbxi_zero_size_buffer[64] = { 0 }; +#endif + +static ufbxi_forceinline size_t ufbxi_align_to_mask(size_t value, size_t align_mask) +{ + return value + (((size_t)0 - value) & align_mask); +} + +static ufbxi_forceinline size_t ufbxi_size_align_mask(size_t size) +{ + // Align to the all bits below the lowest set one in `size` up to the maximum alignment. + return ((size ^ (size - 1)) >> 1) & (UFBX_MAXIMUM_ALIGNMENT - 1); +} + +typedef struct { + ufbx_error *error; + size_t current_size; + size_t max_size; + size_t num_allocs; + size_t max_allocs; + size_t huge_size; + size_t chunk_max; + ufbx_allocator_opts ator; + const char *name; +} ufbxi_allocator; + +static ufbxi_forceinline bool ufbxi_does_overflow(size_t total, size_t a, size_t b) +{ + // If `a` and `b` have at most 4 bits per `size_t` byte, the product can't overflow. + if (((a | b) >> sizeof(size_t)*4) != 0) { + if (a != 0 && total / a != b) return true; + } + return false; +} + +static ufbxi_noinline void *ufbxi_alloc_size(ufbxi_allocator *ator, size_t size, size_t n) +{ + // Always succeed with an empty non-NULL buffer for empty allocations + ufbx_assert(size > 0); + if (n == 0) return (void*)ufbxi_zero_size_buffer; + + size_t total = size * n; + ufbxi_check_return_err(ator->error, !ufbxi_does_overflow(total, size, n), NULL); + ufbxi_check_return_err(ator->error, total <= SIZE_MAX / 2, NULL); // Make sure it's always safe to double allocations + if (!(total < ator->max_size - ator->current_size)) { + ufbxi_report_err_msg(ator->error, "total <= ator->max_size - ator->current_size", "Memory limit exceeded"); + ufbxi_fmt_err_info(ator->error, "%s", ator->name); + return NULL; + } + if (!(ator->num_allocs < ator->max_allocs)) { + ufbxi_report_err_msg(ator->error, "ator->num_allocs < ator->max_allocs", "Allocation limit exceeded"); + ufbxi_fmt_err_info(ator->error, "%s", ator->name); + return NULL; + } + ator->num_allocs++; + + void *ptr; + if (ator->ator.allocator.alloc_fn) { + ptr = ator->ator.allocator.alloc_fn(ator->ator.allocator.user, total); + } else if (ator->ator.allocator.realloc_fn) { + ptr = ator->ator.allocator.realloc_fn(ator->ator.allocator.user, NULL, 0, total); + } else { + ptr = ufbx_malloc(total); + } + + if (!ptr) { + ufbxi_report_err_msg(ator->error, "ptr", "Out of memory"); + ufbxi_fmt_err_info(ator->error, "%s", ator->name); + return NULL; + } + ufbx_assert(ufbxi_is_aligned_mask(ptr, ufbxi_size_align_mask(total))); + + ator->current_size += total; + + return ptr; +} + +static void ufbxi_free_size(ufbxi_allocator *ator, size_t size, void *ptr, size_t n); +static ufbxi_noinline void *ufbxi_realloc_size(ufbxi_allocator *ator, size_t size, void *old_ptr, size_t old_n, size_t n) +{ + ufbx_assert(size > 0); + // realloc() with zero old/new size is equivalent to alloc()/free() + if (old_n == 0) return ufbxi_alloc_size(ator, size, n); + if (n == 0) { ufbxi_free_size(ator, size, old_ptr, old_n); return NULL; } + + size_t old_total = size * old_n; + size_t total = size * n; + + // The old values have been checked by a previous allocate call + ufbx_assert(!ufbxi_does_overflow(old_total, size, old_n)); + ufbx_assert(old_total <= ator->current_size); + + ufbxi_check_return_err(ator->error, !ufbxi_does_overflow(total, size, n), NULL); + ufbxi_check_return_err(ator->error, total <= SIZE_MAX / 2, NULL); // Make sure it's always safe to double allocations + ufbxi_check_return_err_msg(ator->error, total <= ator->max_size - ator->current_size, NULL, "Memory limit exceeded"); + ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded"); + ator->num_allocs++; + + void *ptr; + if (ator->ator.allocator.realloc_fn) { + ptr = ator->ator.allocator.realloc_fn(ator->ator.allocator.user, old_ptr, old_total, total); + } else if (ator->ator.allocator.alloc_fn) { + // Use user-provided alloc_fn() and free_fn() + ptr = ator->ator.allocator.alloc_fn(ator->ator.allocator.user, total); + if (ptr) memcpy(ptr, old_ptr, old_total); + if (ator->ator.allocator.free_fn) { + ator->ator.allocator.free_fn(ator->ator.allocator.user, old_ptr, old_total); + } + } else { + ptr = ufbx_realloc(old_ptr, old_total, total); + } + + ufbxi_check_return_err_msg(ator->error, ptr, NULL, "Out of memory"); + ufbx_assert(ufbxi_is_aligned_mask(ptr, ufbxi_size_align_mask(total))); + + ator->current_size += total; + ator->current_size -= old_total; + + return ptr; +} + +static ufbxi_noinline void ufbxi_free_size(ufbxi_allocator *ator, size_t size, void *ptr, size_t n) +{ + ufbx_assert(size > 0); + if (n == 0) return; + ufbx_assert(ptr); + + size_t total = size * n; + + // The old values have been checked by a previous allocate call + ufbx_assert(!ufbxi_does_overflow(total, size, n)); + ufbx_assert(total <= ator->current_size); + + ator->current_size -= total; + + if (ator->ator.allocator.alloc_fn || ator->ator.allocator.realloc_fn) { + // Don't call default free() if there is an user-provided `alloc_fn()` + if (ator->ator.allocator.free_fn) { + ator->ator.allocator.free_fn(ator->ator.allocator.user, ptr, total); + } else if (ator->ator.allocator.realloc_fn) { + ator->ator.allocator.realloc_fn(ator->ator.allocator.user, ptr, total, 0); + } + } else { + ufbx_free(ptr, total); + } +} + +ufbxi_noinline ufbxi_nodiscard static bool ufbxi_grow_array_size(ufbxi_allocator *ator, size_t size, void *p_ptr, size_t *p_cap, size_t n) +{ + #if defined(UFBX_REGRESSION) + { + ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, false, "Allocation limit exceeded"); + ator->num_allocs++; + } + #endif + + if (n <= *p_cap) return true; + void *ptr = *(void**)p_ptr; + size_t old_n = *p_cap; + if (old_n >= n) return true; + size_t new_n = ufbxi_max_sz(old_n * 2, n); + void *new_ptr = ufbxi_realloc_size(ator, size, ptr, old_n, new_n); + if (!new_ptr) return false; + *(void**)p_ptr = new_ptr; + *p_cap = new_n; + return true; +} + +static ufbxi_noinline void ufbxi_free_ator(ufbxi_allocator *ator) +{ + ufbx_assert(ator->current_size == 0); + + ufbx_free_allocator_fn *free_fn = ator->ator.allocator.free_allocator_fn; + if (free_fn) { + void *user = ator->ator.allocator.user; + free_fn(user); + } +} + +#define ufbxi_alloc(ator, type, n) ufbxi_maybe_null((type*)ufbxi_alloc_size((ator), sizeof(type), (n))) +#define ufbxi_alloc_zero(ator, type, n) ufbxi_maybe_null((type*)ufbxi_alloc_zero_size((ator), sizeof(type), (n))) +#define ufbxi_realloc(ator, type, old_ptr, old_n, n) ufbxi_maybe_null((type*)ufbxi_realloc_size((ator), sizeof(type), (old_ptr), (old_n), (n))) +#define ufbxi_realloc_zero(ator, type, old_ptr, old_n, n) ufbxi_maybe_null((type*)ufbxi_realloc_zero_size((ator), sizeof(type), (old_ptr), (old_n), (n))) +#define ufbxi_free(ator, type, ptr, n) ufbxi_free_size((ator), sizeof(type), (ptr), (n)) + +#define ufbxi_grow_array(ator, p_ptr, p_cap, n) ufbxi_grow_array_size((ator), sizeof(**(p_ptr)), (p_ptr), (p_cap), (n)) + +#define UFBXI_SCENE_IMP_MAGIC 0x58424655 +#define UFBXI_MESH_IMP_MAGIC 0x48534d55 +#define UFBXI_LINE_CURVE_IMP_MAGIC 0x55434c55 +#define UFBXI_CACHE_IMP_MAGIC 0x48434355 +#define UFBXI_ANIM_IMP_MAGIC 0x494e4155 +#define UFBXI_BAKED_ANIM_IMP_MAGIC 0x4b414255 +#define UFBXI_REFCOUNT_IMP_MAGIC 0x46455255 +#define UFBXI_BUF_CHUNK_IMP_MAGIC 0x46554255 + +// -- Memory buffer +// +// General purpose memory buffer that can be used either as a chunked linear memory +// allocator or a non-contiguous stack. You can convert the contents of `ufbxi_buf` +// to a contiguous range of memory by calling `ufbxi_make_array[_all]()` + +typedef struct ufbxi_buf_padding ufbxi_buf_padding; +typedef struct ufbxi_buf_chunk ufbxi_buf_chunk; + +struct ufbxi_buf_padding { + size_t original_pos; // < Original position before aligning + size_t prev_padding; // < Starting offset of the previous `ufbxi_buf_padding` +}; + +struct ufbxi_buf_chunk { + + // Linked list of nodes + ufbxi_buf_chunk *root; + ufbxi_buf_chunk *prev; + ufbxi_buf_chunk *next; + + union { + size_t magic; // < Magic for debugging + void *align_0; // < Align to 4x pointer size (16/32 bytes) + }; + + size_t size; // < Size of the chunk `data`, excluding this header + size_t pushed_pos; // < Size of valid data when pushed to the list + size_t next_size; // < Next geometrically growing chunk size to allocate + size_t padding_pos; // < One past the offset of the most recent `ufbxi_buf_padding` + + char data[]; // < Must be aligned to 8 bytes +}; + +ufbx_static_assert(buf_chunk_align, offsetof(ufbxi_buf_chunk, data) % 8 == 0); + +typedef struct { + ufbxi_allocator *ator; + + // Current chunks for normal and huge allocations. + // Ordered buffers (`!ufbxi_buf.unordered`) never use `chunks[1]` + ufbxi_buf_chunk *chunks[2]; + + // Inline state for non-huge chunks + size_t pos; // < Next offset to allocate from + size_t size; // < Size of the current chunk ie. `chunks[0]->size` (or 0 if `chunks[0] == NULL`) + + size_t num_items; // < Number of individual items pushed to the buffer + + size_t pushed_size; // < Cumulative size of pushed chunks, not tracked across pops + + bool unordered; // < Does not support popping from the buffer + bool clearable; // < Supports clearing the whole buffer even if `unordered` +} ufbxi_buf; + +typedef struct { + ufbxi_buf_chunk *chunk; + size_t pos; + size_t num_items; +} ufbxi_buf_state; + +static ufbxi_noinline void *ufbxi_push_size_new_block(ufbxi_buf *b, size_t size) +{ + bool huge = size >= b->ator->huge_size; + + // Use the second chunk "list" for huge unordered chunks. + // The state of these chunks is not tracked by `ufbxi_buf.pos/size`. + uint32_t list_ix = ((uint32_t)b->unordered & (uint32_t)huge); + + ufbxi_buf_chunk *chunk = b->chunks[list_ix]; + if (chunk) { + if (list_ix == 0) { + // Store the final position for the retired chunk and scan free + // chunks in case we find one the allocation fits in. + b->pushed_size += b->pos; + chunk->pushed_pos = b->pos; + ufbxi_buf_chunk *next = chunk->next; + while (next != NULL) { + ufbx_assert(next->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + chunk = next; + ufbx_assert(b->unordered || chunk->pushed_pos == 0); + chunk->pushed_pos = 0; + if (size <= chunk->size) { + b->chunks[0] = chunk; + b->pos = (uint32_t)size; + b->size = chunk->size; + return chunk->data; + } + next = chunk->next; + } + } else if (b->clearable) { + // Keep track of the `UFBXI_HUGE_MAX_SCAN` largest chunks and + // retain them. Overflowing chunks are freed in `ufbxi_buf_clear()` + size_t align_mask = ufbxi_size_align_mask(size); + ufbxi_buf_chunk *next = chunk; + + ufbxi_buf_chunk *best_chunk = NULL; + size_t best_space = SIZE_MAX; + + // Clearable huge chunks are sorted by descending size. Check the first N + // chunks for reuse and find the place a new block should be inserted if + // no suitable space is found. Chunk ordering in the tail doesn't matter + // as those chunks are never reused. + // Unreachable chunks in the tail are freed in `ufbxi_buf_clear()`. + for (size_t i = 0; next && i < UFBXI_HUGE_MAX_SCAN; i++) { + ufbx_assert(next->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + if (next->size < size) break; + chunk = next; + + // Try to reuse chunks using a best-fit strategy. + size_t pos = ufbxi_align_to_mask(chunk->pushed_pos, align_mask); + size_t space = chunk->size - pos; + if (size <= space) { + if (space < best_space) { + best_chunk = chunk; + best_space = space; + } + } + + next = chunk->next; + } + + // Early return if we found a slot. + if (best_chunk) { + size_t pos = ufbxi_align_to_mask(best_chunk->pushed_pos, align_mask); + best_chunk->pushed_pos = pos + size; + b->pushed_size += size; + return best_chunk->data + pos; + } + } + } + + // Allocate a new chunk, grow `next_size` geometrically but don't double + // the current or previous user sizes if they are larger. + size_t chunk_size, next_size; + + // If `size` is larger than `huge_size` don't grow `next_size` geometrically, + // but use a dedicated allocation. + if (huge) { + next_size = chunk ? chunk->next_size : 4096; + if (next_size > b->ator->chunk_max) next_size = b->ator->chunk_max; + chunk_size = size; + } else { + next_size = chunk ? chunk->next_size * 2 : 4096; + if (next_size > b->ator->chunk_max) next_size = b->ator->chunk_max; + chunk_size = next_size - sizeof(ufbxi_buf_chunk); + if (chunk_size < size) chunk_size = size; + } + + // Align chunk sizes to 16 bytes + chunk_size = ufbxi_align_to_mask(chunk_size, 0xf); + + ufbxi_buf_chunk *new_chunk = (ufbxi_buf_chunk*)ufbxi_alloc_size(b->ator, 1, sizeof(ufbxi_buf_chunk) + chunk_size); + if (!new_chunk) return NULL; + + new_chunk->prev = chunk; + new_chunk->size = chunk_size; + new_chunk->next_size = next_size; + new_chunk->magic = UFBXI_BUF_CHUNK_IMP_MAGIC; + new_chunk->padding_pos = 0; + new_chunk->pushed_pos = 0; + + // Link the chunk to the list and set it as the active one + if (chunk) { + ufbxi_buf_chunk *next = chunk->next; + if (next) next->prev = new_chunk; + new_chunk->next = next; + chunk->next = new_chunk; + new_chunk->root = chunk->root; + } else { + new_chunk->next = NULL; + new_chunk->root = new_chunk; + } + + if (list_ix == 0) { + b->chunks[0] = new_chunk; + b->pos = size; + b->size = chunk_size; + } else { + ufbxi_buf_chunk *root = b->chunks[1]; + b->pushed_size += size; + if (!root) { + b->chunks[1] = new_chunk; + } else if (root->size < chunk_size) { + // Swap root and self if necessary, we should have bailed out + // in the search loop in the first iteration so `new_chunk` should + // directly follow `root`. + // HACK: This ends up with `chunks[1]` entries having inconsistent + // `ufbxi_buf_chunk.root` pointers but other code only reads `chunks[1].root` + // TODO: Move roots out of the chunks? + ufbx_assert(root->next == new_chunk); + ufbx_assert(new_chunk->prev == root); + if (new_chunk->next) new_chunk->next->prev = root; + root->next = new_chunk->next; + new_chunk->next = root; + new_chunk->prev = NULL; + new_chunk->root = new_chunk; + b->chunks[1] = new_chunk; + } + new_chunk->pushed_pos = size; + } + + return new_chunk->data; +} + +static ufbxi_noinline void *ufbxi_push_size(ufbxi_buf *b, size_t size, size_t n) +{ + // Always succeed with an empty non-NULL buffer for empty allocations + ufbx_assert(size > 0); + if (n == 0) return (void*)ufbxi_zero_size_buffer; + + size_t total = size * n; + if (ufbxi_does_overflow(total, size, n)) return NULL; + + #if defined(UFBX_REGRESSION) + { + ufbxi_allocator *ator = b->ator; + ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded"); + ator->num_allocs++; + } + #endif + + b->num_items += n; + + // Align to the natural alignment based on the size + size_t align_mask = ufbxi_size_align_mask(size); + size_t pos = ufbxi_align_to_mask(b->pos, align_mask); + + if (!b->unordered && pos != b->pos) { + // Alignment mismatch in an unordered block. Align to 16 bytes to guarantee + // sufficient alignment for anything afterwards and mark the padding. + // If we overflow the current block we don't need to care as the block + // boundaries are not contiguous. + pos = ufbxi_align_to_mask(b->pos, 0xf); + if (total < SIZE_MAX - 16 && total + 16 <= b->size - pos) { + ufbxi_buf_chunk *chunk = b->chunks[0]; + ufbxi_buf_padding *padding = (ufbxi_buf_padding*)(chunk->data + pos); + padding->original_pos = b->pos; + padding->prev_padding = chunk->padding_pos; + chunk->padding_pos = pos + 16 + 1; + b->pos = pos + 16 + total; + return (char*)padding + 16; + } else { + return ufbxi_push_size_new_block(b, total); + } + } else { + // Try to push to the current block. Allocate a new block + // if the aligned size doesn't fit. + if (total <= b->size - pos) { + b->pos = pos + total; + return b->chunks[0]->data + pos; + } else { + return ufbxi_push_size_new_block(b, total); + } + } +} + +static ufbxi_forceinline void *ufbxi_push_size_fast(ufbxi_buf *b, size_t size, size_t n) +{ + // Always succeed with an empty non-NULL buffer for empty allocations + ufbxi_regression_assert(size > 0); + ufbxi_regression_assert(n > 0); + + size_t total = size * n; + ufbxi_regression_assert(!ufbxi_does_overflow(total, size, n)); + + #if defined(UFBX_REGRESSION) + { + ufbxi_allocator *ator = b->ator; + ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, NULL, "Allocation limit exceeded"); + ator->num_allocs++; + } + #endif + + b->num_items += n; + + // Homogeneous arrays should always be aligned + size_t pos = b->pos; + ufbxi_regression_assert((pos & ufbxi_size_align_mask(size)) == 0); + + // Try to push to the current block. Allocate a new block + // if the aligned size doesn't fit. + if (total <= b->size - pos) { + b->pos = pos + total; + return b->chunks[0]->data + pos; + } else { + return ufbxi_push_size_new_block(b, total); + } +} + +static ufbxi_noinline void *ufbxi_push_size_zero(ufbxi_buf *b, size_t size, size_t n) +{ + void *ptr = ufbxi_push_size(b, size, n); + if (ptr) memset(ptr, 0, size * n); + return ptr; +} + +ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_size_copy(ufbxi_buf *b, size_t size, size_t n, const void *data) +{ + // Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL` + ufbx_assert(size > 0); + if (n == 0) return (void*)ufbxi_zero_size_buffer; + + ufbx_assert(data); + void *ptr = ufbxi_push_size(b, size, n); + if (ptr) memcpy(ptr, data, size * n); + return ptr; +} + +ufbxi_nodiscard static ufbxi_forceinline void *ufbxi_push_size_copy_fast(ufbxi_buf *b, size_t size, size_t n, const void *data) +{ + // Always succeed with an empty non-NULL buffer for empty allocations, even if `data == NULL` + ufbx_assert(size > 0); + if (n == 0) return (void*)ufbxi_zero_size_buffer; + + ufbx_assert(data); + void *ptr = ufbxi_push_size_fast(b, size, n); + if (ptr) memcpy(ptr, data, size * n); + return ptr; +} + +static ufbxi_noinline void ufbxi_buf_free_unused(ufbxi_buf *b) +{ + ufbx_assert(!b->unordered); + + ufbxi_buf_chunk *chunk = b->chunks[0]; + if (!chunk) return; + + ufbxi_buf_chunk *next = chunk->next; + while (next) { + ufbxi_buf_chunk *to_free = next; + next = next->next; + ufbx_assert(to_free->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + to_free->magic = 0; + ufbxi_free_size(b->ator, 1, to_free, sizeof(ufbxi_buf_chunk) + to_free->size); + } + chunk->next = NULL; + + while (b->pos == 0 && chunk) { + ufbxi_buf_chunk *prev = chunk->prev; + ufbx_assert(chunk->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + chunk->magic = 0; + ufbxi_free_size(b->ator, 1, chunk, sizeof(ufbxi_buf_chunk) + chunk->size); + chunk = prev; + b->chunks[0] = prev; + if (prev) { + prev->next = NULL; + b->pos = prev->pushed_pos; + b->size = prev->size; + } else { + b->pos = 0; + b->size = 0; + } + } +} + +static ufbxi_noinline void ufbxi_pop_size(ufbxi_buf *b, size_t size, size_t n, void *dst, bool peek) +{ + ufbx_assert(!b->unordered); + ufbx_assert(size > 0); + ufbx_assert(b->num_items >= n); + if (!peek) b->num_items -= n; + + char *ptr = (char*)dst; + size_t bytes_left = size * n; + + // We've already pushed this, it better not overflow + ufbx_assert(!ufbxi_does_overflow(bytes_left, size, n)); + + if (ptr) { + ptr += bytes_left; + size_t pos = b->pos; + ufbxi_buf_chunk *chunk = b->chunks[0]; + for (;;) { + if (bytes_left <= pos) { + // Rest of the data is in this single chunk + pos -= bytes_left; + if (!peek) b->pos = pos; + ptr -= bytes_left; + if (bytes_left > 0) { + memcpy(ptr, chunk->data + pos, bytes_left); + } + break; + } else { + // Pop the whole chunk + ptr -= pos; + bytes_left -= pos; + memcpy(ptr, chunk->data, pos); + if (!peek) { + chunk->pushed_pos = 0; + chunk = chunk->prev; + b->chunks[0] = chunk; + b->size = chunk->size; + } else { + chunk = chunk->prev; + } + pos = chunk->pushed_pos; + } + } + } else { + size_t pos = b->pos; + ufbxi_buf_chunk *chunk = b->chunks[0]; + for (;;) { + if (bytes_left <= pos) { + // Rest of the data is in this single chunk + pos -= bytes_left; + if (!peek) b->pos = pos; + break; + } else { + // Pop the whole chunk + bytes_left -= pos; + if (!peek) { + chunk->pushed_pos = 0; + chunk = chunk->prev; + b->chunks[0] = chunk; + b->size = chunk->size; + } else { + chunk = chunk->prev; + } + pos = chunk->pushed_pos; + } + } + } + + if (!peek) { + // Check if we need to rewind past some alignment padding + ufbxi_buf_chunk *chunk = b->chunks[0]; + if (chunk) { + size_t pos = b->pos, padding_pos = chunk->padding_pos; + if (pos < padding_pos) { + ufbx_assert(pos + 1 == padding_pos); + ufbxi_buf_padding *padding = (ufbxi_buf_padding*)(chunk->data + padding_pos - 1 - 16); + b->pos = padding->original_pos; + chunk->padding_pos = padding->prev_padding; + } + } + + // Immediately free popped items if all the allocations are huge + // as it means we want to have dedicated allocations for each push. + if (b->ator->huge_size <= 1) { + ufbxi_buf_free_unused(b); + } + } +} + +static ufbxi_noinline void *ufbxi_push_pop_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n) +{ + void *data = ufbxi_push_size(dst, size, n); + if (!data) return NULL; + ufbxi_pop_size(src, size, n, data, false); + return data; +} + +static ufbxi_noinline void *ufbxi_push_peek_size(ufbxi_buf *dst, ufbxi_buf *src, size_t size, size_t n) +{ + void *data = ufbxi_push_size(dst, size, n); + if (!data) return NULL; + ufbxi_pop_size(src, size, n, data, true); + return data; +} + +static ufbxi_noinline void ufbxi_buf_free(ufbxi_buf *buf) +{ + ufbxi_nounroll for (size_t i = 0; i < 2; i++) { + ufbxi_buf_chunk *chunk = buf->chunks[i]; + if (chunk) { + chunk = chunk->root; + while (chunk) { + ufbxi_buf_chunk *next = chunk->next; + ufbx_assert(chunk->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + chunk->magic = 0; + ufbxi_free_size(buf->ator, 1, chunk, sizeof(ufbxi_buf_chunk) + chunk->size); + chunk = next; + } + } + buf->chunks[i] = NULL; + } + buf->pos = 0; + buf->size = 0; + buf->num_items = 0; +} + +static ufbxi_noinline void ufbxi_buf_clear(ufbxi_buf *buf) +{ + // Only unordered or clearable buffers can be cleared + ufbx_assert(!buf->unordered || buf->clearable); + + // Free the memory if using ASAN + if (buf->ator->huge_size <= 1) { + ufbxi_buf_free(buf); + return; + } + + // Reset the non-huge chunks as `chunk->next` is always free. + ufbxi_buf_chunk *chunk = buf->chunks[0]; + if (chunk) { + ufbxi_buf_chunk *root = chunk->root; + buf->chunks[0] = root; + buf->pos = 0; + buf->size = root->size; + } + buf->num_items = 0; + buf->pushed_size = 0; + + // Huge chunks are always sorted by descending size and + // `chunks[1]` points to the largest one. + ufbxi_buf_chunk *huge = buf->chunks[1]; + if (huge) { + // Reset the first N ones that are tracked. + for (size_t i = 0; huge && i < UFBXI_HUGE_MAX_SCAN; i++) { + huge->pushed_pos = 0; + huge = huge->next; + } + + // Got unreachable tail that should be freed: Unlink from the last + // tracked chunk and free the rest. + if (huge) { + huge->prev->next = NULL; + while (huge) { + ufbxi_buf_chunk *next = huge->next; + ufbx_assert(huge->magic == UFBXI_BUF_CHUNK_IMP_MAGIC); + huge->magic = 0; + ufbxi_free_size(buf->ator, 1, huge, sizeof(ufbxi_buf_chunk) + huge->size); + huge = next; + } + } + } +} + +#define ufbxi_push(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size((b), sizeof(type), (n))) +#define ufbxi_push_zero(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_zero((b), sizeof(type), (n))) +#define ufbxi_push_copy(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy((b), sizeof(type), (n), (data))) +#define ufbxi_push_copy_fast(b, type, n, data) ufbxi_maybe_null((type*)ufbxi_push_size_copy_fast((b), sizeof(type), (n), (data))) +#define ufbxi_push_fast(b, type, n) ufbxi_maybe_null((type*)ufbxi_push_size_fast((b), sizeof(type), (n))) +#define ufbxi_pop(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), false) +#define ufbxi_peek(b, type, n, dst) ufbxi_pop_size((b), sizeof(type), (n), (dst), true) +#define ufbxi_push_pop(dst, src, type, n) ufbxi_maybe_null((type*)ufbxi_push_pop_size((dst), (src), sizeof(type), (n))) +#define ufbxi_push_peek(dst, src, type, n) ufbxi_maybe_null((type*)ufbxi_push_peek_size((dst), (src), sizeof(type), (n))) + +// -- Hash map +// +// The actual element comparison is left to the user of `ufbxi_map`, see usage below. +// +// NOTES: +// ufbxi_map_insert() does not support duplicate values, use find first if duplicates are possible! +// Inserting duplicate elements fails with an assertion if `UFBX_REGRESSION` is enabled. + +typedef struct ufbxi_aa_node ufbxi_aa_node; + +typedef int ufbxi_cmp_fn(void *user, const void *a, const void *b); + +struct ufbxi_aa_node { + ufbxi_aa_node *left, *right; + uint32_t level; + uint32_t index; +}; + +typedef struct { + ufbxi_allocator *ator; + size_t data_size; + + void *items; + uint64_t *entries; + uint32_t mask; + + uint32_t capacity; + uint32_t size; + + ufbxi_cmp_fn *cmp_fn; + void *cmp_user; + + ufbxi_buf aa_buf; + ufbxi_aa_node *aa_root; + +} ufbxi_map; + +static ufbxi_noinline void ufbxi_map_init(ufbxi_map *map, ufbxi_allocator *ator, ufbxi_cmp_fn *cmp_fn, void *cmp_user) +{ + map->ator = ator; +#if defined(UFBX_REGRESSION) + // HACK: Maps contain pointers that are not stable between runs, in regression + // mode this causes instability in allocation patterns due to different AA trees + // being built, which is a problem in fuzz checks that need to have deterministic + // allocation counts. We can work around this using a local allocator that doesn't + // count the allocations. + { + ufbxi_allocator *regression_ator = (ufbxi_allocator*)ufbx_malloc(sizeof(ufbxi_allocator)); + ufbx_assert(regression_ator); + memset(regression_ator, 0, sizeof(ufbxi_allocator)); + regression_ator->name = "regression"; + regression_ator->error = ator->error; + regression_ator->huge_size = ator->huge_size; + regression_ator->max_size = SIZE_MAX; + regression_ator->max_allocs = SIZE_MAX; + regression_ator->chunk_max = 0x1000000; + map->aa_buf.ator = regression_ator; + } +#else + map->aa_buf.ator = ator; +#endif + map->cmp_fn = cmp_fn; + map->cmp_user = cmp_user; +} + +static ufbxi_noinline void ufbxi_map_free(ufbxi_map *map) +{ +#if defined(UFBX_REGRESSION) + ufbxi_allocator *regression_ator = map->aa_buf.ator; +#endif + + ufbxi_buf_free(&map->aa_buf); + ufbxi_free(map->ator, char, map->entries, map->data_size); + map->entries = NULL; + map->items = NULL; + map->aa_root = NULL; + map->mask = map->capacity = map->size = 0; + +#if defined(UFBX_REGRESSION) + if (regression_ator) { + ufbxi_free_ator(regression_ator); + ufbx_free(regression_ator, sizeof(ufbxi_allocator)); + } +#endif +} + +// Recursion limit: log2(2^64 / sizeof(ufbxi_aa_node)) +static ufbxi_noinline ufbxi_aa_node *ufbxi_aa_tree_insert(ufbxi_map *map, ufbxi_aa_node *node, const void *value, uint32_t index, size_t item_size) + ufbxi_recursive_function(ufbxi_aa_node *, ufbxi_aa_tree_insert, (map, node, value, index, item_size), 59, + (ufbxi_map *map, ufbxi_aa_node *node, const void *value, uint32_t index, size_t item_size)) +{ + if (!node) { + ufbxi_aa_node *new_node = ufbxi_push(&map->aa_buf, ufbxi_aa_node, 1); + if (!new_node) return NULL; + new_node->left = NULL; + new_node->right = NULL; + new_node->level = 1; + new_node->index = index; + return new_node; + } + + void *entry = (char*)map->items + node->index * item_size; + int cmp = map->cmp_fn(map->cmp_user, value, entry); + if (cmp < 0) { + node->left = ufbxi_aa_tree_insert(map, node->left, value, index, item_size); + } else if (cmp >= 0) { + node->right = ufbxi_aa_tree_insert(map, node->right, value, index, item_size); + } + + if (node->left && node->left->level == node->level) { + ufbxi_aa_node *left = node->left; + node->left = left->right; + left->right = node; + node = left; + } + + if (node->right && node->right->right && node->right->right->level == node->level) { + ufbxi_aa_node *right = node->right; + node->right = right->left; + right->left = node; + right->level += 1; + node = right; + } + + return node; +} + +static ufbxi_noinline void *ufbxi_aa_tree_find(ufbxi_map *map, const void *value, size_t item_size) +{ + ufbxi_aa_node *node = map->aa_root; + while (node) { + void *entry = (char*)map->items + node->index * item_size; + int cmp = map->cmp_fn(map->cmp_user, value, entry); + if (cmp < 0) { + node = node->left; + } else if (cmp > 0) { + node = node->right; + } else { + return entry; + } + } + return NULL; +} + +static ufbxi_noinline bool ufbxi_map_grow_size_imp(ufbxi_map *map, size_t item_size, size_t min_size) +{ + ufbx_assert(min_size > 0); + const double load_factor = 0.7; + + // Find the lowest power of two size that fits `min_size` within `load_factor` + size_t num_entries = map->mask + 1; + size_t new_size = (size_t)((double)num_entries * load_factor); + if (min_size < map->capacity + 1) min_size = map->capacity + 1; + while (new_size < min_size) { + num_entries *= 2; + new_size = (size_t)((double)num_entries * load_factor); + } + + // Check for overflow + ufbxi_check_return_err(map->ator->error, SIZE_MAX / num_entries > sizeof(uint64_t), false); + size_t alloc_size = num_entries * sizeof(uint64_t); + + // Allocate a combined entry/item memory block + ufbxi_check_return_err(map->ator->error, (SIZE_MAX - alloc_size) / new_size > item_size, false); + size_t data_size = alloc_size + new_size * item_size; + + char *data = ufbxi_alloc(map->ator, char, data_size); + ufbxi_check_return_err(map->ator->error, data, false); + + // Copy the previous user items over + uint64_t *old_entries = map->entries; + uint64_t *new_entries = (uint64_t*)data; + void *new_items = data + alloc_size; + if (map->size > 0) { + memcpy(new_items, map->items, item_size * map->size); + } + + // Re-hash the entries + uint32_t old_mask = map->mask; + uint32_t new_mask = (uint32_t)(num_entries) - 1; + memset(new_entries, 0, sizeof(uint64_t) * num_entries); + if (old_mask) { + for (uint32_t i = 0; i <= old_mask; i++) { + uint64_t entry, new_entry = old_entries[i]; + if (!new_entry) continue; + + // Reconstruct the hash of the old entry at `i` + uint32_t old_scan = (uint32_t)(new_entry & old_mask) - 1; + uint32_t hash = ((uint32_t)new_entry & ~old_mask) | ((i - old_scan) & old_mask); + uint32_t slot = hash & new_mask; + new_entry &= ~(uint64_t)new_mask; + + // Scan forward until we find an empty slot, potentially swapping + // `new_element` if it has a shorter scan distance (Robin Hood). + uint32_t scan = 1; + while ((entry = new_entries[slot]) != 0) { + uint32_t entry_scan = (uint32_t)(entry & new_mask); + if (entry_scan < scan) { + new_entries[slot] = new_entry + scan; + new_entry = (entry & ~(uint64_t)new_mask); + scan = entry_scan; + } + scan += 1; + slot = (slot + 1) & new_mask; + } + new_entries[slot] = new_entry + scan; + } + } + + // And finally free the previous allocation + ufbxi_free(map->ator, char, (char*)old_entries, map->data_size); + map->items = new_items; + map->data_size = data_size; + map->entries = new_entries; + map->mask = new_mask; + map->capacity = (uint32_t)new_size; + + return true; +} + +static ufbxi_forceinline bool ufbxi_map_grow_size(ufbxi_map *map, size_t size, size_t min_size) +{ + #if defined(UFBX_REGRESSION) + { + ufbxi_allocator *ator = map->ator; + ufbxi_check_return_err_msg(ator->error, ator->num_allocs < ator->max_allocs, false, "Allocation limit exceeded"); + ator->num_allocs++; + } + #endif + + if (map->size < map->capacity && map->capacity >= min_size) return true; + return ufbxi_map_grow_size_imp(map, size, min_size); +} + +static ufbxi_noinline void *ufbxi_map_find_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value) +{ + uint64_t *entries = map->entries; + uint32_t mask = map->mask, scan = 0; + + uint32_t ref = hash & ~mask; + if (!mask || scan == UINT32_MAX) return 0; + + // Scan entries until we find an exact match of the hash or until we hit + // an element that has lower scan distance than our search (Robin Hood). + // The encoding guarantees that zero slots also terminate with the same test. + for (;;) { + uint64_t entry = entries[(hash + scan) & mask]; + scan += 1; + if ((uint32_t)entry == ref + scan) { + uint32_t index = (uint32_t)(entry >> 32u); + void *data = (char*)map->items + size * index; + int cmp = map->cmp_fn(map->cmp_user, value, data); + if (cmp == 0) return data; + } else if ((entry & mask) < scan) { + if (map->aa_root) { + return ufbxi_aa_tree_find(map, value, size); + } else { + return NULL; + } + } + } +} + +static ufbxi_noinline void *ufbxi_map_insert_size(ufbxi_map *map, size_t size, uint32_t hash, const void *value) +{ + if (!ufbxi_map_grow_size(map, size, 64)) return NULL; + + ufbxi_regression_assert(ufbxi_map_find_size(map, size, hash, value) == NULL); + + uint32_t index = map->size++; + + uint64_t *entries = map->entries; + uint32_t mask = map->mask; + + // Scan forward until we find an empty slot, potentially swapping + // `new_element` if it has a shorter scan distance (Robin Hood). + uint32_t slot = hash & mask; + uint64_t entry, new_entry = (uint64_t)index << 32u | (hash & ~mask); + uint32_t scan = 1; + while ((entry = entries[slot]) != 0) { + uint32_t entry_scan = (uint32_t)(entry & mask); + if (entry_scan < scan) { + entries[slot] = new_entry + scan; + new_entry = (entry & ~(uint64_t)mask); + scan = entry_scan; + } + scan += 1; + slot = (slot + 1) & mask; + + if (scan > UFBXI_MAP_MAX_SCAN) { + uint32_t new_index = (uint32_t)(new_entry >> 32u); + const void *new_value = new_index == index ? value : (const void*)((char*)map->items + size * new_index); + map->aa_root = ufbxi_aa_tree_insert(map, map->aa_root, new_value, new_index, size); + return (char*)map->items + size * index; + } + } + entries[slot] = new_entry + scan; + + return (char*)map->items + size * index; +} + +#define ufbxi_map_grow(map, type, min_size) ufbxi_map_grow_size((map), sizeof(type), (min_size)) +#define ufbxi_map_find(map, type, hash, value) ufbxi_maybe_null((type*)ufbxi_map_find_size((map), sizeof(type), (hash), (value))) +#define ufbxi_map_insert(map, type, hash, value) ufbxi_maybe_null((type*)ufbxi_map_insert_size((map), sizeof(type), (hash), (value))) + +static int ufbxi_map_cmp_uint64(void *user, const void *va, const void *vb) +{ + (void)user; + uint64_t a = *(const uint64_t*)va, b = *(const uint64_t*)vb; + if (a < b) return -1; + if (a > b) return +1; + return 0; +} + +static int ufbxi_map_cmp_const_char_ptr(void *user, const void *va, const void *vb) +{ + (void)user; + const char *a = *(const char **)va, *b = *(const char **)vb; + if (a < b) return -1; + if (a > b) return +1; + return 0; +} + +static int ufbxi_map_cmp_uintptr(void *user, const void *va, const void *vb) +{ + (void)user; + uintptr_t a = *(const uintptr_t*)va, b = *(const uintptr_t*)vb; + if (a < b) return -1; + if (a > b) return +1; + return 0; +} + +typedef struct { + uintptr_t ptr; + uint64_t id; +} ufbxi_ptr_id; + +static int ufbxi_map_cmp_ptr_id(void *user, const void *va, const void *vb) +{ + (void)user; + ufbxi_ptr_id a = *(const ufbxi_ptr_id*)va, b = *(const ufbxi_ptr_id*)vb; + if (a.id != b.id) return a.id < b.id ? -1 : +1; + if (a.ptr != b.ptr) return a.ptr < b.ptr ? -1 : +1; + return 0; +} + +// -- Hash functions + +static ufbxi_noinline uint32_t ufbxi_hash_string(const char *str, size_t length) +{ + uint32_t hash = (uint32_t)length; + uint32_t seed = UINT32_C(0x9e3779b9); + if (length >= 4) { + do { + uint32_t word = ufbxi_read_u32(str); + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + str += 4; + length -= 4; + } while (length >= 4); + + uint32_t word = ufbxi_read_u32(str + length - 4); + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + } else { + uint32_t word = 0; + if (length >= 1) word |= (uint32_t)(uint8_t)str[0] << 0; + if (length >= 2) word |= (uint32_t)(uint8_t)str[1] << 8; + if (length >= 3) word |= (uint32_t)(uint8_t)str[2] << 16; + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + } + hash ^= hash >> 16; + hash *= UINT32_C(0x7feb352d); + hash ^= hash >> 15; + return hash; +} + +// NOTE: _Must_ match `ufbxi_hash_string()` +static ufbxi_noinline uint32_t ufbxi_hash_string_check_ascii(const char *str, size_t length, bool *p_non_ascii) +{ + uint32_t ascii_mask = 0; + uint32_t zero_mask = 0; + + ufbx_assert(length > 0); + + uint32_t hash = (uint32_t)length; + uint32_t seed = UINT32_C(0x9e3779b9); + if (length >= 4) { + do { + uint32_t word = ufbxi_read_u32(str); + ascii_mask |= word; + zero_mask |= UINT32_C(0x80808080) - word; + + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + str += 4; + length -= 4; + } while (length >= 4); + + uint32_t word = ufbxi_read_u32(str + length - 4); + ascii_mask |= word; + zero_mask |= UINT32_C(0x80808080) - word; + + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + } else { + uint32_t word = 0; + if (length >= 1) word |= (uint32_t)(uint8_t)str[0] << 0; + if (length >= 2) word |= (uint32_t)(uint8_t)str[1] << 8; + if (length >= 3) word |= (uint32_t)(uint8_t)str[2] << 16; + + ascii_mask |= word; + zero_mask |= (UINT32_C(0x80808080) >> ((4u - length) * 8u)) - word; + + hash = ((hash << 5u | hash >> 27u) ^ word) * seed; + } + + // If any character has high bit set or is zero we're not ASCII + if (((ascii_mask | zero_mask) & 0x80808080u) != 0) { + *p_non_ascii = true; + } + + hash ^= hash >> 16; + hash *= UINT32_C(0x7feb352d); + hash ^= hash >> 15; + + return hash; +} + +static ufbxi_unused ufbxi_forceinline uint32_t ufbxi_hash32(uint32_t x) +{ + x ^= x >> 16; + x *= UINT32_C(0x7feb352d); + x ^= x >> 15; + x *= UINT32_C(0x846ca68b); + x ^= x >> 16; + return x; +} + +static ufbxi_unused ufbxi_forceinline uint32_t ufbxi_hash64(uint64_t x) +{ + x ^= x >> 32; + x *= UINT64_C(0xd6e8feb86659fd93); + x ^= x >> 32; + x *= UINT64_C(0xd6e8feb86659fd93); + x ^= x >> 32; + return (uint32_t)x; +} + +static ufbxi_forceinline uint32_t ufbxi_hash_uptr(uintptr_t ptr) +{ +#if UFBXI_UINTPTR_SIZE == 8 + return ufbxi_hash64((uint64_t)ptr); +#elif UFBXI_UINTPTR_SIZE == 4 + return ufbxi_hash32((uint32_t)ptr); +#else + if (sizeof(ptr) == 8) return ufbxi_hash64((uint64_t)ptr); + else if (sizeof(ptr) == 4) return ufbxi_hash32((uint32_t)ptr); + else return ufbxi_hash_string((const char*)&ptr, sizeof(uintptr_t)); +#endif +} + +static ufbxi_forceinline uint32_t ufbxi_hash_ptr_id(ufbxi_ptr_id id) +{ + // Trivial reduction is fine: Only `ptr` or `id` is defined. + return ufbxi_hash_uptr(id.ptr) ^ ufbxi_hash64(id.id); +} + +#define ufbxi_hash_ptr(ptr) ufbxi_hash_uptr((uintptr_t)(ptr)) + +// -- Warnings + +typedef struct { + ufbx_error *error; + ufbxi_buf *result; + ufbxi_buf tmp_stack; + uint32_t deferred_element_id_plus_one; + // Separate lists for specific and non-specific warnings + ufbx_warning *prev_warnings[UFBX_WARNING_TYPE_COUNT][2]; +} ufbxi_warnings; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_vwarnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, va_list args) +{ + if (!ws) return 1; + + // HACK(warning-element): Encode potential deferred element ID into `ufbx_warning.element_id`, + // `ws->element_id_index_plus_one` contains index to `uc->tmp_element_id`. + // Tag deferred indices with the high bit. + if (element_id == ~0u && ws->deferred_element_id_plus_one > 0) { + element_id = (ws->deferred_element_id_plus_one - 1) | 0x80000000u; + } + + uint32_t has_element_id = element_id != ~0u; + if (type >= UFBX_WARNING_TYPE_FIRST_DEDUPLICATED) { + ufbx_warning *prev = ws->prev_warnings[type][has_element_id]; + if (prev && prev->element_id == element_id) { + prev->count++; + return 1; + } + } + + char desc[256]; + size_t desc_len = (size_t)ufbxi_vsnprintf(desc, sizeof(desc), fmt, args); + + ufbxi_clean_string_utf8(desc, desc_len); + + char *desc_copy = ufbxi_push_copy(ws->result, char, desc_len + 1, desc); + ufbxi_check_err(ws->error, desc_copy); + + ufbx_warning *warning = ufbxi_push(&ws->tmp_stack, ufbx_warning, 1); + ufbxi_check_err(ws->error, warning); + + warning->type = type; + warning->description.data = desc_copy; + warning->description.length = desc_len; + warning->element_id = element_id; + warning->count = 1; + ws->prev_warnings[type][has_element_id] = warning; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_warnf_imp(ufbxi_warnings *ws, ufbx_warning_type type, uint32_t element_id, const char *fmt, ...) +{ + // NOTE: `ws` may be `NULL` here, handled by `ufbxi_vwarnf()` + va_list args; // ufbxi_uninit + va_start(args, fmt); + int ok = ufbxi_vwarnf_imp(ws, type, element_id, fmt, args); + va_end(args); + return ok; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_pop_warnings(ufbxi_warnings *ws, ufbx_warning_list *warnings, bool *p_has_warning) +{ + warnings->count = ws->tmp_stack.num_items; + warnings->data = ufbxi_push_pop(ws->result, &ws->tmp_stack, ufbx_warning, warnings->count); + ufbxi_check_err(ws->error, warnings->data); + ufbxi_for_list(ufbx_warning, warning, *warnings) { + p_has_warning[warning->type] = true; + } + return 1; +} + +// -- String pool + +// All strings found in FBX files are interned for deduplication and fast +// comparison. Our fixed internal strings (`ufbxi_String`) are considered the +// canonical pointers for said strings so we can compare them by address. + +typedef struct { + ufbx_error *error; + ufbxi_buf buf; // < Buffer for the actual string data + ufbxi_map map; // < Map of `ufbxi_string` + size_t initial_size; // < Number of initial entries + char *temp_str; // < Temporary string buffer of `temp_cap` + size_t temp_cap; // < Capacity of the temporary buffer + ufbx_unicode_error_handling error_handling; + ufbxi_warnings *warnings; +} ufbxi_string_pool; + +typedef struct { + const char *raw_data; // < UTF-8 data follows at `raw_length+1` if `utf8_length > 0` + uint32_t raw_length; // < Length of the non-sanitized original string + uint32_t utf8_length; // < Length of sanitized UTF-8 string (or zero) +} ufbxi_sanitized_string; + +static ufbxi_forceinline bool ufbxi_str_equal(ufbx_string a, ufbx_string b) +{ + return a.length == b.length && !memcmp(a.data, b.data, a.length); +} + +static ufbxi_forceinline bool ufbxi_str_less(ufbx_string a, ufbx_string b) +{ + size_t len = ufbxi_min_sz(a.length, b.length); + int cmp = memcmp(a.data, b.data, len); + if (cmp != 0) return cmp < 0; + return a.length < b.length; +} + +static ufbxi_forceinline int ufbxi_str_cmp(ufbx_string a, ufbx_string b) +{ + size_t len = ufbxi_min_sz(a.length, b.length); + int cmp = memcmp(a.data, b.data, len); + if (cmp != 0) return cmp; + if (a.length != b.length) return a.length < b.length ? -1 : 1; + return 0; +} + +static ufbxi_forceinline ufbx_string ufbxi_str_c(const char *str) +{ + ufbx_string s = { str, strlen(str) }; + return s; +} + +static ufbxi_noinline uint32_t ufbxi_get_concat_key(const ufbx_string *parts, size_t num_parts) +{ + uint32_t key = 0, shift = 32; + ufbxi_for(const ufbx_string, part, parts, num_parts) { + size_t length = part->length != SIZE_MAX ? part->length : strlen(part->data); + for (size_t i = 0; i < length; i++) { + shift -= 8; + key |= (uint32_t)(uint8_t)part->data[i] << shift; + if (shift == 0) return key; + } + } + return key; +} + +static ufbxi_noinline int ufbxi_concat_str_cmp(const ufbx_string *ref, const ufbx_string *parts, size_t num_parts) +{ + const char *ptr = ref->data, *end = ptr + ref->length; + ufbxi_for(const ufbx_string, part, parts, num_parts) { + size_t length = part->length != SIZE_MAX ? part->length : strlen(part->data); + size_t to_cmp = ufbxi_min_sz(ufbxi_to_size(end - ptr), length); + int cmp = to_cmp > 0 ? memcmp(ptr, part->data, to_cmp) : 0; + if (cmp != 0) return cmp; + if (to_cmp != length) return -1; + ptr += length; + } + return ptr == end ? 0 : +1; +} + +static ufbxi_forceinline bool ufbxi_starts_with(ufbx_string str, ufbx_string prefix) +{ + return str.length >= prefix.length && !memcmp(str.data, prefix.data, prefix.length); +} + +static ufbxi_forceinline bool ufbxi_ends_with(ufbx_string str, ufbx_string suffix) +{ + return str.length >= suffix.length && !memcmp(str.data + str.length - suffix.length, suffix.data, suffix.length); +} + +static ufbxi_noinline bool ufbxi_remove_prefix_len(ufbx_string *str, const char *prefix, size_t prefix_len) +{ + ufbx_string prefix_str = { prefix, prefix_len }; + if (ufbxi_starts_with(*str, prefix_str)) { + str->data += prefix_len; + str->length -= prefix_len; + return true; + } + return false; +} + +static ufbxi_noinline bool ufbxi_remove_suffix_len(ufbx_string *str, const char *suffix, size_t suffix_len) +{ + ufbx_string suffix_str = { suffix, suffix_len }; + if (ufbxi_ends_with(*str, suffix_str)) { + str->length -= suffix_len; + return true; + } + return false; +} + +static ufbxi_forceinline bool ufbxi_remove_prefix_str(ufbx_string *str, ufbx_string prefix) +{ + return ufbxi_remove_prefix_len(str, prefix.data, prefix.length); +} + +static ufbxi_forceinline bool ufbxi_remove_suffix_c(ufbx_string *str, const char *suffix) +{ + return ufbxi_remove_suffix_len(str, suffix, strlen(suffix)); +} + +static int ufbxi_map_cmp_string(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_string *a = (const ufbx_string*)va, *b = (const ufbx_string*)vb; + return ufbxi_str_cmp(*a, *b); +} + +static ufbxi_forceinline ufbx_string ufbxi_safe_string(const char *data, size_t length) +{ + ufbx_string str = { length > 0 ? data : ufbxi_empty_char, length }; + return str; +} + +static void ufbxi_string_pool_temp_free(ufbxi_string_pool *pool) +{ + ufbxi_free(pool->map.ator, char, pool->temp_str, pool->temp_cap); + ufbxi_map_free(&pool->map); +} + +ufbxi_nodiscard static size_t ufbxi_add_replacement_char(ufbxi_string_pool *pool, char *dst, char c) +{ + switch (pool->error_handling) { + + case UFBX_UNICODE_ERROR_HANDLING_REPLACEMENT_CHARACTER: + dst[0] = (char)(uint8_t)0xefu; + dst[1] = (char)(uint8_t)0xbfu; + dst[2] = (char)(uint8_t)0xbdu; + return 3; + + case UFBX_UNICODE_ERROR_HANDLING_UNDERSCORE: + dst[0] = '_'; + return 1; + + case UFBX_UNICODE_ERROR_HANDLING_QUESTION_MARK: + dst[0] = '?'; + return 1; + + case UFBX_UNICODE_ERROR_HANDLING_REMOVE: + return 0; + + case UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE: + dst[0] = c; + return 1; + + default: + return 0; + + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_sanitize_string(ufbxi_string_pool *pool, ufbxi_sanitized_string *sanitized, const char *str, size_t length, size_t valid_length, bool push_both) +{ + // Handle only invalid cases here + ufbx_assert(valid_length < length); + ufbxi_check_err_msg(pool->error, pool->error_handling != UFBX_UNICODE_ERROR_HANDLING_ABORT_LOADING, "Invalid UTF-8"); + ufbxi_check_err(pool->error, ufbxi_warnf_imp(pool->warnings, UFBX_WARNING_BAD_UNICODE, ~0u, "Bad UTF-8 string")); + + size_t index = valid_length; + size_t dst_len = index; + if (push_both) { + // Copy both the full raw string and the initial valid part + ufbxi_check_err(pool->error, length <= SIZE_MAX / 2 - 64); + ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, length * 2 + 64)); + memcpy(pool->temp_str, str, length); + pool->temp_str[length] = '\0'; + memcpy(pool->temp_str + length + 1, str, index); + dst_len += length + 1; + } else { + + // Copy the initial valid part + ufbxi_check_err(pool->error, length <= SIZE_MAX - 64); + ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, length + 64)); + memcpy(pool->temp_str, str, index); + } + + char *dst = pool->temp_str; + while (index < length) { + uint8_t c = (uint8_t)str[index]; + size_t left = length - index; + + // Not optimal but not the worst thing ever + if (pool->temp_cap - dst_len < 16) { + ufbxi_check_err(pool->error, ufbxi_grow_array(pool->map.ator, &pool->temp_str, &pool->temp_cap, dst_len + 16)); + dst = pool->temp_str; + } + + if ((c & 0x80) == 0) { + if (c != 0) { + dst[dst_len] = (char)c; + dst_len += 1; + index += 1; + continue; + } + } else if ((c & 0xe0) == 0xc0 && left >= 2) { + uint8_t t0 = (uint8_t)str[index + 1]; + uint32_t code = (uint32_t)c << 8 | (uint32_t)t0 << 0; + if ((code & 0xc0) == 0x80 && code >= 0xc280) { + dst[dst_len + 0] = (char)c; + dst[dst_len + 1] = (char)t0; + dst_len += 2; + index += 2; + continue; + } + } else if ((c & 0xf0) == 0xe0 && left >= 3) { + uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2]; + uint32_t code = (uint32_t)c << 16 | (uint32_t)t0 << 8 | (uint32_t)t1; + if ((code & 0xc0c0) == 0x8080 && code >= 0xe0a080 && (code < 0xeda080 || code >= 0xee8080)) { + dst[dst_len + 0] = (char)c; + dst[dst_len + 1] = (char)t0; + dst[dst_len + 2] = (char)t1; + dst_len += 3; + index += 3; + continue; + } + } else if ((c & 0xf8) == 0xf0 && left >= 4) { + uint8_t t0 = (uint8_t)str[index + 1], t1 = (uint8_t)str[index + 2], t2 = (uint8_t)str[index + 3]; + uint32_t code = (uint32_t)c << 24 | (uint32_t)t0 << 16 | (uint32_t)t1 << 8 | (uint32_t)t2; + if ((code & 0xc0c0c0) == 0x808080 && code >= 0xf0908080u && code <= 0xf48fbfbfu) { + dst[dst_len + 0] = (char)c; + dst[dst_len + 1] = (char)t0; + dst[dst_len + 2] = (char)t1; + dst[dst_len + 3] = (char)t2; + dst_len += 4; + index += 4; + continue; + } + } + + dst_len += ufbxi_add_replacement_char(pool, dst + dst_len, (char)c); + index++; + } + + // Sanitized strings are packed to 32-bit integers, in practice this should be fine + // as strings are limited to 32-bit length in FBX itself. + // The only problem case is a massive string that is full of unicode errors, ie. + // >1GB binary blob, but these should never be sanitized. + ufbxi_check_err(pool->error, length <= UINT32_MAX); + sanitized->raw_data = pool->temp_str; + if (push_both) { + // Reserve `UINT32_MAX` for invalid UTF-8 without sanitization + size_t utf8_length = dst_len - (length + 1); + ufbxi_check_err(pool->error, utf8_length < UINT32_MAX); + sanitized->raw_length = (uint32_t)length; + sanitized->utf8_length = (uint32_t)utf8_length; + } else { + ufbxi_check_err(pool->error, dst_len <= UINT32_MAX); + sanitized->raw_length = (uint32_t)dst_len; + sanitized->utf8_length = 0; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_sanitized_string(ufbxi_string_pool *pool, ufbxi_sanitized_string *sanitized, const char *str, size_t length, uint32_t hash, bool raw) +{ + ufbxi_regression_assert(hash == ufbxi_hash_string(str, length)); + + ufbxi_check_err(pool->error, length <= UINT32_MAX); + ufbxi_check_err(pool->error, ufbxi_map_grow(&pool->map, ufbx_string, pool->initial_size)); + + const char *total_data = str; + size_t total_length = length; + + sanitized->raw_length = (uint32_t)length; + sanitized->utf8_length = 0; + + if (!raw) { + size_t valid_length = ufbxi_utf8_valid_length(str, length); + if (valid_length != length) { + ufbxi_check_err(pool->error, ufbxi_sanitize_string(pool, sanitized, str, length, valid_length, true)); + total_data = sanitized->raw_data; + total_length = sanitized->raw_length + sanitized->utf8_length + 1; + hash = ufbxi_hash_string(str, length); + } + } + + ufbx_string ref = { total_data, total_length }; + + ufbx_string *entry = ufbxi_map_find(&pool->map, ufbx_string, hash, &ref); + if (entry) { + sanitized->raw_data = entry->data; + } else { + entry = ufbxi_map_insert(&pool->map, ufbx_string, hash, &ref); + ufbxi_check_err(pool->error, entry); + entry->length = total_length; + char *dst = ufbxi_push(&pool->buf, char, total_length + 1); + ufbxi_check_err(pool->error, dst); + memcpy(dst, total_data, total_length); + dst[total_length] = '\0'; + entry->data = dst; + sanitized->raw_data = dst; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_push_string_imp(ufbxi_string_pool *pool, const char *str, size_t length, size_t *p_out_length, bool copy, bool raw) +{ + if (length == 0) return ufbxi_empty_char; + + ufbxi_check_return_err(pool->error, ufbxi_map_grow(&pool->map, ufbx_string, pool->initial_size), NULL); + + uint32_t hash; + if (raw) { + hash = ufbxi_hash_string(str, length); + } else { + bool non_ascii = false; + hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii); + if (non_ascii) { + size_t valid_length = ufbxi_utf8_valid_length(str, length); + if (valid_length < length) { + ufbxi_sanitized_string sanitized; + ufbxi_check_return_err(pool->error, ufbxi_sanitize_string(pool, &sanitized, str, length, valid_length, false), NULL); + str = sanitized.raw_data; + length = sanitized.raw_length; + hash = ufbxi_hash_string(str, length); + *p_out_length = length; + } + } + } + + ufbx_string ref = { str, length }; + + ufbx_string *entry = ufbxi_map_find(&pool->map, ufbx_string, hash, &ref); + if (entry) return entry->data; + entry = ufbxi_map_insert(&pool->map, ufbx_string, hash, &ref); + ufbxi_check_return_err(pool->error, entry, NULL); + entry->length = length; + if (copy) { + char *dst = ufbxi_push(&pool->buf, char, length + 1); + ufbxi_check_return_err(pool->error, dst, NULL); + memcpy(dst, str, length); + dst[length] = '\0'; + entry->data = dst; + } else { + entry->data = str; + } + return entry->data; +} + +ufbxi_nodiscard static ufbxi_forceinline const char *ufbxi_push_string(ufbxi_string_pool *pool, const char *str, size_t length, size_t *p_out_length, bool raw) +{ + return ufbxi_push_string_imp(pool, str, length, p_out_length, true, raw); +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_push_string_place(ufbxi_string_pool *pool, const char **p_str, size_t *p_length, bool raw) +{ + const char *str = *p_str; + size_t length = *p_length; + ufbxi_check_err(pool->error, str || length == 0); + str = ufbxi_push_string(pool, str, length, p_length, raw); + ufbxi_check_err(pool->error, str); + *p_str = str; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_str(ufbxi_string_pool *pool, ufbx_string *p_str, bool raw) +{ + ufbxi_check_err(pool->error, p_str); + return ufbxi_push_string_place(pool, &p_str->data, &p_str->length, raw); +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_string_place_blob(ufbxi_string_pool *pool, ufbx_blob *p_blob, bool raw) +{ + if (p_blob->size == 0) { + p_blob->data = NULL; + return 1; + } + p_blob->data = ufbxi_push_string(pool, (const char*)p_blob->data, p_blob->size, &p_blob->size, raw); + ufbxi_check_err(pool->error, p_blob->data); + return 1; +} + +// -- String constants +// +// All strings in FBX files are pooled so by having canonical string constant +// addresses we can compare strings to these constants by comparing pointers. +// Keep the list alphabetically sorted! + +static const char ufbxi_AllSame[] = "AllSame"; +static const char ufbxi_Alphas[] = "Alphas"; +static const char ufbxi_AmbientColor[] = "AmbientColor"; +static const char ufbxi_AnimationCurveNode[] = "AnimationCurveNode"; +static const char ufbxi_AnimationCurve[] = "AnimationCurve"; +static const char ufbxi_AnimationLayer[] = "AnimationLayer"; +static const char ufbxi_AnimationStack[] = "AnimationStack"; +static const char ufbxi_ApertureFormat[] = "ApertureFormat"; +static const char ufbxi_ApertureMode[] = "ApertureMode"; +static const char ufbxi_AreaLightShape[] = "AreaLightShape"; +static const char ufbxi_AspectH[] = "AspectH"; +static const char ufbxi_AspectHeight[] = "AspectHeight"; +static const char ufbxi_AspectRatioMode[] = "AspectRatioMode"; +static const char ufbxi_AspectW[] = "AspectW"; +static const char ufbxi_AspectWidth[] = "AspectWidth"; +static const char ufbxi_Audio[] = "Audio"; +static const char ufbxi_AudioLayer[] = "AudioLayer"; +static const char ufbxi_BaseLayer[] = "BaseLayer"; +static const char ufbxi_BinaryData[] = "BinaryData"; +static const char ufbxi_BindPose[] = "BindPose"; +static const char ufbxi_BindingTable[] = "BindingTable"; +static const char ufbxi_Binormals[] = "Binormals"; +static const char ufbxi_BinormalsIndex[] = "BinormalsIndex"; +static const char ufbxi_BinormalsW[] = "BinormalsW"; +static const char ufbxi_BlendMode[] = "BlendMode"; +static const char ufbxi_BlendModes[] = "BlendModes"; +static const char ufbxi_BlendShapeChannel[] = "BlendShapeChannel"; +static const char ufbxi_BlendShape[] = "BlendShape"; +static const char ufbxi_BlendWeights[] = "BlendWeights"; +static const char ufbxi_BoundaryRule[] = "BoundaryRule"; +static const char ufbxi_Boundary[] = "Boundary"; +static const char ufbxi_ByEdge[] = "ByEdge"; +static const char ufbxi_ByPolygonVertex[] = "ByPolygonVertex"; +static const char ufbxi_ByPolygon[] = "ByPolygon"; +static const char ufbxi_ByVertex[] = "ByVertex"; +static const char ufbxi_ByVertice[] = "ByVertice"; +static const char ufbxi_Cache[] = "Cache"; +static const char ufbxi_CameraProjectionType[] = "CameraProjectionType"; +static const char ufbxi_CameraStereo[] = "CameraStereo"; +static const char ufbxi_CameraSwitcher[] = "CameraSwitcher"; +static const char ufbxi_Camera[] = "Camera"; +static const char ufbxi_CastLight[] = "CastLight"; +static const char ufbxi_CastShadows[] = "CastShadows"; +static const char ufbxi_Channel[] = "Channel"; +static const char ufbxi_Character[] = "Character"; +static const char ufbxi_Children[] = "Children"; +static const char ufbxi_Cluster[] = "Cluster"; +static const char ufbxi_CollectionExclusive[] = "CollectionExclusive"; +static const char ufbxi_Collection[] = "Collection"; +static const char ufbxi_ColorIndex[] = "ColorIndex"; +static const char ufbxi_Color[] = "Color"; +static const char ufbxi_Colors[] = "Colors"; +static const char ufbxi_Cone_angle[] = "Cone angle"; +static const char ufbxi_ConeAngle[] = "ConeAngle"; +static const char ufbxi_Connections[] = "Connections"; +static const char ufbxi_Constraint[] = "Constraint"; +static const char ufbxi_Content[] = "Content"; +static const char ufbxi_CoordAxisSign[] = "CoordAxisSign"; +static const char ufbxi_CoordAxis[] = "CoordAxis"; +static const char ufbxi_Count[] = "Count"; +static const char ufbxi_Creator[] = "Creator"; +static const char ufbxi_CurrentTextureBlendMode[] = "CurrentTextureBlendMode"; +static const char ufbxi_CurrentTimeMarker[] = "CurrentTimeMarker"; +static const char ufbxi_CustomFrameRate[] = "CustomFrameRate"; +static const char ufbxi_DecayType[] = "DecayType"; +static const char ufbxi_DefaultCamera[] = "DefaultCamera"; +static const char ufbxi_Default[] = "Default"; +static const char ufbxi_Definitions[] = "Definitions"; +static const char ufbxi_DeformPercent[] = "DeformPercent"; +static const char ufbxi_Deformer[] = "Deformer"; +static const char ufbxi_DiffuseColor[] = "DiffuseColor"; +static const char ufbxi_Dimension[] = "Dimension"; +static const char ufbxi_Dimensions[] = "Dimensions"; +static const char ufbxi_DisplayLayer[] = "DisplayLayer"; +static const char ufbxi_Document[] = "Document"; +static const char ufbxi_Documents[] = "Documents"; +static const char ufbxi_EdgeCrease[] = "EdgeCrease"; +static const char ufbxi_EdgeIndexArray[] = "EdgeIndexArray"; +static const char ufbxi_Edges[] = "Edges"; +static const char ufbxi_EmissiveColor[] = "EmissiveColor"; +static const char ufbxi_Entry[] = "Entry"; +static const char ufbxi_FBXHeaderExtension[] = "FBXHeaderExtension"; +static const char ufbxi_FBXHeaderVersion[] = "FBXHeaderVersion"; +static const char ufbxi_FBXVersion[] = "FBXVersion"; +static const char ufbxi_FKEffector[] = "FKEffector"; +static const char ufbxi_FarPlane[] = "FarPlane"; +static const char ufbxi_FbxPropertyEntry[] = "FbxPropertyEntry"; +static const char ufbxi_FbxSemanticEntry[] = "FbxSemanticEntry"; +static const char ufbxi_FieldOfViewX[] = "FieldOfViewX"; +static const char ufbxi_FieldOfViewY[] = "FieldOfViewY"; +static const char ufbxi_FieldOfView[] = "FieldOfView"; +static const char ufbxi_FileName[] = "FileName"; +static const char ufbxi_Filename[] = "Filename"; +static const char ufbxi_FilmHeight[] = "FilmHeight"; +static const char ufbxi_FilmSqueezeRatio[] = "FilmSqueezeRatio"; +static const char ufbxi_FilmWidth[] = "FilmWidth"; +static const char ufbxi_FlipNormals[] = "FlipNormals"; +static const char ufbxi_FocalLength[] = "FocalLength"; +static const char ufbxi_Form[] = "Form"; +static const char ufbxi_Freeze[] = "Freeze"; +static const char ufbxi_FrontAxisSign[] = "FrontAxisSign"; +static const char ufbxi_FrontAxis[] = "FrontAxis"; +static const char ufbxi_FullWeights[] = "FullWeights"; +static const char ufbxi_GateFit[] = "GateFit"; +static const char ufbxi_GeometricRotation[] = "GeometricRotation"; +static const char ufbxi_GeometricScaling[] = "GeometricScaling"; +static const char ufbxi_GeometricTranslation[] = "GeometricTranslation"; +static const char ufbxi_GeometryUVInfo[] = "GeometryUVInfo"; +static const char ufbxi_Geometry[] = "Geometry"; +static const char ufbxi_GlobalSettings[] = "GlobalSettings"; +static const char ufbxi_Hole[] = "Hole"; +static const char ufbxi_HotSpot[] = "HotSpot"; +static const char ufbxi_IKEffector[] = "IKEffector"; +static const char ufbxi_ImageData[] = "ImageData"; +static const char ufbxi_Implementation[] = "Implementation"; +static const char ufbxi_Indexes[] = "Indexes"; +static const char ufbxi_InheritType[] = "InheritType"; +static const char ufbxi_InnerAngle[] = "InnerAngle"; +static const char ufbxi_Intensity[] = "Intensity"; +static const char ufbxi_IsTheNodeInSet[] = "IsTheNodeInSet"; +static const char ufbxi_KeyAttrDataFloat[] = "KeyAttrDataFloat"; +static const char ufbxi_KeyAttrFlags[] = "KeyAttrFlags"; +static const char ufbxi_KeyAttrRefCount[] = "KeyAttrRefCount"; +static const char ufbxi_KeyCount[] = "KeyCount"; +static const char ufbxi_KeyTime[] = "KeyTime"; +static const char ufbxi_KeyValueFloat[] = "KeyValueFloat"; +static const char ufbxi_Key[] = "Key"; +static const char ufbxi_KnotVectorU[] = "KnotVectorU"; +static const char ufbxi_KnotVectorV[] = "KnotVectorV"; +static const char ufbxi_KnotVector[] = "KnotVector"; +static const char ufbxi_LayerElementBinormal[] = "LayerElementBinormal"; +static const char ufbxi_LayerElementColor[] = "LayerElementColor"; +static const char ufbxi_LayerElementEdgeCrease[] = "LayerElementEdgeCrease"; +static const char ufbxi_LayerElementHole[] = "LayerElementHole"; +static const char ufbxi_LayerElementMaterial[] = "LayerElementMaterial"; +static const char ufbxi_LayerElementNormal[] = "LayerElementNormal"; +static const char ufbxi_LayerElementPolygonGroup[] = "LayerElementPolygonGroup"; +static const char ufbxi_LayerElementSmoothing[] = "LayerElementSmoothing"; +static const char ufbxi_LayerElementTangent[] = "LayerElementTangent"; +static const char ufbxi_LayerElementUV[] = "LayerElementUV"; +static const char ufbxi_LayerElementVertexCrease[] = "LayerElementVertexCrease"; +static const char ufbxi_LayerElementVisibility[] = "LayerElementVisibility"; +static const char ufbxi_LayerElement[] = "LayerElement"; +static const char ufbxi_Layer[] = "Layer"; +static const char ufbxi_LayeredTexture[] = "LayeredTexture"; +static const char ufbxi_Lcl_Rotation[] = "Lcl Rotation"; +static const char ufbxi_Lcl_Scaling[] = "Lcl Scaling"; +static const char ufbxi_Lcl_Translation[] = "Lcl Translation"; +static const char ufbxi_LeftCamera[] = "LeftCamera"; +static const char ufbxi_LightType[] = "LightType"; +static const char ufbxi_Light[] = "Light"; +static const char ufbxi_LimbLength[] = "LimbLength"; +static const char ufbxi_LimbNode[] = "LimbNode"; +static const char ufbxi_Limb[] = "Limb"; +static const char ufbxi_Line[] = "Line"; +static const char ufbxi_Link[] = "Link"; +static const char ufbxi_LocalStart[] = "LocalStart"; +static const char ufbxi_LocalStop[] = "LocalStop"; +static const char ufbxi_LocalTime[] = "LocalTime"; +static const char ufbxi_LodGroup[] = "LodGroup"; +static const char ufbxi_MappingInformationType[] = "MappingInformationType"; +static const char ufbxi_Marker[] = "Marker"; +static const char ufbxi_MaterialAssignation[] = "MaterialAssignation"; +static const char ufbxi_Material[] = "Material"; +static const char ufbxi_Materials[] = "Materials"; +static const char ufbxi_Matrix[] = "Matrix"; +static const char ufbxi_Media[] = "Media"; +static const char ufbxi_Mesh[] = "Mesh"; +static const char ufbxi_Model[] = "Model"; +static const char ufbxi_Name[] = "Name"; +static const char ufbxi_NearPlane[] = "NearPlane"; +static const char ufbxi_NodeAttributeName[] = "NodeAttributeName"; +static const char ufbxi_NodeAttribute[] = "NodeAttribute"; +static const char ufbxi_Node[] = "Node"; +static const char ufbxi_Normals[] = "Normals"; +static const char ufbxi_NormalsIndex[] = "NormalsIndex"; +static const char ufbxi_NormalsW[] = "NormalsW"; +static const char ufbxi_Null[] = "Null"; +static const char ufbxi_NurbsCurve[] = "NurbsCurve"; +static const char ufbxi_NurbsSurfaceOrder[] = "NurbsSurfaceOrder"; +static const char ufbxi_NurbsSurface[] = "NurbsSurface"; +static const char ufbxi_Nurbs[] = "Nurbs"; +static const char ufbxi_OO[] = "OO\0"; +static const char ufbxi_OP[] = "OP\0"; +static const char ufbxi_ObjectMetaData[] = "ObjectMetaData"; +static const char ufbxi_ObjectType[] = "ObjectType"; +static const char ufbxi_Objects[] = "Objects"; +static const char ufbxi_Order[] = "Order"; +static const char ufbxi_OriginalUnitScaleFactor[] = "OriginalUnitScaleFactor"; +static const char ufbxi_OriginalUpAxis[] = "OriginalUpAxis"; +static const char ufbxi_OriginalUpAxisSign[] = "OriginalUpAxisSign"; +static const char ufbxi_OrthoZoom[] = "OrthoZoom"; +static const char ufbxi_OtherFlags[] = "OtherFlags"; +static const char ufbxi_OuterAngle[] = "OuterAngle"; +static const char ufbxi_PO[] = "PO\0"; +static const char ufbxi_PP[] = "PP\0"; +static const char ufbxi_PointsIndex[] = "PointsIndex"; +static const char ufbxi_Points[] = "Points"; +static const char ufbxi_PolygonGroup[] = "PolygonGroup"; +static const char ufbxi_PolygonIndexArray[] = "PolygonIndexArray"; +static const char ufbxi_PolygonVertexIndex[] = "PolygonVertexIndex"; +static const char ufbxi_PoseNode[] = "PoseNode"; +static const char ufbxi_Pose[] = "Pose"; +static const char ufbxi_Post_Extrapolation[] = "Post-Extrapolation"; +static const char ufbxi_PostRotation[] = "PostRotation"; +static const char ufbxi_Pre_Extrapolation[] = "Pre-Extrapolation"; +static const char ufbxi_PreRotation[] = "PreRotation"; +static const char ufbxi_PreviewDivisionLevels[] = "PreviewDivisionLevels"; +static const char ufbxi_Properties60[] = "Properties60"; +static const char ufbxi_Properties70[] = "Properties70"; +static const char ufbxi_PropertyTemplate[] = "PropertyTemplate"; +static const char ufbxi_R[] = "R\0\0"; +static const char ufbxi_ReferenceStart[] = "ReferenceStart"; +static const char ufbxi_ReferenceStop[] = "ReferenceStop"; +static const char ufbxi_ReferenceTime[] = "ReferenceTime"; +static const char ufbxi_RelativeFileName[] = "RelativeFileName"; +static const char ufbxi_RelativeFilename[] = "RelativeFilename"; +static const char ufbxi_RenderDivisionLevels[] = "RenderDivisionLevels"; +static const char ufbxi_Repetition[] = "Repetition"; +static const char ufbxi_RightCamera[] = "RightCamera"; +static const char ufbxi_RootNode[] = "RootNode"; +static const char ufbxi_Root[] = "Root"; +static const char ufbxi_RotationAccumulationMode[] = "RotationAccumulationMode"; +static const char ufbxi_RotationOffset[] = "RotationOffset"; +static const char ufbxi_RotationOrder[] = "RotationOrder"; +static const char ufbxi_RotationPivot[] = "RotationPivot"; +static const char ufbxi_Rotation[] = "Rotation"; +static const char ufbxi_S[] = "S\0\0"; +static const char ufbxi_ScaleAccumulationMode[] = "ScaleAccumulationMode"; +static const char ufbxi_ScalingOffset[] = "ScalingOffset"; +static const char ufbxi_ScalingPivot[] = "ScalingPivot"; +static const char ufbxi_Scaling[] = "Scaling"; +static const char ufbxi_SceneInfo[] = "SceneInfo"; +static const char ufbxi_SelectionNode[] = "SelectionNode"; +static const char ufbxi_SelectionSet[] = "SelectionSet"; +static const char ufbxi_ShadingModel[] = "ShadingModel"; +static const char ufbxi_Shape[] = "Shape"; +static const char ufbxi_Shininess[] = "Shininess"; +static const char ufbxi_Show[] = "Show"; +static const char ufbxi_Size[] = "Size"; +static const char ufbxi_Skin[] = "Skin"; +static const char ufbxi_SkinningType[] = "SkinningType"; +static const char ufbxi_Smoothing[] = "Smoothing"; +static const char ufbxi_Smoothness[] = "Smoothness"; +static const char ufbxi_SnapOnFrameMode[] = "SnapOnFrameMode"; +static const char ufbxi_SpecularColor[] = "SpecularColor"; +static const char ufbxi_Step[] = "Step"; +static const char ufbxi_SubDeformer[] = "SubDeformer"; +static const char ufbxi_T[] = "T\0\0"; +static const char ufbxi_TCDefinition[] = "TCDefinition"; +static const char ufbxi_Take[] = "Take"; +static const char ufbxi_Takes[] = "Takes"; +static const char ufbxi_Tangents[] = "Tangents"; +static const char ufbxi_TangentsIndex[] = "TangentsIndex"; +static const char ufbxi_TangentsW[] = "TangentsW"; +static const char ufbxi_Texture[] = "Texture"; +static const char ufbxi_Texture_alpha[] = "Texture alpha"; +static const char ufbxi_TextureId[] = "TextureId"; +static const char ufbxi_TextureRotationPivot[] = "TextureRotationPivot"; +static const char ufbxi_TextureScalingPivot[] = "TextureScalingPivot"; +static const char ufbxi_TextureUV[] = "TextureUV"; +static const char ufbxi_TextureUVVerticeIndex[] = "TextureUVVerticeIndex"; +static const char ufbxi_Thumbnail[] = "Thumbnail"; +static const char ufbxi_TimeMarker[] = "TimeMarker"; +static const char ufbxi_TimeMode[] = "TimeMode"; +static const char ufbxi_TimeProtocol[] = "TimeProtocol"; +static const char ufbxi_TimeSpanStart[] = "TimeSpanStart"; +static const char ufbxi_TimeSpanStop[] = "TimeSpanStop"; +static const char ufbxi_TransformLink[] = "TransformLink"; +static const char ufbxi_Transform[] = "Transform"; +static const char ufbxi_Translation[] = "Translation"; +static const char ufbxi_TrimNurbsSurface[] = "TrimNurbsSurface"; +static const char ufbxi_Type[] = "Type"; +static const char ufbxi_TypedIndex[] = "TypedIndex"; +static const char ufbxi_UVIndex[] = "UVIndex"; +static const char ufbxi_UVSet[] = "UVSet"; +static const char ufbxi_UVSwap[] = "UVSwap"; +static const char ufbxi_UV[] = "UV\0"; +static const char ufbxi_UnitScaleFactor[] = "UnitScaleFactor"; +static const char ufbxi_UpAxisSign[] = "UpAxisSign"; +static const char ufbxi_UpAxis[] = "UpAxis"; +static const char ufbxi_Version5[] = "Version5"; +static const char ufbxi_VertexCacheDeformer[] = "VertexCacheDeformer"; +static const char ufbxi_VertexCrease[] = "VertexCrease"; +static const char ufbxi_VertexCreaseIndex[] = "VertexCreaseIndex"; +static const char ufbxi_VertexIndexArray[] = "VertexIndexArray"; +static const char ufbxi_Vertices[] = "Vertices"; +static const char ufbxi_Video[] = "Video"; +static const char ufbxi_Visibility[] = "Visibility"; +static const char ufbxi_Weight[] = "Weight"; +static const char ufbxi_Weights[] = "Weights"; +static const char ufbxi_WrapModeU[] = "WrapModeU"; +static const char ufbxi_WrapModeV[] = "WrapModeV"; +static const char ufbxi_X[] = "X\0\0"; +static const char ufbxi_Y[] = "Y\0\0"; +static const char ufbxi_Z[] = "Z\0\0"; +static const char ufbxi_d_X[] = "d|X"; +static const char ufbxi_d_Y[] = "d|Y"; +static const char ufbxi_d_Z[] = "d|Z"; + +static const ufbx_string ufbxi_strings[] = { + { ufbxi_AllSame, 7 }, + { ufbxi_Alphas, 6 }, + { ufbxi_AmbientColor, 12 }, + { ufbxi_AnimationCurve, 14 }, + { ufbxi_AnimationCurveNode, 18 }, + { ufbxi_AnimationLayer, 14 }, + { ufbxi_AnimationStack, 14 }, + { ufbxi_ApertureFormat, 14 }, + { ufbxi_ApertureMode, 12 }, + { ufbxi_AreaLightShape, 14 }, + { ufbxi_AspectH, 7 }, + { ufbxi_AspectHeight, 12 }, + { ufbxi_AspectRatioMode, 15 }, + { ufbxi_AspectW, 7 }, + { ufbxi_AspectWidth, 11 }, + { ufbxi_Audio, 5 }, + { ufbxi_AudioLayer, 10 }, + { ufbxi_BaseLayer, 9 }, + { ufbxi_BinaryData, 10 }, + { ufbxi_BindPose, 8 }, + { ufbxi_BindingTable, 12 }, + { ufbxi_Binormals, 9 }, + { ufbxi_BinormalsIndex, 14 }, + { ufbxi_BinormalsW, 10 }, + { ufbxi_BlendMode, 9 }, + { ufbxi_BlendModes, 10 }, + { ufbxi_BlendShape, 10 }, + { ufbxi_BlendShapeChannel, 17 }, + { ufbxi_BlendWeights, 12 }, + { ufbxi_Boundary, 8 }, + { ufbxi_BoundaryRule, 12 }, + { ufbxi_ByEdge, 6 }, + { ufbxi_ByPolygon, 9 }, + { ufbxi_ByPolygonVertex, 15 }, + { ufbxi_ByVertex, 8 }, + { ufbxi_ByVertice, 9 }, + { ufbxi_Cache, 5 }, + { ufbxi_Camera, 6 }, + { ufbxi_CameraProjectionType, 20 }, + { ufbxi_CameraStereo, 12 }, + { ufbxi_CameraSwitcher, 14 }, + { ufbxi_CastLight, 9 }, + { ufbxi_CastShadows, 11 }, + { ufbxi_Channel, 7 }, + { ufbxi_Character, sizeof(ufbxi_Character) - 1 }, + { ufbxi_Children, 8 }, + { ufbxi_Cluster, 7 }, + { ufbxi_Collection, 10 }, + { ufbxi_CollectionExclusive, 19 }, + { ufbxi_Color, 5 }, + { ufbxi_ColorIndex, 10 }, + { ufbxi_Colors, 6 }, + { ufbxi_Cone_angle, 10 }, + { ufbxi_ConeAngle, 9 }, + { ufbxi_Connections, 11 }, + { ufbxi_Constraint, sizeof(ufbxi_Constraint) - 1 }, + { ufbxi_Content, 7 }, + { ufbxi_CoordAxis, 9 }, + { ufbxi_CoordAxisSign, 13 }, + { ufbxi_Count, 5 }, + { ufbxi_Creator, 7 }, + { ufbxi_CurrentTextureBlendMode, 23 }, + { ufbxi_CurrentTimeMarker, 17 }, + { ufbxi_CustomFrameRate, 15 }, + { ufbxi_DecayType, 9 }, + { ufbxi_Default, 7 }, + { ufbxi_DefaultCamera, 13 }, + { ufbxi_Definitions, 11 }, + { ufbxi_DeformPercent, 13 }, + { ufbxi_Deformer, 8 }, + { ufbxi_DiffuseColor, 12 }, + { ufbxi_Dimension, 9 }, + { ufbxi_Dimensions, 10 }, + { ufbxi_DisplayLayer, 12 }, + { ufbxi_Document, 8 }, + { ufbxi_Documents, 9 }, + { ufbxi_EdgeCrease, 10 }, + { ufbxi_EdgeIndexArray, 14 }, + { ufbxi_Edges, 5 }, + { ufbxi_EmissiveColor, 13 }, + { ufbxi_Entry, 5 }, + { ufbxi_FBXHeaderExtension, 18 }, + { ufbxi_FBXHeaderVersion, 16 }, + { ufbxi_FBXVersion, 10 }, + { ufbxi_FKEffector, 10 }, + { ufbxi_FarPlane, 8 }, + { ufbxi_FbxPropertyEntry, 16 }, + { ufbxi_FbxSemanticEntry, 16 }, + { ufbxi_FieldOfView, 11 }, + { ufbxi_FieldOfViewX, 12 }, + { ufbxi_FieldOfViewY, 12 }, + { ufbxi_FileName, 8 }, + { ufbxi_Filename, 8 }, + { ufbxi_FilmHeight, 10 }, + { ufbxi_FilmSqueezeRatio, 16 }, + { ufbxi_FilmWidth, 9 }, + { ufbxi_FlipNormals, 11 }, + { ufbxi_FocalLength, 11 }, + { ufbxi_Form, 4 }, + { ufbxi_Freeze, 6 }, + { ufbxi_FrontAxis, 9 }, + { ufbxi_FrontAxisSign, 13 }, + { ufbxi_FullWeights, 11 }, + { ufbxi_GateFit, 7 }, + { ufbxi_GeometricRotation, 17 }, + { ufbxi_GeometricScaling, 16 }, + { ufbxi_GeometricTranslation, 20 }, + { ufbxi_Geometry, 8 }, + { ufbxi_GeometryUVInfo, 14 }, + { ufbxi_GlobalSettings, 14 }, + { ufbxi_Hole, 4 }, + { ufbxi_HotSpot, 7 }, + { ufbxi_IKEffector, 10 }, + { ufbxi_ImageData, 9 }, + { ufbxi_Implementation, 14 }, + { ufbxi_Indexes, 7 }, + { ufbxi_InheritType, 11 }, + { ufbxi_InnerAngle, 10 }, + { ufbxi_Intensity, 9 }, + { ufbxi_IsTheNodeInSet, 14 }, + { ufbxi_Key, 3 }, + { ufbxi_KeyAttrDataFloat, 16 }, + { ufbxi_KeyAttrFlags, 12 }, + { ufbxi_KeyAttrRefCount, 15 }, + { ufbxi_KeyCount, 8 }, + { ufbxi_KeyTime, 7 }, + { ufbxi_KeyValueFloat, 13 }, + { ufbxi_KnotVector, 10 }, + { ufbxi_KnotVectorU, 11 }, + { ufbxi_KnotVectorV, 11 }, + { ufbxi_Layer, 5 }, + { ufbxi_LayerElement, 12 }, + { ufbxi_LayerElementBinormal, 20 }, + { ufbxi_LayerElementColor, 17 }, + { ufbxi_LayerElementEdgeCrease, 22 }, + { ufbxi_LayerElementHole, 16 }, + { ufbxi_LayerElementMaterial, 20 }, + { ufbxi_LayerElementNormal, 18 }, + { ufbxi_LayerElementPolygonGroup, 24 }, + { ufbxi_LayerElementSmoothing, 21 }, + { ufbxi_LayerElementTangent, 19 }, + { ufbxi_LayerElementUV, 14 }, + { ufbxi_LayerElementVertexCrease, 24 }, + { ufbxi_LayerElementVisibility, 22 }, + { ufbxi_LayeredTexture, 14 }, + { ufbxi_Lcl_Rotation, 12 }, + { ufbxi_Lcl_Scaling, 11 }, + { ufbxi_Lcl_Translation, 15 }, + { ufbxi_LeftCamera, 10 }, + { ufbxi_Light, 5 }, + { ufbxi_LightType, 9 }, + { ufbxi_Limb, 4 }, + { ufbxi_LimbLength, 10 }, + { ufbxi_LimbNode, 8 }, + { ufbxi_Line, 4 }, + { ufbxi_Link, 4 }, + { ufbxi_LocalStart, 10 }, + { ufbxi_LocalStop, 9 }, + { ufbxi_LocalTime, 9 }, + { ufbxi_LodGroup, 8 }, + { ufbxi_MappingInformationType, 22 }, + { ufbxi_Marker, 6 }, + { ufbxi_Material, 8 }, + { ufbxi_MaterialAssignation, 19 }, + { ufbxi_Materials, 9 }, + { ufbxi_Matrix, 6 }, + { ufbxi_Media, 5 }, + { ufbxi_Mesh, 4 }, + { ufbxi_Model, 5 }, + { ufbxi_Name, 4 }, + { ufbxi_NearPlane, 9 }, + { ufbxi_Node, 4 }, + { ufbxi_NodeAttribute, 13 }, + { ufbxi_NodeAttributeName, 17 }, + { ufbxi_Normals, 7 }, + { ufbxi_NormalsIndex, 12 }, + { ufbxi_NormalsW, 8 }, + { ufbxi_Null, 4 }, + { ufbxi_Nurbs, 5 }, + { ufbxi_NurbsCurve, 10 }, + { ufbxi_NurbsSurface, 12 }, + { ufbxi_NurbsSurfaceOrder, 17 }, + { ufbxi_OO, 2 }, + { ufbxi_OP, 2 }, + { ufbxi_ObjectMetaData, 14 }, + { ufbxi_ObjectType, 10 }, + { ufbxi_Objects, 7 }, + { ufbxi_Order, 5 }, + { ufbxi_OriginalUnitScaleFactor, 23 }, + { ufbxi_OriginalUpAxis, 14 }, + { ufbxi_OriginalUpAxisSign, 18 }, + { ufbxi_OrthoZoom, 9 }, + { ufbxi_OtherFlags, 10 }, + { ufbxi_OuterAngle, 10 }, + { ufbxi_PO, 2 }, + { ufbxi_PP, 2 }, + { ufbxi_Points, 6 }, + { ufbxi_PointsIndex, 11 }, + { ufbxi_PolygonGroup, 12 }, + { ufbxi_PolygonIndexArray, 17 }, + { ufbxi_PolygonVertexIndex, 18 }, + { ufbxi_Pose, 4 }, + { ufbxi_PoseNode, 8 }, + { ufbxi_Post_Extrapolation, 18 }, + { ufbxi_PostRotation, 12 }, + { ufbxi_Pre_Extrapolation, 17 }, + { ufbxi_PreRotation, 11 }, + { ufbxi_PreviewDivisionLevels, 21 }, + { ufbxi_Properties60, 12 }, + { ufbxi_Properties70, 12 }, + { ufbxi_PropertyTemplate, 16 }, + { ufbxi_R, 1 }, + { ufbxi_ReferenceStart, 14 }, + { ufbxi_ReferenceStop, 13 }, + { ufbxi_ReferenceTime, 13 }, + { ufbxi_RelativeFileName, 16 }, + { ufbxi_RelativeFilename, 16 }, + { ufbxi_RenderDivisionLevels, 20 }, + { ufbxi_Repetition, 10 }, + { ufbxi_RightCamera, 11 }, + { ufbxi_Root, 4 }, + { ufbxi_RootNode, 8 }, + { ufbxi_Rotation, 8 }, + { ufbxi_RotationAccumulationMode, 24 }, + { ufbxi_RotationOffset, 14 }, + { ufbxi_RotationOrder, 13 }, + { ufbxi_RotationPivot, 13 }, + { ufbxi_S, 1 }, + { ufbxi_ScaleAccumulationMode, 21 }, + { ufbxi_Scaling, 7 }, + { ufbxi_ScalingOffset, 13 }, + { ufbxi_ScalingPivot, 12 }, + { ufbxi_SceneInfo, 9 }, + { ufbxi_SelectionNode, 13 }, + { ufbxi_SelectionSet, 12 }, + { ufbxi_ShadingModel, 12 }, + { ufbxi_Shape, 5 }, + { ufbxi_Shininess, 9 }, + { ufbxi_Show, 4 }, + { ufbxi_Size, 4 }, + { ufbxi_Skin, 4 }, + { ufbxi_SkinningType, 12 }, + { ufbxi_Smoothing, 9 }, + { ufbxi_Smoothness, 10 }, + { ufbxi_SnapOnFrameMode, 15 }, + { ufbxi_SpecularColor, 13 }, + { ufbxi_Step, 4 }, + { ufbxi_SubDeformer, 11 }, + { ufbxi_T, 1 }, + { ufbxi_TCDefinition, 12 }, + { ufbxi_Take, 4 }, + { ufbxi_Takes, 5 }, + { ufbxi_Tangents, 8 }, + { ufbxi_TangentsIndex, 13 }, + { ufbxi_TangentsW, 9 }, + { ufbxi_Texture, 7 }, + { ufbxi_Texture_alpha, 13 }, + { ufbxi_TextureId, 9 }, + { ufbxi_TextureRotationPivot, 20 }, + { ufbxi_TextureScalingPivot, 19 }, + { ufbxi_TextureUV, 9 }, + { ufbxi_TextureUVVerticeIndex, 21 }, + { ufbxi_Thumbnail, 9 }, + { ufbxi_TimeMarker, 10 }, + { ufbxi_TimeMode, 8 }, + { ufbxi_TimeProtocol, 12 }, + { ufbxi_TimeSpanStart, 13 }, + { ufbxi_TimeSpanStop, 12 }, + { ufbxi_Transform, 9 }, + { ufbxi_TransformLink, 13 }, + { ufbxi_Translation, 11 }, + { ufbxi_TrimNurbsSurface, 16 }, + { ufbxi_Type, 4 }, + { ufbxi_TypedIndex, 10 }, + { ufbxi_UV, 2 }, + { ufbxi_UVIndex, 7 }, + { ufbxi_UVSet, 5 }, + { ufbxi_UVSwap, 6 }, + { ufbxi_UnitScaleFactor, 15 }, + { ufbxi_UpAxis, 6 }, + { ufbxi_UpAxisSign, 10 }, + { ufbxi_Version5, 8 }, + { ufbxi_VertexCacheDeformer, 19 }, + { ufbxi_VertexCrease, 12 }, + { ufbxi_VertexCreaseIndex, 17 }, + { ufbxi_VertexIndexArray, 16 }, + { ufbxi_Vertices, 8 }, + { ufbxi_Video, 5 }, + { ufbxi_Visibility, 10 }, + { ufbxi_Weight, 6 }, + { ufbxi_Weights, 7 }, + { ufbxi_WrapModeU, 9 }, + { ufbxi_WrapModeV, 9 }, + { ufbxi_X, 1 }, + { ufbxi_Y, 1 }, + { ufbxi_Z, 1 }, + { ufbxi_d_X, 3 }, + { ufbxi_d_Y, 3 }, + { ufbxi_d_Z, 3 }, +}; + +static const ufbx_vec3 ufbxi_one_vec3 = { 1.0f, 1.0f, 1.0f }; + +#define UFBXI_PI ((ufbx_real)3.14159265358979323846) +#define UFBXI_DPI (3.14159265358979323846) +#define UFBXI_DEG_TO_RAD ((ufbx_real)(UFBXI_PI / 180.0)) +#define UFBXI_RAD_TO_DEG ((ufbx_real)(180.0 / UFBXI_PI)) +#define UFBXI_DEG_TO_RAD_DOUBLE (UFBXI_DPI / 180.0) +#define UFBXI_RAD_TO_DEG_DOUBLE (180.0 / UFBXI_DPI) +#define UFBXI_MM_TO_INCH ((ufbx_real)0.0393700787) + +ufbx_inline ufbx_vec3 ufbxi_add3(ufbx_vec3 a, ufbx_vec3 b) { + ufbx_vec3 v = { a.x + b.x, a.y + b.y, a.z + b.z }; + return v; +} + +ufbx_inline ufbx_vec3 ufbxi_sub3(ufbx_vec3 a, ufbx_vec3 b) { + ufbx_vec3 v = { a.x - b.x, a.y - b.y, a.z - b.z }; + return v; +} + +ufbx_inline ufbx_vec3 ufbxi_mul3(ufbx_vec3 a, ufbx_real b) { + ufbx_vec3 v = { a.x * b, a.y * b, a.z * b }; + return v; +} + +ufbx_inline ufbx_vec3 ufbxi_lerp3(ufbx_vec3 a, ufbx_vec3 b, ufbx_real t) { + ufbx_real u = 1.0f - t; + ufbx_vec3 v = { a.x*u + b.x*t, a.y*u + b.y*t, a.z*u + b.z*t }; + return v; +} + +ufbx_inline ufbx_real ufbxi_dot3(ufbx_vec3 a, ufbx_vec3 b) { + return a.x*b.x + a.y*b.y + a.z*b.z; +} + +ufbx_inline ufbx_real ufbxi_length3(ufbx_vec3 v) +{ + return (ufbx_real)ufbx_sqrt(v.x*v.x + v.y*v.y + v.z*v.z); +} + +ufbx_inline ufbx_real ufbxi_min3(ufbx_vec3 v) +{ + return ufbxi_min_real(ufbxi_min_real(v.x, v.y), v.z); +} + +ufbx_inline ufbx_vec3 ufbxi_cross3(ufbx_vec3 a, ufbx_vec3 b) { + ufbx_vec3 v = { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x }; + return v; +} + +ufbx_inline ufbx_vec3 ufbxi_normalize3(ufbx_vec3 a) { + ufbx_real len = (ufbx_real)ufbx_sqrt(ufbxi_dot3(a, a)); + if (len > UFBX_EPSILON) { + return ufbxi_mul3(a, (ufbx_real)1.0 / len); + } else { + ufbx_vec3 zero = { (ufbx_real)0 }; + return zero; + } +} + +ufbx_inline ufbx_vec3 ufbxi_neg3(ufbx_vec3 a) { + ufbx_vec3 v = { -a.x, -a.y, -a.z }; + return v; +} + +ufbx_inline ufbx_real ufbxi_distsq2(ufbx_vec2 a, ufbx_vec2 b) { + ufbx_real dx = a.x - b.x, dy = a.y - b.y; + return dx*dx + dy*dy; +} + +static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalize3(const ufbx_vec3 *a) { + return ufbxi_normalize3(*a); +} + +static ufbxi_noinline ufbx_vec3 ufbxi_slow_normalized_cross3(const ufbx_vec3 *a, const ufbx_vec3 *b) { + return ufbxi_normalize3(ufbxi_cross3(*a, *b)); +} + +// -- Threading + +typedef struct ufbxi_task ufbxi_task; +typedef struct ufbxi_thread_pool ufbxi_thread_pool; + +typedef bool ufbxi_task_fn(ufbxi_task *task); + +struct ufbxi_task { + void *data; + const char *error; +}; + +typedef struct { + ufbxi_task task; + ufbxi_task_fn *fn; +} ufbxi_task_imp; + +typedef struct { + uint32_t max_index; + uint32_t wait_index; +} ufbxi_task_group; + +struct ufbxi_thread_pool { + ufbx_thread_opts opts; + ufbxi_allocator *ator; + ufbx_error *error; + void *user_ptr; + + bool enabled; + bool failed; + const char *error_desc; + + uint32_t start_index; + uint32_t execute_index; + uint32_t wait_index; + + ufbxi_task_group groups[UFBX_THREAD_GROUP_COUNT]; + uint32_t group; + + uint32_t num_tasks; + ufbxi_task_imp *tasks; +}; + +static void ufbxi_thread_pool_execute(ufbxi_thread_pool *pool, uint32_t index) +{ + ufbxi_task_imp *imp = &pool->tasks[index % pool->num_tasks]; + if (imp->fn(&imp->task)) { + imp->task.error = NULL; + } else if (!imp->task.error) { + imp->task.error = ""; + } +} + +ufbxi_noinline static void ufbxi_thread_pool_update_finished(ufbxi_thread_pool *pool, uint32_t max_index) +{ + while (pool->wait_index < max_index) { + ufbxi_task_imp *task = &pool->tasks[pool->wait_index % pool->num_tasks]; + if (!pool->failed && task->task.error) { + pool->failed = true; + pool->error_desc = task->task.error; + } + pool->wait_index += 1; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_imp(ufbxi_thread_pool *pool, uint32_t group, bool can_fail) +{ + uint32_t max_index = pool->groups[group].max_index; + + if (pool->groups[group].wait_index < max_index) { + pool->opts.pool.wait_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, group, max_index); + pool->groups[group].wait_index = max_index; + } + ufbxi_thread_pool_update_finished(pool, max_index); + + if (pool->failed && can_fail) { + ufbx_error *error = pool->error; + if (pool->error_desc) { + error->description.data = pool->error_desc; + error->description.length = strlen(pool->error_desc); + } + ufbxi_fail_err(error, "Task failed"); + } + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_group(ufbxi_thread_pool *pool) +{ + ufbxi_check_err(pool->error, ufbxi_thread_pool_wait_imp(pool, pool->group, true)); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_wait_all(ufbxi_thread_pool *pool) +{ + for (uint32_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) { + ufbxi_check_err(pool->error, ufbxi_thread_pool_wait_imp(pool, pool->group, true)); + pool->group = (pool->group + 1) % UFBX_THREAD_GROUP_COUNT; + } + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_thread_pool_init(ufbxi_thread_pool *pool, ufbx_error *error, ufbxi_allocator *ator, const ufbx_thread_opts *opts) +{ + if (!(opts->pool.run_fn && opts->pool.wait_fn)) return 1; + pool->enabled = true; + + uint32_t num_tasks = (uint32_t)ufbxi_min_sz(opts->num_tasks, INT32_MAX); + if (num_tasks == 0) { + num_tasks = 2048; + } + + pool->opts = *opts; + if (pool->opts.pool.init_fn) { + ufbx_thread_pool_info info; // ufbxi_uninit + info.max_concurrent_tasks = num_tasks; + ufbxi_check_err(error, pool->opts.pool.init_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, &info)); + } + pool->ator = ator; + pool->error = error; + + pool->num_tasks = num_tasks; + pool->tasks = ufbxi_alloc(ator, ufbxi_task_imp, num_tasks); + ufbxi_check_err(error, pool->tasks); + + return 1; +} + +ufbxi_noinline static void ufbxi_thread_pool_free(ufbxi_thread_pool *pool) +{ + if (!pool->enabled) return; + + // Wait for all pending tasks + for (uint32_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) { + pool->group = (pool->group + 1) % UFBX_THREAD_GROUP_COUNT; + ufbxi_ignore(ufbxi_thread_pool_wait_imp(pool, pool->group, false)); + } + + if (pool->opts.pool.free_fn) { + pool->opts.pool.free_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool); + } + + ufbxi_free(pool->ator, ufbxi_task_imp, pool->tasks, pool->num_tasks); +} + +ufbxi_nodiscard ufbxi_noinline static uint32_t ufbxi_thread_pool_available_tasks(ufbxi_thread_pool *pool) +{ + return pool->num_tasks - (pool->start_index - pool->wait_index); +} + +ufbxi_noinline static void ufbxi_thread_pool_flush_group(ufbxi_thread_pool *pool) +{ + uint32_t group = pool->group; + uint32_t start_index = pool->execute_index; + uint32_t count = pool->start_index - start_index; + if (count > 0) { + if (pool->opts.pool.run_fn) { + pool->opts.pool.run_fn(pool->opts.pool.user, (ufbx_thread_pool_context)pool, group, start_index, count); + } + pool->groups[group].max_index = start_index + count; + pool->execute_index = start_index + count; + } + pool->group = (group + 1) % UFBX_THREAD_GROUP_COUNT; +} + +ufbxi_nodiscard ufbxi_noinline static ufbxi_task *ufbxi_thread_pool_create_task(ufbxi_thread_pool *pool, ufbxi_task_fn *fn) +{ + uint32_t index = pool->start_index; + if (index - pool->wait_index >= pool->num_tasks) { + if (index - pool->wait_index >= pool->num_tasks) { + // No space left + return NULL; + } + } else if (index == INT32_MAX) { + // TODO: Expand to 64 bits if possible? + return NULL; + } + + ufbxi_task_imp *imp = &pool->tasks[index % pool->num_tasks]; + if (index < pool->num_tasks) { + memset(imp, 0, sizeof(ufbxi_task_imp)); + } + + imp->fn = fn; + + return &imp->task; +} + +static void ufbxi_thread_pool_run_task(ufbxi_thread_pool *pool, ufbxi_task *task) +{ + (void)task; + uint32_t index = pool->start_index; + ufbx_assert(task == &pool->tasks[index % pool->num_tasks].task); + pool->start_index = index + 1; +} + +// -- Type definitions + +typedef struct ufbxi_node ufbxi_node; + +typedef enum { + UFBXI_VALUE_NONE, + UFBXI_VALUE_NUMBER, + UFBXI_VALUE_STRING, + UFBXI_VALUE_ARRAY, +} ufbxi_value_type; + +typedef union { + struct { double f; int64_t i; }; // < if `UFBXI_PROP_NUMBER` + ufbxi_sanitized_string s; // < if `UFBXI_PROP_STRING` +} ufbxi_value; + +typedef struct { + void *data; // < Pointer to `size` bool/int32_t/int64_t/float/double elements + size_t size; // < Number of elements + char type; // < FBX type code: b/i/l/f/d +} ufbxi_value_array; + +struct ufbxi_node { + const char *name; // < Name of the node (pooled, compare with == to ufbxi_* strings) + uint32_t num_children; // < Number of child nodes + uint8_t name_len; // < Length of `name` in bytes + + // If `value_type_mask == UFBXI_PROP_ARRAY` then the node is an array + // (`array` field is valid) otherwise the node has N values in `vals` + // where the type of each value is stored in 2 bits per value from LSB. + // ie. `vals[ix]` type is `(value_type_mask >> (ix*2)) & 0x3` + uint16_t value_type_mask; + + ufbxi_node *children; + union { + ufbxi_value_array *array; // if `prop_type_mask == UFBXI_PROP_ARRAY` + ufbxi_value *vals; // otherwise + }; +}; + +typedef struct ufbxi_refcount ufbxi_refcount; + +struct ufbxi_refcount { + ufbxi_refcount *parent; + void *align_0; + uint32_t self_magic; + uint32_t type_magic; + ufbxi_buf buf; + ufbxi_allocator ator; + uint64_t zero_pad_pre[8]; + ufbxi_atomic_counter refcount; + uint64_t zero_pad_post[8]; +}; + +static ufbxi_noinline void ufbxi_init_ref(ufbxi_refcount *refcount, uint32_t magic, ufbxi_refcount *parent); +static ufbxi_noinline void ufbxi_retain_ref(ufbxi_refcount *refcount); + +#define ufbxi_get_imp(type, ptr) ((type*)((char*)ptr - sizeof(ufbxi_refcount))) + +typedef struct { + ufbxi_refcount refcount; + ufbx_scene scene; + uint32_t magic; + + ufbxi_buf string_buf; +} ufbxi_scene_imp; + +ufbx_static_assert(scene_imp_offset, offsetof(ufbxi_scene_imp, scene) == sizeof(ufbxi_refcount)); + +typedef struct { + ufbxi_refcount refcount; + ufbx_mesh mesh; + uint32_t magic; +} ufbxi_mesh_imp; + +ufbx_static_assert(mesh_imp_offset, offsetof(ufbxi_mesh_imp, mesh) == sizeof(ufbxi_refcount)); + +typedef struct { + // Semantic string data and length eg. for a string token + // this string doesn't include the quotes. + char *str_data; + size_t str_len; + size_t str_cap; + + // Type of the token, either single character such as '{' or ':' + // or one of UFBXI_ASCII_* defines. + char type; + + // Sign for integer if negative. + bool negative; + + // Parsed semantic value + union { + double f64; + int64_t i64; + size_t name_len; + } value; +} ufbxi_ascii_token; + +typedef struct { + size_t max_token_length; + + const char *src; + const char *src_yield; + const char *src_end; + + bool read_first_comment; + bool found_version; + bool parse_as_f32; + bool src_is_retained; + + ufbxi_buf *retain_buf; + ufbxi_buf *src_buf; + + ufbxi_ascii_token prev_token; + ufbxi_ascii_token token; +} ufbxi_ascii; + +typedef struct { + const char *type; + ufbx_string sub_type; + ufbx_props props; +} ufbxi_template; + +typedef struct { + uint64_t fbx_id; + uint32_t element_id; + uint32_t user_id; +} ufbxi_fbx_id_entry; + +typedef struct { + ufbxi_ptr_id ptr_id; + uint64_t fbx_id; +} ufbxi_ptr_fbx_id_entry; + +typedef struct { + uint64_t node_fbx_id; + uint64_t attr_fbx_id; +} ufbxi_fbx_attr_entry; + +// Temporary connection before we resolve the element pointers +typedef struct { + uint64_t src, dst; + ufbx_string src_prop; + ufbx_string dst_prop; +} ufbxi_tmp_connection; + +typedef struct { + uint64_t fbx_id; + ufbx_string name; + ufbx_props props; + ufbx_dom_node *dom_node; +} ufbxi_element_info; + +typedef struct { + uint64_t bone_fbx_id; + ufbx_matrix bone_to_world; +} ufbxi_tmp_bone_pose; + +typedef struct { + ufbx_string prop_name; + uint32_t *face_texture; + size_t num_faces; + bool all_same; +} ufbxi_tmp_mesh_texture; + +typedef struct { + ufbxi_tmp_mesh_texture *texture_arr; + size_t texture_count; +} ufbxi_mesh_extra; + +typedef struct { + int32_t material_id; + int32_t texture_id; + ufbx_string prop_name; +} ufbxi_tmp_material_texture; + +typedef struct { + int32_t *blend_modes; + size_t num_blend_modes; + + ufbx_real *alphas; + size_t num_alphas; +} ufbxi_texture_extra; + +typedef enum { + UFBXI_OBJ_ATTRIB_POSITION, + UFBXI_OBJ_ATTRIB_UV, + UFBXI_OBJ_ATTRIB_NORMAL, + UFBXI_OBJ_ATTRIB_COLOR, +} ufbxi_obj_attrib; + +#define UFBXI_OBJ_NUM_ATTRIBS 3 +#define UFBXI_OBJ_NUM_ATTRIBS_EXT 4 + +typedef struct { + uint64_t min_ix, max_ix; +} ufbxi_obj_index_range; + +typedef struct { + size_t num_faces; + size_t num_indices; + ufbxi_obj_index_range vertex_range[UFBXI_OBJ_NUM_ATTRIBS]; + + ufbx_node *fbx_node; + ufbx_mesh *fbx_mesh; + + uint64_t fbx_node_id; + uint64_t fbx_mesh_id; + + uint32_t usemtl_base; + + uint32_t num_groups; +} ufbxi_obj_mesh; + +typedef struct { + const char *name; + uint32_t local_id; + uint32_t mesh_id; +} ufbxi_obj_group_entry; + +typedef struct { + uint64_t *indices; + size_t num_left; +} ufbxi_obj_fast_indices; + +// Temporary pointer to a `ufbx_anim_stack` by name used to patch start/stop +// time from "Takes" if necessary. +typedef struct { + const char *name; + ufbx_anim_stack *stack; +} ufbxi_tmp_anim_stack; + +typedef struct { + ufbx_string absolute_filename; + ufbx_blob content; +} ufbxi_file_content; + +typedef struct { + + // Current line and tokens. + // NOTE: `line` and `tokens` are not NULL-terminated nor UTF-8! + // `line` is guaranteed to be terminated by a `\n` + ufbx_string line; + ufbx_string *tokens; + size_t tokens_cap; + size_t num_tokens; + + ufbxi_obj_fast_indices fast_indices[UFBXI_OBJ_NUM_ATTRIBS]; + + size_t vertex_count[UFBXI_OBJ_NUM_ATTRIBS_EXT]; + ufbxi_buf tmp_vertices[UFBXI_OBJ_NUM_ATTRIBS_EXT]; + ufbxi_buf tmp_indices[UFBXI_OBJ_NUM_ATTRIBS_EXT]; + ufbxi_buf tmp_color_valid; + ufbxi_buf tmp_faces; + ufbxi_buf tmp_face_smoothing; + ufbxi_buf tmp_face_group; + ufbxi_buf tmp_face_group_infos; + ufbxi_buf tmp_face_material; + ufbxi_buf tmp_meshes; + ufbxi_buf tmp_props; + + ufbxi_map group_map; + + size_t read_progress; + + ufbxi_obj_mesh *mesh; + + uint64_t usemtl_fbx_id; + uint32_t usemtl_index; + + uint32_t face_material; + + uint32_t face_group; + bool has_face_group; + + bool face_smoothing; + bool has_face_smoothing; + + bool has_vertex_color; + size_t mrgb_vertex_count; + + bool eof; + bool initialized; + + ufbx_blob mtllib_relative_path; + + ufbx_material **tmp_materials; + size_t tmp_materials_cap; + + ufbx_string object; + ufbx_string group; + bool material_dirty; + bool object_dirty; + bool group_dirty; + bool face_group_dirty; + +} ufbxi_obj_context; + +typedef struct { + + ufbx_error error; + uint32_t version; + ufbx_exporter exporter; + uint32_t exporter_version; + bool from_ascii; + bool local_big_endian; + bool file_big_endian; + bool sure_fbx; + bool retain_mesh_parts; + bool read_legacy_settings; + uint32_t double_parse_flags; + + ufbx_load_opts opts; + + // IO + uint64_t data_offset; + ufbx_read_fn *read_fn; + ufbx_skip_fn *skip_fn; + void *read_user; + + char *read_buffer; + size_t read_buffer_size; + + const char *data_begin; + const char *data; + size_t yield_size; + size_t data_size; + + // Allocators + ufbxi_allocator ator_result; + ufbxi_allocator ator_tmp; + + // Temporary maps + ufbxi_map prop_type_map; // < `ufbxi_prop_type_name` Property type to enum + ufbxi_map fbx_id_map; // < `ufbxi_fbx_id_entry` FBX ID to local ID + ufbxi_map ptr_fbx_id_map; // < `ufbxi_ptr_fbx_id_entry` Pointer/negative ID to FBX ID + ufbxi_map texture_file_map; // < `ufbxi_texture_file_entry` absolute raw filename to element ID + ufbxi_map anim_stack_map; // < `ufbxi_tmp_anim_stack` anim stacks by name before finalization + + // 6x00 specific maps + ufbxi_map fbx_attr_map; // < `ufbxi_fbx_attr_entry` Node ID to attrib ID + ufbxi_map node_prop_set; // < `const char*` Node property names + + // DOM nodes + ufbxi_map dom_node_map; // < `const char*` Node property names + + // Temporary array + char *tmp_arr; + size_t tmp_arr_size; + char *swap_arr; + size_t swap_arr_size; + + // Generated index buffers + size_t max_zero_indices; + size_t max_consecutive_indices; + + // Temporary buffers + ufbxi_buf tmp; + ufbxi_buf tmp_parse; + ufbxi_buf tmp_stack; + ufbxi_buf tmp_connections; + ufbxi_buf tmp_node_ids; + ufbxi_buf tmp_elements; + ufbxi_buf tmp_element_offsets; + ufbxi_buf tmp_element_fbx_ids; + ufbxi_buf tmp_element_ptrs; + ufbxi_buf tmp_typed_element_offsets[UFBX_ELEMENT_TYPE_COUNT]; + ufbxi_buf tmp_mesh_textures; + ufbxi_buf tmp_full_weights; + ufbxi_buf tmp_dom_nodes; + ufbxi_buf tmp_element_id; + ufbxi_buf tmp_ascii_spans; + ufbxi_buf tmp_thread_parse[UFBX_THREAD_GROUP_COUNT]; + size_t tmp_element_byte_offset; + + ufbxi_template *templates; + size_t num_templates; + + ufbx_dom_node *dom_parse_toplevel; + size_t dom_parse_num_children; + + uint32_t *p_element_id; + + // String pool + ufbxi_string_pool string_pool; + + // Result buffers, these are retained in `ufbx_scene` returned to user. + ufbxi_buf result; + + // Top-level state + ufbxi_node *top_nodes; + size_t top_nodes_len, top_nodes_cap; + bool parsed_to_end; + + // "Focused" top-level node and child index, if `top_child_index == SIZE_MAX` + // the children are parsed on demand. + ufbxi_node *top_node; + size_t top_child_index; + ufbxi_node top_child; + bool has_next_child; + + // Shared consecutive and all-zero index buffers + uint32_t *zero_indices; + uint32_t *consecutive_indices; + + // Call progress function periodically + ptrdiff_t progress_timer; + uint64_t progress_bytes_total; + uint64_t latest_progress_bytes; + size_t progress_interval; + + // Extra data on the side of elements + void **element_extra_arr; + size_t element_extra_cap; + + // Temporary per-element flags + uint8_t *tmp_element_flag; + + // IO (cold) + ufbx_close_fn *close_fn; + ufbx_size_fn *size_fn; + + ufbxi_ascii ascii; + + uint64_t synthetic_id_counter; + + bool has_geometry_transform_nodes; + bool has_scale_helper_nodes; + bool retain_vertex_w; + bool blender_full_weights; + + ufbx_mirror_axis mirror_axis; + + ufbxi_node root; + + ufbx_scene scene; + ufbxi_scene_imp *scene_imp; + + ufbx_inflate_retain *inflate_retain; + + // Per-mesh consecutive indices used by `ufbxi_flip_winding()`. + uint32_t *tmp_mesh_consecutive_indices; + + uint64_t root_id; + uint32_t num_elements; + + ufbxi_node legacy_node; + uint64_t legacy_implicit_anim_layer_id; + + ufbxi_file_content *file_content; + size_t num_file_content; + + int64_t ktime_sec; + double ktime_sec_double; + + bool eof; + ufbxi_obj_context obj; + + ufbx_matrix axis_matrix; + ufbx_real unit_scale; + + ufbxi_warnings warnings; + + bool deferred_failure; + bool deferred_load; + + const char *load_filename; + size_t load_filename_len; + + bool parse_threaded; + ufbxi_thread_pool thread_pool; + + uint8_t *base64_table; + +} ufbxi_context; + +static ufbxi_noinline int ufbxi_fail_imp(ufbxi_context *uc, const char *cond, const char *func, uint32_t line) +{ + return ufbxi_fail_imp_err(&uc->error, cond, func, line); +} + +#if UFBXI_FEATURE_ERROR_STACK + #define ufbxi_fail_no_msg(uc, cond, func, line) ufbxi_fail_imp((uc), (cond), (func), (line)) +#else + static ufbxi_noinline int ufbxi_fail_imp_no_stack(ufbxi_context *uc) { return ufbxi_fail_imp_err(&uc->error, NULL, NULL, 0); } + #define ufbxi_fail_no_msg(uc, cond, func, line) ufbxi_fail_imp_no_stack((uc)) +#endif + +#define ufbxi_check(cond) if (ufbxi_unlikely(!ufbxi_trace(cond))) return ufbxi_fail_no_msg(uc, ufbxi_cond_str(cond), ufbxi_function, ufbxi_line) +#define ufbxi_check_return(cond, ret) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_no_msg(uc, ufbxi_cond_str(cond), ufbxi_function, ufbxi_line); return ret; } } while (0) +#define ufbxi_fail(desc) return ufbxi_fail_no_msg(uc, desc, ufbxi_function, ufbxi_line) +#define ufbxi_fail_return(desc, ret) do { ufbxi_fail_no_msg(uc, desc, ufbxi_function, ufbxi_line); return ret; } while (0) + +#define ufbxi_check_msg(cond, msg) if (ufbxi_unlikely(!ufbxi_trace(cond))) return ufbxi_fail_imp(uc, ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line) +#define ufbxi_check_return_msg(cond, ret, msg) do { if (ufbxi_unlikely(!ufbxi_trace(cond))) { ufbxi_fail_imp(uc, ufbxi_error_msg(ufbxi_cond_str(cond), msg), ufbxi_function, ufbxi_line); return ret; } } while (0) +#define ufbxi_fail_msg(desc, msg) return ufbxi_fail_imp(uc, ufbxi_error_msg(desc, msg), ufbxi_function, ufbxi_line) + +#define ufbxi_warnf(type, ...) ufbxi_warnf_imp(&uc->warnings, type, ~0u, __VA_ARGS__) +#define ufbxi_warnf_tag(type, element_id, ...) ufbxi_warnf_imp(&uc->warnings, type, (element_id), __VA_ARGS__) + +// -- Progress + +static ufbxi_forceinline uint64_t ufbxi_get_read_offset(ufbxi_context *uc) +{ + return uc->data_offset + ufbxi_to_size(uc->data - uc->data_begin); +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_report_progress(ufbxi_context *uc) +{ + if (!uc->opts.progress_cb.fn) return 1; + + uint64_t read_offset = ufbxi_get_read_offset(uc); + uc->latest_progress_bytes = read_offset; + + ufbx_progress progress = { 0 }; + progress.bytes_read = read_offset; + progress.bytes_total = uc->progress_bytes_total; + if (progress.bytes_total < progress.bytes_read) { + progress.bytes_total = progress.bytes_read; + } + + uc->progress_timer = 1024; + uint32_t result = (uint32_t)uc->opts.progress_cb.fn(uc->opts.progress_cb.user, &progress); + ufbx_assert(result == UFBX_PROGRESS_CONTINUE || result == UFBX_PROGRESS_CANCEL); + ufbxi_check_msg(result != UFBX_PROGRESS_CANCEL, "Cancelled"); + return 1; +} + +// TODO: Remove `ufbxi_unused` when it's not needed anymore +ufbxi_unused ufbxi_nodiscard static ufbxi_forceinline int ufbxi_progress(ufbxi_context *uc, size_t work_units) +{ + if (!uc->opts.progress_cb.fn) return 1; + ptrdiff_t left = uc->progress_timer - (ptrdiff_t)work_units; + uc->progress_timer = left; + if (left > 0) return 1; + return ufbxi_report_progress(uc); +} + +// -- IO + +static ufbxi_noinline const char *ufbxi_refill(ufbxi_context *uc, size_t size, bool require_size) +{ + ufbx_assert(uc->data_size < size); + ufbxi_check_return(!uc->eof, NULL); + if (require_size) { + ufbxi_check_return_msg(uc->read_fn || uc->data_size > 0, NULL, "Empty file"); + ufbxi_check_return_msg(uc->read_fn, NULL, "Truncated file"); + } else if (!uc->read_fn) { + uc->eof = true; + return uc->data; + } + + void *data_to_free = NULL; + size_t size_to_free = 0; + + // Grow the read buffer if necessary, data is copied over below with the + // usual path so the free is deferred (`size_to_free`, `data_to_free`) + if (size > uc->read_buffer_size) { + size_t new_size = ufbxi_max_sz(size, uc->opts.read_buffer_size); + new_size = ufbxi_max_sz(new_size, uc->read_buffer_size * 2); + size_to_free = uc->read_buffer_size; + data_to_free = uc->read_buffer; + char *new_buffer = ufbxi_alloc(&uc->ator_tmp, char, new_size); + ufbxi_check_return(new_buffer, NULL); + uc->read_buffer = new_buffer; + uc->read_buffer_size = new_size; + } + + // Copy the remains of the previous buffer to the beginning of the new one + size_t data_size = uc->data_size; + if (data_size > 0) { + ufbx_assert(uc->read_buffer != NULL && uc->data != NULL); + memmove(uc->read_buffer, uc->data, data_size); + } + + if (size_to_free) { + ufbxi_free(&uc->ator_tmp, char, data_to_free, size_to_free); + } + + // Fill the rest of the buffer with user data + size_t data_capacity = uc->read_buffer_size; + while (data_size < data_capacity) { + size_t to_read = data_capacity - data_size; + size_t read_result = uc->read_fn(uc->read_user, uc->read_buffer + data_size, to_read); + ufbxi_check_return_msg(read_result != SIZE_MAX, NULL, "IO error"); + ufbxi_check_return(read_result <= to_read, NULL); + data_size += read_result; + if (read_result == 0) { + uc->eof = true; + break; + } + } + + if (require_size) { + if (uc->data_offset == 0) { + ufbxi_check_return_msg(data_size > 0, NULL, "Empty file"); + } + ufbxi_check_return_msg(data_size >= size, NULL, "Truncated file"); + } + + uc->data_offset += ufbxi_to_size(uc->data - uc->data_begin); + uc->data_begin = uc->data = uc->read_buffer; + uc->data_size = data_size; + + return uc->read_buffer; +} + +static ufbxi_forceinline void ufbxi_pause_progress(ufbxi_context *uc) +{ + uc->data_size += uc->yield_size; + uc->yield_size = 0; +} + +static ufbxi_noinline int ufbxi_resume_progress(ufbxi_context *uc) +{ + uc->yield_size = ufbxi_min_sz(uc->data_size, uc->progress_interval); + uc->data_size -= uc->yield_size; + + if (ufbxi_get_read_offset(uc) - uc->latest_progress_bytes >= uc->progress_interval) { + ufbxi_check(ufbxi_report_progress(uc)); + } + + return 1; +} + +static ufbxi_noinline const char *ufbxi_yield(ufbxi_context *uc, size_t size) +{ + const char *ret; + uc->data_size += uc->yield_size; + if (uc->data_size >= size) { + ret = uc->data; + } else { + ret = ufbxi_refill(uc, size, true); + } + uc->yield_size = ufbxi_min_sz(uc->data_size, ufbxi_max_sz(size, uc->progress_interval)); + uc->data_size -= uc->yield_size; + + ufbxi_check_return(ufbxi_report_progress(uc), NULL); + return ret; +} + +static ufbxi_forceinline const char *ufbxi_peek_bytes(ufbxi_context *uc, size_t size) +{ + if (uc->yield_size >= size) { + return uc->data; + } else { + return ufbxi_yield(uc, size); + } +} + +static ufbxi_forceinline const char *ufbxi_read_bytes(ufbxi_context *uc, size_t size) +{ + // Refill the current buffer if necessary + const char *ret; + if (uc->yield_size >= size) { + ret = uc->data; + } else { + ret = ufbxi_yield(uc, size); + if (!ret) return NULL; + } + + // Advance the read position inside the current buffer + uc->yield_size -= size; + uc->data = ret + size; + return ret; +} + +static ufbxi_forceinline void ufbxi_consume_bytes(ufbxi_context *uc, size_t size) +{ + // Bytes must have been checked first with `ufbxi_peek_bytes()` + ufbx_assert(size <= uc->yield_size); + uc->yield_size -= size; + uc->data += size; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_skip_bytes(ufbxi_context *uc, uint64_t size) +{ + if (uc->skip_fn) { + ufbxi_pause_progress(uc); + + if (size > uc->data_size) { + size -= uc->data_size; + uc->data += uc->data_size; + uc->data_size = 0; + + uc->data_offset += size; + while (size >= UFBXI_MAX_SKIP_SIZE) { + size -= UFBXI_MAX_SKIP_SIZE; + ufbxi_check_msg(uc->skip_fn(uc->read_user, UFBXI_MAX_SKIP_SIZE - 1), "Truncated file"); + + // Check that we can read at least one byte in case the file is broken + // and causes us to seek indefinitely forwards as `fseek()` does not + // report if we hit EOF... + char single_byte[1]; // ufbxi_uninit + size_t num_read = uc->read_fn(uc->read_user, single_byte, 1); + ufbxi_check_msg(num_read <= 1, "IO error"); + ufbxi_check_msg(num_read == 1, "Truncated file"); + } + + if (size > 0) { + ufbxi_check_msg(uc->skip_fn(uc->read_user, (size_t)size), "Truncated file"); + } + + } else { + uc->data += (size_t)size; + uc->data_size -= (size_t)size; + } + + ufbxi_check(ufbxi_resume_progress(uc)); + } else { + // Read and discard bytes in reasonable chunks + uint64_t skip_size = ufbxi_max64(uc->read_buffer_size, uc->opts.read_buffer_size); + while (size > 0) { + uint64_t to_skip = ufbxi_min64(size, skip_size); + ufbxi_check(ufbxi_read_bytes(uc, (size_t)to_skip)); + size -= to_skip; + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_to(ufbxi_context *uc, void *dst, size_t size) +{ + char *ptr = (char*)dst; + + ufbxi_pause_progress(uc); + + // Copy data from the current buffer first + size_t len = ufbxi_min_sz(uc->data_size, size); + memcpy(ptr, uc->data, len); + uc->data += len; + uc->data_size -= len; + ptr += len; + size -= len; + + // If there's data left to copy try to read from user IO + if (size > 0) { + uc->data_offset += ufbxi_to_size(uc->data - uc->data_begin); + + uc->data_begin = uc->data = NULL; + uc->data_size = 0; + ufbxi_check(uc->read_fn); + + while (size > 0) { + size_t read_result = uc->read_fn(uc->read_user, ptr, size); + ufbxi_check_msg(read_result != SIZE_MAX, "IO error"); + ufbxi_check(read_result != 0); + + ptr += read_result; + size -= read_result; + uc->data_offset += read_result; + } + } + + ufbxi_check(ufbxi_resume_progress(uc)); + + return 1; +} + +static ufbxi_noinline void ufbxi_init_ator(ufbx_error *error, ufbxi_allocator *ator, const ufbx_allocator_opts *opts, const char *name) +{ + ufbx_allocator_opts zero_opts; + if (!opts) { + memset(&zero_opts, 0, sizeof(zero_opts)); + opts = &zero_opts; + } + + // `opts` is either passed in or `zero_opts`. + // cppcheck-suppress uninitvar + ator->ator = *opts; + ator->error = error; + ator->max_size = opts->memory_limit ? opts->memory_limit : SIZE_MAX; + ator->max_allocs = opts->allocation_limit ? opts->allocation_limit : SIZE_MAX; + ator->huge_size = opts->huge_threshold ? opts->huge_threshold : 0x100000; + ator->chunk_max = opts->max_chunk_size ? opts->max_chunk_size : 0x1000000; + ator->name = name; +} + +typedef struct { + ufbx_error error; + + ufbxi_allocator *parent_ator; + ufbxi_allocator ator; +} ufbxi_file_context; + +static ufbxi_noinline void ufbxi_begin_file_context(ufbxi_file_context *fc, ufbx_open_file_context ctx, const ufbx_allocator_opts *ator_opts) +{ + memset(fc, 0, sizeof(ufbxi_file_context)); + if (ctx) { + fc->parent_ator = (ufbxi_allocator*)ctx; + fc->ator = *fc->parent_ator; + fc->ator.error = &fc->error; + } else { + ufbxi_init_ator(&fc->error, &fc->ator, ator_opts, "file"); + } +} + +static ufbxi_noinline void ufbxi_end_file_context(ufbxi_file_context *fc, ufbx_error *error, bool ok) +{ + if (fc->parent_ator) { + fc->ator.error = fc->parent_ator->error; + *fc->parent_ator = fc->ator; + } else { + ufbxi_free_ator(&fc->ator); + } + if (error) { + if (!ok) { + ufbxi_fix_error_type(&fc->error, "Failed to open file", error); + } else { + ufbxi_clear_error(error); + } + } +} + +// -- File IO + +#if !defined(UFBX_NO_STDIO) && !defined(UFBX_EXTERNAL_STDIO) + +static ufbxi_noinline FILE *ufbxi_fopen(ufbxi_file_context *fc, const char *path, size_t path_len, bool null_terminated) +{ + FILE *file = NULL; +#if !defined(UFBX_STANDARD_C) && defined(_WIN32) + (void)null_terminated; + wchar_t wpath_buf[256], *wpath = NULL; // ufbxi_uninit + if (path_len < ufbxi_arraycount(wpath_buf) - 1) { + wpath = wpath_buf; + } else { + wpath = ufbxi_alloc(&fc->ator, wchar_t, path_len + 1); + if (!wpath) return NULL; + } + + // Convert UTF-8 to UTF-16 but allow stray surrogate pairs as the Windows + // file system encoding allows them as well.. + size_t wlen = 0; + for (size_t i = 0; i < path_len; ) { + uint32_t code = UINT32_MAX; + char c = path[i++]; + if ((c & 0x80) == 0) { + code = (uint32_t)c; + } else if ((c & 0xe0) == 0xc0) { + code = (uint32_t)(c & 0x1f); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + } else if ((c & 0xf0) == 0xe0) { + code = (uint32_t)(c & 0x0f); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + } else if ((c & 0xf8) == 0xf0) { + code = (uint32_t)(c & 0x07); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + if (i < path_len) code = code << 6 | (uint32_t)(path[i++] & 0x3f); + } + if (code < 0x10000) { + wpath[wlen++] = (wchar_t)code; + } else { + code -= 0x10000; + wpath[wlen++] = (wchar_t)(0xd800 + (code >> 10)); + wpath[wlen++] = (wchar_t)(0xdc00 + (code & 0x3ff)); + } + } + wpath[wlen] = 0; + + #if UFBXI_MSC_VER >= 1400 + if (_wfopen_s(&file, wpath, L"rb") != 0) file = NULL; + #else + file = _wfopen(wpath, L"rb"); + #endif + if (wpath != wpath_buf) { + ufbxi_free(&fc->ator, wchar_t, wpath, path_len + 1); + } +#else + char copy_buf[256], *copy = NULL; // ufbxi_uninit + if (null_terminated) { + copy = (char*)path; + } else { + if (path_len < ufbxi_arraycount(copy_buf) - 1) { + copy = copy_buf; + } else { + copy = ufbxi_alloc(&fc->ator, char, path_len + 1); + if (!copy) return NULL; + } + memcpy(copy, path, path_len); + copy[path_len] = '\0'; + } + file = fopen(copy, "rb"); + if (!null_terminated && copy != copy_buf) { + ufbxi_free(&fc->ator, char, copy, path_len + 1); + } +#endif + if (!file) { + ufbxi_set_err_info(&fc->error, path, path_len); + ufbxi_report_err_msg(&fc->error, "file", "File not found"); + } + return file; +} + +static uint64_t ufbxi_ftell(FILE *file) +{ +#if !defined(UFBX_STANDARD_C) && defined(UFBX_HAS_FTELLO) + off_t result = ftello(file); + if (result >= 0) return (uint64_t)result; +#elif !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + int64_t result = _ftelli64(file); + if (result >= 0) return (uint64_t)result; +#else + int64_t result = ftell(file); + if (result >= 0) return (uint64_t)result; +#endif + return UINT64_MAX; +} + +static size_t ufbxi_stdio_read(void *user, void *data, size_t max_size) +{ + FILE *file = (FILE*)user; + if (ferror(file)) return SIZE_MAX; + return fread(data, 1, max_size, file); +} + +static bool ufbxi_stdio_skip(void *user, size_t size) +{ + FILE *file = (FILE*)user; + ufbx_assert(size <= UFBXI_MAX_SKIP_SIZE); + if (fseek(file, (long)size, SEEK_CUR) != 0) return false; + if (ferror(file)) return false; + return true; +} + +static uint64_t ufbxi_stdio_size(void *user) +{ + FILE *file = (FILE*)user; + uint64_t result = 0; + uint64_t begin = ufbxi_ftell(file); + if (begin < UINT64_MAX) { + fpos_t pos; // ufbxi_uninit + if (fgetpos(file, &pos) == 0) { + if (fseek(file, 0, SEEK_END) == 0) { + uint64_t end = ufbxi_ftell(file); + if (end != UINT64_MAX && begin < end) { + result = end - begin; + } + // Both `rewind()` and `fsetpos()` to reset error and EOF + rewind(file); + fsetpos(file, &pos); + } + } + } + return result; +} + +static void ufbxi_stdio_close(void *user) +{ + FILE *file = (FILE*)user; + fclose(file); +} + +static ufbxi_noinline void ufbxi_stdio_init(ufbx_stream *stream, void *file, bool close) +{ + stream->read_fn = &ufbxi_stdio_read; + stream->skip_fn = &ufbxi_stdio_skip; + stream->size_fn = &ufbxi_stdio_size; + stream->close_fn = close ? &ufbxi_stdio_close : NULL; + stream->user = file; +} + +static ufbxi_noinline bool ufbxi_stdio_open(ufbxi_file_context *fc, ufbx_stream *stream, const char *path, size_t path_len, bool null_terminated) +{ + FILE *file = ufbxi_fopen(fc, path, path_len, null_terminated); + if (!file) return false; + ufbxi_stdio_init(stream, file, true); + return true; +} + +#elif defined(UFBX_EXTERNAL_STDIO) + +static ufbxi_noinline void ufbxi_stdio_init(ufbx_stream *stream, void *file, bool close) +{ + stream->read_fn = &ufbx_stdio_read; + stream->skip_fn = &ufbx_stdio_skip; + stream->size_fn = &ufbx_stdio_size; + stream->close_fn = close ? &ufbx_stdio_close : NULL; + stream->user = file; +} + +static ufbxi_noinline bool ufbxi_stdio_open(ufbxi_file_context *fc, ufbx_stream *stream, const char *path, size_t path_len, bool null_terminated) +{ + char copy_buf[256], *copy = NULL; // ufbxi_uninit + if (null_terminated) { + copy = (char*)path; + } else { + if (path_len < ufbxi_arraycount(copy_buf) - 1) { + copy = copy_buf; + } else { + copy = ufbxi_alloc(&fc->ator, char, path_len + 1); + if (!copy) return false; + } + memcpy(copy, path, path_len); + copy[path_len] = '\0'; + } + void *file = ufbx_stdio_open(copy, path_len); + if (!null_terminated && copy != copy_buf) { + ufbxi_free(&fc->ator, char, copy, path_len + 1); + } + if (!file) { + ufbxi_set_err_info(&fc->error, path, path_len); + ufbxi_report_err_msg(&fc->error, "file", "File not found"); + return false; + } + ufbxi_stdio_init(stream, file, true); + return true; +} + +#endif + +// -- Memory IO + +typedef struct { + const void *data; + size_t size; + size_t position; + ufbx_close_memory_cb close_cb; + + // Own allocation information + size_t self_size; + ufbxi_allocator *parent_ator; + ufbxi_allocator local_ator; + ufbx_error error; + char data_copy[]; +} ufbxi_memory_stream; + +static size_t ufbxi_memory_read(void *user, void *data, size_t max_size) +{ + ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user; + size_t to_read = ufbxi_min_sz(stream->size - stream->position, max_size); + memcpy(data, (const char*)stream->data + stream->position, to_read); + stream->position += to_read; + return to_read; +} + +static bool ufbxi_memory_skip(void *user, size_t size) +{ + ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user; + if (stream->size - stream->position < size) return false; + stream->position += size; + return true; +} + +static uint64_t ufbxi_memory_size(void *user) +{ + ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user; + return stream->size; +} + +static void ufbxi_memory_close(void *user) +{ + ufbxi_memory_stream *stream = (ufbxi_memory_stream*)user; + if (stream->close_cb.fn) { + stream->close_cb.fn(stream->close_cb.user, (void*)stream->data, stream->size); + } + + if (stream->parent_ator) { + ufbxi_free(stream->parent_ator, char, stream, stream->self_size); + } else { + ufbxi_allocator ator = stream->local_ator; + ufbxi_free(&ator, char, stream, stream->self_size); + ufbxi_free_ator(&ator); + } +} + +// -- XML + +#if UFBXI_FEATURE_XML + +typedef struct ufbxi_xml_tag ufbxi_xml_tag; +typedef struct ufbxi_xml_attrib ufbxi_xml_attrib; +typedef struct ufbxi_xml_document ufbxi_xml_document; + +struct ufbxi_xml_attrib { + ufbx_string name; + ufbx_string value; +}; + +struct ufbxi_xml_tag { + ufbx_string name; + ufbx_string text; + + ufbxi_xml_attrib *attribs; + size_t num_attribs; + + ufbxi_xml_tag *children; + size_t num_children; +}; + +struct ufbxi_xml_document { + ufbxi_xml_tag *root; + ufbxi_buf buf; +}; + +typedef struct { + ufbx_error error; + + ufbxi_allocator *ator; + + ufbxi_buf tmp_stack; + ufbxi_buf result; + + ufbxi_xml_document *doc; + + ufbx_read_fn *read_fn; + void *read_user; + + char *tok; + size_t tok_cap; + size_t tok_len; + + const char *pos, *pos_end; + char data[4096]; + + bool io_error; +} ufbxi_xml_context; + +enum { + UFBXI_XML_CTYPE_WHITESPACE = 0x1, + UFBXI_XML_CTYPE_SINGLE_QUOTE = 0x2, + UFBXI_XML_CTYPE_DOUBLE_QUOTE = 0x4, + UFBXI_XML_CTYPE_NAME_END = 0x8, + UFBXI_XML_CTYPE_TAG_START = 0x10, + UFBXI_XML_CTYPE_END_OF_FILE = 0x20, +}; + +// Generated by `misc/gen_xml_ctype.py` +static const uint8_t ufbxi_xml_ctype[256] = { + 32,0,0,0,0,0,0,0,0,9,9,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 9,0,12,0,0,0,0,10,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,16,8,8,8, +}; + +static ufbxi_noinline void ufbxi_xml_refill(ufbxi_xml_context *xc) +{ + size_t num = xc->read_fn(xc->read_user, xc->data, sizeof(xc->data)); + if (num == SIZE_MAX || num < sizeof(xc->data)) xc->io_error = true; + if (num < sizeof(xc->data)) { + xc->data[num++] = '\0'; + } + xc->pos = xc->data; + xc->pos_end = xc->data + num; +} + +static ufbxi_forceinline void ufbxi_xml_advance(ufbxi_xml_context *xc) +{ + if (++xc->pos == xc->pos_end) ufbxi_xml_refill(xc); +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_xml_push_token_char(ufbxi_xml_context *xc, char c) +{ + if (xc->tok_len == xc->tok_cap || UFBXI_IS_REGRESSION) { + ufbxi_check_err(&xc->error, ufbxi_grow_array(xc->ator, &xc->tok, &xc->tok_cap, xc->tok_len + 1)); + } + xc->tok[xc->tok_len++] = c; + return 1; +} + +static ufbxi_noinline int ufbxi_xml_accept(ufbxi_xml_context *xc, char ch) +{ + if (*xc->pos == ch) { + ufbxi_xml_advance(xc); + return 1; + } else { + return 0; + } +} + +static ufbxi_noinline void ufbxi_xml_skip_while(ufbxi_xml_context *xc, uint32_t ctypes) +{ + while (ufbxi_xml_ctype[(uint8_t)*xc->pos] & ctypes) { + ufbxi_xml_advance(xc); + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_xml_skip_until_string(ufbxi_xml_context *xc, ufbx_string *dst, const char *suffix) +{ + xc->tok_len = 0; + size_t match_len = 0, ix = 0, suffix_len = strlen(suffix); + char buf[16] = { 0 }; + size_t wrap_mask = sizeof(buf) - 1; + ufbx_assert(suffix_len < sizeof(buf)); + for (;;) { + char c = *xc->pos; + ufbxi_check_err_msg(&xc->error, c != 0, "Truncated file"); + ufbxi_xml_advance(xc); + if (ix >= suffix_len) { + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, buf[(ix - suffix_len) & wrap_mask])); + } + + buf[ix++ & wrap_mask] = c; + for (match_len = 0; match_len < suffix_len; match_len++) { + if (buf[(ix - suffix_len + match_len) & wrap_mask] != suffix[match_len]) { + break; + } + } + if (match_len == suffix_len) break; + } + + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0')); + if (dst) { + dst->length = xc->tok_len - 1; + dst->data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok); + ufbxi_check_err(&xc->error, dst->data); + } + + return 1; +} + +static ufbxi_noinline int ufbxi_xml_read_until(ufbxi_xml_context *xc, ufbx_string *dst, uint32_t ctypes) +{ + xc->tok_len = 0; + for (;;) { + char c = *xc->pos; + + if (c == '&') { + size_t entity_begin = xc->tok_len; + for (;;) { + ufbxi_xml_advance(xc); + c = *xc->pos; + ufbxi_check_err(&xc->error, c != '\0'); + if (c == ';') break; + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, c)); + } + ufbxi_xml_advance(xc); + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0')); + + char *entity = xc->tok + entity_begin; + xc->tok_len = entity_begin; + + if (entity[0] == '#') { + unsigned long code = 0; + if (entity[1] == 'x') { + code = ufbxi_parse_uint32_radix(entity + 2, 16); + } else { + code = ufbxi_parse_uint32_radix(entity + 1, 10); + } + + char bytes[5] = { 0 }; + if (code < 0x80) { + bytes[0] = (char)code; + } else if (code < 0x800) { + bytes[0] = (char)(0xc0 | (code>>6)); + bytes[1] = (char)(0x80 | (code & 0x3f)); + } else if (code < 0x10000) { + bytes[0] = (char)(0xe0 | (code>>12)); + bytes[1] = (char)(0x80 | ((code>>6) & 0x3f)); + bytes[2] = (char)(0x80 | (code & 0x3f)); + } else { + bytes[0] = (char)(0xf0 | (code>>18)); + bytes[1] = (char)(0x80 | ((code>>12) & 0x3f)); + bytes[2] = (char)(0x80 | ((code>>6) & 0x3f)); + bytes[3] = (char)(0x80 | (code & 0x3f)); + } + for (char *b = bytes; *b; b++) { + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, *b)); + } + } else { + char ch = '\0'; + if (!strcmp(entity, "lt")) ch = '<'; + else if (!strcmp(entity, "quot")) ch = '"'; + else if (!strcmp(entity, "amp")) ch = '&'; + else if (!strcmp(entity, "apos")) ch = '\''; + else if (!strcmp(entity, "gt")) ch = '>'; + if (ch) { + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, ch)); + } + } + } else { + if ((ufbxi_xml_ctype[(uint8_t)c] & ctypes) != 0) break; + ufbxi_check_err_msg(&xc->error, c != 0, "Truncated file"); + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, c)); + ufbxi_xml_advance(xc); + } + } + + ufbxi_check_err(&xc->error, ufbxi_xml_push_token_char(xc, '\0')); + if (dst) { + dst->length = xc->tok_len - 1; + dst->data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok); + ufbxi_check_err(&xc->error, dst->data); + } + + return 1; +} + +// Recursion limited by check at the start +static ufbxi_noinline int ufbxi_xml_parse_tag(ufbxi_xml_context *xc, size_t depth, bool *p_closing, const char *opening) + ufbxi_recursive_function(int, ufbxi_xml_parse_tag, (xc, depth, p_closing, opening), UFBXI_MAX_XML_DEPTH + 1, + (ufbxi_xml_context *xc, size_t depth, bool *p_closing, const char *opening)) +{ + ufbxi_check_err(&xc->error, depth < UFBXI_MAX_XML_DEPTH); + + if (!ufbxi_xml_accept(xc, '<')) { + if (*xc->pos == '\0') { + *p_closing = true; + } else { + ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, NULL, UFBXI_XML_CTYPE_TAG_START | UFBXI_XML_CTYPE_END_OF_FILE)); + bool has_text = false; + for (size_t i = 0; i < xc->tok_len; i++) { + if ((ufbxi_xml_ctype[(uint8_t)xc->tok[i]] & UFBXI_XML_CTYPE_WHITESPACE) == 0) { + has_text = true; + break; + } + } + + if (has_text) { + ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1); + ufbxi_check_err(&xc->error, tag); + tag->name.data = ufbxi_empty_char; + + tag->text.length = xc->tok_len - 1; + tag->text.data = ufbxi_push_copy(&xc->result, char, xc->tok_len, xc->tok); + ufbxi_check_err(&xc->error, tag->text.data); + } + } + return 1; + } + + if (ufbxi_xml_accept(xc, '/')) { + ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, NULL, UFBXI_XML_CTYPE_NAME_END)); + ufbxi_check_err(&xc->error, opening && !strcmp(xc->tok, opening)); + ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE); + if (!ufbxi_xml_accept(xc, '>')) return 0; + *p_closing = true; + return 1; + } else if (ufbxi_xml_accept(xc, '!')) { + if (ufbxi_xml_accept(xc, '[')) { + for (const char *ch = "CDATA["; *ch; ch++) { + if (!ufbxi_xml_accept(xc, *ch)) return 0; + } + + ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1); + ufbxi_check_err(&xc->error, tag); + ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, &tag->text, "]]>")); + tag->name.data = ufbxi_empty_char; + + } else if (ufbxi_xml_accept(xc, '-')) { + if (!ufbxi_xml_accept(xc, '-')) return 0; + ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, "-->")); + } else { + // TODO: !DOCTYPE + ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, ">")); + } + return 1; + } else if (ufbxi_xml_accept(xc, '?')) { + ufbxi_check_err(&xc->error, ufbxi_xml_skip_until_string(xc, NULL, "?>")); + return 1; + } + + ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_tag, 1); + ufbxi_check_err(&xc->error, tag); + ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &tag->name, UFBXI_XML_CTYPE_NAME_END)); + tag->text.data = ufbxi_empty_char; + + bool has_children = false; + + size_t num_attribs = 0; + for (;;) { + ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE); + if (ufbxi_xml_accept(xc, '/')) { + if (!ufbxi_xml_accept(xc, '>')) return 0; + break; + } else if (ufbxi_xml_accept(xc, '>')) { + has_children = true; + break; + } else { + ufbxi_xml_attrib *attrib = ufbxi_push_zero(&xc->tmp_stack, ufbxi_xml_attrib, 1); + ufbxi_check_err(&xc->error, attrib); + ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &attrib->name, UFBXI_XML_CTYPE_NAME_END)); + ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE); + if (!ufbxi_xml_accept(xc, '=')) return 0; + ufbxi_xml_skip_while(xc, UFBXI_XML_CTYPE_WHITESPACE); + uint32_t quote_ctype = 0; + if (ufbxi_xml_accept(xc, '"')) { + quote_ctype = UFBXI_XML_CTYPE_DOUBLE_QUOTE; + } else if (ufbxi_xml_accept(xc, '\'')) { + quote_ctype = UFBXI_XML_CTYPE_SINGLE_QUOTE; + } else { + ufbxi_fail_err(&xc->error, "Bad attrib value"); + } + ufbxi_check_err(&xc->error, ufbxi_xml_read_until(xc, &attrib->value, quote_ctype)); + ufbxi_xml_advance(xc); + num_attribs++; + } + } + + tag->num_attribs = num_attribs; + tag->attribs = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_attrib, num_attribs); + ufbxi_check_err(&xc->error, tag->attribs); + + if (has_children) { + size_t children_begin = xc->tmp_stack.num_items; + for (;;) { + bool closing = false; + ufbxi_check_err(&xc->error, ufbxi_xml_parse_tag(xc, depth + 1, &closing, tag->name.data)); + if (closing) break; + } + + tag->num_children = xc->tmp_stack.num_items - children_begin; + tag->children = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_tag, tag->num_children); + ufbxi_check_err(&xc->error, tag->children); + } + + return 1; +} + +static ufbxi_noinline int ufbxi_xml_parse_root(ufbxi_xml_context *xc) +{ + ufbxi_xml_tag *tag = ufbxi_push_zero(&xc->result, ufbxi_xml_tag, 1); + ufbxi_check_err(&xc->error, tag); + tag->name.data = ufbxi_empty_char; + tag->text.data = ufbxi_empty_char; + + for (;;) { + bool closing = false; + ufbxi_check_err(&xc->error, ufbxi_xml_parse_tag(xc, 0, &closing, NULL)); + if (closing) break; + } + + tag->num_children = xc->tmp_stack.num_items; + tag->children = ufbxi_push_pop(&xc->result, &xc->tmp_stack, ufbxi_xml_tag, tag->num_children); + ufbxi_check_err(&xc->error, tag->children); + + xc->doc = ufbxi_push(&xc->result, ufbxi_xml_document, 1); + ufbxi_check_err(&xc->error, xc->doc); + + xc->doc->root = tag; + xc->doc->buf = xc->result; + + return 1; +} + +typedef struct { + ufbxi_allocator *ator; + ufbx_read_fn *read_fn; + void *read_user; + const char *prefix; + size_t prefix_length; +} ufbxi_xml_load_opts; + +static ufbxi_noinline ufbxi_xml_document *ufbxi_load_xml(ufbxi_xml_load_opts *opts, ufbx_error *error) +{ + ufbxi_xml_context xc = { UFBX_ERROR_NONE }; + xc.ator = opts->ator; + xc.read_fn = opts->read_fn; + xc.read_user = opts->read_user; + + xc.tmp_stack.ator = xc.ator; + xc.result.ator = xc.ator; + + xc.result.unordered = true; + + if (opts->prefix_length > 0) { + xc.pos = opts->prefix; + xc.pos_end = opts->prefix + opts->prefix_length; + } else { + ufbxi_xml_refill(&xc); + } + + int ok = ufbxi_xml_parse_root(&xc); + + ufbxi_buf_free(&xc.tmp_stack); + ufbxi_free(xc.ator, char, xc.tok, xc.tok_cap); + + if (ok) { + return xc.doc; + } else { + ufbxi_buf_free(&xc.result); + if (error) { + *error = xc.error; + } + + return NULL; + } +} + +static ufbxi_noinline void ufbxi_free_xml(ufbxi_xml_document *doc) +{ + ufbxi_buf buf = doc->buf; + ufbxi_buf_free(&buf); +} + +static ufbxi_noinline ufbxi_xml_tag *ufbxi_xml_find_child(ufbxi_xml_tag *tag, const char *name) +{ + ufbxi_for(ufbxi_xml_tag, child, tag->children, tag->num_children) { + if (!strcmp(child->name.data, name)) { + return child; + } + } + return NULL; +} + +static ufbxi_noinline ufbxi_xml_attrib *ufbxi_xml_find_attrib(ufbxi_xml_tag *tag, const char *name) +{ + ufbxi_for(ufbxi_xml_attrib, attrib, tag->attribs, tag->num_attribs) { + if (!strcmp(attrib->name.data, name)) { + return attrib; + } + } + return NULL; +} + +#endif + +// -- FBX value type information + +static char ufbxi_normalize_array_type(char type, char bool_type) { + switch (type) { + case 'r': return sizeof(ufbx_real) == sizeof(float) ? 'f' : 'd'; + case 'b': return bool_type; + default: return type; + } +} + +static ufbxi_noinline size_t ufbxi_array_type_size(char type) +{ + switch (type) { + case 'r': return sizeof(ufbx_real); + case 'b': return sizeof(bool); + case 'c': return sizeof(uint8_t); + case 'i': return sizeof(int32_t); + case 'l': return sizeof(int64_t); + case 'f': return sizeof(float); + case 'd': return sizeof(double); + case 's': return sizeof(ufbx_string); + case 'S': return sizeof(ufbx_string); + case 'C': return sizeof(ufbx_string); + default: return 1; + } +} + +// -- Node operations + +static ufbxi_noinline ufbxi_node *ufbxi_find_child(ufbxi_node *node, const char *name) +{ + ufbxi_for(ufbxi_node, c, node->children, node->num_children) { + if (c->name == name) return c; + } + return NULL; +} + +// Retrieve the type of a given value +ufbxi_forceinline static ufbxi_value_type ufbxi_get_val_type(ufbxi_node *node, size_t ix) +{ + return (ufbxi_value_type)((node->value_type_mask >> (ix*2)) & 0x3); +} + +// Retrieve values from nodes with type codes: +// Any: '_' (ignore) +// NUMBER: 'I' int32_t 'L' int64_t 'F' float 'D' double 'R' ufbxi_real 'B' bool 'Z' size_t +// STRING: 'S' ufbx_string 'C' const char* (checked) 's' ufbx_string 'c' const char * (unchecked) 'b' ufbx_blob +ufbxi_nodiscard ufbxi_forceinline static int ufbxi_get_val_at(ufbxi_node *node, size_t ix, char fmt, void *v) +{ + ufbxi_dev_assert(ix < UFBXI_MAX_NON_ARRAY_VALUES); + ufbxi_value_type type = (ufbxi_value_type)((node->value_type_mask >> (ix*2)) & 0x3); + switch (fmt) { + case '_': return 1; + case 'I': if (type == UFBXI_VALUE_NUMBER) { *(int32_t*)v = (int32_t)node->vals[ix].i; return 1; } else return 0; + case 'L': if (type == UFBXI_VALUE_NUMBER) { *(int64_t*)v = (int64_t)node->vals[ix].i; return 1; } else return 0; + case 'F': if (type == UFBXI_VALUE_NUMBER) { *(float*)v = (float)node->vals[ix].f; return 1; } else return 0; + case 'D': if (type == UFBXI_VALUE_NUMBER) { *(double*)v = (double)node->vals[ix].f; return 1; } else return 0; + case 'R': if (type == UFBXI_VALUE_NUMBER) { *(ufbx_real*)v = (ufbx_real)node->vals[ix].f; return 1; } else return 0; + case 'B': if (type == UFBXI_VALUE_NUMBER) { *(bool*)v = node->vals[ix].i != 0; return 1; } else return 0; + case 'Z': if (type == UFBXI_VALUE_NUMBER) { if (node->vals[ix].i < 0) return 0; *(size_t*)v = (size_t)node->vals[ix].i; return 1; } else return 0; + case 'S': if (type == UFBXI_VALUE_STRING) { + ufbxi_sanitized_string src = node->vals[ix].s; + ufbx_string *dst = (ufbx_string*)v; + if (src.utf8_length > 0) { + if (src.utf8_length == UINT32_MAX) return 0; + dst->data = src.raw_data + src.raw_length + 1; + dst->length = src.utf8_length; + } else { + dst->data = src.raw_data; + dst->length = src.raw_length; + } + return 1; + } else return 0; + case 's': if (type == UFBXI_VALUE_STRING) { + ufbxi_sanitized_string src = node->vals[ix].s; + ufbx_string *dst = (ufbx_string*)v; + dst->data = src.raw_data; + dst->length = src.raw_length; + return 1; + } else return 0; + case 'C': if (type == UFBXI_VALUE_STRING) { + ufbxi_sanitized_string src = node->vals[ix].s; + const char **dst = (const char **)v; + if (src.utf8_length > 0) { + if (src.utf8_length == UINT32_MAX) return 0; + *dst = src.raw_data + src.raw_length + 1; + } else { + *dst = src.raw_data; + } + return 1; + } else return 0; + case 'c': if (type == UFBXI_VALUE_STRING) { + ufbxi_sanitized_string src = node->vals[ix].s; + const char **dst = (const char **)v; + *dst = src.raw_data; + return 1; + } else return 0; + case 'b': if (type == UFBXI_VALUE_STRING) { + ufbxi_sanitized_string src = node->vals[ix].s; + ufbx_blob *dst = (ufbx_blob*)v; + dst->data = src.raw_data; + dst->size = src.raw_length; + return 1; + } else return 0; + default: + ufbxi_unreachable("Bad format char"); + return 0; + } +} + +ufbxi_nodiscard ufbxi_noinline static ufbxi_value_array *ufbxi_get_array(ufbxi_node *node, char fmt) +{ + if (node->value_type_mask != UFBXI_VALUE_ARRAY) return NULL; + ufbxi_value_array *array = node->array; + if (fmt != '?') { + fmt = ufbxi_normalize_array_type(fmt, 'b'); + if (array->type != fmt) return NULL; + } + return array; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val1(ufbxi_node *node, const char *fmt, void *v0) +{ + if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val2(ufbxi_node *node, const char *fmt, void *v0, void *v1) +{ + if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0; + if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val3(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2) +{ + if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0; + if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0; + if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val4(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2, void *v3) +{ + if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0; + if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0; + if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0; + if (!ufbxi_get_val_at(node, 3, fmt[3], v3)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_get_val5(ufbxi_node *node, const char *fmt, void *v0, void *v1, void *v2, void *v3, void *v4) +{ + if (!ufbxi_get_val_at(node, 0, fmt[0], v0)) return 0; + if (!ufbxi_get_val_at(node, 1, fmt[1], v1)) return 0; + if (!ufbxi_get_val_at(node, 2, fmt[2], v2)) return 0; + if (!ufbxi_get_val_at(node, 3, fmt[3], v3)) return 0; + if (!ufbxi_get_val_at(node, 4, fmt[4], v4)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val1(ufbxi_node *node, const char *name, const char *fmt, void *v0) +{ + ufbxi_node *child = ufbxi_find_child(node, name); + if (!child) return 0; + if (!ufbxi_get_val_at(child, 0, fmt[0], v0)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_find_val2(ufbxi_node *node, const char *name, const char *fmt, void *v0, void *v1) +{ + ufbxi_node *child = ufbxi_find_child(node, name); + if (!child) return 0; + if (!ufbxi_get_val_at(child, 0, fmt[0], v0)) return 0; + if (!ufbxi_get_val_at(child, 1, fmt[1], v1)) return 0; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline ufbxi_value_array *ufbxi_find_array(ufbxi_node *node, const char *name, char fmt) +{ + ufbxi_node *child = ufbxi_find_child(node, name); + if (!child) return NULL; + return ufbxi_get_array(child, fmt); +} + +static ufbxi_node *ufbxi_find_child_strcmp(ufbxi_node *node, const char *name) +{ + char leading = name[0]; + ufbxi_for(ufbxi_node, c, node->children, node->num_children) { + if (c->name[0] != leading) continue; + if (!strcmp(c->name, name)) return c; + } + return NULL; +} + +// -- Element extra data allocation + +ufbxi_nodiscard static ufbxi_noinline void *ufbxi_push_element_extra_size(ufbxi_context *uc, uint32_t id, size_t size) +{ + if (uc->element_extra_cap <= id) { + size_t old_cap = uc->element_extra_cap; + ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->element_extra_arr, &uc->element_extra_cap, id + 1), NULL); + memset(uc->element_extra_arr + old_cap, 0, (uc->element_extra_cap - old_cap) * sizeof(void*)); + } + + if (uc->element_extra_arr[id]) return uc->element_extra_arr[id]; + + void *extra = ufbxi_push_size_zero(&uc->tmp, size, 1); + ufbxi_check_return(extra, NULL); + uc->element_extra_arr[id] = extra; + + return extra; +} + +static ufbxi_noinline void *ufbxi_get_element_extra(ufbxi_context *uc, uint32_t id) +{ + if (id < uc->element_extra_cap) { + return uc->element_extra_arr[id]; + } else { + return NULL; + } +} + +#define ufbxi_push_element_extra(uc, id, type) (type*)ufbxi_push_element_extra_size((uc), (id), sizeof(type)) + +// -- Parsing state machine +// +// When reading the file we maintain a coarse representation of the structure so +// that we can resolve array info (type, included in result, etc). Using this info +// we can often read/decompress the contents directly into the right memory area. + +typedef enum { + UFBXI_PARSE_ROOT, + UFBXI_PARSE_FBX_HEADER_EXTENSION, + UFBXI_PARSE_SCENE_INFO, + UFBXI_PARSE_THUMBNAIL, + UFBXI_PARSE_DEFINITIONS, + UFBXI_PARSE_OBJECTS, + UFBXI_PARSE_CONNECTIONS, + UFBXI_PARSE_RELATIONS, + UFBXI_PARSE_TAKES, + UFBXI_PARSE_FBX_VERSION, + UFBXI_PARSE_MODEL, + UFBXI_PARSE_GEOMETRY, + UFBXI_PARSE_NODE_ATTRIBUTE, + UFBXI_PARSE_LEGACY_MODEL, + UFBXI_PARSE_LEGACY_MEDIA, + UFBXI_PARSE_LEGACY_VIDEO, + UFBXI_PARSE_LEGACY_SWITCHER, + UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE, + UFBXI_PARSE_REFERENCES, + UFBXI_PARSE_REFERENCE, + UFBXI_PARSE_ANIMATION_CURVE, + UFBXI_PARSE_DEFORMER, + UFBXI_PARSE_ASSOCIATE_MODEL, + UFBXI_PARSE_LEGACY_LINK, + UFBXI_PARSE_POSE, + UFBXI_PARSE_POSE_NODE, + UFBXI_PARSE_TEXTURE, + UFBXI_PARSE_VIDEO, + UFBXI_PARSE_LAYERED_TEXTURE, + UFBXI_PARSE_SELECTION_NODE, + UFBXI_PARSE_COLLECTION, + UFBXI_PARSE_AUDIO, + UFBXI_PARSE_UNKNOWN_OBJECT, + UFBXI_PARSE_LAYER_ELEMENT_NORMAL, + UFBXI_PARSE_LAYER_ELEMENT_BINORMAL, + UFBXI_PARSE_LAYER_ELEMENT_TANGENT, + UFBXI_PARSE_LAYER_ELEMENT_UV, + UFBXI_PARSE_LAYER_ELEMENT_COLOR, + UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE, + UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE, + UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING, + UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY, + UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP, + UFBXI_PARSE_LAYER_ELEMENT_HOLE, + UFBXI_PARSE_LAYER_ELEMENT_MATERIAL, + UFBXI_PARSE_LAYER_ELEMENT_OTHER, + UFBXI_PARSE_GEOMETRY_UV_INFO, + UFBXI_PARSE_SHAPE, + UFBXI_PARSE_TAKE, + UFBXI_PARSE_TAKE_OBJECT, + UFBXI_PARSE_CHANNEL, + UFBXI_PARSE_UNKNOWN, +} ufbxi_parse_state; + +typedef enum { + UFBXI_ARRAY_FLAG_RESULT = 0x1, // < Allocate the array from the result buffer + UFBXI_ARRAY_FLAG_TMP_BUF = 0x2, // < Allocate the array from the long-term temporary buffer + UFBXI_ARRAY_FLAG_PAD_BEGIN = 0x4, // < Pad the begin of the array with 4 zero elements to guard from invalid -1 index accesses + UFBXI_ARRAY_FLAG_ACCURATE_F32 = 0x8, // < Must be parsed as bit-accurate 32-bit floats +} ufbxi_array_flags; + +typedef struct { + char type; // < FBX type code of the array: b,i,l,f,d (or 'r' meaning ufbx_real '-' ignore, 's'/'S' for strings, 'C' for content) + uint8_t flags; // < Combination of `ufbxi_array_flags` +} ufbxi_array_info; + +static ufbxi_noinline ufbxi_parse_state ufbxi_update_parse_state(ufbxi_parse_state parent, const char *name) +{ + switch (parent) { + + case UFBXI_PARSE_ROOT: + if (name == ufbxi_FBXHeaderExtension) return UFBXI_PARSE_FBX_HEADER_EXTENSION; + if (name == ufbxi_Definitions) return UFBXI_PARSE_DEFINITIONS; + if (name == ufbxi_Objects) return UFBXI_PARSE_OBJECTS; + if (name == ufbxi_Connections) return UFBXI_PARSE_CONNECTIONS; + if (name == ufbxi_Takes) return UFBXI_PARSE_TAKES; + if (name == ufbxi_Model) return UFBXI_PARSE_LEGACY_MODEL; + if (!strcmp(name, "References")) return UFBXI_PARSE_REFERENCES; + if (!strcmp(name, "Relations")) return UFBXI_PARSE_RELATIONS; + if (name == ufbxi_Media) return UFBXI_PARSE_LEGACY_MEDIA; + if (!strcmp(name, "Switcher")) return UFBXI_PARSE_LEGACY_SWITCHER; + if (!strcmp(name, "SceneGenericPersistence")) return UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE; + break; + + case UFBXI_PARSE_FBX_HEADER_EXTENSION: + if (name == ufbxi_FBXVersion) return UFBXI_PARSE_FBX_VERSION; + if (name == ufbxi_SceneInfo) return UFBXI_PARSE_SCENE_INFO; + break; + + case UFBXI_PARSE_SCENE_INFO: + if (name == ufbxi_Thumbnail) return UFBXI_PARSE_THUMBNAIL; + break; + + case UFBXI_PARSE_OBJECTS: + if (name == ufbxi_Model) return UFBXI_PARSE_MODEL; + if (name == ufbxi_Geometry) return UFBXI_PARSE_GEOMETRY; + if (name == ufbxi_NodeAttribute) return UFBXI_PARSE_NODE_ATTRIBUTE; + if (name == ufbxi_AnimationCurve) return UFBXI_PARSE_ANIMATION_CURVE; + if (name == ufbxi_Deformer) return UFBXI_PARSE_DEFORMER; + if (name == ufbxi_Pose) return UFBXI_PARSE_POSE; + if (name == ufbxi_Texture) return UFBXI_PARSE_TEXTURE; + if (name == ufbxi_Video) return UFBXI_PARSE_VIDEO; + if (name == ufbxi_LayeredTexture) return UFBXI_PARSE_LAYERED_TEXTURE; + if (name == ufbxi_SelectionNode) return UFBXI_PARSE_SELECTION_NODE; + if (name == ufbxi_Collection) return UFBXI_PARSE_COLLECTION; + if (name == ufbxi_Audio) return UFBXI_PARSE_AUDIO; + return UFBXI_PARSE_UNKNOWN_OBJECT; + + case UFBXI_PARSE_MODEL: + case UFBXI_PARSE_GEOMETRY: + if (name[0] == 'L') { + if (name == ufbxi_LayerElementNormal) return UFBXI_PARSE_LAYER_ELEMENT_NORMAL; + if (name == ufbxi_LayerElementBinormal) return UFBXI_PARSE_LAYER_ELEMENT_BINORMAL; + if (name == ufbxi_LayerElementTangent) return UFBXI_PARSE_LAYER_ELEMENT_TANGENT; + if (name == ufbxi_LayerElementUV) return UFBXI_PARSE_LAYER_ELEMENT_UV; + if (name == ufbxi_LayerElementColor) return UFBXI_PARSE_LAYER_ELEMENT_COLOR; + if (name == ufbxi_LayerElementVertexCrease) return UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE; + if (name == ufbxi_LayerElementEdgeCrease) return UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE; + if (name == ufbxi_LayerElementSmoothing) return UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING; + if (name == ufbxi_LayerElementVisibility) return UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY; + if (name == ufbxi_LayerElementPolygonGroup) return UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP; + if (name == ufbxi_LayerElementHole) return UFBXI_PARSE_LAYER_ELEMENT_HOLE; + if (name == ufbxi_LayerElementMaterial) return UFBXI_PARSE_LAYER_ELEMENT_MATERIAL; + if (!strncmp(name, "LayerElement", 12)) return UFBXI_PARSE_LAYER_ELEMENT_OTHER; + } + if (name == ufbxi_Shape) return UFBXI_PARSE_SHAPE; + break; + + case UFBXI_PARSE_DEFORMER: + if (!strcmp(name, "AssociateModel")) return UFBXI_PARSE_ASSOCIATE_MODEL; + break; + + case UFBXI_PARSE_LEGACY_MEDIA: + if (name == ufbxi_Video) return UFBXI_PARSE_LEGACY_VIDEO; + break; + + case UFBXI_PARSE_LEGACY_VIDEO: + return UFBXI_PARSE_VIDEO; + + case UFBXI_PARSE_LEGACY_MODEL: + if (name == ufbxi_GeometryUVInfo) return UFBXI_PARSE_GEOMETRY_UV_INFO; + if (name == ufbxi_Link) return UFBXI_PARSE_LEGACY_LINK; + if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL; + if (name == ufbxi_Shape) return UFBXI_PARSE_SHAPE; + break; + + case UFBXI_PARSE_POSE: + if (name == ufbxi_PoseNode) return UFBXI_PARSE_POSE_NODE; + break; + + case UFBXI_PARSE_TAKES: + if (name == ufbxi_Take) return UFBXI_PARSE_TAKE; + break; + + case UFBXI_PARSE_TAKE: + return UFBXI_PARSE_TAKE_OBJECT; + + case UFBXI_PARSE_TAKE_OBJECT: + if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL; + break; + + case UFBXI_PARSE_CHANNEL: + if (name == ufbxi_Channel) return UFBXI_PARSE_CHANNEL; + break; + + case UFBXI_PARSE_REFERENCES: + return UFBXI_PARSE_REFERENCE; + + default: + break; + + } + + return UFBXI_PARSE_UNKNOWN; +} + +static bool ufbxi_is_array_node(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, ufbxi_array_info *info) +{ + info->flags = 0; + + // Retain all arrays if user wants the DOM representation + if (uc->opts.retain_dom) { + info->flags |= UFBXI_ARRAY_FLAG_RESULT; + } + + switch (parent) { + + case UFBXI_PARSE_THUMBNAIL: + if (name == ufbxi_ImageData) { + info->type = 'c'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_GEOMETRY: + case UFBXI_PARSE_MODEL: + if (name == ufbxi_Vertices) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_PolygonVertexIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Edges) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + return true; + } else if (name == ufbxi_Indexes) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Points) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_KnotVector) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_KnotVectorU) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_KnotVectorV) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_PointsIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Normals) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_LEGACY_MODEL: + if (name == ufbxi_Vertices) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_Normals) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_Materials) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_PolygonVertexIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Children) { + info->type = 's'; + return true; + } + break; + + case UFBXI_PARSE_ANIMATION_CURVE: + if (name == ufbxi_KeyTime) { + info->type = uc->opts.ignore_animation ? '-' : 'l'; + return true; + } else if (name == ufbxi_KeyValueFloat) { + info->type = uc->opts.ignore_animation ? '-' : 'r'; + return true; + } else if (name == ufbxi_KeyAttrFlags) { + info->type = uc->opts.ignore_animation ? '-' : 'i'; + return true; + } else if (name == ufbxi_KeyAttrDataFloat) { + // The float data in a keyframe attribute array is represented as integers + // in versions >= 7200 as some of the elements aren't actually floats (!) + info->type = uc->from_ascii && uc->version >= 7200 ? 'i' : 'f'; + if (uc->opts.ignore_animation) info->type = '-'; + if (uc->from_ascii && uc->version < 7200) { + info->flags |= UFBXI_ARRAY_FLAG_ACCURATE_F32; + } + return true; + } else if (name == ufbxi_KeyAttrRefCount) { + info->type = uc->opts.ignore_animation ? '-' : 'i'; + return true; + } + break; + + case UFBXI_PARSE_TEXTURE: + if (!strcmp(name, "ModelUVTranslation") || !strcmp(name, "ModelUVScaling") || !strcmp(name, "Cropping")) { + info->type = uc->opts.retain_dom ? 'r' : '-'; + return true; + } + break; + + case UFBXI_PARSE_VIDEO: + if (name == ufbxi_Content) { + info->type = uc->opts.ignore_embedded ? '-' : 'C'; + return true; + } + break; + + case UFBXI_PARSE_LAYERED_TEXTURE: + if (name == ufbxi_BlendModes) { + info->type = 'i'; + info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF; + return true; + } else if (name == ufbxi_Alphas) { + info->type = 'r'; + info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF; + return true; + } + break; + + case UFBXI_PARSE_SELECTION_NODE: + if (name == ufbxi_VertexIndexArray) { + info->type = 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_EdgeIndexArray) { + info->type = 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_PolygonIndexArray) { + info->type = 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_NORMAL: + if (name == ufbxi_Normals) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_NormalsIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_NormalsW) { + info->type = uc->retain_vertex_w ? 'r' : '-'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_BINORMAL: + if (name == ufbxi_Binormals) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_BinormalsIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_BinormalsW) { + info->type = uc->retain_vertex_w ? 'r' : '-'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_TANGENT: + if (name == ufbxi_Tangents) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_TangentsIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_TangentsW) { + info->type = uc->retain_vertex_w ? 'r' : '-'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_UV: + if (name == ufbxi_UV) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_UVIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_COLOR: + if (name == ufbxi_Colors) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_ColorIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_VERTEX_CREASE: + if (name == ufbxi_VertexCrease) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_VertexCreaseIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_EDGE_CREASE: + if (name == ufbxi_EdgeCrease) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_SMOOTHING: + if (name == ufbxi_Smoothing) { + info->type = uc->opts.ignore_geometry ? '-' : 'b'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_VISIBILITY: + if (name == ufbxi_Visibility) { + info->type = uc->opts.ignore_geometry ? '-' : 'b'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_POLYGON_GROUP: + if (name == ufbxi_PolygonGroup) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_HOLE: + if (name == ufbxi_Hole) { + info->type = uc->opts.ignore_geometry ? '-' : 'b'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_MATERIAL: + if (name == ufbxi_Materials) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_LAYER_ELEMENT_OTHER: + if (name == ufbxi_TextureId) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags |= UFBXI_ARRAY_FLAG_TMP_BUF; + return true; + } else if (name == ufbxi_UV) { + info->type = uc->opts.retain_dom ? 'r' : '-'; + return true; + } else if (name == ufbxi_UVIndex) { + info->type = uc->opts.retain_dom ? 'i' : '-'; + return true; + } + break; + + case UFBXI_PARSE_GEOMETRY_UV_INFO: + if (name == ufbxi_TextureUV) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } else if (name == ufbxi_TextureUVVerticeIndex) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_SHAPE: + if (name == ufbxi_Indexes) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + if (name == ufbxi_Vertices) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + if (name == ufbxi_Normals) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT | UFBXI_ARRAY_FLAG_PAD_BEGIN; + return true; + } + break; + + case UFBXI_PARSE_DEFORMER: + if (name == ufbxi_Transform) { + info->type = 'r'; + return true; + } else if (name == ufbxi_TransformLink) { + info->type = 'r'; + return true; + } else if (name == ufbxi_Indexes) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Weights) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_BlendWeights) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_FullWeights) { + info->type = 'r'; + info->flags = (uint8_t)(info->flags | (uc->blender_full_weights ? UFBXI_ARRAY_FLAG_RESULT : UFBXI_ARRAY_FLAG_TMP_BUF)); + return true; + } else if (!strcmp(name, "TransformAssociateModel")) { + info->type = uc->opts.retain_dom ? 'r' : '-'; + return true; + } + break; + + case UFBXI_PARSE_ASSOCIATE_MODEL: + if (name == ufbxi_Transform) { + info->type = uc->opts.retain_dom ? 'r' : '-'; + return true; + } + break; + + case UFBXI_PARSE_LEGACY_LINK: + if (name == ufbxi_Transform) { + info->type = 'r'; + return true; + } else if (name == ufbxi_TransformLink) { + info->type = 'r'; + return true; + } else if (name == ufbxi_Indexes) { + info->type = uc->opts.ignore_geometry ? '-' : 'i'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } else if (name == ufbxi_Weights) { + info->type = uc->opts.ignore_geometry ? '-' : 'r'; + info->flags = UFBXI_ARRAY_FLAG_RESULT; + return true; + } + break; + + case UFBXI_PARSE_POSE_NODE: + if (name == ufbxi_Matrix) { + info->type = 'r'; + return true; + } + break; + + case UFBXI_PARSE_CHANNEL: + if (name == ufbxi_Key) { + info->type = uc->opts.ignore_animation ? '-' : 'd'; + return true; + } + break; + + case UFBXI_PARSE_AUDIO: + if (name == ufbxi_Content) { + info->type = uc->opts.ignore_embedded ? '-' : 'C'; + return true; + } + break; + + default: + if (name == ufbxi_BinaryData) { + info->type = uc->opts.ignore_embedded ? '-' : 'C'; + return true; + } + break; + + } + + return false; +} + +static ufbxi_noinline bool ufbxi_is_raw_string(ufbxi_context *uc, ufbxi_parse_state parent, const char *name, size_t index) +{ + (void)index; + + switch (parent) { + + case UFBXI_PARSE_ROOT: + if (name == ufbxi_Model) return true; + if (!strcmp(name, "FileId")) return true; + break; + + case UFBXI_PARSE_FBX_HEADER_EXTENSION: + if (name == ufbxi_SceneInfo) return true; + break; + + case UFBXI_PARSE_OBJECTS: + return true; + + case UFBXI_PARSE_CONNECTIONS: + case UFBXI_PARSE_RELATIONS: + // Pre-7000 needs raw strings for "Name\x00\x01Type" pairs, post-7000 uses it only + // for properties that are non-raw by default. + return uc->version < 7000; + + case UFBXI_PARSE_MODEL: + if (name == ufbxi_NodeAttributeName) return true; + if (name == ufbxi_Name) return true; + break; + + case UFBXI_PARSE_VIDEO: + if (name == ufbxi_Content) return true; + break; + + case UFBXI_PARSE_TEXTURE: + if (!strcmp(name, "TextureName")) return true; + if (name == ufbxi_Media) return true; + break; + + case UFBXI_PARSE_GEOMETRY: + if (name == ufbxi_NodeAttributeName) return true; + if (name == ufbxi_Name) return true; + break; + + case UFBXI_PARSE_NODE_ATTRIBUTE: + if (name == ufbxi_NodeAttributeName) return true; + if (name == ufbxi_Name) return true; + break; + + case UFBXI_PARSE_POSE_NODE: + if (name == ufbxi_Node) return true; + break; + + case UFBXI_PARSE_SELECTION_NODE: + if (name == ufbxi_Node) return true; + break; + + case UFBXI_PARSE_UNKNOWN_OBJECT: + if (name == ufbxi_NodeAttributeName) return true; + if (name == ufbxi_Name) return true; + break; + + case UFBXI_PARSE_COLLECTION: + if (!strcmp(name, "Member")) return true; + break; + + case UFBXI_PARSE_AUDIO: + if (name == ufbxi_Content) return true; + break; + + case UFBXI_PARSE_LEGACY_MODEL: + if (name == ufbxi_Material) return true; + if (name == ufbxi_Link) return true; + if (name == ufbxi_Name) return true; + break; + + case UFBXI_PARSE_LEGACY_SWITCHER: + if (!strcmp(name, "CameraIndexName")) return true; + break; + + case UFBXI_PARSE_LEGACY_SCENE_PERSISTENCE: + if (name == ufbxi_SceneInfo) return true; + break; + + case UFBXI_PARSE_REFERENCE: + if (!strcmp(name, "Object")) return true; + break; + + case UFBXI_PARSE_TAKE: + if (name == ufbxi_Model) return true; + break; + + default: + break; + + } + + return false; +} + +// -- Binary parsing + +ufbxi_nodiscard static ufbxi_noinline char *ufbxi_swap_endian(ufbxi_context *uc, const void *src, size_t count, size_t elem_size) +{ + ufbxi_dev_assert(elem_size > 1); + size_t total_size = count * elem_size; + ufbxi_check_return(!ufbxi_does_overflow(total_size, count, elem_size), NULL); + if (uc->swap_arr_size < total_size) { + ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->swap_arr, &uc->swap_arr_size, total_size), NULL); + } + char *dst = uc->swap_arr, *d = dst; + + const char *s = (const char*)src; + switch (elem_size) { + case 2: + ufbxi_nounroll for (size_t i = 0; i < count; i++) { + d[0] = s[1]; d[1] = s[0]; + d += 2; s += 2; + } + break; + case 4: + ufbxi_nounroll for (size_t i = 0; i < count; i++) { + d[0] = s[3]; d[1] = s[2]; d[2] = s[1]; d[3] = s[0]; + d += 4; s += 4; + } + break; + case 8: + ufbxi_nounroll for (size_t i = 0; i < count; i++) { + d[0] = s[7]; d[1] = s[6]; d[2] = s[5]; d[3] = s[4]; + d[4] = s[3]; d[5] = s[2]; d[6] = s[1]; d[7] = s[0]; + d += 8; s += 8; + } + break; + default: + ufbxi_unreachable("Bad endian swap size"); + } + + return dst; +} + +// Swap the endianness of an array typed with a lowercase letter +ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_swap_endian_array(ufbxi_context *uc, const void *src, size_t count, char type) +{ + switch (type) { + case 'i': case 'f': return ufbxi_swap_endian(uc, src, count, 4); + case 'l': case 'd': return ufbxi_swap_endian(uc, src, count, 8); + default: return (const char*)src; + } +} + +// Swap the endianness of a single value (shallow, swaps string/array header words) +ufbxi_nodiscard static ufbxi_noinline const char *ufbxi_swap_endian_value(ufbxi_context *uc, const void *src, char type) +{ + switch (type) { + case 'Y': return ufbxi_swap_endian(uc, src, 1, 2); + case 'I': case 'F': return ufbxi_swap_endian(uc, src, 1, 4); + case 'L': case 'D': return ufbxi_swap_endian(uc, src, 1, 8); + case 'S': case 'R': return ufbxi_swap_endian(uc, src, 1, 4); + case 'i': case 'l': case 'f': case 'd': case 'b': return ufbxi_swap_endian(uc, src, 3, 4); + default: return (const char*)src; + } +} + +// Read and convert a post-7000 FBX data array into a different format. `src_type` may be equal to `dst_type` +// if the platform is not binary compatible with the FBX data representation. +ufbxi_nodiscard static ufbxi_noinline int ufbxi_binary_convert_array(ufbxi_context *maybe_uc, char src_type, char dst_type, const void *src, void *dst, size_t size) +{ + // TODO: We might want to use the slow path if the machine float/double doesn't match IEEE 754! + // Convert commented out lines under some `#if UFBX_NON_IEE754` define or something. + if (src_type == dst_type) { + ufbx_assert(maybe_uc && maybe_uc->file_big_endian != maybe_uc->local_big_endian); + src = ufbxi_swap_endian_array(maybe_uc, src, size, src_type); + ufbxi_check_err(&maybe_uc->error, src); + memcpy(dst, src, size * ufbxi_array_type_size(dst_type)); + return 1; + } + + if (maybe_uc && maybe_uc->file_big_endian) { + src = ufbxi_swap_endian_array(maybe_uc, src, size, src_type); + ufbxi_check_err(&maybe_uc->error, src); + } + + switch (dst_type) + { + + #define ufbxi_convert_loop_fast(m_dst, m_cast, m_size, m_expr) do { \ + const char *val = (const char*)src, *val_end = val + size*m_size; \ + m_dst *d = (m_dst*)dst; \ + while (val != val_end) { *d++ = m_cast(m_expr); val += m_size; } \ + } while (0) + + #define ufbxi_convert_loop_slow(m_dst, m_cast, m_size, m_expr) do { \ + const char *val = (const char*)src, *val_end = val + size*m_size; \ + m_dst *d = (m_dst*)dst; \ + ufbxi_nounroll while (val != val_end) { *d++ = m_cast(m_expr); val += m_size; } \ + } while (0) + + case 'c': + switch (src_type) { + // case 'c': ufbxi_convert_loop_fast(char, (char), 1, *val != 0); break; + case 'i': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 4, (uint8_t)ufbxi_read_i32(val)); break; + case 'l': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 8, (uint8_t)ufbxi_read_i64(val)); break; + case 'f': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 4, (uint8_t)ufbxi_read_f32(val)); break; + case 'd': ufbxi_convert_loop_slow(uint8_t, (uint8_t), 8, (uint8_t)ufbxi_read_f64(val)); break; + default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0; + } + break; + + case 'i': + switch (src_type) { + case 'c': ufbxi_convert_loop_slow(int32_t, (int32_t), 1, *val); break; + // case 'i': ufbxi_convert_loop_slow(int32_t, (int32_t), 4, ufbxi_read_i32(val)); break; + case 'l': ufbxi_convert_loop_slow(int32_t, (int32_t), 8, ufbxi_read_i64(val)); break; + case 'f': ufbxi_convert_loop_slow(int32_t, ufbxi_f64_to_i32, 4, ufbxi_read_f32(val)); break; + case 'd': ufbxi_convert_loop_slow(int32_t, ufbxi_f64_to_i32, 8, ufbxi_read_f64(val)); break; + default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0; + } + break; + + case 'l': + switch (src_type) { + case 'c': ufbxi_convert_loop_slow(int64_t, (int64_t), 1, *val); break; + case 'i': ufbxi_convert_loop_slow(int64_t, (int64_t), 4, ufbxi_read_i32(val)); break; + // case 'l': ufbxi_convert_loop_slow(int64_t, (int64_t), 8, ufbxi_read_i64(val)); break; + case 'f': ufbxi_convert_loop_slow(int64_t, ufbxi_f64_to_i64, 4, ufbxi_read_f32(val)); break; + case 'd': ufbxi_convert_loop_slow(int64_t, ufbxi_f64_to_i64, 8, ufbxi_read_f64(val)); break; + default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0; + } + break; + + case 'f': + switch (src_type) { + case 'c': ufbxi_convert_loop_slow(float, (float), 1, *val); break; + case 'i': ufbxi_convert_loop_slow(float, (float), 4, ufbxi_read_i32(val)); break; + case 'l': ufbxi_convert_loop_slow(float, (float), 8, ufbxi_read_i64(val)); break; + // case 'f': ufbxi_convert_loop_slow(float, (float), 4, ufbxi_read_f32(val)); break; + case 'd': ufbxi_convert_loop_fast(float, (float), 8, ufbxi_read_f64(val)); break; + default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0; + } + break; + + case 'd': + switch (src_type) { + case 'c': ufbxi_convert_loop_slow(double, (double), 1, *val); break; + case 'i': ufbxi_convert_loop_slow(double, (double), 4, ufbxi_read_i32(val)); break; + case 'l': ufbxi_convert_loop_slow(double, (double), 8, ufbxi_read_i64(val)); break; + case 'f': ufbxi_convert_loop_fast(double, (double), 4, ufbxi_read_f32(val)); break; + // case 'd': ufbxi_convert_loop_slow(double, (double), 8, ufbxi_read_f64(val)); break; + default: if (maybe_uc) ufbxi_fail_err(&maybe_uc->error, "Bad array source type"); return 0; + } + break; + + default: return 0; + + } + + return 1; +} + +// Read pre-7000 separate properties as an array. +ufbxi_nodiscard static ufbxi_noinline int ufbxi_binary_parse_multivalue_array(ufbxi_context *uc, char dst_type, void *dst, size_t size, ufbxi_buf *tmp_buf) +{ + if (size == 0) return 1; + const char *val; + size_t val_size; + + bool file_big_endian = uc->file_big_endian; + + #define ufbxi_convert_parse_fast(m_dst, m_type, m_expr) do { \ + m_dst *d = (m_dst*)dst; \ + for (; base < size; base++) { \ + val = ufbxi_peek_bytes(uc, 13); \ + ufbxi_check(val); \ + if (*val != m_type) break; \ + val++; \ + *d++ = (m_dst)(m_expr); \ + ufbxi_consume_bytes(uc, 1 + sizeof(m_dst)); \ + } \ + } while (0) + + // String array special case + if (dst_type == 's' || dst_type == 'S' || dst_type == 'C') { + bool raw = dst_type == 's'; + ufbx_string *d = (ufbx_string*)dst; + for (size_t i = 0; i < size; i++) { + val = ufbxi_peek_bytes(uc, 13); + ufbxi_check(val); + char type = *val++; + ufbxi_check(type == 'S' || type == 'R'); + if (file_big_endian) { + val = ufbxi_swap_endian_value(uc, val, type); + ufbxi_check(val); + } + size_t len = ufbxi_read_u32(val); + ufbxi_consume_bytes(uc, 5); + d->data = ufbxi_read_bytes(uc, len); + d->length = len; + ufbxi_check(d->data); + if (dst_type == 'C') { + ufbxi_buf *buf = size == 1 || uc->opts.retain_dom ? &uc->result : tmp_buf; + d->data = ufbxi_push_copy(buf, char, len, d->data); + ufbxi_check(d->data); + } else { + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, d, raw)); + } + d++; + } + return 1; + } + + // Optimize a couple of common cases + size_t base = 0; + if (!file_big_endian) { + switch (dst_type) { + case 'i': ufbxi_convert_parse_fast(int32_t, 'I', ufbxi_read_i32(val)); break; + case 'l': ufbxi_convert_parse_fast(int64_t, 'L', ufbxi_read_i64(val)); break; + case 'f': ufbxi_convert_parse_fast(float, 'F', ufbxi_read_f32(val)); break; + case 'd': ufbxi_convert_parse_fast(double, 'D', ufbxi_read_f64(val)); break; + default: break; // Fallthrough to rest + } + + // Early return if we handled everything + if (base == size) return 1; + } + + switch (dst_type) + { + + #define ufbxi_convert_parse(m_cast, m_size, m_expr) \ + *d++ = m_cast(m_expr); val_size = m_size + 1; \ + + #define ufbxi_convert_parse_switch(m_dst, m_cast_int, m_cast_float) do { \ + m_dst *d = (m_dst*)dst + base; \ + for (size_t i = base; i < size; i++) { \ + val = ufbxi_peek_bytes(uc, 13); \ + ufbxi_check(val); \ + char type = *val++; \ + if (file_big_endian) { \ + val = ufbxi_swap_endian_value(uc, val, type); \ + ufbxi_check(val); \ + } \ + switch (type) { \ + case 'C': \ + case 'B': ufbxi_convert_parse(m_cast_int, 1, *val); break; \ + case 'Y': ufbxi_convert_parse(m_cast_int, 2, ufbxi_read_i16(val)); break; \ + case 'I': ufbxi_convert_parse(m_cast_int, 4, ufbxi_read_i32(val)); break; \ + case 'L': ufbxi_convert_parse(m_cast_int, 8, ufbxi_read_i64(val)); break; \ + case 'F': ufbxi_convert_parse(m_cast_float, 4, ufbxi_read_f32(val)); break; \ + case 'D': ufbxi_convert_parse(m_cast_float, 8, ufbxi_read_f64(val)); break; \ + default: ufbxi_fail("Bad multivalue array type"); \ + } \ + ufbxi_consume_bytes(uc, val_size); \ + } \ + } while (0) + + case 'c': ufbxi_convert_parse_switch(uint8_t, (uint8_t), (uint8_t)); break; + case 'i': ufbxi_convert_parse_switch(int32_t, (int32_t), ufbxi_f64_to_i32); break; + case 'l': ufbxi_convert_parse_switch(int64_t, (int64_t), ufbxi_f64_to_i64); break; + case 'f': ufbxi_convert_parse_switch(float, (float), (float)); break; + case 'd': ufbxi_convert_parse_switch(double, (double), (double)); break; + + default: return 0; + + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static void *ufbxi_push_array_data(ufbxi_context *uc, const ufbxi_array_info *info, size_t size, ufbxi_buf *tmp_buf) +{ + size_t elem_size = ufbxi_array_type_size(info->type); + uint32_t flags = info->flags; + if (flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) size += 4; + + // The array may be pushed either to the result or temporary buffer depending + // if it's already in the right format + ufbxi_buf *arr_buf = tmp_buf; + if (flags & UFBXI_ARRAY_FLAG_RESULT) arr_buf = &uc->result; + else if (flags & UFBXI_ARRAY_FLAG_TMP_BUF) arr_buf = &uc->tmp; + char *data = (char*)ufbxi_push_size(arr_buf, elem_size, size); + ufbxi_check_return(data, NULL); + + if (flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) { + memset(data, 0, elem_size * 4); + data += elem_size * 4; + } + + return data; +} + +ufbxi_noinline static void ufbxi_postprocess_bool_array(char *data, size_t size) +{ + ufbxi_for(char, b, (char*)data, size) { + *b = (char)(*b != 0); + } +} + +typedef struct { + size_t encoded_size; + size_t src_elem_size; + size_t array_size; + char src_type; + char dst_type; + char arr_type; + const void *encoded_data; + void *decoded_data; + void *dst_data; + ufbx_inflate_retain *inflate_retain; +} ufbxi_deflate_task; + +static bool ufbxi_deflate_task_fn(ufbxi_task *task) +{ + ufbxi_deflate_task *t = (ufbxi_deflate_task*)task->data; + + ufbx_inflate_input input; // ufbxi_uninit + input.total_size = t->encoded_size; + input.data = t->encoded_data; + input.data_size = t->encoded_size; + input.no_header = false; + input.no_checksum = false; + input.internal_fast_bits = 0; + input.progress_cb.fn = NULL; + input.progress_cb.user = NULL; + input.progress_size_before = 0; + input.progress_size_after = 0; + input.progress_interval_hint = 0; + input.buffer = NULL; + input.buffer_size = 0; + input.read_fn = NULL; + input.read_user = NULL; + + size_t decoded_data_size = t->src_elem_size * t->array_size; + ptrdiff_t res = ufbx_inflate(t->decoded_data, decoded_data_size, &input, t->inflate_retain); + if (res == -28) { + task->error = "Cancelled"; + return false; + } else if (res != (ptrdiff_t)decoded_data_size) { + task->error = "Bad DEFLATE data"; + return false; + } + + if (t->decoded_data != t->dst_data) { + int ok = ufbxi_binary_convert_array(NULL, t->src_type, t->dst_type, t->decoded_data, t->dst_data, t->array_size); + if (!ok) { + task->error = "Failed to convert array"; + return false; + } + } + + if (t->arr_type == 'b') { + ufbxi_postprocess_bool_array((char*)t->dst_data, t->array_size); + } + + return true; +} + +// Recursion limited by check at the start +ufbxi_nodiscard ufbxi_noinline static int ufbxi_binary_parse_node(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive) + ufbxi_recursive_function(int, ufbxi_binary_parse_node, (uc, depth, parent_state, p_end, tmp_buf, recursive), UFBXI_MAX_NODE_DEPTH + 1, + (ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive)) +{ + // https://code.blender.org/2013/08/fbx-binary-file-format-specification + // Parse an FBX document node in the binary format + ufbxi_check(depth < UFBXI_MAX_NODE_DEPTH); + + // Parse the node header, post-7500 versions use 64-bit values for most + // header fields. + uint64_t end_offset, num_values64, values_len; + uint8_t name_len; + size_t header_size = (uc->version >= 7500) ? 25 : 13; + const char *header = ufbxi_read_bytes(uc, header_size), *header_words = header; + ufbxi_check(header); + if (uc->version >= 7500) { + if (uc->file_big_endian) { + header_words = ufbxi_swap_endian(uc, header_words, 3, 8); + ufbxi_check(header_words); + } + end_offset = ufbxi_read_u64(header_words + 0); + num_values64 = ufbxi_read_u64(header_words + 8); + values_len = ufbxi_read_u64(header_words + 16); + name_len = ufbxi_read_u8(header + 24); + } else { + if (uc->file_big_endian) { + header_words = ufbxi_swap_endian(uc, header_words, 3, 4); + ufbxi_check(header_words); + } + end_offset = ufbxi_read_u32(header_words + 0); + num_values64 = ufbxi_read_u32(header_words + 4); + values_len = ufbxi_read_u32(header_words + 8); + name_len = ufbxi_read_u8(header + 12); + } + + ufbxi_check(num_values64 <= UINT32_MAX); + uint32_t num_values = (uint32_t)num_values64; + + // If `end_offset` and `name_len` is zero we treat as the node as a NULL-sentinel + // that terminates a node list. + if (end_offset == 0 && name_len == 0) { + *p_end = true; + return 1; + } + + // Update estimated end offset if possible + if (end_offset > uc->progress_bytes_total) { + uc->progress_bytes_total = end_offset; + } + + // Push the parsed node into the `tmp_stack` buffer, the nodes will be popped by + // calling code after its done parsing all of it's children. + ufbxi_node *node = ufbxi_push_zero(&uc->tmp_stack, ufbxi_node, 1); + ufbxi_check(node); + + // Parse and intern the name to the string pool. + const char *name = ufbxi_read_bytes(uc, name_len); + ufbxi_check(name); + name = ufbxi_push_string(&uc->string_pool, name, name_len, NULL, true); + ufbxi_check(name); + node->name_len = name_len; + node->name = name; + + uint64_t values_end_offset = ufbxi_get_read_offset(uc) + values_len; + + // Check if the values of the node we're parsing currently should be + // treated as an array. + ufbxi_array_info arr_info; + if (ufbxi_is_array_node(uc, parent_state, name, &arr_info)) { + + // Normalize the array type (eg. 'r' to 'f'/'d' depending on the build) + // and get the per-element size of the array. + // Boolean arrays 'b' are normalized to 'c' as they are postprocessed + // below based on `arr_info.type`. + char dst_type = ufbxi_normalize_array_type(arr_info.type, 'c'); + + ufbxi_value_array *arr = ufbxi_push(tmp_buf, ufbxi_value_array, 1); + ufbxi_check(arr); + + node->value_type_mask = UFBXI_VALUE_ARRAY; + node->array = arr; + arr->type = ufbxi_normalize_array_type(arr_info.type, 'b'); + + // Peek the first bytes of the array. We can always look at least 13 bytes + // ahead safely as valid FBX files must end in a 13/25 byte NULL record. + const char *data = ufbxi_peek_bytes(uc, 13); + ufbxi_check(data); + + // Check if the data type is one of the explicit array types (post-7000). + // Otherwise we form the array by concatenating all the normal values of the + // node (pre-7000) + char c = data[0]; + + // HACK: Override the "type" if either the array is empty or we want to + // specifically ignore the contents. + if (num_values == 0) c = '0'; + if (dst_type == '-') c = '-'; + + bool deferred = false; + + if (c=='c' || c=='b' || c=='i' || c=='l' || c =='f' || c=='d') { + + const char *arr_words = data + 1; + if (uc->file_big_endian) { + arr_words = ufbxi_swap_endian(uc, arr_words, 3, 4); + ufbxi_check(arr_words); + } + + // Parse the array header from the prefix we already peeked above. + char src_type = data[0]; + uint32_t size = ufbxi_read_u32(arr_words + 0); + uint32_t encoding = ufbxi_read_u32(arr_words + 4); + uint32_t encoded_size = ufbxi_read_u32(arr_words + 8); + ufbxi_consume_bytes(uc, 13); + + // Normalize the source type as well, but don't convert UFBX-specific + // 'r' to 'f'/'d', but fail later instead. + if (src_type != 'r') src_type = ufbxi_normalize_array_type(src_type, 'c'); + size_t src_elem_size = ufbxi_array_type_size(src_type); + size_t decoded_data_size = src_elem_size * size; + + // Allocate `size` elements for the array. + char *arr_data = (char*)ufbxi_push_array_data(uc, &arr_info, size, tmp_buf); + ufbxi_check(arr_data); + + uint64_t arr_begin = ufbxi_get_read_offset(uc); + ufbxi_check(UINT64_MAX - encoded_size > arr_begin); + uint64_t arr_end = arr_begin + encoded_size; + if (arr_end > uc->progress_bytes_total) { + uc->progress_bytes_total = arr_end; + } + + // Threading + if (uc->parse_threaded && encoding == 1 && encoded_size >= UFBXI_MIN_THREADED_DEFLATE_BYTES && !uc->file_big_endian && !uc->local_big_endian) { + ufbxi_task *task = ufbxi_thread_pool_create_task(&uc->thread_pool, &ufbxi_deflate_task_fn); + if (task) { + ufbxi_deflate_task *t = ufbxi_push_zero(tmp_buf, ufbxi_deflate_task, 1); + ufbxi_check(t); + + ufbxi_inflate_init_retain(uc->inflate_retain); + + t->src_elem_size = src_elem_size; + t->encoded_size = encoded_size; + t->array_size = size; + t->src_type = src_type; + t->dst_type = dst_type; + t->arr_type = arr->type; + t->dst_data = arr_data; + t->inflate_retain = uc->inflate_retain; + + if (!uc->read_fn) { + // From memory, no need to copy + t->encoded_data = uc->data; + } else { + void *encoded_data = ufbxi_push(tmp_buf, char, encoded_size); + ufbxi_check(encoded_data); + ufbxi_check(ufbxi_read_to(uc, encoded_data, encoded_size)); + t->encoded_data = encoded_data; + } + + if (src_type != dst_type) { + t->decoded_data = ufbxi_push_size(tmp_buf, src_elem_size, size); + ufbxi_check(t->decoded_data); + } else { + t->decoded_data = arr_data; + } + + task->data = t; + ufbxi_thread_pool_run_task(&uc->thread_pool, task); + deferred = true; + } + } + + // If the source and destination types are equal and our build is binary-compatible + // with the FBX format we can read the decoded data directly into the array buffer. + // Otherwise we need a temporary buffer to decode the array into before conversion. + void *decoded_data = arr_data; + if (!deferred && (src_type != dst_type || uc->local_big_endian != uc->file_big_endian)) { + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, decoded_data_size)); + decoded_data = uc->tmp_arr; + } + + if (deferred) { + // Nop + } else if (encoding == 0) { + // Encoding 0: Plain binary data. + ufbxi_check(encoded_size == decoded_data_size); + + // If the array is contained in the current read buffer and we need to convert + // the data anyway we can use the read buffer as the decoded array source, otherwise + // do a plain byte copy to the array/conversion buffer. + if (uc->yield_size + uc->data_size >= encoded_size && decoded_data != arr_data) { + // Yield right after this if we crossed the yield threshold + if (encoded_size > uc->yield_size) { + uc->data_size += uc->yield_size; + uc->yield_size = encoded_size; + uc->data_size -= uc->yield_size; + } + + decoded_data = (void*)uc->data; + ufbxi_consume_bytes(uc, encoded_size); + } else { + ufbxi_check(ufbxi_read_to(uc, decoded_data, encoded_size)); + } + } else if (encoding == 1) { + // Encoding 1: DEFLATE + + ufbxi_pause_progress(uc); + + // Inflate the data from the user-provided IO buffer / read callbacks + ufbx_inflate_input input; + input.total_size = encoded_size; + input.data = uc->data; + input.data_size = uc->data_size; + input.no_header = false; + input.no_checksum = false; + input.internal_fast_bits = 0; + + if (uc->opts.progress_cb.fn) { + input.progress_cb = uc->opts.progress_cb; + input.progress_size_before = arr_begin; + input.progress_size_after = uc->progress_bytes_total - arr_end; + input.progress_interval_hint = uc->progress_interval; + } else { + input.progress_cb.fn = NULL; + input.progress_cb.user = NULL; + input.progress_size_before = 0; + input.progress_size_after = 0; + input.progress_interval_hint = 0; + } + + // If the encoded array is larger than the data we have currently buffered + // we need to allow `ufbx_inflate()` to read from the IO callback. We can + // let `ufbx_inflate()` freely clobber our `read_buffer` as all the data + // in the buffer will be consumed. `ufbx_inflate()` always reads exactly + // the amount of bytes needed so we can continue reading from `read_fn` as + // usual (given that we clear the `uc->data/_size` buffer below). + // NOTE: We _cannot_ share `read_buffer` if we plan to read later from it + // as `ufbx_inflate()` overwrites parts of it with zeroes. + if (encoded_size > input.data_size) { + input.buffer = uc->read_buffer; + input.buffer_size = uc->read_buffer_size; + input.read_fn = uc->read_fn; + input.read_user = uc->read_user; + uc->data_offset += encoded_size - input.data_size; + uc->data += input.data_size; + uc->data_size = 0; + } else { + input.buffer = NULL; + input.buffer_size = 0; + input.read_fn = NULL; + input.read_user = NULL; + uc->data += encoded_size; + uc->data_size -= encoded_size; + ufbxi_check(ufbxi_resume_progress(uc)); + } + + ptrdiff_t res = ufbx_inflate(decoded_data, decoded_data_size, &input, uc->inflate_retain); + ufbxi_check_msg(res != -28, "Cancelled"); + ufbxi_check_msg(res == (ptrdiff_t)decoded_data_size, "Bad DEFLATE data"); + + } else { + ufbxi_fail("Bad array encoding"); + } + + // Convert the decoded array if necessary. + if (!deferred && decoded_data != arr_data) { + ufbxi_check(ufbxi_binary_convert_array(uc, src_type, dst_type, decoded_data, arr_data, size)); + } + + arr->data = arr_data; + arr->size = size; + + } else if (c == '0' || c == '-') { + // Ignore the array + arr->type = c == '-' ? '-' : dst_type; + arr->data = (char*)ufbxi_zero_size_buffer + 32; + arr->size = 0; + } else { + // Allocate `num_values` elements for the array and parse single values into it. + char *arr_data = (char*)ufbxi_push_array_data(uc, &arr_info, num_values, tmp_buf); + ufbxi_check(arr_data); + ufbxi_check(ufbxi_binary_parse_multivalue_array(uc, dst_type, arr_data, num_values, tmp_buf)); + arr->data = arr_data; + arr->size = num_values; + } + + // Post-process boolean arrays + if (!deferred && arr_info.type == 'b') { + ufbxi_postprocess_bool_array((char*)arr->data, arr->size); + } + + } else { + // Parse up to UFBXI_MAX_NON_ARRAY_VALUES as plain values + num_values = ufbxi_min32(num_values, UFBXI_MAX_NON_ARRAY_VALUES); + ufbxi_value *vals = ufbxi_push(tmp_buf, ufbxi_value, num_values); + ufbxi_check(vals); + node->vals = vals; + + uint32_t type_mask = 0; + for (size_t i = 0; i < (size_t)num_values; i++) { + // The file must end in a 13/25 byte NULL record, so we can peek + // up to 13 bytes safely here. + const char *data = ufbxi_peek_bytes(uc, 13); + ufbxi_check(data); + + const char *value = data + 1; + + char type = data[0]; + if (uc->file_big_endian) { + value = ufbxi_swap_endian_value(uc, value, type); + ufbxi_check(value); + } + + switch (type) { + + case 'C': case 'B': case 'Z': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].f = (double)(vals[i].i = (int64_t)(uint8_t)value[0]); + ufbxi_consume_bytes(uc, 2); + break; + + case 'Y': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].f = (double)(vals[i].i = ufbxi_read_i16(value)); + ufbxi_consume_bytes(uc, 3); + break; + + case 'I': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].f = (double)(vals[i].i = ufbxi_read_i32(value)); + ufbxi_consume_bytes(uc, 5); + break; + + case 'L': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].f = (double)(vals[i].i = ufbxi_read_i64(value)); + ufbxi_consume_bytes(uc, 9); + break; + + case 'F': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].i = ufbxi_f64_to_i64(vals[i].f = ufbxi_read_f32(value)); + ufbxi_consume_bytes(uc, 5); + break; + + case 'D': + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (i*2); + vals[i].i = ufbxi_f64_to_i64(vals[i].f = ufbxi_read_f64(value)); + ufbxi_consume_bytes(uc, 9); + break; + + case 'S': case 'R': + { + uint32_t length = ufbxi_read_u32(value); + ufbxi_consume_bytes(uc, 5); + const char *str = ufbxi_read_bytes(uc, length); + ufbxi_check(str); + + if (length == 0) { + vals[i].s.raw_data = ufbxi_empty_char; + vals[i].s.raw_length = 0; + vals[i].s.utf8_length = 0; + } else { + bool non_ascii = false; + uint32_t hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii); + bool raw = !non_ascii || ufbxi_is_raw_string(uc, parent_state, name, i); + ufbxi_check(ufbxi_push_sanitized_string(&uc->string_pool, &vals[i].s, str, length, hash, raw)); + + // Mark the data as invalid UTF-8 + if (non_ascii && raw) vals[i].s.utf8_length = UINT32_MAX; + } + + type_mask |= (uint32_t)UFBXI_VALUE_STRING << (i*2); + } + break; + + // Treat arrays as non-values and skip them + case 'c': case 'b': case 'i': case 'l': case 'f': case 'd': + { + uint32_t encoded_size = ufbxi_read_u32(value + 8); + ufbxi_consume_bytes(uc, 13); + ufbxi_check(ufbxi_skip_bytes(uc, encoded_size)); + } + break; + + default: + ufbxi_fail("Bad value type"); + + } + } + + node->value_type_mask = (uint16_t)type_mask; + } + + // Skip over remaining values if necessary if we for example truncated + // the list of values or if there are values after an array + uint64_t offset = ufbxi_get_read_offset(uc); + ufbxi_check(offset <= values_end_offset); + if (offset < values_end_offset) { + ufbxi_check(ufbxi_skip_bytes(uc, values_end_offset - offset)); + } + + if (recursive) { + // Recursively parse the children of this node. Update the parse state + // to provide context for child node parsing. + ufbxi_parse_state parse_state = ufbxi_update_parse_state(parent_state, node->name); + uint32_t num_children = 0; + for (;;) { + // Stop at end offset + uint64_t current_offset = ufbxi_get_read_offset(uc); + if (current_offset >= end_offset) { + ufbxi_check(current_offset == end_offset || end_offset == 0); + break; + } + + bool end = false; + ufbxi_check(ufbxi_binary_parse_node(uc, depth + 1, parse_state, &end, tmp_buf, true)); + if (end) break; + num_children++; + } + + // Pop children from `tmp_stack` to a contiguous array + node->num_children = num_children; + if (num_children > 0) { + node->children = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node, num_children); + ufbxi_check(node->children); + } + } else { + uint64_t current_offset = ufbxi_get_read_offset(uc); + uc->has_next_child = (current_offset < end_offset); + } + + return 1; +} + +#define UFBXI_BINARY_MAGIC_SIZE 22 +#define UFBXI_BINARY_HEADER_SIZE 27 +static const char ufbxi_binary_magic[] = "Kaydara FBX Binary \x00\x1a"; + +// -- ASCII parsing + +#define UFBXI_ASCII_END '\0' +#define UFBXI_ASCII_NAME 'N' +#define UFBXI_ASCII_BARE_WORD 'B' +#define UFBXI_ASCII_INT 'I' +#define UFBXI_ASCII_FLOAT 'F' +#define UFBXI_ASCII_STRING 'S' + +static ufbxi_noinline char ufbxi_ascii_refill(ufbxi_context *uc) +{ + ufbxi_ascii *ua = &uc->ascii; + uc->data_offset += ufbxi_to_size(ua->src - uc->data_begin); + if (uc->read_fn) { + char *dst_buffer = NULL; + size_t dst_size = 0; + + if (ua->retain_buf != NULL) { + dst_size = uc->opts.read_buffer_size; + dst_buffer = ufbxi_push(ua->retain_buf, char, dst_size); + ufbxi_check_return(dst_buffer, '\0'); + ua->src_is_retained = true; + ua->src_buf = ua->retain_buf; + } else { + // Grow the read buffer if necessary + if (uc->read_buffer_size < uc->opts.read_buffer_size) { + size_t new_size = uc->opts.read_buffer_size; + ufbxi_check_return(ufbxi_grow_array(&uc->ator_tmp, &uc->read_buffer, &uc->read_buffer_size, new_size), '\0'); + } + dst_buffer = uc->read_buffer; + dst_size = uc->read_buffer_size; + ua->src_is_retained = false; + ua->src_buf = NULL; + } + + // Read user data, return '\0' on EOF + // TODO: Very unoptimal for non-full-size reads in some cases + size_t num_read = uc->read_fn(uc->read_user, dst_buffer, dst_size); + ufbxi_check_return_msg(num_read != SIZE_MAX, '\0', "IO error"); + ufbxi_check_return(num_read <= dst_size, '\0'); + if (num_read == 0) return '\0'; + + uc->data = uc->data_begin = ua->src = dst_buffer; + ua->src_end = dst_buffer + num_read; + return *ua->src; + } else { + // If the user didn't specify a `read_fn()` treat anything + // past the initial data buffer as EOF. + uc->data = uc->data_begin = ua->src = ""; + ua->src_end = ua->src + 1; + return '\0'; + } +} + +static ufbxi_noinline char ufbxi_ascii_yield(ufbxi_context *uc) +{ + ufbxi_ascii *ua = &uc->ascii; + + char ret; + if (ua->src == ua->src_end) { + ret = ufbxi_ascii_refill(uc); + } else { + ret = *ua->src; + } + + if (ufbxi_to_size(ua->src_end - ua->src) < uc->progress_interval) { + ua->src_yield = ua->src_end; + } else { + ua->src_yield = ua->src + uc->progress_interval; + } + + // TODO: Unify these properly + uc->data = ua->src; + ufbxi_check_return(ufbxi_report_progress(uc), '\0'); + return ret; +} + +static ufbxi_forceinline char ufbxi_ascii_peek(ufbxi_context *uc) +{ + ufbxi_ascii *ua = &uc->ascii; + if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc); + return *ua->src; +} + +static ufbxi_forceinline char ufbxi_ascii_next(ufbxi_context *uc) +{ + ufbxi_ascii *ua = &uc->ascii; + if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc); + ua->src++; + if (ua->src == ua->src_yield) return ufbxi_ascii_yield(uc); + return *ua->src; +} + +static ufbxi_noinline uint32_t ufbxi_ascii_parse_version(ufbxi_context *uc) +{ + uint8_t digits[3]; + uint32_t num_digits = 0; + + char c = ufbxi_ascii_next(uc); + + const char fmt[] = " FBX ?.?.?"; + uint32_t ix = 0; + while (num_digits < 3) { + char ref = fmt[ix++]; + switch (ref) { + + // Digit + case '?': + if (c < '0' || c > '9') return 0; + digits[num_digits++] = (uint8_t)(c - '0'); + c = ufbxi_ascii_next(uc); + break; + + // Whitespace + case ' ': + while (c == ' ' || c == '\t') { + c = ufbxi_ascii_next(uc); + } + break; + + // Literal character + default: + if (c != ref) return 0; + c = ufbxi_ascii_next(uc); + break; + } + } + + if (num_digits != 3) return 0; + return 1000u*(uint32_t)digits[0] + 100u*(uint32_t)digits[1] + 10u*(uint32_t)digits[2]; +} + +static const uint32_t ufbxi_space_mask = + (1u << ((uint32_t)' ' - 1)) | + (1u << ((uint32_t)'\t' - 1)) | + (1u << ((uint32_t)'\r' - 1)) | + (1u << ((uint32_t)'\n' - 1)) ; + +ufbx_static_assert(space_codepoint, + (uint32_t)' ' <= 32u && (uint32_t)'\t' <= 32u && + (uint32_t)'\r' <= 32u && (uint32_t)'\n' <= 32u); + +static ufbxi_forceinline bool ufbxi_is_space(char c) +{ + uint32_t v = (uint32_t)(uint8_t)c - 1; + return v < 32 && ((ufbxi_space_mask >> v) & 0x1) != 0; +} + +static ufbxi_noinline char ufbxi_ascii_skip_whitespace(ufbxi_context *uc) +{ + ufbxi_ascii *ua = &uc->ascii; + + // Ignore whitespace + char c = ufbxi_ascii_peek(uc); + for (;;) { + while (ufbxi_is_space(c)) { + c = ufbxi_ascii_next(uc); + } + + // Line comment + if (c == ';') { + + bool read_magic = false; + // FBX ASCII files begin with a magic comment of form "; FBX 7.7.0 project file" + // Try to extract the version number from the magic comment + if (!ua->read_first_comment) { + ua->read_first_comment = true; + uint32_t version = ufbxi_ascii_parse_version(uc); + if (version) { + uc->version = version; + ua->found_version = true; + read_magic = true; + } + } + + c = ufbxi_ascii_next(uc); + while (c != '\n' && c != '\0') { + c = ufbxi_ascii_next(uc); + } + c = ufbxi_ascii_next(uc); + + // Try to determine if this is a Blender 6100 ASCII file + if (read_magic) { + if (c == ';') { + char line[32]; + size_t line_len = 0; + + c = ufbxi_ascii_next(uc); + while (c != '\n' && c != '\0') { + if (line_len < sizeof(line)) { + line[line_len++] = c; + } + c = ufbxi_ascii_next(uc); + } + + if (line_len >= 19 && !memcmp(line, " Created by Blender", 19)) { + uc->exporter = UFBX_EXPORTER_BLENDER_ASCII; + } + } + } + + } else { + break; + } + } + return c; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_char(ufbxi_context *uc, ufbxi_ascii_token *token, char c) +{ + // Grow the string data buffer if necessary + if (token->str_len == token->str_cap) { + size_t len = ufbxi_max_sz(token->str_len + 1, 256); + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &token->str_data, &token->str_cap, len)); + } + + token->str_data[token->str_len++] = c; + + return 1; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_ascii_push_token_string(ufbxi_context *uc, ufbxi_ascii_token *token, const char *data, size_t length) +{ + // Grow the string data buffer if necessary + if (token->str_len + length >= token->str_cap) { + size_t len = ufbxi_max_sz(token->str_len + length, 256); + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &token->str_data, &token->str_cap, len)); + } + + memcpy(token->str_data + token->str_len, data, length); + token->str_len += length; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_skip_until(ufbxi_context *uc, char dst) +{ + ufbxi_ascii *ua = &uc->ascii; + + for (;;) { + size_t buffered = ufbxi_to_size(ua->src_yield - ua->src); + const char *match = (const char*)memchr(ua->src, dst, buffered); + if (match) { + ua->src = match; + break; + } else { + ua->src += buffered; + } + if (buffered == 0) { + char c = ufbxi_ascii_yield(uc); + ufbxi_check(c != '\0'); + } + } + + return 1; +} + +typedef struct { + const char *source; + size_t length; +} ufbxi_ascii_span; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_store_array(ufbxi_context *uc, ufbxi_buf *tmp_buf) +{ + ufbxi_ascii *ua = &uc->ascii; + + ua->retain_buf = tmp_buf; + + for (;;) { + size_t buffered = ufbxi_to_size(ua->src_yield - ua->src); + if (buffered == 0) { + char c = ufbxi_ascii_yield(uc); + ufbxi_check(c != '\0'); + continue; + } + + const char *begin = ua->src, *end; + const char *match = (const char*)memchr(begin, '}', buffered); + if (match) { + end = match; + } else { + end = begin + buffered; + } + ua->src = end; + + size_t length = ufbxi_to_size(end - begin); + ufbxi_ascii_span *span = ufbxi_push(&uc->tmp_ascii_spans, ufbxi_ascii_span, 1); + ufbxi_check(span); + // Store the trailing '}' for parsing + if (match) length += 1; + span->length = length; + if (ua->src_is_retained || !uc->read_fn) { + span->source = begin; + } else { + span->source = ufbxi_push_copy(tmp_buf, char, length, begin); + ufbxi_check(span->source); + } + + if (match) break; + } + + ua->retain_buf = NULL; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_try_ignore_string(ufbxi_context *uc, ufbxi_ascii_token *token) +{ + ufbxi_ascii *ua = &uc->ascii; + + char c = ufbxi_ascii_skip_whitespace(uc); + token->str_len = 0; + + if (c == '"') { + // Replace `prev_token` with `token` but swap the buffers so `token` uses + // the now-unused string buffer of the old `prev_token`. + char *swap_data = ua->prev_token.str_data; + size_t swap_cap = ua->prev_token.str_cap; + ua->prev_token = ua->token; + ua->token.str_data = swap_data; + ua->token.str_cap = swap_cap; + + token->type = UFBXI_ASCII_STRING; + // Skip opening quote + ufbxi_ascii_next(uc); + ufbxi_check(ufbxi_ascii_skip_until(uc, '"')); + // Skip closing quote + ufbxi_ascii_next(uc); + return true; + } + + return false; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_next_token(ufbxi_context *uc, ufbxi_ascii_token *token) +{ + ufbxi_ascii *ua = &uc->ascii; + + // Replace `prev_token` with `token` but swap the buffers so `token` uses + // the now-unused string buffer of the old `prev_token`. + char *swap_data = ua->prev_token.str_data; + size_t swap_cap = ua->prev_token.str_cap; + ua->prev_token = ua->token; + ua->token.str_data = swap_data; + ua->token.str_cap = swap_cap; + + char c = ufbxi_ascii_skip_whitespace(uc); + token->str_len = 0; + + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') { + token->type = UFBXI_ASCII_BARE_WORD; + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '(' || c == ')') { + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c)); + c = ufbxi_ascii_next(uc); + } + + // Skip whitespace to find if there's a following ':' + c = ufbxi_ascii_skip_whitespace(uc); + if (c == ':') { + token->value.name_len = token->str_len; + token->type = UFBXI_ASCII_NAME; + ufbxi_ascii_next(uc); + } + } else if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') { + token->type = UFBXI_ASCII_INT; + + token->negative = c == '-'; + while ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') { + if (c == '.' || c == 'e' || c == 'E') { + token->type = UFBXI_ASCII_FLOAT; + } + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c)); + c = ufbxi_ascii_next(uc); + } + + bool nan_like = false; + while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#' || c == '(' || c == ')') { + nan_like = true; + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c)); + c = ufbxi_ascii_next(uc); + } + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, '\0')); + if (nan_like) { + token->type = UFBXI_ASCII_FLOAT; + } + + char *end; + if (token->type == UFBXI_ASCII_INT) { + token->value.i64 = ufbxi_parse_int64(token->str_data, &end); + ufbxi_check(end == token->str_data + token->str_len - 1); + } else if (token->type == UFBXI_ASCII_FLOAT) { + uint32_t flags = uc->double_parse_flags; + if (ua->parse_as_f32) flags = UFBXI_PARSE_DOUBLE_AS_BINARY32; + token->value.f64 = ufbxi_parse_double(token->str_data, token->str_len, &end, flags); + ufbxi_check(end == token->str_data + token->str_len - 1); + } + } else if (c == '"') { + token->type = UFBXI_ASCII_STRING; + c = ufbxi_ascii_next(uc); + while (c != '"') { + + // Optimized string parsing for non-special characters + if (ua->src + 1 < ua->src_yield) { + const char *begin = ua->src; + const char *end = ua->src_yield; + const char *quot = (const char*)memchr(begin, '"', ufbxi_to_size(end - begin)); + if (quot) end = quot; + const char *esc = (const char*)memchr(begin, '&', ufbxi_to_size(end - begin)); + if (esc) end = esc; + + if (begin < end) { + ufbxi_check(ufbxi_ascii_push_token_string(uc, token, begin, ufbxi_to_size(end - begin))); + ua->src = end; + c = ufbxi_ascii_peek(uc); + continue; + } + } + + // Escape XML-like elements, funny enough there is no way to escape '&' itself, there is no `&`. + // '"' -> '"' + // '&cr;' -> '\r' + // '&lf;' -> '\n' + if (c == '&') { + const char *entity = NULL; + char replacement = '\0'; + + c = ufbxi_ascii_next(uc); + switch (c) { + case 'q': + entity = """; + replacement = '"'; + break; + case 'c': + entity = "&cr;"; + replacement = '\r'; + break; + case 'l': + entity = "&lf;"; + replacement = '\n'; + break; + default: + // As '&' is not escaped in any way just map '&' -> '&' + entity = "&"; + replacement = '&'; + break; + } + + size_t step = 1; + + ufbxi_dev_assert(entity && *entity); + // `entity` is a NULL terminated string longer than a single character + // cppcheck-suppress arrayIndexOutOfBounds + for (; entity[step]; step++) { + if (c != entity[step]) break; + c = ufbxi_ascii_next(uc); + } + + if (entity[step] == '\0') { + // Full match: Push the replacement character + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, replacement)); + } else { + // Partial match: Push the prefix we have skipped already + for (size_t i = 0; i < step; i++) { + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, entity[i])); + } + } + continue; + } + + ufbxi_check(c != '\0'); + ufbxi_check(ufbxi_ascii_push_token_char(uc, token, c)); + c = ufbxi_ascii_next(uc); + } + // Skip closing quote + ufbxi_ascii_next(uc); + } else { + // Single character token + token->type = c; + ufbxi_ascii_next(uc); + } + + return 1; +} + +ufbxi_nodiscard static int ufbxi_ascii_accept(ufbxi_context *uc, char type) +{ + ufbxi_ascii *ua = &uc->ascii; + + if (ua->token.type == type) { + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + return 1; + } else { + return 0; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_int_array(ufbxi_context *uc, char type, size_t *p_num_read) +{ + ufbxi_ascii *ua = &uc->ascii; + if (ua->parse_as_f32) return 1; + size_t initial_items = uc->tmp_stack.num_items; + + int64_t val; + if (ua->token.type == UFBXI_ASCII_INT) { + val = ua->token.value.i64; + } else { + return 1; + } + + const char *src = ua->src; + const char *end = ua->src_yield; + const char *src_scan = src; + + for (;;) { + + // Skip '\s*,\s*' between array elements. If we don't find a comma after an element + // don't push it as we can't be 100% certain whether it's a part of the array. + while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++; + if (src_scan == end || *src_scan != ',') break; + src_scan++; + while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++; + + // Found comma, commit to the position and push the previous value to the array + src = src_scan; + if (type == 'i') { + int32_t *v = ufbxi_push_fast(&uc->tmp_stack, int32_t, 1); + ufbxi_check(v); + *v = (int32_t)val; + } else if (type == 'l') { + int64_t *v = ufbxi_push_fast(&uc->tmp_stack, int64_t, 1); + ufbxi_check(v); + *v = (int64_t)val; + } + + // Try to parse the next value, we don't commit this until we find a comma after it above. + size_t left = ufbxi_to_size(end - src_scan); + if (left < 32) break; + + val = ufbxi_parse_int64(src_scan, (char**)&src_scan); + if (!src_scan) break; + } + + // Resume conventional parsing if we moved `src`. + if (src != ua->src) { + ua->src = src; + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + } + + *p_num_read = uc->tmp_stack.num_items - initial_items; + return 1; +} + +typedef struct { + void *arr_data; + char arr_type; + size_t arr_size; + const ufbxi_ascii_span *spans; + size_t num_spans; + size_t offset; +} ufbxi_ascii_array_task; + +ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_floats(ufbxi_ascii_array_task *t, const char *src, const char *src_end, uint32_t parse_flags) +{ + size_t offset = t->offset; + float *dst_float = t->arr_type == 'f' ? (float*)t->arr_data + offset : NULL; + double *dst_double = t->arr_type == 'd' ? (double*)t->arr_data + offset : NULL; + ufbx_assert(dst_float || dst_double); + const char *src_begin = src; + + while (src != src_end) { + while (ufbxi_is_space(*src)) src++; + + // Try to parse the next value, we don't commit this until we find a comma after it above. + char *num_end = NULL; + double val = ufbxi_parse_double(src, ufbxi_to_size(src_end - src), &num_end, parse_flags); + if (!num_end) return src_begin; + src = num_end; + + while (ufbxi_is_space(*src)) src++; + if (*src != ',') break; + src++; + src_begin = src; + + if (offset >= t->arr_size) return NULL; + if (dst_double) { + *dst_double++ = val; + } else { + *dst_float++ = (float)val; + } + offset++; + } + + t->offset = offset; + return src_begin; +} + +ufbxi_noinline static const char *ufbxi_ascii_array_task_parse_ints(ufbxi_ascii_array_task *t, const char *src, const char *src_end) +{ + size_t offset = t->offset; + int32_t *dst32 = t->arr_type == 'i' ? (int32_t*)t->arr_data + offset : NULL; + int64_t *dst64 = t->arr_type == 'l' ? (int64_t*)t->arr_data + offset : NULL; + ufbx_assert(dst32 || dst64); + const char *src_begin = src; + + while (src != src_end) { + while (ufbxi_is_space(*src)) src++; + + int64_t val = ufbxi_parse_int64(src, (char**)&src); + if (!src) return NULL; + + while (ufbxi_is_space(*src)) src++; + if (*src != ',') break; + src++; + src_begin = src; + + if (offset >= t->arr_size) return NULL; + if (dst32) { + *dst32++ = (int32_t)val; + } else { + *dst64++ = val; + } + offset++; + } + + t->offset = offset; + return src_begin; +} + +ufbxi_noinline static const char *ufbxi_ascii_array_task_parse(ufbxi_ascii_array_task *t, const char *src, const char *src_end) +{ + if (t->arr_type == 'f' || t->arr_type == 'd') { + uint32_t flags = ufbxi_parse_double_init_flags(); + return ufbxi_ascii_array_task_parse_floats(t, src, src_end, flags); + } else { + return ufbxi_ascii_array_task_parse_ints(t, src, src_end); + } +} + +typedef enum { + UFBXI_ASCII_SCAN_STATE_VALUE, + UFBXI_ASCII_SCAN_STATE_WHITESPACE, + UFBXI_ASCII_SCAN_STATE_COMMENT, + UFBXI_ASCII_SCAN_STATE_COMMA, +} ufbxi_ascii_scan_state; + +ufbxi_noinline static bool ufbxi_ascii_array_task_imp(ufbxi_ascii_array_task *t) +{ + // Temporary buffer for parsing between spans + char buffer[128]; // ufbxi_uninit + size_t buffer_len = 0; + bool buffer_value = false; + + ufbxi_ascii_scan_state state = UFBXI_ASCII_SCAN_STATE_WHITESPACE; + ufbxi_for(const ufbxi_ascii_span, span, t->spans, t->num_spans) { + const char *src = span->source; + const char *end = src + span->length; + + while (src != end) { + + // State machine for skipping whitespace and comments, potentially + // between multiple spans. + while (src != end) { + char c = *src; + if (state == UFBXI_ASCII_SCAN_STATE_VALUE) { + if (buffer_len >= sizeof(buffer) - 1) return false; + if (c == '"') { + return false; + } else if (c == ';' || ufbxi_is_space(c)) { + state = UFBXI_ASCII_SCAN_STATE_WHITESPACE; + buffer[buffer_len] = ' '; + buffer_len++; + } else if (c == ',' || c == '}') { + state = UFBXI_ASCII_SCAN_STATE_COMMA; + buffer[buffer_len] = ','; + buffer_len++; + src++; + break; + } else { + buffer_value = true; + buffer[buffer_len] = c; + buffer_len++; + src++; + } + } else if (state == UFBXI_ASCII_SCAN_STATE_WHITESPACE) { + if (c == ';') { + state = UFBXI_ASCII_SCAN_STATE_COMMENT; + } else if (ufbxi_is_space(c)) { + src++; + } else { + state = UFBXI_ASCII_SCAN_STATE_VALUE; + } + } else if (state == UFBXI_ASCII_SCAN_STATE_COMMENT) { + if (c == '\n') { + state = UFBXI_ASCII_SCAN_STATE_WHITESPACE; + } else { + src++; + } + } else if (state == UFBXI_ASCII_SCAN_STATE_COMMA) { + state = UFBXI_ASCII_SCAN_STATE_WHITESPACE; + } + } + + if (state == UFBXI_ASCII_SCAN_STATE_COMMA) { + // Parse a value from the buffer + if (buffer_value) { + const char *buffer_end = ufbxi_ascii_array_task_parse(t, buffer, buffer + buffer_len); + if (buffer_end == NULL || buffer_end == buffer) { + return false; + } + } + + // If not at end, we are past the last comma, so try to find a + // safe range to parse. + if (src != end) { + const char *parse_end = end; + while (parse_end > src) { + if (parse_end[-1] == ',') break; + parse_end--; + } + if (src < parse_end) { + src = ufbxi_ascii_array_task_parse(t, src, parse_end); + if (src == NULL) return false; + } + } + + buffer_len = 0; + buffer_value = false; + } + } + } + + if (t->offset != t->arr_size) return false; + + return true; +} + +ufbxi_noinline static bool ufbxi_ascii_array_task_fn(ufbxi_task *task) +{ + ufbxi_ascii_array_task *t = (ufbxi_ascii_array_task *)task->data; + if (!ufbxi_ascii_array_task_imp(t)) { + task->error = "Threaded ASCII parse error"; + return false; + } + return true; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_ascii_read_float_array(ufbxi_context *uc, char type, size_t *p_num_read) +{ + ufbxi_ascii *ua = &uc->ascii; + if (ua->parse_as_f32) return 1; + + double val; + if (ua->token.type == UFBXI_ASCII_FLOAT) { + val = ua->token.value.f64; + } else if (ua->token.type == UFBXI_ASCII_INT) { + double fsign = !ua->token.value.i64 && ua->token.negative ? -1.0 : 1.0; + val = (double)ua->token.value.i64 * fsign; + } else { + return 1; + } + + const char *src = ua->src; + const char *end = ua->src_yield; + + uint32_t parse_flags = uc->double_parse_flags; + + size_t initial_items = uc->tmp_stack.num_items; + const char *src_scan = src; + for (;;) { + + // Skip '\s*,\s*' between array elements. If we don't find a comma after an element + // don't push it as we can't be 100% certain whether it's a part of the array. + while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++; + if (src_scan == end || *src_scan != ',') break; + src_scan++; + while (src_scan != end && ufbxi_is_space(*src_scan)) src_scan++; + + // Found comma, commit to the position and push the previous value to the array + src = src_scan; + if (type == 'd') { + double *v = ufbxi_push_fast(&uc->tmp_stack, double, 1); + ufbxi_check(v); + *v = (double)val; + } else if (type == 'f') { + float *v = ufbxi_push_fast(&uc->tmp_stack, float, 1); + ufbxi_check(v); + *v = (float)val; + } + + // Try to parse the next value, we don't commit this until we find a comma after it above. + char *num_end = NULL; + size_t left = ufbxi_to_size(end - src_scan); + val = ufbxi_parse_double(src_scan, left, &num_end, parse_flags); + if (!num_end || num_end == src_scan || num_end >= end) { + break; + } + + src_scan = num_end; + } + + // Resume conventional parsing if we moved `src`. + if (src != ua->src) { + ua->src = src; + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + } + + *p_num_read = uc->tmp_stack.num_items - initial_items; + return 1; +} + +ufbxi_noinline static int ufbxi_setup_base64(ufbxi_context *uc) +{ + uint8_t *table = ufbxi_push(&uc->tmp, uint8_t, 256); + ufbxi_check(table); + uc->base64_table = table; + + memset(table, 0x80, 256); + ufbxi_nounroll for (char c = 'A'; c <= 'Z'; c++) table[(size_t)c] = (uint8_t)(c - 'A'); + ufbxi_nounroll for (char c = 'a'; c <= 'z'; c++) table[(size_t)c] = (uint8_t)(26 + (c - 'a')); + ufbxi_nounroll for (char c = '0'; c <= '9'; c++) table[(size_t)c] = (uint8_t)(52 + (c - '0')); + table[(size_t)'+'] = 62; + table[(size_t)'/'] = 63; + table[(size_t)'='] = 0x40; + + return 1; +} + +ufbxi_noinline static int ufbxi_decode_base64(ufbxi_context *uc, ufbx_string *p_result, const char *src, size_t src_length, bool *p_failed) +{ + if (!uc->base64_table) ufbxi_check(ufbxi_setup_base64(uc)); + + uint8_t *table = uc->base64_table; + uint32_t error_mask = 0, pad_error = 0; + + char *p = (char*)p_result->data; + for (size_t i = 0; i + 4 <= src_length; i += 4) { + uint32_t a = table[(size_t)(uint8_t)src[i + 0]]; + uint32_t b = table[(size_t)(uint8_t)src[i + 1]]; + uint32_t c = table[(size_t)(uint8_t)src[i + 2]]; + uint32_t d = table[(size_t)(uint8_t)src[i + 3]]; + pad_error = error_mask; + error_mask |= a | b | c | d; + + p[0] = (char)(uint8_t)(a << 2 | b >> 4); + p[1] = (char)(uint8_t)(b << 4 | c >> 2); + p[2] = (char)(uint8_t)(c << 6 | d); + p += 3; + } + + if (src_length >= 4) { + const char *end = src + src_length - 4; + uint32_t padding = 0; + padding |= end[0] == '=' ? 0x8 : 0x0; + padding |= end[1] == '=' ? 0x4 : 0x0; + padding |= end[2] == '=' ? 0x2 : 0x0; + padding |= end[3] == '=' ? 0x1 : 0x0; + if (padding <= 0x1) p -= padding; // "xxx=" or "xxxx" + else if (padding == 0x3) p -= 2; // "xx==" + else pad_error |= 0x40; // anything else + } + + if (((error_mask & 0x80) != 0 || (pad_error & 0x40) != 0 || src_length % 4 != 0) && !*p_failed) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_BAD_BASE64_CONTENT, "Ignored bad base64 embedded content")); + *p_failed = true; + } + + p_result->length = ufbxi_to_size(p - p_result->data); + return 1; +} + +// Recursion limited by check at the start +ufbxi_nodiscard ufbxi_noinline static int ufbxi_ascii_parse_node(ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive) + ufbxi_recursive_function(int, ufbxi_ascii_parse_node, (uc, depth, parent_state, p_end, tmp_buf, recursive), UFBXI_MAX_NODE_DEPTH + 1, + (ufbxi_context *uc, uint32_t depth, ufbxi_parse_state parent_state, bool *p_end, ufbxi_buf *tmp_buf, bool recursive)) +{ + ufbxi_ascii *ua = &uc->ascii; + + if (ua->token.type == '}') { + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + *p_end = true; + return 1; + } + + if (ua->token.type == UFBXI_ASCII_END) { + ufbxi_check_msg(depth == 0, "Truncated file"); + *p_end = true; + return 1; + } + + // Parse the name eg. "Node:" token and intern the name + ufbxi_check(depth < UFBXI_MAX_NODE_DEPTH); + if (!uc->sure_fbx && depth == 0 && ua->token.type != UFBXI_ASCII_NAME) { + ufbxi_fail_msg("Expected a 'Name:' token", "Not an FBX file"); + } + ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_NAME)); + size_t name_len = ua->prev_token.value.name_len; + ufbxi_check(name_len <= 0xff); + const char *name = ufbxi_push_string(&uc->string_pool, ua->prev_token.str_data, ua->prev_token.str_len, NULL, true); + ufbxi_check(name); + + // Push the parsed node into the `tmp_stack` buffer, the nodes will be popped by + // calling code after its done parsing all of it's children. + ufbxi_node *node = ufbxi_push_zero(&uc->tmp_stack, ufbxi_node, 1); + ufbxi_check(node); + node->name = name; + node->name_len = (uint8_t)name_len; + + bool in_ascii_array = false; + + uint32_t num_values = 0; + uint32_t type_mask = 0; + + int arr_type = 0; + ufbxi_buf *arr_buf = NULL; + size_t arr_elem_size = 0; + bool arr_error = false; + + // Check if the values of the node we're parsing currently should be + // treated as an array. + ufbxi_array_info arr_info; + if (ufbxi_is_array_node(uc, parent_state, name, &arr_info)) { + uint32_t flags = arr_info.flags; + arr_type = ufbxi_normalize_array_type(arr_info.type, 'b'); + arr_buf = tmp_buf; + if (flags & UFBXI_ARRAY_FLAG_RESULT) arr_buf = &uc->result; + else if (flags & UFBXI_ARRAY_FLAG_TMP_BUF) arr_buf = &uc->tmp; + + ufbxi_value_array *arr = ufbxi_push(tmp_buf, ufbxi_value_array, 1); + ufbxi_check(arr); + node->value_type_mask = UFBXI_VALUE_ARRAY; + node->array = arr; + arr->type = (char)arr_type; + + // Parse array values using strtof() if the array destination is 32-bit float + // since KeyAttrDataFloat packs integer data (!) into floating point values so we + // should try to be as exact as possible. + if (arr_info.flags & UFBXI_ARRAY_FLAG_ACCURATE_F32) { + ua->parse_as_f32 = true; + } + + arr_elem_size = ufbxi_array_type_size((char)arr_type); + + if (arr_type != '-') { + // Force alignment for array contents: This allows us to use `ufbxi_push_fast()` + // in fast parsing functions. + ufbxi_check(ufbxi_push_size_zero(&uc->tmp_stack, 8, 1)); + + // Pad with 4 zero elements to make indexing with `-1` safe. + if ((flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) != 0) { + ufbxi_check(ufbxi_push_size_zero(&uc->tmp_stack, arr_elem_size, 4)); + num_values += 4; + } + } + } + + // Some fields in ASCII may have leading commas eg. `Content: , "base64-string"` + if (ua->token.type == ',') { + // HACK: If we are parsing an "array" that should be ignored, ie. `Content` when + // `opts.ignore_embedded == true` try to skip the next token string if possible. + if (arr_type == '-') { + if (!ufbxi_ascii_try_ignore_string(uc, &ua->token)) { + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + } + } else { + ufbxi_check(ufbxi_ascii_next_token(uc, &ua->token)); + } + } + + ufbxi_parse_state parse_state = ufbxi_update_parse_state(parent_state, node->name); + ufbxi_value vals[UFBXI_MAX_NON_ARRAY_VALUES]; + + uint32_t deferred_size = 0; + + // NOTE: Infinite loop to allow skipping the comma parsing via `continue`. + for (;;) { + ufbxi_ascii_token *tok = &ua->prev_token; + + if (arr_type) { + size_t num_read = 0; + if (arr_type == 'f' || arr_type == 'd') { + ufbxi_check(ufbxi_ascii_read_float_array(uc, (char)arr_type, &num_read)); + } else if (arr_type == 'i' || arr_type == 'l') { + ufbxi_check(ufbxi_ascii_read_int_array(uc, (char)arr_type, &num_read)); + } + ufbxi_check(UINT32_MAX - num_values > num_read); + num_values += (uint32_t)num_read; + } + + if (ufbxi_ascii_accept(uc, UFBXI_ASCII_STRING)) { + + if (arr_type) { + + if (arr_type == 's' || arr_type == 'S' || arr_type == 'C') { + bool raw = arr_type == 's'; + ufbx_string *v = ufbxi_push(&uc->tmp_stack, ufbx_string, 1); + ufbxi_check(v); + if (arr_type == 'C') { + ufbxi_buf *buf = uc->opts.retain_dom ? &uc->result : tmp_buf; + size_t capacity = tok->str_len / 4 * 3 + 3; + v->data = ufbxi_push(buf, char, capacity); + ufbxi_check(v->data); + ufbxi_check(ufbxi_decode_base64(uc, v, tok->str_data, tok->str_len, &arr_error)); + ufbx_assert(v->length <= capacity); + } else { + v->data = tok->str_data; + v->length = tok->str_len; + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, v, raw)); + } + } else { + // Ignore strings in non-string arrays, decrement `num_values` as it will be + // incremented after the loop iteration is done to ignore it. + num_values--; + } + + } else if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) { + type_mask |= (uint32_t)UFBXI_VALUE_STRING << (num_values*2); + ufbxi_value *v = &vals[num_values]; + + const char *str = tok->str_data; + size_t length = tok->str_len; + ufbxi_check(str); + + if (length == 0) { + v->s.raw_data = ufbxi_empty_char; + v->s.raw_length = 0; + v->s.utf8_length = 0; + } else { + bool non_ascii = false; + uint32_t hash = ufbxi_hash_string_check_ascii(str, length, &non_ascii); + bool raw = !non_ascii || ufbxi_is_raw_string(uc, parent_state, name, num_values); + ufbxi_check(ufbxi_push_sanitized_string(&uc->string_pool, &v->s, str, length, hash, raw)); + if (non_ascii && raw) v->s.utf8_length = UINT32_MAX; + } + } + + } else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_INT)) { + int64_t val = tok->value.i64; + ufbx_real fsign = !val && tok->negative ? (ufbx_real)-1.0f : (ufbx_real)1.0f; + + switch (arr_type) { + + case 0: + // Parse version from comment if there was no magic comment + if (!ua->found_version && parse_state == UFBXI_PARSE_FBX_VERSION && num_values == 0) { + if (val >= 6000 && val <= 10000) { + ua->found_version = true; + uc->version = (uint32_t)val; + } + } + + if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) { + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2); + ufbxi_value *v = &vals[num_values]; + // False positive: `v->f` and `v->i` do not overlap in the union. + // cppcheck-suppress overlappingWriteUnion + v->f = (double)(v->i = val) * (double)fsign; + } + break; + + case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break; + case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break; + case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = (int32_t)val; } break; + case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = (int64_t)val; } break; + case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val * (float)fsign; } break; + case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val * (double)fsign; } break; + case '-': num_values--; break; + + default: + ufbxi_fail("Bad array dst type"); + + } + + } else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_FLOAT)) { + double val = tok->value.f64; + + switch (arr_type) { + + case 0: + if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) { + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2); + ufbxi_value *v = &vals[num_values]; + // False positive: `v->f` and `v->i` do not overlap in the union. + // cppcheck-suppress overlappingWriteUnion + v->i = ufbxi_f64_to_i64(v->f = val); + } + break; + + case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break; + case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break; + case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = ufbxi_f64_to_i32(val); } break; + case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = ufbxi_f64_to_i64(val); } break; + case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val; } break; + case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val; } break; + case '-': num_values--; break; + + default: + ufbxi_fail("Bad array dst type"); + + } + + } else if (ufbxi_ascii_accept(uc, UFBXI_ASCII_BARE_WORD)) { + + int64_t val = 0; + double val_f = 0.0; + if (tok->str_len >= 1) { + val = (int64_t)tok->str_data[0]; + val_f = (double)val; + if (tok->str_len > 1 && tok->str_len < 64) { + // Try to parse the bare word as NAN/INF + char str_data[64]; // ufbxi_uninit + size_t str_len = tok->str_len; + memcpy(str_data, tok->str_data, str_len); + str_data[str_len] = '\0'; + double inf_nan; + char *end = NULL; + if (ufbxi_parse_inf_nan(&inf_nan, str_data, str_len, &end) && end == str_data + str_len) { + val = 0; + val_f = inf_nan; + } + } + } + + switch (arr_type) { + + case 0: + if (num_values < UFBXI_MAX_NON_ARRAY_VALUES) { + type_mask |= (uint32_t)UFBXI_VALUE_NUMBER << (num_values*2); + ufbxi_value *v = &vals[num_values]; + // False positive: `v->f` and `v->i` do not overlap in the union. + // cppcheck-suppress overlappingWriteUnion + v->i = val; + v->f = val_f; + } + break; + + case 'b': { bool *v = ufbxi_push(&uc->tmp_stack, bool, 1); ufbxi_check(v); *v = val != 0; } break; + case 'c': { uint8_t *v = ufbxi_push(&uc->tmp_stack, uint8_t, 1); ufbxi_check(v); *v = (uint8_t)val; } break; + case 'i': { int32_t *v = ufbxi_push(&uc->tmp_stack, int32_t, 1); ufbxi_check(v); *v = (int32_t)val; } break; + case 'l': { int64_t *v = ufbxi_push(&uc->tmp_stack, int64_t, 1); ufbxi_check(v); *v = (int64_t)val; } break; + case 'f': { float *v = ufbxi_push(&uc->tmp_stack, float, 1); ufbxi_check(v); *v = (float)val_f; } break; + case 'd': { double *v = ufbxi_push(&uc->tmp_stack, double, 1); ufbxi_check(v); *v = (double)val_f; } break; + case '-': num_values--; break; + + default: + ufbxi_fail("Bad array dst type"); + } + + } else if (ufbxi_ascii_accept(uc, '*')) { + // Parse a post-7000 ASCII array eg. "*3 { 1,2,3 }" + ufbxi_check(!in_ascii_array); + ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_INT)); + int64_t count = ua->prev_token.value.i64; + + if (ufbxi_ascii_accept(uc, '{')) { + ufbxi_check(ufbxi_ascii_accept(uc, UFBXI_ASCII_NAME)); + in_ascii_array = true; + + // Optimized array skipping and threaded parsing + if (arr_type == '-') { + ufbxi_check(ufbxi_ascii_skip_until(uc, '}')); + } else if (uc->parse_threaded && !uc->opts.force_single_thread_ascii_parsing + && !ua->parse_as_f32 + && (arr_type == 'i' || arr_type == 'l' || arr_type == 'f' || arr_type == 'd')) { + // Don't bother with small arrays due to fixed overhead + if (count >= UFBXI_MIN_THREADED_ASCII_VALUES && count <= UINT32_MAX) { + deferred_size = (uint32_t)count - 1; + ufbxi_check(ufbxi_ascii_store_array(uc, tmp_buf)); + } + } + } + + // NOTE: This `continue` skips incrementing `num_values` and parsing + // a comma, continuing to parse the values in the array. + continue; + } else { + break; + } + + // Add value and keep parsing if there's a comma. This part may be + // skipped if we enter an array block. + num_values++; + ufbxi_check(num_values < UINT32_MAX); + if (!ufbxi_ascii_accept(uc, ',')) break; + } + + // Close the ASCII array if we are in one + if (in_ascii_array) { + ufbxi_check(ufbxi_ascii_accept(uc, '}')); + } + + ua->parse_as_f32 = false; + + if (arr_type) { + if (arr_type == '-') { + node->array->data = NULL; + node->array->size = 0; + } else { + void *arr_data = NULL; + + if (deferred_size > 0) { + arr_data = ufbxi_push_size(arr_buf, arr_elem_size, num_values + deferred_size); + // Pop any previously pushed values + if (num_values > 0) { + ufbxi_pop_size(&uc->tmp_stack, arr_elem_size, num_values, arr_data, false); + } + } else if (arr_error) { + ufbxi_pop_size(&uc->tmp_stack, arr_elem_size, num_values, NULL, false); + num_values = 0; + arr_data = (void*)ufbxi_zero_size_buffer; + } else { + arr_data = ufbxi_push_pop_size(arr_buf, &uc->tmp_stack, arr_elem_size, num_values); + } + ufbxi_check(arr_data); + if (arr_info.flags & UFBXI_ARRAY_FLAG_PAD_BEGIN) { + node->array->data = (char*)arr_data + 4*arr_elem_size; + node->array->size = num_values + deferred_size - 4; + } else { + node->array->data = arr_data; + node->array->size = num_values + deferred_size; + } + + // Pop alignment helper + ufbxi_pop_size(&uc->tmp_stack, 8, 1, NULL, false); + + // Deferred parsing + if (deferred_size > 0) { + size_t num_spans = uc->tmp_ascii_spans.num_items; + ufbxi_ascii_span *spans = ufbxi_push_pop(tmp_buf, &uc->tmp_ascii_spans, ufbxi_ascii_span, num_spans); + ufbxi_check(spans); + + ufbxi_ascii_array_task t; // ufbxi_uninit + t.arr_data = (char*)arr_data + num_values * arr_elem_size; + t.arr_type = (char)arr_type; + t.arr_size = deferred_size; + t.num_spans = num_spans; + t.spans = spans; + t.offset = 0; + + // TODO: Split these further + ufbxi_task *task = ufbxi_thread_pool_create_task(&uc->thread_pool, &ufbxi_ascii_array_task_fn); + if (task) { + task->data = ufbxi_push_copy(tmp_buf, ufbxi_ascii_array_task, 1, &t); + ufbxi_check(task->data); + ufbxi_thread_pool_run_task(&uc->thread_pool, task); + } else { + ufbxi_check_msg(ufbxi_ascii_array_task_imp(&t), "Threaded ASCII parse error"); + } + } + } + } else { + num_values = ufbxi_min32(num_values, UFBXI_MAX_NON_ARRAY_VALUES); + node->value_type_mask = (uint16_t)type_mask; + node->vals = ufbxi_push_copy(tmp_buf, ufbxi_value, num_values, vals); + ufbxi_check(node->vals); + } + + // Recursively parse the children of this node. Update the parse state + // to provide context for child node parsing. + if (ufbxi_ascii_accept(uc, '{')) { + if (recursive) { + size_t num_children = 0; + for (;;) { + bool end = false; + ufbxi_check(ufbxi_ascii_parse_node(uc, depth + 1, parse_state, &end, tmp_buf, recursive)); + if (end) break; + num_children++; + } + + // Pop children from `tmp_stack` to a contiguous array + node->children = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node, num_children); + ufbxi_check(node->children); + node->num_children = (uint32_t)num_children; + } + + uc->has_next_child = true; + } else { + uc->has_next_child = false; + } + + return 1; +} + +// -- DOM retention + +typedef struct { + uintptr_t node_ptr; + ufbx_dom_node *dom_node; +} ufbxi_dom_mapping; + +ufbxi_nodiscard static ufbxi_noinline ufbx_dom_node *ufbxi_get_dom_node_imp(ufbxi_context *uc, ufbxi_node *node) +{ + if (!node) return NULL; + ufbxi_dom_mapping mapping = { (uintptr_t)node, NULL }; + uint32_t hash = ufbxi_hash_uptr(mapping.node_ptr); + ufbxi_dom_mapping *result = ufbxi_map_find(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping); + return result ? result->dom_node : NULL; +} + +ufbxi_nodiscard static ufbxi_forceinline ufbx_dom_node *ufbxi_get_dom_node(ufbxi_context *uc, ufbxi_node *node) +{ + if (!uc->opts.retain_dom) return NULL; + return ufbxi_get_dom_node_imp(uc, node); +} + +// Recursion limited by check in ufbxi_[binary/ascii]_parse_node() +ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_dom_node(ufbxi_context *uc, ufbxi_node *node, ufbx_dom_node **p_dom_node) + ufbxi_recursive_function(int, ufbxi_retain_dom_node, (uc, node, p_dom_node), UFBXI_MAX_NODE_DEPTH + 1, + (ufbxi_context *uc, ufbxi_node *node, ufbx_dom_node **p_dom_node)) +{ + ufbx_dom_node *dst = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1); + ufbxi_check(dst); + ufbxi_check(ufbxi_push_copy(&uc->tmp_dom_nodes, ufbx_dom_node*, 1, &dst)); + + if (p_dom_node) { + *p_dom_node = dst; + } + + dst->name.data = node->name; + dst->name.length = node->name_len; + + { + ufbxi_dom_mapping mapping = { (uintptr_t)node, NULL }; + uint32_t hash = ufbxi_hash_uptr(mapping.node_ptr); + ufbxi_dom_mapping *result = ufbxi_map_find(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping); + if (!result) { + result = ufbxi_map_insert(&uc->dom_node_map, ufbxi_dom_mapping, hash, &mapping); + ufbxi_check(result); + } + result->node_ptr = (uintptr_t)node; + result->dom_node = dst; + } + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst->name, false)); + + if (node->value_type_mask == UFBXI_VALUE_ARRAY) { + ufbxi_value_array *arr = node->array; + ufbx_dom_value *val = ufbxi_push_zero(&uc->result, ufbx_dom_value, 1); + ufbxi_check(val); + + dst->values.data = val; + dst->values.count = 1; + + size_t elem_size = ufbxi_array_type_size(arr->type); + val->value_str.data = ufbxi_empty_char; + val->value_blob.data = arr->data; + val->value_blob.size = arr->size * elem_size; + val->value_float = (double)(val->value_int = (int64_t)arr->size); + + switch (arr->type) { + case 'c': val->type = UFBX_DOM_VALUE_BLOB; break; + case 'b': val->type = UFBX_DOM_VALUE_BLOB; break; + case 'i': val->type = UFBX_DOM_VALUE_ARRAY_I32; break; + case 'l': val->type = UFBX_DOM_VALUE_ARRAY_I64; break; + case 'f': val->type = UFBX_DOM_VALUE_ARRAY_F32; break; + case 'd': val->type = UFBX_DOM_VALUE_ARRAY_F64; break; + case 's': val->type = UFBX_DOM_VALUE_ARRAY_BLOB; break; + case 'C': val->type = UFBX_DOM_VALUE_ARRAY_BLOB; break; + case '-': val->type = UFBX_DOM_VALUE_ARRAY_IGNORED; break; + default: ufbxi_fail("Bad array type"); + } + } else { + size_t ix; + for (ix = 0; ix < UFBXI_MAX_NON_ARRAY_VALUES; ix++) { + uint32_t mask = (node->value_type_mask >> (2*ix)) & 0x3; + if (!mask) break; + ufbx_dom_value *val = ufbxi_push_zero(&uc->tmp_stack, ufbx_dom_value, 1); + ufbxi_check(val); + val->value_str.data = ufbxi_empty_char; + + if (mask == UFBXI_VALUE_STRING) { + val->type = UFBX_DOM_VALUE_STRING; + ufbxi_ignore(ufbxi_get_val_at(node, ix, 'S', &val->value_str)); + ufbxi_ignore(ufbxi_get_val_at(node, ix, 'b', &val->value_blob)); + } else { + ufbx_assert(mask == UFBXI_VALUE_NUMBER); + val->type = UFBX_DOM_VALUE_NUMBER; + val->value_int = node->vals[ix].i; + val->value_float = node->vals[ix].f; + } + } + + dst->values.count = ix; + dst->values.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_dom_value, ix); + ufbxi_check(dst->values.data); + } + + if (node->num_children > 0) { + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + ufbxi_check(ufbxi_retain_dom_node(uc, child, NULL)); + } + + dst->children.count = node->num_children; + dst->children.data = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, node->num_children); + ufbxi_check(dst->children.data); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel(ufbxi_context *uc, ufbxi_node *node) +{ + if (uc->dom_parse_num_children > 0) { + ufbx_dom_node **children = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, uc->dom_parse_num_children); + ufbxi_check(children); + uc->dom_parse_toplevel->children.data = children; + uc->dom_parse_toplevel->children.count = uc->dom_parse_num_children; + uc->dom_parse_num_children = 0; + } + + if (node) { + ufbxi_check(ufbxi_retain_dom_node(uc, node, &uc->dom_parse_toplevel)); + } else { + uc->dom_parse_toplevel = NULL; + + // Called with NULL argument to finish retaining DOM, collect the final nodes to `ufbx_scene`. + size_t num_top_nodes = uc->tmp_dom_nodes.num_items; + ufbx_dom_node **nodes = ufbxi_push_pop(&uc->result, &uc->tmp_dom_nodes, ufbx_dom_node*, num_top_nodes); + ufbxi_check(nodes); + + ufbx_dom_node *dom_root = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1); + ufbxi_check(dom_root); + + dom_root->name.data = ufbxi_empty_char; + dom_root->children.data = nodes; + dom_root->children.count = num_top_nodes; + + uc->scene.dom_root = dom_root; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_retain_toplevel_child(ufbxi_context *uc, ufbxi_node *child) +{ + ufbx_assert(uc->dom_parse_toplevel); + ufbxi_check(ufbxi_retain_dom_node(uc, child, NULL)); + uc->dom_parse_num_children++; + + return 1; +} + +// -- General parsing + +static ufbxi_noinline bool ufbxi_next_line(ufbx_string *line, ufbx_string *buf, bool skip_space) +{ + if (buf->length == 0) return false; + const char *newline = (const char*)memchr(buf->data, '\n', buf->length); + size_t length = newline ? ufbxi_to_size(newline - buf->data) + 1 : buf->length; + + line->data = buf->data; + line->length = length; + buf->data += length; + buf->length -= length; + + if (skip_space) { + while (line->length > 0 && ufbxi_is_space(line->data[0])) { + line->data++; + line->length--; + } + while (line->length > 0 && ufbxi_is_space(line->data[line->length - 1])) { + line->length--; + } + } + + return true; +} + +// Recursion limited by compile time patterns +static ufbxi_noinline const char *ufbxi_match_skip(const char *fmt, bool alternation) + ufbxi_recursive_function(const char *, ufbxi_match_skip, (fmt, alternation), 4, + (const char *fmt, bool alternation)) +{ + for (;;) { + char c = *fmt++; + switch (c) { + case '(': + fmt = ufbxi_match_skip(fmt, false) + 1; + break; + case '\\': + fmt++; + break; + case '[': + c = *fmt; + while (c != ']') { + c = *fmt++; + if (c == '\\') { + c = *fmt++; + } + } + fmt++; + break; + case '|': + if (alternation) return fmt - 1; + break; + case ')': + case '\0': + return fmt - 1; + default: break; + } + } +} + +// Recursion limited by compile time patterns +static ufbxi_noinline bool ufbxi_match_imp(const char **p_str, const char *end, const char **p_fmt) + ufbxi_recursive_function(bool, ufbxi_match_imp, (p_str, end, p_fmt), 4, + (const char **p_str, const char *end, const char **p_fmt)) +{ + const char *str_original_begin = *p_str; + const char *str = str_original_begin; + const char *fmt_begin = *p_fmt; + const char *fmt = fmt_begin; + bool case_insensitive = false; + + size_t count = 0; + for (;;) { + char c = *fmt++; + if (!c) { + *p_str = str; + *p_fmt = fmt - 1; + return true; + } + + const char *str_begin = str; + char ref = str != end ? *str : '\0'; + + if (case_insensitive) { + if (ref >= 'A' && ref <= 'Z') { + ref = (char)((int)(ref - 'A') + 'a'); + } + } + + bool ok = false; + switch (c) { + + case '\\': { + const char *macro = NULL; + c = *fmt++; + switch (c) { + case 'd': + macro = "[0-9]"; + break; + case 'F': + macro = "[\\-+]?[0-9]+(\\.[0-9]+)?([eE][\\-+]?[0-9]+)?"; + break; + case 's': + if (ufbxi_is_space(ref)) { + ok = true; + str++; + } + break; + case 'S': + if (!ufbxi_is_space(ref)) { + ok = true; + str++; + } + break; + case 'c': + case 'C': + case_insensitive = c == 'c'; + ok = true; + break; + default: + if (ref == c) { + ok = true; + str++; + } + break; + } + if (macro) { + ok = ufbxi_match_imp(&str, end, ¯o); + } + } break; + + case '[': { + while (fmt[0] != ']') { + if (fmt[0] == '\\') { + if (ref == fmt[1]) ok = true; + fmt += 2; + } else if (fmt[1] == '-') { + if (ref >= fmt[0] && ref <= fmt[2]) { + ok = true; + } + fmt += 3; + } else { + if (ref == fmt[0]) ok = true; + fmt += 1; + } + } + fmt++; + if (ok) str++; + } break; + + case '(': + if (ufbxi_match_imp(&str, end, &fmt)) { + ok = true; + } + break; + + case '|': + fmt = ufbxi_match_skip(fmt, false); + ok = true; + break; + + case ')': + *p_str = str; + *p_fmt = fmt; + return true; + + case '.': + if (ref != '\0') { + ok = true; + str++; + } + break; + + default: + if (c == ref) { + str++; + ok = true; + } + break; + } + + bool did_fail = false; + c = *fmt; + switch (c) { + case '*': + fmt++; + if (ok) { + fmt = fmt_begin; + count++; + continue; + } + break; + case '+': + fmt++; + if (ok) { + fmt = fmt_begin; + count++; + continue; + } else if (count == 0) { + did_fail = true; + } + break; + case '?': + fmt++; + break; + default: + did_fail = !ok; + break; + } + + if (did_fail) { + fmt = ufbxi_match_skip(fmt, true); + if (*fmt == '|') { + fmt++; + str = str_original_begin; + } else { + *p_fmt = ufbxi_match_skip(fmt, false) + 1; + return false; + } + } else { + if (!ok) { + str = str_begin; + } + } + + fmt_begin = fmt; + count = 0; + } +} + +static ufbxi_noinline bool ufbxi_match(const ufbx_string *str, const char *fmt) +{ + const char *ptr = str->data, *end = str->data + str->length; + if (ufbxi_match_imp(&ptr, end, &fmt)) { + return ptr == end; + } else { + return false; + } +} + +static ufbxi_noinline bool ufbxi_is_format(const char *data, size_t size, ufbx_file_format format) +{ + ufbx_string line, buf = { data, size }; + + if (format == UFBX_FILE_FORMAT_FBX) { + if (size >= UFBXI_BINARY_MAGIC_SIZE && !memcmp(data, ufbxi_binary_magic, UFBXI_BINARY_MAGIC_SIZE)) { + return true; + } + + while (ufbxi_next_line(&line, &buf, true)) { + if (ufbxi_match(&line, ";\\s*FBX\\s*\\d+\\.\\d+\\.\\d+\\s*project\\s+file")) return true; + if (ufbxi_match(&line, "FBXHeaderExtension:.*")) return true; + } + } else if (format == UFBX_FILE_FORMAT_OBJ) { + while (ufbxi_next_line(&line, &buf, true)) { + const char *pattern = + "(vn?\\s+\\F|vt)\\s+\\F\\s+\\F.*" "|" + "f\\s+[\\-/0-9]+\\s+[\\-/0-9]+\\s*[\\-/0-9]+.*" "|" + "(usemtl|mtllib)\\s+\\S.*"; + if (ufbxi_match(&line, pattern)) return true; + } + } else if (format == UFBX_FILE_FORMAT_MTL) { + while (ufbxi_next_line(&line, &buf, true)) { + const char *pattern = + "newmtl\\s+\\S.*"; + if (ufbxi_match(&line, pattern)) return true; + } + } else { + ufbxi_unreachable("Unhandled format"); + } + + return false; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_determine_format(ufbxi_context *uc) +{ + ufbx_file_format format = uc->opts.file_format; + + if (format == UFBX_FILE_FORMAT_UNKNOWN && !uc->opts.no_format_from_content) { + ufbxi_pause_progress(uc); + + size_t lookahead = UFBXI_MIN_FILE_FORMAT_LOOKAHEAD; + while (format == UFBX_FILE_FORMAT_UNKNOWN && lookahead <= uc->opts.file_format_lookahead) { + if (lookahead > uc->data_size) { + if (uc->eof) break; + ufbxi_check(ufbxi_refill(uc, lookahead, false)); + } + + size_t data_size = ufbxi_min_sz(lookahead, uc->data_size); + ufbxi_check_msg(data_size > 0, "Empty file"); + + for (uint32_t fmt = UFBX_FILE_FORMAT_FBX; fmt < UFBX_FILE_FORMAT_COUNT; fmt++) { + if (ufbxi_is_format(uc->data, data_size, (ufbx_file_format)fmt)) { + format = (ufbx_file_format)fmt; + break; + } + } + + if (lookahead >= uc->opts.file_format_lookahead) { + break; + } else if (lookahead < SIZE_MAX / 2) { + lookahead = ufbxi_min_sz(lookahead * 2, uc->opts.file_format_lookahead); + } else { + lookahead = SIZE_MAX; + } + } + + ufbxi_check(ufbxi_resume_progress(uc)); + } + + if (format == UFBX_FILE_FORMAT_UNKNOWN && !uc->opts.no_format_from_extension) { + if (uc->opts.filename.length > 0) { + ufbx_string extension = uc->opts.filename; + for (size_t i = extension.length; i > 0; i--) { + if (extension.data[i - 1] == '.') { + extension.data += i - 1; + extension.length -= i - 1; + break; + } + } + + if (ufbxi_match(&extension, "\\c\\.fbx")) { + format = UFBX_FILE_FORMAT_FBX; + } else if (ufbxi_match(&extension, "\\c\\.obj")) { + format = UFBX_FILE_FORMAT_OBJ; + } else if (ufbxi_match(&extension, "\\c\\.mtl")) { + format = UFBX_FILE_FORMAT_MTL; + } + } + } + + ufbxi_check_msg(format != UFBX_FILE_FORMAT_UNKNOWN, "Unrecognized file format"); + uc->scene.metadata.file_format = format; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_begin_parse(ufbxi_context *uc) +{ + const char *header = ufbxi_peek_bytes(uc, UFBXI_BINARY_HEADER_SIZE); + ufbxi_check(header); + + // If the file starts with the binary magic parse it as binary, otherwise + // treat it as an ASCII file. + if (!memcmp(header, ufbxi_binary_magic, UFBXI_BINARY_MAGIC_SIZE)) { + + // The byte after the magic indicates endianness + char endian = header[UFBXI_BINARY_MAGIC_SIZE + 0]; + uc->file_big_endian = endian != 0; + + // Read the version directly from the header + const char *version_word = header + UFBXI_BINARY_MAGIC_SIZE + 1; + if (uc->file_big_endian) { + version_word = ufbxi_swap_endian(uc, version_word, 1, 4); + ufbxi_check(version_word); + } + uc->version = ufbxi_read_u32(version_word); + + // This is quite probably an FBX file.. + uc->sure_fbx = true; + ufbxi_consume_bytes(uc, UFBXI_BINARY_HEADER_SIZE); + + } else { + uc->from_ascii = true; + + // Use the current read buffer as the initial parse buffer + memset(&uc->ascii, 0, sizeof(uc->ascii)); + uc->ascii.src = uc->data; + uc->ascii.src_yield = uc->data + uc->yield_size; + uc->ascii.src_end = uc->data + uc->data_size + uc->yield_size; + + // Initialize the first token + ufbxi_check(ufbxi_ascii_next_token(uc, &uc->ascii.token)); + + // Default to version 7400 if not found in header + if (uc->version > 0) { + uc->sure_fbx = true; + } else { + if (!uc->opts.strict) uc->version = 7400; + ufbxi_check_msg(uc->version > 0, "Not an FBX file"); + } + } + + return 1; +} + +ufbxi_nodiscard static int ufbxi_parse_toplevel_child_imp(ufbxi_context *uc, ufbxi_parse_state state, ufbxi_buf *buf, bool *p_end) +{ + if (uc->from_ascii) { + ufbxi_check(ufbxi_ascii_parse_node(uc, 0, state, p_end, buf, true)); + } else { + ufbxi_check(ufbxi_binary_parse_node(uc, 0, state, p_end, buf, true)); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel(ufbxi_context *uc, const char *name) +{ + ufbxi_for(ufbxi_node, node, uc->top_nodes, uc->top_nodes_len) { + if (node->name == name) { + uc->top_node = node; + uc->top_child_index = 0; + return 1; + } + } + + // Reached end and not found in cache + if (uc->parsed_to_end) { + uc->top_node = NULL; + uc->top_child_index = 0; + return 1; + } + + for (;;) { + // Parse the next top-level node + bool end = false; + if (uc->from_ascii) { + ufbxi_check(ufbxi_ascii_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, false)); + } else { + ufbxi_check(ufbxi_binary_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, false)); + } + + // Top-level node not found + if (end) { + uc->top_node = NULL; + uc->top_child_index = 0; + uc->parsed_to_end = true; + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_retain_toplevel(uc, NULL)); + } + + // Not needed anymore + ufbxi_buf_free(&uc->tmp_parse); + + return 1; + } + + uc->top_nodes_len++; + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->top_nodes, &uc->top_nodes_cap, uc->top_nodes_len)); + ufbxi_node *node = &uc->top_nodes[uc->top_nodes_len - 1]; + ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, node); + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_retain_toplevel(uc, node)); + } + + // Return if we parsed the right one + if (node->name == name) { + uc->top_node = node; + uc->top_child_index = SIZE_MAX; + return 1; + } + + // If not we need to parse all the children of the node for later + uint32_t num_children = 0; + ufbxi_parse_state state = ufbxi_update_parse_state(UFBXI_PARSE_ROOT, node->name); + if (uc->has_next_child) { + for (;;) { + ufbxi_check(ufbxi_parse_toplevel_child_imp(uc, state, &uc->tmp, &end)); + if (end) break; + num_children++; + } + } + + node->num_children = num_children; + node->children = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_node, num_children); + ufbxi_check(node->children); + + if (uc->opts.retain_dom) { + for (size_t i = 0; i < num_children; i++) { + ufbxi_check(ufbxi_retain_toplevel_child(uc, &node->children[i])); + } + } + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_toplevel_child(ufbxi_context *uc, ufbxi_node **p_node, ufbxi_buf *tmp_buf) +{ + // Top-level node not found + if (!uc->top_node) { + *p_node = NULL; + return 1; + } + + if (uc->top_child_index == SIZE_MAX) { + // Parse children on demand + if (!tmp_buf) { + ufbxi_buf_clear(&uc->tmp_parse); + } + bool end = false; + ufbxi_parse_state state = ufbxi_update_parse_state(UFBXI_PARSE_ROOT, uc->top_node->name); + ufbxi_check(ufbxi_parse_toplevel_child_imp(uc, state, tmp_buf ? tmp_buf : &uc->tmp_parse, &end)); + if (end) { + *p_node = NULL; + } else { + // Parse to either reused `uc->top_child` or push if retaining to `tmp_buf`. + ufbxi_node *dst = &uc->top_child; + if (tmp_buf) { + dst = ufbxi_push_zero(tmp_buf, ufbxi_node, 1); + ufbxi_check(dst); + } + + ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, dst); + *p_node = dst; + + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_retain_toplevel_child(uc, dst)); + } + } + } else { + // Iterate already parsed nodes + size_t child_index = uc->top_child_index; + if (child_index == uc->top_node->num_children) { + *p_node = NULL; + } else { + uc->top_child_index++; + *p_node = &uc->top_node->children[child_index]; + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_parse_legacy_toplevel(ufbxi_context *uc) +{ + ufbx_assert(uc->top_nodes_len == 0); + + bool end = false; + if (uc->from_ascii) { + ufbxi_check(ufbxi_ascii_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, true)); + } else { + ufbxi_check(ufbxi_binary_parse_node(uc, 0, UFBXI_PARSE_ROOT, &end, &uc->tmp, true)); + } + + // Top-level node not found + if (end) { + uc->top_node = NULL; + uc->top_child_index = 0; + uc->parsed_to_end = true; + return 1; + } + + ufbxi_pop(&uc->tmp_stack, ufbxi_node, 1, &uc->legacy_node); + uc->top_child_index = 0; + uc->top_node = &uc->legacy_node; + + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_retain_toplevel(uc, &uc->legacy_node)); + } + + return 1; +} + +// -- Setup + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_strings(ufbxi_context *uc) +{ +#if defined(UFBX_REGRESSION) + ufbx_string reg_prev = ufbx_empty_string; +#endif + + // Push all the global 'ufbxi_*' strings into the pool without copying them + // This allows us to compare name pointers to the global values + ufbxi_for(const ufbx_string, str, ufbxi_strings, ufbxi_arraycount(ufbxi_strings)) { +#if defined(UFBX_REGRESSION) + ufbx_assert(strlen(str->data) == str->length); + ufbx_assert(ufbxi_str_less(reg_prev, *str)); + reg_prev = *str; +#endif + ufbxi_check(ufbxi_push_string_imp(&uc->string_pool, str->data, str->length, NULL, false, true)); + } + + return 1; +} + +typedef struct { + const char *name; + ufbx_prop_type type; +} ufbxi_prop_type_name; + +static const ufbxi_prop_type_name ufbxi_prop_type_names[] = { + { "Boolean", UFBX_PROP_BOOLEAN }, + { "bool", UFBX_PROP_BOOLEAN }, + { "Bool", UFBX_PROP_BOOLEAN }, + { "Integer", UFBX_PROP_INTEGER }, + { "int", UFBX_PROP_INTEGER }, + { "enum", UFBX_PROP_INTEGER }, + { "Enum", UFBX_PROP_INTEGER }, + { "Visibility", UFBX_PROP_INTEGER }, + { "Visibility Inheritance", UFBX_PROP_INTEGER }, + { "KTime", UFBX_PROP_INTEGER }, + { "Number", UFBX_PROP_NUMBER }, + { "double", UFBX_PROP_NUMBER }, + { "Real", UFBX_PROP_NUMBER }, + { "Float", UFBX_PROP_NUMBER }, + { "Intensity", UFBX_PROP_NUMBER }, + { "Vector", UFBX_PROP_VECTOR }, + { "Vector3D", UFBX_PROP_VECTOR }, + { "Color", UFBX_PROP_COLOR }, + { "ColorAndAlpha", UFBX_PROP_COLOR_WITH_ALPHA }, + { "ColorRGB", UFBX_PROP_COLOR }, + { "String", UFBX_PROP_STRING }, + { "KString", UFBX_PROP_STRING }, + { "object", UFBX_PROP_STRING }, + { "DateTime", UFBX_PROP_DATE_TIME }, + { "Lcl Translation", UFBX_PROP_TRANSLATION }, + { "Lcl Rotation", UFBX_PROP_ROTATION }, + { "Lcl Scaling", UFBX_PROP_SCALING }, + { "Distance", UFBX_PROP_DISTANCE }, + { "Compound", UFBX_PROP_COMPOUND }, + { "Blob", UFBX_PROP_BLOB }, + { "Reference", UFBX_PROP_REFERENCE }, +}; + +static ufbx_prop_type ufbxi_get_prop_type(ufbxi_context *uc, const char *name) +{ + uint32_t hash = ufbxi_hash_ptr(name); + ufbxi_prop_type_name *entry = ufbxi_map_find(&uc->prop_type_map, ufbxi_prop_type_name, hash, &name); + if (entry) { + return entry->type; + } + return UFBX_PROP_UNKNOWN; +} + +static ufbxi_noinline ufbx_prop *ufbxi_find_prop_with_key(const ufbx_props *props, const char *name, uint32_t key) +{ + do { + ufbx_prop *prop_data = props->props.data; + size_t begin = 0; + size_t end = props->props.count; + while (end - begin >= 16) { + size_t mid = (begin + end) >> 1; + const ufbx_prop *p = &prop_data[mid]; + if (p->_internal_key < key) { + begin = mid + 1; + } else { + end = mid; + } + } + + end = props->props.count; + for (; begin < end; begin++) { + const ufbx_prop *p = &prop_data[begin]; + if (p->_internal_key > key) break; + if (p->name.data == name && (p->flags & UFBX_PROP_FLAG_NO_VALUE) == 0) { + return (ufbx_prop*)p; + } + } + + props = props->defaults; + } while (props); + + return NULL; +} + +typedef struct { + const char *key; + ufbx_texture_file *file; +} ufbxi_texture_file_entry; + +#define ufbxi_find_prop(props, name) ufbxi_find_prop_with_key((props), (name), \ + ((uint32_t)(uint8_t)name[0] << 24u) | ((uint32_t)(uint8_t)name[1] << 16u) | \ + ((uint32_t)(uint8_t)name[2] << 8u) | (uint32_t)(uint8_t)name[3]) + +static ufbxi_forceinline ufbx_real ufbxi_find_real(const ufbx_props *props, const char *name, ufbx_real def) +{ + ufbx_prop *prop = ufbxi_find_prop(props, name); + if (prop) { + return prop->value_real; + } else { + return def; + } +} + +static ufbxi_forceinline ufbx_vec3 ufbxi_find_vec3(const ufbx_props *props, const char *name, ufbx_real def_x, ufbx_real def_y, ufbx_real def_z) +{ + ufbx_prop *prop = ufbxi_find_prop(props, name); + if (prop) { + return prop->value_vec3; + } else { + ufbx_vec3 def = { def_x, def_y, def_z }; + return def; + } +} + +static ufbxi_forceinline int64_t ufbxi_find_int(const ufbx_props *props, const char *name, int64_t def) +{ + ufbx_prop *prop = ufbxi_find_prop(props, name); + if (prop) { + return prop->value_int; + } else { + return def; + } +} + +static ufbxi_forceinline int64_t ufbxi_find_enum(const ufbx_props *props, const char *name, int64_t def, int64_t max_value) +{ + ufbx_prop *prop = ufbxi_find_prop(props, name); + if (prop) { + int64_t value = prop->value_int; + if (value >= 0 && value <= max_value) { + return value; + } else { + return def; + } + } else { + return def; + } +} + +ufbxi_noinline static bool ufbxi_matrix_all_zero(const ufbx_matrix *matrix) +{ + for (size_t i = 0; i < 12; i++) { + if (matrix->v[i] != 0.0f) return false; + } + return true; +} + +static ufbxi_forceinline bool ufbxi_is_vec3_zero(ufbx_vec3 v) +{ + return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0); +} + +static ufbxi_forceinline bool ufbxi_is_vec4_zero(ufbx_vec4 v) +{ + return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0); +} + +static ufbxi_forceinline bool ufbxi_is_vec3_one(ufbx_vec3 v) +{ + return (v.x == 1.0) & (v.y == 1.0) & (v.z == 1.0); +} + +static ufbxi_forceinline bool ufbxi_is_quat_identity(ufbx_quat v) +{ + return (v.x == 0.0) & (v.y == 0.0) & (v.z == 0.0) & (v.w == 1.0); +} + +static ufbxi_noinline bool ufbxi_is_transform_identity(const ufbx_transform *t) +{ + return (bool)((int)ufbxi_is_vec3_zero(t->translation) & (int)ufbxi_is_quat_identity(t->rotation) & (int)ufbxi_is_vec3_one(t->scale)); +} + +static ufbxi_forceinline uint32_t ufbxi_get_name_key(const char *name, size_t len) +{ + uint32_t key = 0; + if (len >= 4) { + key = (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16 + | (uint32_t)(uint8_t)name[2]<<8 | (uint32_t)(uint8_t)name[3]; + } else { + for (size_t i = 0; i < 4; i++) { + key <<= 8; + if (i < len) key |= (uint8_t)name[i]; + } + } + return key; +} + +static ufbxi_forceinline uint32_t ufbxi_get_name_key_c(const char *name) +{ + if (name[0] == '\0') return 0; + if (name[1] == '\0') return (uint32_t)(uint8_t)name[0]<<24; + if (name[2] == '\0') return (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16; + return (uint32_t)(uint8_t)name[0]<<24 | (uint32_t)(uint8_t)name[1]<<16 + | (uint32_t)(uint8_t)name[2]<<8 | (uint32_t)(uint8_t)name[3]; +} + +static ufbxi_forceinline bool ufbxi_name_key_less(ufbx_prop *prop, const char *data, size_t name_len, uint32_t key) +{ + if (prop->_internal_key < key) return true; + if (prop->_internal_key > key) return false; + + size_t prop_len = prop->name.length; + size_t len = ufbxi_min_sz(prop_len, name_len); + int cmp = memcmp(prop->name.data, data, len); + if (cmp != 0) return cmp < 0; + return prop_len < name_len; +} + +static const char *const ufbxi_node_prop_names[] = { + "AxisLen", + "DefaultAttributeIndex", + "Freeze", + "GeometricRotation", + "GeometricScaling", + "GeometricTranslation", + "InheritType", + "LODBox", + "Lcl Rotation", + "Lcl Scaling", + "Lcl Translation", + "LookAtProperty", + "MaxDampRangeX", + "MaxDampRangeY", + "MaxDampRangeZ", + "MaxDampStrengthX", + "MaxDampStrengthY", + "MaxDampStrengthZ", + "MinDampRangeX", + "MinDampRangeY", + "MinDampRangeZ", + "MinDampStrengthX", + "MinDampStrengthY", + "MinDampStrengthZ", + "NegativePercentShapeSupport", + "PostRotation", + "PreRotation", + "PreferedAngleX", + "PreferedAngleY", + "PreferedAngleZ", + "QuaternionInterpolate", + "RotationActive", + "RotationMax", + "RotationMaxX", + "RotationMaxY", + "RotationMaxZ", + "RotationMin", + "RotationMinX", + "RotationMinY", + "RotationMinZ", + "RotationOffset", + "RotationOrder", + "RotationPivot", + "RotationSpaceForLimitOnly", + "RotationStiffnessX", + "RotationStiffnessY", + "RotationStiffnessZ", + "ScalingActive", + "ScalingMax", + "ScalingMaxX", + "ScalingMaxY", + "ScalingMaxZ", + "ScalingMin", + "ScalingMinX", + "ScalingMinY", + "ScalingMinZ", + "ScalingOffset", + "ScalingPivot", + "Show", + "TranslationActive", + "TranslationMax", + "TranslationMaxX", + "TranslationMaxY", + "TranslationMaxZ", + "TranslationMin", + "TranslationMinX", + "TranslationMinY", + "TranslationMinZ", + "UpVectorProperty", + "Visibility Inheritance", + "Visibility", + "notes", +}; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_init_node_prop_names(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_map_grow(&uc->node_prop_set, const char*, ufbxi_arraycount(ufbxi_node_prop_names))); + for (size_t i = 0; i < ufbxi_arraycount(ufbxi_node_prop_names); i++) { + const char *name = ufbxi_node_prop_names[i]; + const char *pooled = ufbxi_push_string_imp(&uc->string_pool, name, strlen(name), NULL, false, true); + ufbxi_check(pooled); + uint32_t hash = ufbxi_hash_ptr(pooled); + const char **entry = ufbxi_map_insert(&uc->node_prop_set, const char*, hash, &pooled); + ufbxi_check(entry); + *entry = pooled; + } + + return 1; +} + +static bool ufbxi_is_node_property_name(ufbxi_context *uc, const char *name) +{ + // You need to call `ufbxi_init_node_prop_names()` before calling this + ufbx_assert(uc->node_prop_set.size > 0); + + uint32_t hash = ufbxi_hash_ptr(name); + const char **entry = ufbxi_map_find(&uc->node_prop_set, const char*, hash, &name); + return entry != NULL; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_maps(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_map_grow(&uc->prop_type_map, ufbxi_prop_type_name, ufbxi_arraycount(ufbxi_prop_type_names))); + ufbxi_for(const ufbxi_prop_type_name, name, ufbxi_prop_type_names, ufbxi_arraycount(ufbxi_prop_type_names)) { + const char *pooled = ufbxi_push_string_imp(&uc->string_pool, name->name, strlen(name->name), NULL, false, true); + ufbxi_check(pooled); + uint32_t hash = ufbxi_hash_ptr(pooled); + ufbxi_prop_type_name *entry = ufbxi_map_insert(&uc->prop_type_map, ufbxi_prop_type_name, hash, &pooled); + ufbxi_check(entry); + entry->type = name->type; + entry->name = pooled; + } + + return 1; +} + +// -- Reading the parsed data + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_embedded_blob(ufbxi_context *uc, ufbx_blob *dst_blob, ufbxi_node *node) +{ + if (!node) return 1; + + ufbxi_value_array *content_arr = ufbxi_get_array(node, 'C'); + if (content_arr && content_arr->size > 0) { + ufbx_string content; + size_t num_parts = content_arr->size; + ufbx_string *parts = (ufbx_string*)content_arr->data; + + if (num_parts == 1 && !uc->from_ascii) { + content = parts[0]; + } else { + size_t total_size = 0; + ufbxi_for(ufbx_string, part, parts, num_parts) { + total_size += part->length; + } + char *dst = ufbxi_push(&uc->result, char, total_size); + ufbxi_check(dst); + content.data = dst; + content.length = total_size; + ufbxi_for(ufbx_string, part, parts, num_parts) { + memcpy(dst, part->data, part->length); + dst += part->length; + } + } + + dst_blob->data = content.data; + dst_blob->size = content.length; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_property(ufbxi_context *uc, ufbxi_node *node, ufbx_prop *prop, int version) +{ + const char *type_str = NULL, *subtype_str = NULL; + ufbxi_check(ufbxi_get_val2(node, "SC", &prop->name, (char**)&type_str)); + uint32_t val_ix = 2; + if (version == 70) { + ufbxi_check(ufbxi_get_val_at(node, val_ix++, 'C', (char**)&subtype_str)); + } + + uint32_t flags = 0; + prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length); + + ufbx_string flags_str; + if (ufbxi_get_val_at(node, val_ix++, 'S', &flags_str)) { + for (size_t i = 0; i < flags_str.length; i++) { + char next = i + 1 < flags_str.length ? flags_str.data[i + 1] : '0'; + switch (flags_str.data[i]) { + case 'A': flags |= UFBX_PROP_FLAG_ANIMATABLE; break; + case 'U': flags |= UFBX_PROP_FLAG_USER_DEFINED; break; + case 'H': flags |= UFBX_PROP_FLAG_HIDDEN; break; + case 'L': flags |= ((uint32_t)(next - '0') & 0xf) << 4; break; // UFBX_PROP_FLAG_LOCK_* + case 'M': flags |= ((uint32_t)(next - '0') & 0xf) << 8; break; // UFBX_PROP_FLAG_MUTE_* + default: break; // Ignore unknown flags + } + } + } + + prop->type = ufbxi_get_prop_type(uc, type_str); + if (prop->type == UFBX_PROP_UNKNOWN && subtype_str) { + prop->type = ufbxi_get_prop_type(uc, subtype_str); + } + + if (ufbxi_get_val_at(node, val_ix, 'L', &prop->value_int)) { + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_INT; + } + + size_t real_ix; + for (real_ix = 0; real_ix < 4; real_ix++) { + if (!ufbxi_get_val_at(node, val_ix + real_ix, 'R', &prop->value_real_arr[real_ix])) break; + } + if (real_ix > 0) { + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << (real_ix - 1); + } + + // Skip one value forward in case the current value is not a string, as some properties + // contain mixed numbers and strings. Currenltly known cases: + // Lod Distance: P: "Thresholds|Level0", "Distance", "", "",64, "cm" + // User Enum: P: "User_Enum", "Enum", "", "A+U",1, "ValueA~ValueB~ValueC" + if (ufbxi_get_val_type(node, val_ix) != UFBXI_VALUE_STRING) { + val_ix++; + } + + if (ufbxi_get_val_at(node, val_ix, 'S', &prop->value_str)) { + if (prop->value_str.length > 0) { + ufbxi_ignore(ufbxi_get_val_at(node, val_ix, 'b', &prop->value_blob)); + } + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_STR; + } else { + prop->value_str = ufbx_empty_string; + } + + // Very unlikely, seems to only exist in some "non standard" FBX files + if (node->num_children > 0) { + ufbxi_node *binary = ufbxi_find_child(node, ufbxi_BinaryData); + ufbxi_check(ufbxi_read_embedded_blob(uc, &prop->value_blob, binary)); + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_BLOB; + } + + prop->flags = (ufbx_prop_flags)flags; + + return 1; +} + +static ufbxi_forceinline bool ufbxi_prop_less(ufbx_prop *a, ufbx_prop *b) +{ + if (a->_internal_key < b->_internal_key) return true; + if (a->_internal_key > b->_internal_key) return false; + return strcmp(a->name.data, b->name.data) < 0; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_properties(ufbxi_context *uc, ufbx_prop *props, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_prop))); + ufbxi_macro_stable_sort(ufbx_prop, 32, props, uc->tmp_arr, count, ( ufbxi_prop_less(a, b) )); + return 1; +} + +ufbxi_noinline static void ufbxi_deduplicate_properties(ufbx_prop_list *list) +{ + if (list->count >= 2) { + ufbx_prop *ps = list->data; + size_t dst = 0, src = 0, end = list->count; + while (src < end) { + if (src + 1 < end && ps[src].name.data == ps[src + 1].name.data) { + src++; + } else if (dst != src) { + ps[dst++] = ps[src++]; + } else { + dst++; src++; + } + } + list->count = dst; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_properties(ufbxi_context *uc, ufbxi_node *parent, ufbx_props *props) +{ + props->defaults = NULL; + + int version = 70; + ufbxi_node *node = ufbxi_find_child(parent, ufbxi_Properties70); + if (!node) { + node = ufbxi_find_child(parent, ufbxi_Properties60); + if (!node) { + // No properties found, not an error + props->props.data = NULL; + props->props.count = 0; + return 1; + } + version = 60; + } + + props->props.data = ufbxi_push_zero(&uc->result, ufbx_prop, node->num_children); + props->props.count = node->num_children; + ufbxi_check(props->props.data); + + for (size_t i = 0; i < props->props.count; i++) { + ufbxi_check(ufbxi_read_property(uc, &node->children[i], &props->props.data[i], version)); + } + + ufbxi_check(ufbxi_sort_properties(uc, props->props.data, props->props.count)); + ufbxi_deduplicate_properties(&props->props); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_thumbnail(ufbxi_context *uc, ufbxi_node *node, ufbx_thumbnail *thumbnail) +{ + ufbxi_check(ufbxi_read_properties(uc, node, &thumbnail->props)); + + int64_t custom_width = ufbx_find_int(&thumbnail->props, "CustomWidth", 0); + int64_t custom_height = ufbx_find_int(&thumbnail->props, "CustomHeight", 0); + + int32_t format; + ufbxi_node *format_node = ufbxi_find_child_strcmp(node, "Format"); + if (format_node && ufbxi_get_val1(format_node, "I", &format)) { + if (format >= 0 && format + 1 < UFBX_THUMBNAIL_FORMAT_COUNT) { + thumbnail->format = (ufbx_thumbnail_format)(format + 1); + } + } + + int32_t size; + if (ufbxi_find_val1(node, ufbxi_Size, "I", &size)) { + if (size > 0) { + thumbnail->width = (uint32_t)size; + thumbnail->height = (uint32_t)size; + } else if (size < 0 && custom_width > 0 && custom_height > 0) { + thumbnail->width = (uint32_t)custom_width; + thumbnail->height = (uint32_t)custom_height; + } + } + + ufbxi_value_array *data_arr = ufbxi_find_array(node, ufbxi_ImageData, 'c'); + if (data_arr) { + thumbnail->data.data = data_arr->data; + thumbnail->data.size = data_arr->size; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_scene_info(ufbxi_context *uc, ufbxi_node *node) +{ + ufbxi_check(ufbxi_read_properties(uc, node, &uc->scene.metadata.scene_props)); + + ufbxi_node *thumbnail = ufbxi_find_child(node, ufbxi_Thumbnail); + if (thumbnail) { + ufbxi_check(ufbxi_read_thumbnail(uc, thumbnail, &uc->scene.metadata.thumbnail)); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_header_extension(ufbxi_context *uc) +{ + bool has_tc_definition = false; + int32_t tc_definition = 0; + int32_t header_version = 0; + + for (;;) { + ufbxi_node *child; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &child, NULL)); + if (!child) break; + + if (child->name == ufbxi_Creator) { + ufbxi_ignore(ufbxi_get_val1(child, "S", &uc->scene.metadata.creator)); + } + + if (uc->version < 6000 && child->name == ufbxi_FBXVersion) { + int32_t version; + if (ufbxi_get_val1(child, "I", &version)) { + if (version > 0 && version < 6000 && (uint32_t)version > uc->version) { + uc->version = (uint32_t)version; + } + } + } + + if (child->name == ufbxi_FBXHeaderVersion) { + ufbxi_ignore(ufbxi_get_val1(child, "I", &header_version)); + } + + if (child->name == ufbxi_OtherFlags) { + if (ufbxi_find_val1(child, ufbxi_TCDefinition, "I", &tc_definition)) { + has_tc_definition = true; + } + } + + if (child->name == ufbxi_SceneInfo) { + ufbxi_check(ufbxi_read_scene_info(uc, child)); + } + + } + + // FBX 8000 will change the KTime units and the new units are opt-in currently via `TCDefinition`. + // `TCDefinition` seems be accounted in all versions, as long as `FBXHeaderVersion >= 1004`. + // The old KTime units are specified as the value `127` and all other values seem to use the new definition. + bool use_v7_ktime = uc->version < 8000; + if (header_version >= 1004 && has_tc_definition) { + use_v7_ktime = tc_definition == 127; + } + + uc->ktime_sec = use_v7_ktime ? 46186158000 : 141120000; + uc->ktime_sec_double = (double)uc->ktime_sec; + + return 1; +} + +static bool ufbxi_match_version_string(const char *fmt, ufbx_string str, uint32_t *p_version) +{ + size_t num_ix = 0; + size_t pos = 0; + while (*fmt) { + char c = *fmt++; + if (c >= 'a' && c <= 'z') { + if (pos >= str.length) return false; + char s = str.data[pos]; + if (s != c && (int)s + (int)('a' - 'A') != (int)c) return false; + pos++; + } else if (c == ' ') { + while (pos < str.length) { + char s = str.data[pos]; + if (s != ' ' && s != '\t') break; + pos++; + } + } else if (c == '-') { + while (pos < str.length) { + char s = str.data[pos]; + if (s == '-') break; + pos++; + } + if (pos >= str.length) return false; + pos++; + } else if (c == '/' || c == '.' || c == '(' || c == ')' || c == '_') { + if (pos >= str.length) return false; + if (str.data[pos] != c) return false; + pos++; + } else if (c == '?') { + uint32_t num = 0; + size_t len = 0; + while (pos < str.length) { + char s = str.data[pos]; + if (!(s >= '0' && s <= '9')) break; + num = num*10 + (uint32_t)(s - '0'); + pos++; + len++; + } + if (len == 0) return false; + p_version[num_ix++] = num; + } else { + ufbxi_unreachable("Unhandled match character"); + } + } + + return true; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_match_exporter(ufbxi_context *uc) +{ + ufbx_string creator = uc->scene.metadata.creator; + uint32_t version[3] = { 0 }; + if (ufbxi_match_version_string("blender-- ?.?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_BLENDER_BINARY; + uc->exporter_version = ufbx_pack_version(version[0], version[1], version[2]); + } else if (ufbxi_match_version_string("blender- ?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_BLENDER_BINARY; + uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); + } else if (ufbxi_match_version_string("blender version ?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_BLENDER_ASCII; + uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); + } else if (ufbxi_match_version_string("fbx sdk/fbx plugins version ?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_FBX_SDK; + uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); + } else if (ufbxi_match_version_string("fbx sdk/fbx plugins build ?", creator, version)) { + uc->exporter = UFBX_EXPORTER_FBX_SDK; + uc->exporter_version = ufbx_pack_version(version[0]/10000u, version[0]/100u%100u, version[0]%100u); + } else if (ufbxi_match_version_string("motionbuilder version ?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_MOTION_BUILDER; + uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); + } else if (ufbxi_match_version_string("motionbuilder/mocap/online version ?.?", creator, version)) { + uc->exporter = UFBX_EXPORTER_MOTION_BUILDER; + uc->exporter_version = ufbx_pack_version(version[0], version[1], 0); + } else if (ufbxi_match_version_string("ufbx_write", creator, version)) { + uc->exporter = UFBX_EXPORTER_UFBX_WRITE; + uc->exporter_version = ufbx_pack_version(0, 0, 1); + } + + uc->scene.metadata.exporter = uc->exporter; + uc->scene.metadata.exporter_version = uc->exporter_version; + + // Un-detect the exporter in `ufbxi_context` to disable special cases + if (uc->opts.disable_quirks) { + uc->exporter = UFBX_EXPORTER_UNKNOWN; + uc->exporter_version = 0; + } + + if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) { + uc->blender_full_weights = true; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_document(ufbxi_context *uc) +{ + bool found_root_id = 0; + + for (;;) { + ufbxi_node *child; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &child, NULL)); + if (!child) break; + + if (child->name == ufbxi_Document && !found_root_id) { + // Post-7000: Try to find the first document node and root ID. + // TODO: Multiple documents / roots? + if (ufbxi_find_val1(child, ufbxi_RootNode, "L", &uc->root_id)) { + found_root_id = true; + } + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_definitions(ufbxi_context *uc) +{ + for (;;) { + ufbxi_node *object; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &object, NULL)); + if (!object) break; + + if (object->name != ufbxi_ObjectType) continue; + + ufbxi_template *tmpl = ufbxi_push_zero(&uc->tmp_stack, ufbxi_template, 1); + uc->num_templates++; + ufbxi_check(tmpl); + ufbxi_check(ufbxi_get_val1(object, "C", (char**)&tmpl->type)); + + // Pre-7000 FBX versions don't have property templates, they just have + // the object counts by themselves. + ufbxi_node *props = ufbxi_find_child(object, ufbxi_PropertyTemplate); + if (props) { + ufbxi_check(ufbxi_get_val1(props, "S", &tmpl->sub_type)); + + // Remove the "Fbx" prefix from sub-types, remember to re-intern! + if (tmpl->sub_type.length > 3 && !strncmp(tmpl->sub_type.data, "Fbx", 3)) { + tmpl->sub_type.data += 3; + tmpl->sub_type.length -= 3; + + // HACK: LOD groups use LODGroup for Template, LodGroup for Object? + if (tmpl->sub_type.length == 8 && !memcmp(tmpl->sub_type.data, "LODGroup", 8)) { + tmpl->sub_type.data = "LodGroup"; + } + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &tmpl->sub_type, false)); + } + + ufbxi_check(ufbxi_read_properties(uc, props, &tmpl->props)); + } + } + + // TODO: Preserve only the `props` part of the templates + uc->templates = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbxi_template, uc->num_templates); + ufbxi_check(uc->templates); + + return 1; +} + +ufbxi_nodiscard static ufbx_props *ufbxi_find_template(ufbxi_context *uc, const char *name, const char *sub_type) +{ + // TODO: Binary search + ufbxi_for(ufbxi_template, tmpl, uc->templates, uc->num_templates) { + if (tmpl->type == name) { + + // Check that sub_type matches unless the type is Material, Model, AnimationStack, AnimationLayer. + // Those match to all sub-types. + if (tmpl->type != ufbxi_Material && tmpl->type != ufbxi_Model + && tmpl->type != ufbxi_AnimationStack && tmpl->type != ufbxi_AnimationLayer) { + if (tmpl->sub_type.data != sub_type) { + return NULL; + } + } + + if (tmpl->props.props.count > 0) { + return &tmpl->props; + } else { + return NULL; + } + } + } + return NULL; +} + +// Name ID categories +#if defined(UFBX_REGRESSION) + #define UFBXI_MAXIMUM_FAST_POINTER_ID UINT64_C(0x100) +#else + #define UFBXI_MAXIMUM_FAST_POINTER_ID UINT64_C(0x4000000000000000) +#endif +#define UFBXI_POINTER_ID_START UINT64_C(0x8000000000000000) +#define UFBXI_SYNTHETIC_ID_START (UFBXI_POINTER_ID_START + UFBXI_MAXIMUM_FAST_POINTER_ID) + +static ufbxi_forceinline uint64_t ufbxi_push_synthetic_id(ufbxi_context *uc) +{ + return ++uc->synthetic_id_counter; +} + +static ufbxi_noinline uint64_t ufbxi_synthetic_id_from_ptr_id(ufbxi_context *uc, uintptr_t ptr, uint64_t id) +{ + ufbxi_ptr_id ptr_id = { ptr, id }; + uint32_t hash = ufbxi_hash_ptr_id(ptr_id); + ufbxi_ptr_fbx_id_entry *entry = ufbxi_map_find(&uc->ptr_fbx_id_map, ufbxi_ptr_fbx_id_entry, hash, &ptr_id); + + if (!entry) { + entry = ufbxi_map_insert(&uc->ptr_fbx_id_map, ufbxi_ptr_fbx_id_entry, hash, &ptr_id); + ufbxi_check_return(entry, 0); + entry->ptr_id = ptr_id; + entry->fbx_id = ufbxi_push_synthetic_id(uc); + } + + return entry->fbx_id; +} + +static ufbxi_forceinline uint64_t ufbxi_synthetic_id_from_string(ufbxi_context *uc, const char *str) +{ + uintptr_t uptr = (uintptr_t)str; + if (uptr < (UINTPTR_MAX < UFBXI_MAXIMUM_FAST_POINTER_ID ? UINTPTR_MAX : UFBXI_MAXIMUM_FAST_POINTER_ID)) { + return (uint64_t)uptr; + } else { + return ufbxi_synthetic_id_from_ptr_id(uc, uptr, 0); + } +} + +ufbxi_nodiscard ufbxi_forceinline static int ufbxi_validate_fbx_id(ufbxi_context *uc, uint64_t *p_fbx_id) +{ + uint64_t fbx_id = *p_fbx_id; + if (fbx_id >= UFBXI_POINTER_ID_START) { + fbx_id = ufbxi_synthetic_id_from_ptr_id(uc, 0, fbx_id); + ufbxi_check(fbx_id); + *p_fbx_id = fbx_id; + } + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_split_type_and_name(ufbxi_context *uc, ufbx_string type_and_name, ufbx_string *type, ufbx_string *name) +{ + // Name and type are packed in a single property as Type::Name (in ASCII) + // or Name\x00\x01Type (in binary) + const char *sep = uc->from_ascii ? "::" : "\x00\x01"; + size_t type_end = 2; + for (; type_end <= type_and_name.length; type_end++) { + const char *ch = type_and_name.data + type_end - 2; + if (ch[0] == sep[0] && ch[1] == sep[1]) break; + } + + // ???: ASCII and binary store type and name in different order + if (type_end <= type_and_name.length) { + if (uc->from_ascii) { + name->data = type_and_name.data + type_end; + name->length = type_and_name.length - type_end; + type->data = type_and_name.data; + type->length = type_end - 2; + } else { + name->data = type_and_name.data; + name->length = type_end - 2; + type->data = type_and_name.data + type_end; + type->length = type_and_name.length - type_end; + } + } else { + *name = type_and_name; + type->data = ufbxi_empty_char; + type->length = 0; + } + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, type, false)); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, name, false)); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_id(ufbxi_context *uc, uint64_t fbx_id, uint32_t element_id) +{ + uint32_t hash = ufbxi_hash64(fbx_id); + ufbxi_fbx_id_entry *entry = ufbxi_map_find(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id); + + if (!entry) { + entry = ufbxi_map_insert(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id); + ufbxi_check(entry); + entry->fbx_id = fbx_id; + entry->element_id = element_id; + entry->user_id = 0; + } else { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_DUPLICATE_OBJECT_ID, "Duplicate object ID")); + } + + return 1; +} + +static ufbxi_noinline ufbxi_fbx_id_entry *ufbxi_find_fbx_id(ufbxi_context *uc, uint64_t fbx_id) +{ + uint32_t hash = ufbxi_hash64(fbx_id); + return ufbxi_map_find(&uc->fbx_id_map, ufbxi_fbx_id_entry, hash, &fbx_id); +} + +static ufbxi_forceinline bool ufbxi_fbx_id_exists(ufbxi_context *uc, uint64_t fbx_id) +{ + return ufbxi_find_fbx_id(uc, fbx_id) != NULL; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_fbx_attr(ufbxi_context *uc, uint64_t fbx_id, uint64_t attrib_fbx_id) +{ + uint32_t hash = ufbxi_hash64(fbx_id); + ufbxi_fbx_attr_entry *entry = ufbxi_map_find(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &fbx_id); + // TODO: Strict / warn about duplicate objects + + if (!entry) { + entry = ufbxi_map_insert(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &fbx_id); + ufbxi_check(entry); + entry->node_fbx_id = fbx_id; + entry->attr_fbx_id = attrib_fbx_id; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_element_size(ufbxi_context *uc, ufbxi_element_info *info, size_t size, ufbx_element_type type) +{ + size_t aligned_size = (size + 7u) & ~0x7u; + + uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items; + uint32_t element_id = uc->num_elements++; + + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, &info->fbx_id), NULL); + uc->tmp_element_byte_offset += aligned_size; + + ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8); + ufbxi_check_return(elem, NULL); + elem->type = type; + elem->element_id = element_id; + elem->typed_id = typed_id; + elem->name = info->name; + elem->props = info->props; + elem->dom_node = info->dom_node; + + if (uc->p_element_id) { + *uc->p_element_id = element_id; + } + + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); + + ufbxi_check_return(ufbxi_insert_fbx_id(uc, info->fbx_id, element_id), NULL); + + return elem; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_push_synthetic_element_size(ufbxi_context *uc, uint64_t *p_fbx_id, ufbxi_node *node, const char *name, size_t size, ufbx_element_type type) +{ + size_t aligned_size = (size + 7u) & ~0x7u; + + uint32_t typed_id = (uint32_t)uc->tmp_typed_element_offsets[type].num_items; + uint32_t element_id = uc->num_elements++; + + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_typed_element_offsets[type], size_t, 1, &uc->tmp_element_byte_offset), NULL); + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_offsets, size_t, 1, &uc->tmp_element_byte_offset), NULL); + uc->tmp_element_byte_offset += aligned_size; + + ufbx_element *elem = (ufbx_element*)ufbxi_push_zero(&uc->tmp_elements, uint64_t, aligned_size/8); + ufbxi_check_return(elem, NULL); + elem->type = type; + elem->element_id = element_id; + elem->typed_id = typed_id; + elem->dom_node = ufbxi_get_dom_node(uc, node); + if (name) { + elem->name.data = name; + elem->name.length = strlen(name); + } + + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_ptrs, ufbx_element*, 1, &elem), NULL); + + *p_fbx_id = ufbxi_push_synthetic_id(uc); + + ufbxi_check_return(ufbxi_push_copy_fast(&uc->tmp_element_fbx_ids, uint64_t, 1, p_fbx_id), NULL); + ufbxi_check_return(ufbxi_insert_fbx_id(uc, *p_fbx_id, element_id), NULL); + + return elem; +} + +#define ufbxi_push_element(uc, info, type_name, type_enum) ufbxi_maybe_null((type_name*)ufbxi_push_element_size((uc), (info), sizeof(type_name), (type_enum))) +#define ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, type_name, type_enum) ufbxi_maybe_null((type_name*)ufbxi_push_synthetic_element_size((uc), (p_fbx_id), (node), (name), sizeof(type_name), (type_enum))) + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_oo(ufbxi_context *uc, uint64_t src, uint64_t dst) +{ + ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1); + ufbxi_check(conn); + conn->src = src; + conn->dst = dst; + conn->src_prop = conn->dst_prop = ufbx_empty_string; + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_op(ufbxi_context *uc, uint64_t src, uint64_t dst, ufbx_string prop) +{ + ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1); + ufbxi_check(conn); + conn->src = src; + conn->dst = dst; + conn->src_prop = ufbx_empty_string; + conn->dst_prop = prop; + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_connect_pp(ufbxi_context *uc, uint64_t src, uint64_t dst, ufbx_string src_prop, ufbx_string dst_prop) +{ + ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1); + ufbxi_check(conn); + conn->src = src; + conn->dst = dst; + conn->src_prop = src_prop; + conn->dst_prop = dst_prop; + return 1; +} + +ufbxi_noinline static void ufbxi_init_synthetic_int_prop(ufbx_prop *dst, const char *name, int64_t value, ufbx_prop_type type) +{ + dst->type = type; + dst->name.data = name; + dst->name.length = strlen(name); + dst->value_real = (ufbx_real)value; + dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_REAL|UFBX_PROP_FLAG_VALUE_INT); + dst->value_int = value; + dst->value_str.data = ufbxi_empty_char; + + ufbxi_dev_assert(dst->name.length >= 4); + dst->_internal_key = ufbxi_get_name_key(name, 4); +} + +ufbxi_noinline static void ufbxi_init_synthetic_real_prop(ufbx_prop *dst, const char *name, ufbx_real value, ufbx_prop_type type) +{ + dst->type = type; + dst->name.data = name; + dst->name.length = strlen(name); + dst->value_real = value; + dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_REAL); + dst->value_int = (int64_t)value; + dst->value_str.data = ufbxi_empty_char; + + ufbxi_dev_assert(dst->name.length >= 4); + dst->_internal_key = ufbxi_get_name_key(name, 4); +} + +ufbxi_noinline static void ufbxi_init_synthetic_vec3_prop(ufbx_prop *dst, const char *name, const ufbx_vec3 *value, ufbx_prop_type type) +{ + dst->type = type; + dst->name.data = name; + dst->name.length = strlen(name); + dst->value_vec3 = *value; + dst->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_SYNTHETIC|UFBX_PROP_FLAG_VALUE_VEC3); + dst->value_int = ufbxi_f64_to_i64(dst->value_real); + dst->value_str.data = ufbxi_empty_char; + + ufbxi_dev_assert(dst->name.length >= 4); + dst->_internal_key = ufbxi_get_name_key(name, 4); +} + +ufbxi_noinline static void ufbxi_set_own_prop_vec3_uniform(ufbx_props *props, const char *name, ufbx_real value) +{ + ufbx_props local_props = *props; + local_props.defaults = NULL; + ufbx_prop *prop = ufbx_find_prop(&local_props, name); + if (prop) { + prop->value_vec4.x = value; + prop->value_vec4.y = value; + prop->value_vec4.z = value; + prop->value_vec4.w = 0.0f; + prop->value_int = (int64_t)value; + } +} + +typedef struct { + uint32_t geometry_helper_id; + uint32_t scale_helper_id; +} ufbxi_node_extra; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_setup_geometry_transform_helper(ufbxi_context *uc, ufbx_node *node, uint64_t node_fbx_id) +{ + ufbx_vec3 geo_translation = ufbxi_find_vec3(&node->props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 geo_rotation = ufbxi_find_vec3(&node->props, ufbxi_GeometricRotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 geo_scaling = ufbxi_find_vec3(&node->props, ufbxi_GeometricScaling, 1.0f, 1.0f, 1.0f); + if (!ufbxi_is_vec3_zero(geo_translation) || !ufbxi_is_vec3_zero(geo_rotation) || !ufbxi_is_vec3_one(geo_scaling)) { + + uint64_t geo_fbx_id; + ufbx_node *geo_node = ufbxi_push_synthetic_element(uc, &geo_fbx_id, NULL, uc->opts.geometry_transform_helper_name.data, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(geo_node); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &geo_node->element.element_id)); + geo_node->element.dom_node = node->element.dom_node; + + ufbx_prop *props = ufbxi_push_zero(&uc->result, ufbx_prop, 3); + ufbxi_check(props); + ufbxi_init_synthetic_vec3_prop(&props[0], ufbxi_Lcl_Rotation, &geo_rotation, UFBX_PROP_ROTATION); + ufbxi_init_synthetic_vec3_prop(&props[1], ufbxi_Lcl_Scaling, &geo_scaling, UFBX_PROP_SCALING); + ufbxi_init_synthetic_vec3_prop(&props[2], ufbxi_Lcl_Translation, &geo_translation, UFBX_PROP_TRANSLATION); + + geo_node->props.props.data = props; + geo_node->props.props.count = 3; + + node->has_geometry_transform = true; + geo_node->is_geometry_transform_helper = true; + + ufbxi_check(ufbxi_connect_oo(uc, geo_fbx_id, node_fbx_id)); + uc->has_geometry_transform_nodes = true; + + ufbxi_node_extra *extra = ufbxi_push_element_extra(uc, node->element_id, ufbxi_node_extra); + ufbxi_check(extra); + extra->geometry_helper_id = geo_node->element_id; + } + + return 1; +} + +typedef struct { + const char *name; + ufbx_vec3 default_value; +} ufbxi_scale_helper_prop; + +static const ufbxi_scale_helper_prop ufbxi_scale_helper_props[] = { + { ufbxi_GeometricRotation, { 0.0f, 0.0f, 0.0f } }, + { ufbxi_GeometricScaling, { 1.0f, 1.0f, 1.0f } }, + { ufbxi_GeometricTranslation, { 0.0f, 0.0f, 0.0f } }, + { ufbxi_Lcl_Scaling, { 1.0f, 1.0f, 1.0f } }, +}; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_setup_scale_helper(ufbxi_context *uc, ufbx_node *node, uint64_t node_fbx_id) +{ + uint64_t scale_fbx_id; + ufbx_node *scale_node = ufbxi_push_synthetic_element(uc, &scale_fbx_id, NULL, uc->opts.scale_helper_name.data, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(scale_node); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &scale_node->element.element_id)); + scale_node->element.dom_node = node->element.dom_node; + + node->scale_helper = scale_node; + scale_node->is_scale_helper = true; + + ufbxi_check(ufbxi_connect_oo(uc, scale_fbx_id, node_fbx_id)); + uc->has_scale_helper_nodes = true; + + ufbxi_node_extra *extra = ufbxi_push_element_extra(uc, node->element.element_id, ufbxi_node_extra); + ufbxi_check(extra); + extra->scale_helper_id = scale_node->element_id; + + size_t max_props = ufbxi_arraycount(ufbxi_scale_helper_props); + ufbx_prop *helper_props = ufbxi_push(&uc->result, ufbx_prop, max_props); + ufbxi_check(helper_props); + + size_t num_props = 0; + ufbx_props props_copy = node->props; + props_copy.defaults = NULL; + for (size_t i = 0; i < max_props; i++) { + const ufbxi_scale_helper_prop *hp = &ufbxi_scale_helper_props[i]; + ufbx_prop *src_prop = ufbxi_find_prop(&props_copy, hp->name); + if (!src_prop) continue; + + helper_props[num_props++] = *src_prop; + src_prop->value_vec3 = hp->default_value; + src_prop->value_int = (int64_t)src_prop->value_vec3.x; + } + + scale_node->props.props.data = helper_props; + scale_node->props.props.count = num_props; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_model(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + (void)node; + ufbx_node *elem_node = ufbxi_push_element(uc, info, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(elem_node); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &elem_node->element.element_id)); + + int64_t inherit_type = ufbxi_find_int(&elem_node->props, ufbxi_InheritType, -1); + switch (inherit_type) { + case 0: // RrSs + elem_node->original_inherit_mode = UFBX_INHERIT_MODE_COMPONENTWISE_SCALE; + break; + case 2: // Rrs + elem_node->original_inherit_mode = UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE; + break; + default: break; + } + + if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_PRESERVE) { + elem_node->inherit_mode = elem_node->original_inherit_mode; + } else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_IGNORE) { + elem_node->original_inherit_mode = UFBX_INHERIT_MODE_NORMAL; + elem_node->inherit_mode = UFBX_INHERIT_MODE_NORMAL; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_element(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, size_t size, ufbx_element_type type) +{ + (void)node; + ufbx_element *elem = ufbxi_push_element_size(uc, info, size, type); + ufbxi_check(elem); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_unknown(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *element, ufbx_string type, ufbx_string sub_type, const char *node_name) +{ + (void)node; + ufbx_unknown *unknown = ufbxi_push_element(uc, element, ufbx_unknown, UFBX_ELEMENT_UNKNOWN); + ufbxi_check(unknown); + unknown->type = type; + unknown->sub_type = sub_type; + unknown->super_type.data = node_name; + unknown->super_type.length = strlen(node_name); + + // `type`, `sub_type` and `node_name` are raw strings so they may need to be sanitized. + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->type, false)); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->sub_type, false)); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &unknown->super_type, false)); + + return 1; +} + +typedef struct { + ufbx_vertex_vec3 elem; + uint32_t index; +} ufbxi_tangent_layer; + +static ufbx_real ufbxi_zero_element[8] = { 0 }; + +// Sentinel pointers used for zero/sequential index buffers +static const uint32_t ufbxi_sentinel_index_zero[1] = { 100000000 }; +static const uint32_t ufbxi_sentinel_index_consecutive[1] = { 123456789 }; + +ufbxi_noinline static int ufbxi_fix_index(ufbxi_context *uc, uint32_t *p_dst, uint32_t index, size_t one_past_max_val) +{ + switch (uc->opts.index_error_handling) { + case UFBX_INDEX_ERROR_HANDLING_CLAMP: + ufbxi_check(one_past_max_val > 0); + ufbxi_check(one_past_max_val <= UINT32_MAX); + *p_dst = (uint32_t)one_past_max_val - 1; + ufbxi_check(ufbxi_warnf(UFBX_WARNING_INDEX_CLAMPED, "Clamped index")); + break; + case UFBX_INDEX_ERROR_HANDLING_NO_INDEX: + *p_dst = UFBX_NO_INDEX; + break; + case UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING: + ufbxi_fmt_err_info(&uc->error, "%u (max %u)", index, one_past_max_val ? (one_past_max_val - 1) : 0); + ufbxi_fail_msg("UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING", "Bad index"); + case UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE: + *p_dst = index; + break; + default: + ufbxi_unreachable("Unhandled index_error_handling"); + return 0; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_check_indices(ufbxi_context *uc, uint32_t **p_dst, uint32_t *indices, bool owns_indices, size_t num_indices, size_t num_indexers, size_t num_elems) +{ + // If the indices are truncated extend them with `UFBX_NO_INDEX`, the following normalization pass + // will handle them the same way as other out-of-bounds indices. + if (num_indices < num_indexers) { + uint32_t *new_indices = ufbxi_push(&uc->result, uint32_t, num_indexers); + ufbxi_check(new_indices); + + memcpy(new_indices, indices, sizeof(uint32_t) * num_indices); + for (size_t i = num_indices; i < num_indexers; i++) { + new_indices[i] = UFBX_NO_INDEX; + } + + indices = new_indices; + num_indices = num_indexers; + owns_indices = true; + } + + // Normalize out-of-bounds indices to `invalid_index` + for (size_t i = 0; i < num_indices; i++) { + uint32_t ix = indices[i]; + if (ix >= num_elems) { + // If the indices refer to an external buffer we need to + // allocate a separate buffer for them + if (!owns_indices) { + indices = ufbxi_push_copy(&uc->result, uint32_t, num_indices, indices); + ufbxi_check(indices); + owns_indices = true; + } + ufbxi_check(ufbxi_fix_index(uc, &indices[i], ix, num_elems)); + } + } + + *p_dst = indices; + + return 1; +} + +ufbx_static_assert(vertex_real_size, sizeof(ufbx_vertex_real) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec2_size, sizeof(ufbx_vertex_vec2) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec3_size, sizeof(ufbx_vertex_vec3) == sizeof(ufbx_vertex_attrib)); +ufbx_static_assert(vertex_vec4_size, sizeof(ufbx_vertex_vec4) == sizeof(ufbx_vertex_attrib)); + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_warn_polygon_mapping(ufbxi_context *uc, const char *data_name, const char *mapping) +{ + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_POLYGON_MAPPING, "Ignoring geometry '%s' with bad mapping mode '%s'", data_name, mapping)); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_vertex_element(ufbxi_context *uc, ufbx_mesh *mesh, ufbxi_node *node, + ufbx_vertex_attrib *attrib, const char *data_name, const char *index_name, const char *w_name, char data_type, size_t num_components) +{ + ufbx_real **p_dst_data = (ufbx_real**)&attrib->values.data; + + ufbxi_value_array *data = ufbxi_find_array(node, data_name, data_type); + ufbxi_value_array *indices = ufbxi_find_array(node, index_name, 'i'); + + if (!uc->opts.strict) { + if (!data) return 1; + } + + ufbxi_check(data); + ufbxi_check(data->size % num_components == 0); + + size_t num_elems = data->size / num_components; + + // HACK: If there's no elements at all keep the attribute as NULL + // TODO: Strict mode for this? + if (num_elems == 0) { + return 1; + } + + ufbxi_check(num_elems > 0 && num_elems < INT32_MAX); + + attrib->exists = true; + attrib->indices.count = mesh->num_indices; + + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_MappingInformationType, "C", (char**)&mapping)); + + attrib->values.count = num_elems ? num_elems : 1; + + // Data array is always used as-is, if empty set the data to a global + // zero buffer so invalid zero index can point to some valid data. + // The zero data is offset by 4 elements to accommodate for invalid index (-1) + if (num_elems > 0) { + *p_dst_data = (ufbx_real*)data->data; + } else { + *p_dst_data = ufbxi_zero_element + 4; + } + + // HACK: Some old exporters seem to use ByPolygon to mean ByPolygonVertex, + // it should be quite safe to remap this + if (mapping == ufbxi_ByPolygon) { + size_t num_indices = indices ? indices->size : num_elems; + if (num_indices == mesh->num_indices) { + mapping = ufbxi_ByPolygonVertex; + } + } + + if (indices) { + size_t num_indices = indices->size; + uint32_t *index_data = (uint32_t*)indices->data; + + if (mapping == ufbxi_ByPolygonVertex) { + + // Indexed by polygon vertex: We can use the provided indices directly. + ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, index_data, true, num_indices, mesh->num_indices, num_elems)); + + } else if (mapping == ufbxi_ByVertex || mapping == ufbxi_ByVertice) { + + // Indexed by vertex: Follow through the position index mapping to get the final indices. + uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices); + ufbxi_check(new_index_data); + + uint32_t *vert_ix = mesh->vertex_indices.data; + for (size_t i = 0; i < mesh->num_indices; i++) { + uint32_t ix = vert_ix[i]; + if (ix < num_indices) { + new_index_data[i] = index_data[ix]; + } else { + ufbxi_check(ufbxi_fix_index(uc, &new_index_data[i], ix, num_elems)); + } + } + + ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, new_index_data, true, mesh->num_indices, mesh->num_indices, num_elems)); + attrib->unique_per_vertex = true; + + } else if (mapping == ufbxi_ByPolygon) { + + // Indexed by polygon: Generate new indices based on polygons + uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices); + ufbxi_check(new_index_data); + + size_t num_faces = mesh->num_faces; + for (size_t face_ix = 0; face_ix < num_faces; face_ix++) { + ufbx_face face = mesh->faces.data[face_ix]; + uint32_t index = UFBX_NO_INDEX; + if (face_ix < num_indices) { + index = index_data[face_ix]; + } + if (index >= num_elems) { + ufbxi_check(ufbxi_fix_index(uc, &index, index, num_elems)); + } + for (size_t i = 0; i < face.num_indices; i++) { + new_index_data[face.index_begin + i] = index; + } + } + + attrib->indices.data = new_index_data; + + } else if (mapping == ufbxi_AllSame) { + + // Indexed by all same: ??? This could be possibly used for making + // holes with invalid indices, but that seems really fringe. + // Just use the shared zero index buffer for this. + uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_indices); + attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_zero; + attrib->unique_per_vertex = true; + + } else { + memset(attrib, 0, sizeof(ufbx_vertex_attrib)); + ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping)); + return 1; + } + + } else { + + if (mapping == ufbxi_ByPolygonVertex) { + + // Direct by polygon index: Use shared consecutive array if there's enough + // elements, otherwise use a unique truncated consecutive index array. + if (num_elems >= mesh->num_indices) { + uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_indices); + attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive; + } else { + uint32_t *index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices); + ufbxi_check(index_data); + for (size_t i = 0; i < mesh->num_indices; i++) { + index_data[i] = (uint32_t)i; + } + ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, index_data, true, mesh->num_indices, mesh->num_indices, num_elems)); + } + + } else if (mapping == ufbxi_ByVertex || mapping == ufbxi_ByVertice) { + + // Direct by vertex: We can re-use the position indices.. + ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, mesh->vertex_position.indices.data, false, mesh->num_indices, mesh->num_indices, num_elems)); + attrib->unique_per_vertex = true; + + } else if (mapping == ufbxi_ByPolygon) { + + // Direct by polygon: Generate new indices based on polygons + uint32_t *new_index_data = ufbxi_push(&uc->result, uint32_t, mesh->num_indices); + ufbxi_check(new_index_data); + + uint32_t num_faces = (uint32_t)mesh->num_faces; + for (uint32_t face_ix = 0; face_ix < num_faces; face_ix++) { + ufbx_face face = mesh->faces.data[face_ix]; + for (size_t i = 0; i < face.num_indices; i++) { + new_index_data[face.index_begin + i] = face_ix; + } + } + + ufbxi_check(ufbxi_check_indices(uc, &attrib->indices.data, new_index_data, true, mesh->num_indices, mesh->num_indices, num_elems)); + + } else if (mapping == ufbxi_AllSame) { + + // Direct by all same: This cannot fail as the index list is just zero. + uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_indices); + attrib->indices.data = (uint32_t*)ufbxi_sentinel_index_zero; + attrib->unique_per_vertex = true; + + } else { + memset(attrib, 0, sizeof(ufbx_vertex_attrib)); + ufbxi_check(ufbxi_warn_polygon_mapping(uc, data_name, mapping)); + return 1; + } + } + + if (uc->opts.retain_vertex_attrib_w && w_name) { + ufbxi_value_array *w_data = ufbxi_find_array(node, w_name, 'r'); + if (w_data) { + if (w_data->size == num_elems) { + attrib->values_w.count = w_data->size; + attrib->values_w.data = (ufbx_real*)w_data->data; + } else { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_BAD_VERTEX_W_ATTRIBUTE, "Bad W array size %s=%zu, %s=%zu", + w_name, w_data->size, data_name, num_elems)); + } + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_truncated_array(ufbxi_context *uc, void *p_data, size_t *p_count, ufbxi_node *node, const char *name, char fmt, size_t size) +{ + ufbxi_value_array *arr = ufbxi_find_array(node, name, fmt); + if (!arr) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_GEOMETRY_DATA, "Missing geometry data: %s", name)); + return 1; + } + + *p_count = size; + + void *data = arr->data; + if (arr->size < size) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_TRUNCATED_ARRAY, "Truncated array: %s", name)); + + size_t elem_size = ufbxi_array_type_size(fmt); + void *new_data = ufbxi_push_size(&uc->result, elem_size, size); + ufbxi_check(new_data); + memcpy(new_data, data, arr->size * elem_size); + // Extend the array with the last element if possible + if (arr->size > 0) { + char *first_elem = (char*)data + (arr->size - 1) * elem_size; + for (size_t i = arr->size; i < size; i++) { + memcpy((char*)new_data + i * elem_size, first_elem, elem_size); + } + } else { + memset(new_data, 0, size * elem_size); + } + data = new_data; + } + + *(void**)p_data = data; + return 1; +} + +ufbxi_noinline static bool ufbxi_uv_set_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_uv_set *a = (const ufbx_uv_set *)va, *b = (const ufbx_uv_set *)vb; + return a->index < b->index; +} + +ufbxi_noinline static bool ufbxi_color_set_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_color_set *a = (const ufbx_color_set *)va, *b = (const ufbx_color_set *)vb; + return a->index < b->index; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_uv_sets(ufbxi_context *uc, ufbx_uv_set *sets, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_uv_set))); + ufbxi_stable_sort(sizeof(ufbx_uv_set), 32, sets, uc->tmp_arr, count, &ufbxi_uv_set_less, NULL); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_color_sets(ufbxi_context *uc, ufbx_color_set *sets, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_color_set))); + ufbxi_stable_sort(sizeof(ufbx_color_set), 32, sets, uc->tmp_arr, count, &ufbxi_color_set_less, NULL); + return 1; +} + +typedef struct ufbxi_blend_offset { + uint32_t vertex; + ufbx_vec3 position_offset; + ufbx_vec3 normal_offset; +} ufbxi_blend_offset; + +static ufbxi_noinline bool ufbxi_blend_offset_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_blend_offset *a = (const ufbxi_blend_offset*)va, *b = (const ufbxi_blend_offset*)vb; + return a->vertex < b->vertex; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_offsets(ufbxi_context *uc, ufbxi_blend_offset *offsets, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_blend_offset))); + ufbxi_stable_sort(sizeof(ufbxi_blend_offset), 16, offsets, uc->tmp_arr, count, &ufbxi_blend_offset_less, NULL); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_shape(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices); + ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_Indexes); + ufbxi_node *node_normals = ufbxi_find_child(node, ufbxi_Normals); + if (!node_vertices || !node_indices) return 1; + + ufbx_blend_shape *shape = ufbxi_push_element(uc, info, ufbx_blend_shape, UFBX_ELEMENT_BLEND_SHAPE); + ufbxi_check(shape); + + if (uc->opts.ignore_geometry) return 1; + + ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r'); + ufbxi_value_array *indices = ufbxi_get_array(node_indices, 'i'); + + ufbxi_check(vertices && indices); + ufbxi_check(vertices->size % 3 == 0); + ufbxi_check(indices->size == vertices->size / 3); + + size_t num_offsets = indices->size; + uint32_t *vertex_indices = (uint32_t*)indices->data; + + shape->num_offsets = num_offsets; + shape->position_offsets.data = (ufbx_vec3*)vertices->data; + shape->offset_vertices.data = vertex_indices; + shape->position_offsets.count = num_offsets; + shape->offset_vertices.count = num_offsets; + + if (node_normals) { + ufbxi_value_array *normals = ufbxi_get_array(node_normals, 'r'); + ufbxi_check(normals && normals->size == vertices->size); + shape->normal_offsets.data = (ufbx_vec3*)normals->data; + shape->normal_offsets.count = num_offsets; + } + + // Sort the blend shape vertices only if absolutely necessary + bool sorted = true; + for (size_t i = 1; i < num_offsets; i++) { + if (vertex_indices[i - 1] > vertex_indices[i]) { + sorted = false; + break; + } + } + + if (!sorted) { + ufbxi_blend_offset *offsets = ufbxi_push(&uc->tmp_stack, ufbxi_blend_offset, num_offsets); + ufbxi_check(offsets); + + for (size_t i = 0; i < num_offsets; i++) { + offsets[i].vertex = shape->offset_vertices.data[i]; + offsets[i].position_offset = shape->position_offsets.data[i]; + if (node_normals) offsets[i].normal_offset = shape->normal_offsets.data[i]; + } + + ufbxi_check(ufbxi_sort_blend_offsets(uc, offsets, num_offsets)); + + for (size_t i = 0; i < num_offsets; i++) { + shape->offset_vertices.data[i] = offsets[i].vertex; + shape->position_offsets.data[i] = offsets[i].position_offset; + if (node_normals) shape->normal_offsets.data[i] = offsets[i].normal_offset; + } + ufbxi_pop(&uc->tmp_stack, ufbxi_blend_offset, num_offsets, NULL); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_blend_shapes(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_blend_deformer *deformer = NULL; + uint64_t deformer_fbx_id = 0; + + ufbxi_for (ufbxi_node, n, node->children, node->num_children) { + if (n->name != ufbxi_Shape) continue; + + ufbx_string name; + ufbxi_check(ufbxi_get_val1(n, "S", &name)); + + if (deformer == NULL) { + deformer = ufbxi_push_synthetic_element(uc, &deformer_fbx_id, n, name.data, ufbx_blend_deformer, UFBX_ELEMENT_BLEND_DEFORMER); + ufbxi_check(deformer); + ufbxi_check(ufbxi_connect_oo(uc, deformer_fbx_id, info->fbx_id)); + } + + uint64_t channel_fbx_id = 0; + ufbx_blend_channel *channel = ufbxi_push_synthetic_element(uc, &channel_fbx_id, n, name.data, ufbx_blend_channel, UFBX_ELEMENT_BLEND_CHANNEL); + ufbxi_check(channel); + + ufbx_real_list weight_list = { NULL, 0 }; + ufbxi_check(ufbxi_push_copy(&uc->tmp_full_weights, ufbx_real_list, 1, &weight_list)); + + size_t num_shape_props = 1; + ufbx_prop *shape_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_shape_props); + ufbxi_check(shape_props); + shape_props[0].name.data = ufbxi_DeformPercent; + shape_props[0].name.length = sizeof(ufbxi_DeformPercent) - 1; + shape_props[0]._internal_key = ufbxi_get_name_key_c(ufbxi_DeformPercent); + shape_props[0].type = UFBX_PROP_NUMBER; + shape_props[0].value_real = (ufbx_real)0.0; + shape_props[0].value_str = ufbx_empty_string; + shape_props[0].value_blob = ufbx_empty_blob; + + ufbx_prop *self_prop = ufbx_find_prop_len(&info->props, name.data, name.length); + if (self_prop && (self_prop->type == UFBX_PROP_NUMBER || self_prop->type == UFBX_PROP_INTEGER)) { + shape_props[0].value_real = self_prop->value_real; + ufbxi_check(ufbxi_connect_pp(uc, info->fbx_id, channel_fbx_id, name, shape_props[0].name)); + } else if (uc->version < 6000) { + ufbxi_check(ufbxi_connect_pp(uc, info->fbx_id, channel_fbx_id, name, shape_props[0].name)); + } + + channel->name = name; + channel->props.props.data = shape_props; + channel->props.props.count = num_shape_props; + + ufbxi_element_info shape_info = { 0 }; + + shape_info.fbx_id = ufbxi_push_synthetic_id(uc); + shape_info.name = name; + shape_info.dom_node = ufbxi_get_dom_node(uc, n); + + ufbxi_check(ufbxi_read_shape(uc, n, &shape_info)); + + ufbxi_check(ufbxi_connect_oo(uc, channel_fbx_id, deformer_fbx_id)); + ufbxi_check(ufbxi_connect_oo(uc, shape_info.fbx_id, channel_fbx_id)); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_process_indices(ufbxi_context *uc, ufbx_mesh *mesh, uint32_t *index_data) +{ + // Count the number of faces and allocate the index list + // Indices less than zero (~actual_index) ends a polygon + size_t num_total_faces = 0; + ufbxi_for (uint32_t, p_ix, index_data, mesh->num_indices) { + num_total_faces += ((int32_t)*p_ix < 0) ? 1u : 0u; + } + mesh->faces.data = ufbxi_push(&uc->result, ufbx_face, num_total_faces); + ufbxi_check(mesh->faces.data); + + size_t num_triangles = 0; + size_t max_face_triangles = 0; + size_t num_bad_faces[3] = { 0 }; + + ufbx_face *dst_face = mesh->faces.data; + uint32_t *p_face_begin = index_data; + ufbxi_for (uint32_t, p_ix, index_data, mesh->num_indices) { + uint32_t ix = *p_ix; + // Un-negate final indices of polygons + if ((int32_t)ix < 0) { + ix = ~ix; + *p_ix = ix; + uint32_t num_indices = (uint32_t)((p_ix - p_face_begin) + 1); + dst_face->index_begin = (uint32_t)(p_face_begin - index_data); + dst_face->num_indices = num_indices; + if (num_indices >= 3) { + num_triangles += num_indices - 2; + max_face_triangles = ufbxi_max_sz(max_face_triangles, num_indices - 2); + } else { + num_bad_faces[num_indices]++; + } + dst_face++; + p_face_begin = p_ix + 1; + } + ufbxi_check((size_t)ix < mesh->num_vertices); + } + + mesh->vertex_position.indices.data = index_data; + mesh->num_faces = ufbxi_to_size(dst_face - mesh->faces.data); + mesh->faces.count = mesh->num_faces; + mesh->num_triangles = num_triangles; + mesh->max_face_triangles = max_face_triangles; + mesh->num_empty_faces = num_bad_faces[0]; + mesh->num_point_faces = num_bad_faces[1]; + mesh->num_line_faces = num_bad_faces[2]; + + mesh->vertex_first_index.count = mesh->num_vertices; + mesh->vertex_first_index.data = ufbxi_push(&uc->result, uint32_t, mesh->num_vertices); + ufbxi_check(mesh->vertex_first_index.data); + + ufbxi_for_list(uint32_t, p_vx_ix, mesh->vertex_first_index) { + *p_vx_ix = UFBX_NO_INDEX; + } + + { + size_t num_indices = mesh->num_indices; + size_t num_vertices = mesh->num_vertices; + uint32_t *vertex_indices = mesh->vertex_indices.data; + uint32_t *vertex_first_index = mesh->vertex_first_index.data; + for (size_t ix = 0; ix < num_indices; ix++) { + uint32_t vx = vertex_indices[ix]; + if (vx < num_vertices) { + if (vertex_first_index[vx] == UFBX_NO_INDEX) { + vertex_first_index[vx] = (uint32_t)ix; + } + } else { + ufbxi_check(ufbxi_fix_index(uc, &vertex_indices[ix], vx, mesh->num_vertices)); + } + } + } + + // HACK(consecutive-faces): Prepare for finalize to re-use a consecutive/zero + // index buffer for face materials.. + uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_faces); + uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_faces); + + return 1; +} + +ufbxi_noinline static void ufbxi_patch_mesh_reals(ufbx_mesh *mesh) +{ + mesh->vertex_position.value_reals = 3; + mesh->vertex_normal.value_reals = 3; + mesh->vertex_uv.value_reals = 2; + mesh->vertex_tangent.value_reals = 3; + mesh->vertex_bitangent.value_reals = 3; + mesh->vertex_color.value_reals = 4; + mesh->vertex_crease.value_reals = 1; + mesh->skinned_position.value_reals = 3; + mesh->skinned_normal.value_reals = 3; + + ufbxi_nounroll ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + set->vertex_uv.value_reals = 2; + set->vertex_tangent.value_reals = 3; + set->vertex_bitangent.value_reals = 3; + } + + ufbxi_nounroll ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) { + set->vertex_color.value_reals = 4; + } +} + +typedef struct { + uint32_t id, index; +} ufbxi_id_group; + +static bool ufbxi_less_int32(void *user, const void *va, const void *vb) +{ + (void)user; + const int32_t a = *(const int32_t*)va, b = *(const int32_t*)vb; + return a < b; +} + +ufbx_static_assert(mesh_mat_point_faces, offsetof(ufbx_mesh_part, num_point_faces) - offsetof(ufbx_mesh_part, num_empty_faces) == 1 * sizeof(size_t)); +ufbx_static_assert(mesh_mat_line_faces, offsetof(ufbx_mesh_part, num_line_faces) - offsetof(ufbx_mesh_part, num_empty_faces) == 2 * sizeof(size_t)); +static ufbxi_forceinline void ufbxi_mesh_part_add_face(ufbx_mesh_part *part, uint32_t num_indices) +{ + part->num_faces++; + if (num_indices >= 3) { + part->num_triangles += num_indices - 2; + } else { + // `num_empty/point/line_faces` are consecutive, see static asserts above. + // cppcheck-suppress objectIndex + (&part->num_empty_faces)[num_indices]++; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_assign_face_groups(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh, size_t *p_consecutive_indices, bool retain_parts) +{ + size_t num_faces = mesh->num_faces; + ufbxi_check_err(error, num_faces > 0); + ufbxi_check_err(error, num_faces < UINT32_MAX); + ufbxi_check_err(error, mesh->face_group.count == num_faces); + + uint32_t *ids = ufbxi_push(buf, uint32_t, num_faces); + ufbxi_check_err(error, ids); + + uint32_t num_ids = 0; + + ufbxi_id_group seen_ids[1 << UFBXI_FACE_GROUP_HASH_BITS]; + memset(seen_ids, 0, sizeof(seen_ids)); + + uint32_t seed = 2654435769u; + uint32_t rehash_threshold = 256; + + // Loosely deduplicate group IDs + ufbxi_for_list(uint32_t, p_id, mesh->face_group) { + uint32_t id = *p_id; + uint32_t id_hash = (id * seed) >> (32u - UFBXI_FACE_GROUP_HASH_BITS); + if (seen_ids[id_hash].id != id || seen_ids[id_hash].index == 0) { + seen_ids[id_hash].id = id; + if (++seen_ids[id_hash].index > rehash_threshold) { + seed *= seed; + rehash_threshold *= 2; + } + ids[num_ids++] = id; + } + } + + // Sort and deduplicate remaining IDs + ufbxi_unstable_sort(ids, num_ids, sizeof(uint32_t), &ufbxi_less_int32, NULL); + + size_t num_groups = 0; + for (size_t i = 0; i < num_ids; ) { + uint32_t id = ids[i]; + ids[num_groups++] = id; + do { i++; } while (i < num_ids && ids[i] == id); + } + + // Allocate group info structs + ufbx_face_group *groups = ufbxi_push_zero(buf, ufbx_face_group, num_groups); + ufbxi_check_err(error, groups); + for (size_t i = 0; i < num_groups; i++) { + groups[i].id = (int32_t)ids[i]; + groups[i].name.data = ufbxi_empty_char; + } + + mesh->face_groups.data = groups; + mesh->face_groups.count = num_groups; + + ufbx_mesh_part *parts = NULL; + if (retain_parts) { + parts = ufbxi_push_zero(buf, ufbx_mesh_part, num_groups); + ufbxi_check_err(error, parts); + mesh->face_group_parts.data = parts; + mesh->face_group_parts.count = num_groups; + } + + // Optimization: Use `consecutive_indices` for a single group + if (p_consecutive_indices && num_groups == 1) { + memset(mesh->face_group.data, 0, sizeof(uint32_t) * num_faces); + + if (parts) { + parts[0].face_indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive; + parts[0].face_indices.count = num_faces; + parts[0].num_empty_faces = mesh->num_empty_faces; + parts[0].num_point_faces = mesh->num_point_faces; + parts[0].num_line_faces = mesh->num_line_faces; + parts[0].num_faces = num_faces; + parts[0].num_triangles = mesh->num_triangles; + } + + *p_consecutive_indices = ufbxi_max_sz(*p_consecutive_indices, num_faces); + return 1; + } + + memset(seen_ids, 0, sizeof(seen_ids)); + + // Count faces and triangles per group and reassign IDs + const ufbx_face *p_face = mesh->faces.data; + ufbxi_for_list(uint32_t, p_id, mesh->face_group) { + uint32_t id = *p_id; + uint32_t id_hash = (id * seed) >> (32u - UFBXI_FACE_GROUP_HASH_BITS); + + uint32_t num_indices = p_face->num_indices; + + size_t index; + if (seen_ids[id_hash].id == id && seen_ids[id_hash].index > 0) { + index = seen_ids[id_hash].index - 1; + *p_id = (uint32_t)index; + } else { + int32_t signed_id = (int32_t)id; + index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_face_group, 8, &index, groups, 0, num_groups, ( a->id < signed_id ), ( a->id == signed_id )); + ufbx_assert(index < num_groups); + seen_ids[id_hash].id = id; + seen_ids[id_hash].index = (uint32_t)index + 1; + } + + if (parts) { + ufbxi_mesh_part_add_face(&parts[index], num_indices); + } + + *p_id = (uint32_t)index; + p_face++; + } + + if (!parts) return 1; + + // Subdivide `ids` for per-group `face_indices` + uint32_t *face_indices = ids; + uint32_t part_index = 0; + ufbxi_for(ufbx_mesh_part, part, parts, num_groups) { + part->index = part_index++; + part->face_indices.data = face_indices; + face_indices += part->num_faces; + } + ufbx_assert(face_indices == ids + num_faces); + + // Collect per-group faces + uint32_t face_index = 0; + ufbxi_for_list(uint32_t, p_id, mesh->face_group) { + ufbx_mesh_part *part = &parts[*p_id]; + part->face_indices.data[part->face_indices.count++] = face_index++; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_update_face_groups(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh, bool need_copy) +{ + size_t num_faces = mesh->faces.count; + size_t num_groups = mesh->face_group_parts.count; + if (num_groups == 0) return 1; + + if (need_copy) { + mesh->face_group_parts.data = ufbxi_push_zero(buf, ufbx_mesh_part, num_groups); + ufbxi_check_err(error, mesh->face_group_parts.data); + } + + uint32_t *face_indices = ufbxi_push(buf, uint32_t, num_faces); + ufbxi_check_err(error, face_indices); + + ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) { + ufbx_mesh_part *part = &mesh->face_group_parts.data[mesh->face_group.data[i]]; + ufbxi_mesh_part_add_face(part, mesh->faces.data[i].num_indices); + } + + uint32_t part_index = 0; + ufbxi_for_list(ufbx_mesh_part, part, mesh->face_group_parts) { + part->index = part_index++; + part->face_indices.data = face_indices; + part->face_indices.count = 0; + face_indices += part->num_faces; + } + + ufbxi_nounroll for (uint32_t i = 0; i < num_faces; i++) { + ufbx_mesh_part *part = &mesh->face_group_parts.data[mesh->face_group.data[i]]; + part->face_indices.data[part->face_indices.count++] = i; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_mesh *ufbxi_restrict mesh = ufbxi_push_element(uc, info, ufbx_mesh, UFBX_ELEMENT_MESH); + ufbxi_check(mesh); + + // In up to version 7100 FBX files blend shapes are contained within the same geometry node + if (uc->version <= 7100) { + ufbxi_check(ufbxi_read_synthetic_blend_shapes(uc, node, info)); + } + + ufbxi_patch_mesh_reals(mesh); + + // Sometimes there are empty meshes in FBX files? + // TODO: Should these be included in output? option? strict mode? + ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices); + ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_PolygonVertexIndex); + if (!node_vertices) return 1; + + if (uc->opts.ignore_geometry) return 1; + + ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r'); + ufbxi_value_array *indices = node_indices ? ufbxi_get_array(node_indices, 'i') : NULL; + ufbxi_value_array *edge_indices = ufbxi_find_array(node, ufbxi_Edges, 'i'); + ufbxi_check(vertices); + ufbxi_check(!node_indices || indices); // If node_indices exists, it must be an array + ufbxi_check(vertices->size % 3 == 0); + + mesh->num_vertices = vertices->size / 3; + mesh->num_indices = indices ? indices->size : 0; + + uint32_t *index_data = indices ? (uint32_t*)indices->data : NULL; + + // Duplicate `index_data` for modification if we retain DOM + if (uc->opts.retain_dom) { + index_data = ufbxi_push_copy(&uc->result, uint32_t, mesh->num_indices, index_data); + ufbxi_check(index_data); + } + + mesh->vertices.data = (ufbx_vec3*)vertices->data; + mesh->vertices.count = mesh->num_vertices; + mesh->vertex_indices.data = index_data; + mesh->vertex_indices.count = mesh->num_indices; + + mesh->vertex_position.exists = true; + mesh->vertex_position.values.data = (ufbx_vec3*)vertices->data; + mesh->vertex_position.values.count = mesh->num_vertices; + mesh->vertex_position.indices.data = index_data; + mesh->vertex_position.indices.count = mesh->num_indices; + mesh->vertex_position.unique_per_vertex = true; + + // Check/make sure that the last index is negated (last of polygon) + if (mesh->num_indices > 0) { + if ((int32_t)index_data[mesh->num_indices - 1] >= 0) { + if (uc->opts.strict) ufbxi_fail("Non-negated last index"); + index_data[mesh->num_indices - 1] = ~index_data[mesh->num_indices - 1]; + } + } + + // Read edges before un-negating the indices + if (edge_indices) { + size_t num_edges = edge_indices->size; + ufbx_edge *edges = ufbxi_push(&uc->result, ufbx_edge, num_edges); + ufbxi_check(edges); + + size_t dst_ix = 0; + + // Edges are represented using a single index into PolygonVertexIndex. + // The edge is between two consecutive vertices in the polygon. + uint32_t *edge_data = (uint32_t*)edge_indices->data; + for (size_t i = 0; i < num_edges; i++) { + uint32_t index_ix = edge_data[i]; + if (index_ix >= mesh->num_indices) { + if (uc->opts.strict) ufbxi_fail("Edge index out of bounds"); + continue; + } + edges[dst_ix].a = index_ix; + if ((int32_t)index_data[index_ix] < 0) { + // Previous index is the last one of this polygon, rewind to first index. + while (index_ix > 0 && (int32_t)index_data[index_ix - 1] >= 0) { + index_ix--; + } + } else { + // Connect to the next index in the same polygon + index_ix++; + } + ufbxi_check(index_ix < mesh->num_indices); + edges[dst_ix].b = index_ix; + dst_ix++; + } + + mesh->edges.data = edges; + mesh->edges.count = dst_ix; + mesh->num_edges = mesh->edges.count; + } + + ufbxi_check(ufbxi_process_indices(uc, mesh, index_data)); + + // Count the number of UV/color sets + size_t num_uv = 0, num_color = 0, num_bitangents = 0, num_tangents = 0; + ufbxi_for (ufbxi_node, n, node->children, node->num_children) { + if (n->name == ufbxi_LayerElementUV) num_uv++; + if (n->name == ufbxi_LayerElementColor) num_color++; + if (n->name == ufbxi_LayerElementBinormal) num_bitangents++; + if (n->name == ufbxi_LayerElementTangent) num_tangents++; + } + + size_t num_textures = 0; + + ufbxi_tangent_layer *bitangents = ufbxi_push_zero(&uc->tmp_stack, ufbxi_tangent_layer, num_bitangents); + ufbxi_tangent_layer *tangents = ufbxi_push_zero(&uc->tmp_stack, ufbxi_tangent_layer, num_tangents); + ufbxi_check(bitangents); + ufbxi_check(tangents); + + mesh->uv_sets.data = ufbxi_push_zero(&uc->result, ufbx_uv_set, num_uv); + mesh->color_sets.data = ufbxi_push_zero(&uc->result, ufbx_color_set, num_color); + ufbxi_check(mesh->uv_sets.data); + ufbxi_check(mesh->color_sets.data); + + size_t num_bitangents_read = 0, num_tangents_read = 0; + ufbxi_for (ufbxi_node, n, node->children, node->num_children) { + if (n->name[0] != 'L') continue; // All names start with 'LayerElement*' + + if (n->name == ufbxi_LayerElementNormal) { + if (mesh->vertex_normal.exists) continue; + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_normal, + ufbxi_Normals, ufbxi_NormalsIndex, ufbxi_NormalsW, 'r', 3)); + } else if (n->name == ufbxi_LayerElementBinormal) { + ufbxi_tangent_layer *layer = &bitangents[num_bitangents_read++]; + + ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index)); + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem, + ufbxi_Binormals, ufbxi_BinormalsIndex, ufbxi_BinormalsW, 'r', 3)); + if (!layer->elem.exists) num_bitangents_read--; + + } else if (n->name == ufbxi_LayerElementTangent) { + ufbxi_tangent_layer *layer = &tangents[num_tangents_read++]; + + ufbxi_ignore(ufbxi_get_val1(n, "I", &layer->index)); + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&layer->elem, + ufbxi_Tangents, ufbxi_TangentsIndex, ufbxi_TangentsW, 'r', 3)); + if (!layer->elem.exists) num_tangents_read--; + + } else if (n->name == ufbxi_LayerElementUV) { + ufbx_uv_set *set = &mesh->uv_sets.data[mesh->uv_sets.count++]; + + ufbxi_ignore(ufbxi_get_val1(n, "I", &set->index)); + if (!ufbxi_find_val1(n, ufbxi_Name, "S", &set->name)) { + set->name = ufbx_empty_string; + } + + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_uv, + ufbxi_UV, ufbxi_UVIndex, NULL, 'r', 2)); + if (!set->vertex_uv.exists) mesh->uv_sets.count--; + + } else if (n->name == ufbxi_LayerElementColor) { + ufbx_color_set *set = &mesh->color_sets.data[mesh->color_sets.count++]; + + ufbxi_ignore(ufbxi_get_val1(n, "I", &set->index)); + if (!ufbxi_find_val1(n, ufbxi_Name, "S", &set->name)) { + set->name = ufbx_empty_string; + } + + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&set->vertex_color, + ufbxi_Colors, ufbxi_ColorIndex, NULL, 'r', 4)); + if (!set->vertex_color.exists) mesh->color_sets.count--; + + } else if (n->name == ufbxi_LayerElementVertexCrease) { + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, n, (ufbx_vertex_attrib*)&mesh->vertex_crease, + ufbxi_VertexCrease, ufbxi_VertexCreaseIndex, NULL, 'r', 1)); + } else if (n->name == ufbxi_LayerElementEdgeCrease) { + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByEdge) { + if (mesh->edge_crease.count) continue; + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_crease.data, &mesh->edge_crease.count, n, ufbxi_EdgeCrease, 'r', mesh->num_edges)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_EdgeCrease, mapping)); + } + } else if (n->name == ufbxi_LayerElementSmoothing) { + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByEdge) { + if (mesh->edge_smoothing.count) continue; + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_smoothing.data, &mesh->edge_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_edges)); + } else if (mapping == ufbxi_ByPolygon) { + if (mesh->face_smoothing.count) continue; + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_smoothing.data, &mesh->face_smoothing.count, n, ufbxi_Smoothing, 'b', mesh->num_faces)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Smoothing, mapping)); + } + } else if (n->name == ufbxi_LayerElementVisibility) { + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByEdge) { + if (mesh->edge_visibility.count) continue; + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->edge_visibility.data, &mesh->edge_visibility.count, n, ufbxi_Visibility, 'b', mesh->num_edges)); + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Visibility, mapping)); + } + } else if (n->name == ufbxi_LayerElementMaterial) { + if (mesh->face_material.count) continue; + const char *mapping = ""; + ufbxi_ignore(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByPolygon) { + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_material.data, &mesh->face_material.count, n, ufbxi_Materials, 'i', mesh->num_faces)); + } else if (mapping == ufbxi_AllSame) { + ufbxi_value_array *arr = ufbxi_find_array(n, ufbxi_Materials, 'i'); + ufbxi_check(arr && arr->size >= 1); + uint32_t material = *(uint32_t*)arr->data; + mesh->face_material.count = mesh->num_faces; + if (material == 0) { + mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero; + } else { + mesh->face_material.data = ufbxi_push(&uc->result, uint32_t, mesh->num_faces); + ufbxi_check(mesh->face_material.data); + ufbxi_for_list(uint32_t, p_mat, mesh->face_material) { + *p_mat = material; + } + } + } else { + ufbxi_check(ufbxi_warn_polygon_mapping(uc, ufbxi_Materials, mapping)); + } + } else if (n->name == ufbxi_LayerElementPolygonGroup) { + if (mesh->face_group.count) continue; + const char *mapping = NULL; + ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByPolygon) { + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_group.data, &mesh->face_group.count, n, ufbxi_PolygonGroup, 'i', mesh->num_faces)); + } + } else if (n->name == ufbxi_LayerElementHole) { + if (mesh->face_group.count) continue; + const char *mapping = NULL; + ufbxi_check(ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)); + if (mapping == ufbxi_ByPolygon) { + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_hole.data, &mesh->face_hole.count, n, ufbxi_Hole, 'b', mesh->num_faces)); + } + } else if (!strncmp(n->name, "LayerElement", 12)) { + + // Make sure the name has no internal zero bytes + ufbxi_check(!memchr(n->name, '\0', n->name_len)); + + // What?! 6x00 stores textures in mesh geometry, eg. "LayerElementTexture", + // "LayerElementDiffuseFactorTextures", "LayerElementEmissive_Textures"... + ufbx_string prop_name = ufbx_empty_string; + if (n->name_len > 20 && !strcmp(n->name + n->name_len - 8, "Textures")) { + prop_name.data = n->name + 12; + prop_name.length = (size_t)n->name_len - 20; + if (prop_name.data[prop_name.length - 1] == '_') { + prop_name.length -= 1; + } + } else if (!strcmp(n->name, "LayerElementTexture")) { + prop_name.data = "Diffuse"; + prop_name.length = 7; + } + + if (prop_name.length > 0) { + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop_name, false)); + const char *mapping = NULL; + if (ufbxi_find_val1(n, ufbxi_MappingInformationType, "c", (char**)&mapping)) { + ufbxi_value_array *arr = ufbxi_find_array(n, ufbxi_TextureId, 'i'); + + ufbxi_tmp_mesh_texture *tex = ufbxi_push_zero(&uc->tmp_mesh_textures, ufbxi_tmp_mesh_texture, 1); + ufbxi_check(tex); + if (arr) { + tex->face_texture = (uint32_t*)arr->data; + tex->num_faces = arr->size; + } + tex->prop_name = prop_name; + tex->all_same = (mapping == ufbxi_AllSame); + num_textures++; + } + } + } + } + + // Always use a default zero material, this will be removed if no materials are found + if (!mesh->face_material.count) { + uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, mesh->num_faces); + mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero; + mesh->face_material.count = mesh->num_faces; + } + + if (uc->opts.strict) { + ufbxi_check(mesh->uv_sets.count == num_uv); + ufbxi_check(mesh->color_sets.count == num_color); + ufbxi_check(num_bitangents_read == num_bitangents); + ufbxi_check(num_tangents_read == num_tangents); + } + + // Connect bitangents/tangents to UV sets + ufbxi_for (ufbxi_node, n, node->children, node->num_children) { + if (n->name != ufbxi_Layer) continue; + ufbx_uv_set *uv_set = NULL; + ufbxi_tangent_layer *bitangent_layer = NULL; + ufbxi_tangent_layer *tangent_layer = NULL; + + ufbxi_for (ufbxi_node, c, n->children, n->num_children) { + uint32_t index; + const char *type; + if (c->name != ufbxi_LayerElement) continue; + if (!ufbxi_find_val1(c, ufbxi_TypedIndex, "I", &index)) continue; + if (!ufbxi_find_val1(c, ufbxi_Type, "C", (char**)&type)) continue; + + if (type == ufbxi_LayerElementUV) { + ufbxi_for(ufbx_uv_set, set, mesh->uv_sets.data, mesh->uv_sets.count) { + if (set->index == index) { + uv_set = set; + break; + } + } + } else if (type == ufbxi_LayerElementBinormal) { + ufbxi_for(ufbxi_tangent_layer, layer, bitangents, num_bitangents_read) { + if (layer->index == index) { + bitangent_layer = layer; + break; + } + } + } else if (type == ufbxi_LayerElementTangent) { + ufbxi_for(ufbxi_tangent_layer, layer, tangents, num_tangents_read) { + if (layer->index == index) { + tangent_layer = layer; + break; + } + } + } + } + + if (uv_set) { + if (bitangent_layer) { + uv_set->vertex_bitangent = bitangent_layer->elem; + } + if (tangent_layer) { + uv_set->vertex_tangent = tangent_layer->elem; + } + } + } + + mesh->skinned_is_local = true; + mesh->skinned_position = mesh->vertex_position; + mesh->skinned_normal = mesh->vertex_normal; + + ufbxi_patch_mesh_reals(mesh); + + if (mesh->face_group.count > 0 && mesh->face_groups.count == 0) { + ufbxi_check(ufbxi_assign_face_groups(&uc->result, &uc->error, mesh, &uc->max_consecutive_indices, uc->retain_mesh_parts)); + } + + // Sort UV and color sets by set index + ufbxi_check(ufbxi_sort_uv_sets(uc, mesh->uv_sets.data, mesh->uv_sets.count)); + ufbxi_check(ufbxi_sort_color_sets(uc, mesh->color_sets.data, mesh->color_sets.count)); + + if (num_textures > 0) { + ufbxi_mesh_extra *extra = ufbxi_push_element_extra(uc, mesh->element.element_id, ufbxi_mesh_extra); + ufbxi_check(extra); + extra->texture_count = num_textures; + extra->texture_arr = ufbxi_push_pop(&uc->tmp, &uc->tmp_mesh_textures, ufbxi_tmp_mesh_texture, num_textures); + ufbxi_check(extra->texture_arr); + } + + // Subdivision + + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_PreviewDivisionLevels, "I", &mesh->subdivision_preview_levels)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RenderDivisionLevels, "I", &mesh->subdivision_render_levels)); + + int32_t smoothness, boundary; + if (ufbxi_find_val1(node, ufbxi_Smoothness, "I", &smoothness)) { + if (smoothness >= 0 && smoothness <= UFBX_SUBDIVISION_DISPLAY_SMOOTH) { + mesh->subdivision_display_mode = (ufbx_subdivision_display_mode)smoothness; + } + } + if (ufbxi_find_val1(node, ufbxi_BoundaryRule, "I", &boundary)) { + if (boundary >= 0 && boundary <= UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS - 1) { + mesh->subdivision_boundary = (ufbx_subdivision_boundary)(boundary + 1); + } + } + + return 1; +} + +ufbxi_noinline static ufbx_nurbs_topology ufbxi_read_nurbs_topology(const char *form) +{ + if (!strcmp(form, "Open")) { + return UFBX_NURBS_TOPOLOGY_OPEN; + } else if (!strcmp(form, "Closed")) { + return UFBX_NURBS_TOPOLOGY_CLOSED; + } else if (!strcmp(form, "Periodic")) { + return UFBX_NURBS_TOPOLOGY_PERIODIC; + } + return UFBX_NURBS_TOPOLOGY_OPEN; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_nurbs_curve *nurbs = ufbxi_push_element(uc, info, ufbx_nurbs_curve, UFBX_ELEMENT_NURBS_CURVE); + ufbxi_check(nurbs); + + int32_t dimension = 3; + + const char *form = NULL; + ufbxi_check(ufbxi_find_val1(node, ufbxi_Order, "I", &nurbs->basis.order)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Dimension, "I", &dimension)); + ufbxi_check(ufbxi_find_val1(node, ufbxi_Form, "C", (char**)&form)); + nurbs->basis.topology = ufbxi_read_nurbs_topology(form); + nurbs->basis.is_2d = dimension == 2; + + if (!uc->opts.ignore_geometry) { + ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r'); + ufbxi_value_array *knot = ufbxi_find_array(node, ufbxi_KnotVector, 'r'); + ufbxi_check(points); + ufbxi_check(knot); + ufbxi_check(points->size % 4 == 0); + + nurbs->control_points.count = points->size / 4; + nurbs->control_points.data = (ufbx_vec4*)points->data; + nurbs->basis.knot_vector.data = (ufbx_real*)knot->data; + nurbs->basis.knot_vector.count = knot->size; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_nurbs_surface(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_nurbs_surface *nurbs = ufbxi_push_element(uc, info, ufbx_nurbs_surface, UFBX_ELEMENT_NURBS_SURFACE); + ufbxi_check(nurbs); + + const char *form_u = NULL, *form_v = NULL; + size_t dimension_u = 0, dimension_v = 0; + int32_t step_u = 0, step_v = 0; + ufbxi_check(ufbxi_find_val2(node, ufbxi_NurbsSurfaceOrder, "II", &nurbs->basis_u.order, &nurbs->basis_v.order)); + ufbxi_check(ufbxi_find_val2(node, ufbxi_Dimensions, "ZZ", &dimension_u, &dimension_v)); + ufbxi_check(ufbxi_find_val2(node, ufbxi_Step, "II", &step_u, &step_v)); + ufbxi_check(ufbxi_find_val2(node, ufbxi_Form, "CC", (char**)&form_u, (char**)&form_v)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FlipNormals, "B", &nurbs->flip_normals)); + nurbs->basis_u.topology = ufbxi_read_nurbs_topology(form_u); + nurbs->basis_v.topology = ufbxi_read_nurbs_topology(form_v); + nurbs->num_control_points_u = dimension_u; + nurbs->num_control_points_v = dimension_v; + nurbs->span_subdivision_u = step_u > 0 ? (uint32_t)step_u : 4u; + nurbs->span_subdivision_v = step_v > 0 ? (uint32_t)step_v : 4u; + + if (!uc->opts.ignore_geometry) { + ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r'); + ufbxi_value_array *knot_u = ufbxi_find_array(node, ufbxi_KnotVectorU, 'r'); + ufbxi_value_array *knot_v = ufbxi_find_array(node, ufbxi_KnotVectorV, 'r'); + ufbxi_check(points); + ufbxi_check(knot_u); + ufbxi_check(knot_v); + ufbxi_check(points->size % 4 == 0); + ufbxi_check(points->size / 4 == (size_t)dimension_u * (size_t)dimension_v); + + nurbs->control_points.count = points->size / 4; + nurbs->control_points.data = (ufbx_vec4*)points->data; + nurbs->basis_u.knot_vector.data = (ufbx_real*)knot_u->data; + nurbs->basis_u.knot_vector.count = knot_u->size; + nurbs->basis_v.knot_vector.data = (ufbx_real*)knot_v->data; + nurbs->basis_v.knot_vector.count = knot_v->size; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_line(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_line_curve *line = ufbxi_push_element(uc, info, ufbx_line_curve, UFBX_ELEMENT_LINE_CURVE); + ufbxi_check(line); + + if (!uc->opts.ignore_geometry) { + ufbxi_value_array *points = ufbxi_find_array(node, ufbxi_Points, 'r'); + ufbxi_value_array *points_index = ufbxi_find_array(node, ufbxi_PointsIndex, 'i'); + ufbxi_check(points); + ufbxi_check(points_index); + ufbxi_check(points->size % 3 == 0); + + if (points->size > 0) { + line->control_points.count = points->size / 3; + line->control_points.data = (ufbx_vec3*)points->data; + line->point_indices.count = points_index->size; + line->point_indices.data = (uint32_t*)points_index->data; + + ufbxi_check(line->control_points.count < INT32_MAX); + + // Count end points + size_t num_segments = 1; + if (line->point_indices.count > 0) { + for (size_t i = 0; i < line->point_indices.count - 1; i++) { + uint32_t ix = line->point_indices.data[i]; + num_segments += (int32_t)ix < 0 ? 1u : 0u; + } + } + + size_t prev_end = 0; + line->segments.data = ufbxi_push(&uc->result, ufbx_line_segment, num_segments); + ufbxi_check(line->segments.data); + for (size_t i = 0; i < line->point_indices.count; i++) { + uint32_t ix = line->point_indices.data[i]; + if ((int32_t)ix < 0) { + ix = ~ix; + if (i + 1 < line->point_indices.count) { + ufbx_line_segment *segment = &line->segments.data[line->segments.count++]; + segment->index_begin = (uint32_t)prev_end; + segment->num_indices = (uint32_t)(i - prev_end); + prev_end = i; + } + } + + if (ix < line->control_points.count) { + line->point_indices.data[i] = ix; + } else { + ufbxi_check(ufbxi_fix_index(uc, &line->point_indices.data[i], ix, line->control_points.count)); + } + } + + ufbx_line_segment *segment = &line->segments.data[line->segments.count++]; + segment->index_begin = (uint32_t)prev_end; + segment->num_indices = (uint32_t)ufbxi_to_size(line->point_indices.count - prev_end); + ufbx_assert(line->segments.count == num_segments); + } + } + + return 1; +} + +ufbxi_noinline static void ufbxi_read_transform_matrix(ufbx_matrix *m, ufbx_real *data) +{ + m->m00 = data[ 0]; m->m10 = data[ 1]; m->m20 = data[ 2]; + m->m01 = data[ 4]; m->m11 = data[ 5]; m->m21 = data[ 6]; + m->m02 = data[ 8]; m->m12 = data[ 9]; m->m22 = data[10]; + m->m03 = data[12]; m->m13 = data[13]; m->m23 = data[14]; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_bone(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type) +{ + (void)node; + + ufbx_bone *bone = ufbxi_push_element(uc, info, ufbx_bone, UFBX_ELEMENT_BONE); + ufbxi_check(bone); + + if (sub_type == ufbxi_Root) { + bone->is_root = true; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_marker(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type, ufbx_marker_type type) +{ + (void)node; + (void)sub_type; + + ufbx_marker *marker = ufbxi_push_element(uc, info, ufbx_marker, UFBX_ELEMENT_MARKER); + ufbxi_check(marker); + + marker->type = type; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_skin_deformer *skin = ufbxi_push_element(uc, info, ufbx_skin_deformer, UFBX_ELEMENT_SKIN_DEFORMER); + ufbxi_check(skin); + + const char *skinning_type = NULL; + if (ufbxi_find_val1(node, ufbxi_SkinningType, "C", (char**)&skinning_type)) { + if (!strcmp(skinning_type, "Rigid")) { + skin->skinning_method = UFBX_SKINNING_METHOD_RIGID; + } else if (!strcmp(skinning_type, "Linear")) { + skin->skinning_method = UFBX_SKINNING_METHOD_LINEAR; + } else if (!strcmp(skinning_type, "DualQuaternion")) { + skin->skinning_method = UFBX_SKINNING_METHOD_DUAL_QUATERNION; + } else if (!strcmp(skinning_type, "Blend")) { + skin->skinning_method = UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR; + } + } + + ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i'); + ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_BlendWeights, 'r'); + if (indices && weights) { + // TODO strict: ufbxi_check(indices->size == weights->size); + skin->num_dq_weights = ufbxi_min_sz(indices->size, weights->size); + skin->dq_vertices.data = (uint32_t*)indices->data; + skin->dq_weights.data = (ufbx_real*)weights->data; + skin->dq_vertices.count = skin->num_dq_weights; + skin->dq_weights.count = skin->num_dq_weights; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_skin_cluster(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_skin_cluster *cluster = ufbxi_push_element(uc, info, ufbx_skin_cluster, UFBX_ELEMENT_SKIN_CLUSTER); + ufbxi_check(cluster); + + ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i'); + ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_Weights, 'r'); + + if (indices && weights) { + ufbxi_check(indices->size == weights->size); + cluster->num_weights = indices->size; + cluster->vertices.data = (uint32_t*)indices->data; + cluster->weights.data = (ufbx_real*)weights->data; + cluster->vertices.count = cluster->num_weights; + cluster->weights.count = cluster->num_weights; + } + + ufbxi_value_array *transform = ufbxi_find_array(node, ufbxi_Transform, 'r'); + ufbxi_value_array *transform_link = ufbxi_find_array(node, ufbxi_TransformLink, 'r'); + if (transform && transform_link) { + ufbxi_check(transform->size >= 16); + ufbxi_check(transform_link->size >= 16); + + ufbxi_read_transform_matrix(&cluster->mesh_node_to_bone, (ufbx_real*)transform->data); + ufbxi_read_transform_matrix(&cluster->bind_to_world, (ufbx_real*)transform_link->data); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_blend_channel(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_blend_channel *channel = ufbxi_push_element(uc, info, ufbx_blend_channel, UFBX_ELEMENT_BLEND_CHANNEL); + ufbxi_check(channel); + + ufbx_real_list list = { NULL, 0 }; + ufbxi_value_array *full_weights = ufbxi_find_array(node, ufbxi_FullWeights, 'r'); + if (full_weights) { + list.data = (ufbx_real*)full_weights->data; + list.count = full_weights->size; + } + ufbxi_check(ufbxi_push_copy(&uc->tmp_full_weights, ufbx_real_list, 1, &list)); + + // Blender saves blend shapes with DeformPercent as a field, not a property. + // However, the animations are mapped to the DeformPercent property. + ufbxi_node *deform_percent = ufbxi_find_child(node, ufbxi_DeformPercent); + if (channel->props.props.count == 0 && deform_percent) { + size_t num_shape_props = 1; + ufbx_prop *shape_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_shape_props); + ufbxi_check(shape_props); + shape_props[0].name.data = ufbxi_DeformPercent; + shape_props[0].name.length = sizeof(ufbxi_DeformPercent) - 1; + shape_props[0]._internal_key = ufbxi_get_name_key_c(ufbxi_DeformPercent); + shape_props[0].type = UFBX_PROP_NUMBER; + shape_props[0].value_str = ufbx_empty_string; + shape_props[0].value_real = 100.0f; + ufbxi_ignore(ufbxi_get_val1(deform_percent, "R", &shape_props[0].value_real)); + channel->props.props.data = shape_props; + channel->props.props.count = num_shape_props; + } + + return 1; +} + +typedef enum { + UFBXI_KEY_INTERPOLATION_CONSTANT = 0x2, + UFBXI_KEY_INTERPOLATION_LINEAR = 0x4, + UFBXI_KEY_INTERPOLATION_CUBIC = 0x8, + UFBXI_KEY_TANGENT_AUTO = 0x100, + UFBXI_KEY_TANGENT_TCB = 0x200, + UFBXI_KEY_TANGENT_USER = 0x400, + UFBXI_KEY_TANGENT_BROKEN = 0x800, + UFBXI_KEY_CONSTANT_NEXT = 0x100, + UFBXI_KEY_CLAMP = 0x1000, + UFBXI_KEY_TIME_INDEPENDENT = 0x2000, + UFBXI_KEY_CLAMP_PROGRESSIVE = 0x4000, + UFBXI_KEY_WEIGHTED_RIGHT = 0x1000000, + UFBXI_KEY_WEIGHTED_NEXT_LEFT = 0x2000000, + UFBXI_KEY_VELOCITY_RIGHT = 0x10000000, + UFBXI_KEY_VELOCITY_NEXT_LEFT = 0x20000000, +} ufbxi_key_flags; + +static ufbxi_noinline float ufbxi_solve_auto_tangent(ufbxi_context *uc, double prev_time, double time, double next_time, ufbx_real prev_value, ufbx_real value, ufbx_real next_value, float weight_left, float weight_right, float auto_bias, uint32_t flags) +{ + // Clamp tangent to zero if near either left or right key + if (flags & UFBXI_KEY_CLAMP) { + if (ufbx_fmin(ufbx_fabs(prev_value - value), ufbx_fabs(next_value - value)) <= uc->opts.key_clamp_threshold) { + return 0.0f; + } + } + + // Time-independent: Set the initial slope to be the difference between the two keyframes. + double slope = (next_value - prev_value) / (next_time - prev_time); + + // Non-time-independent tangents seem to blend between left/right tangent and the total difference. + if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) { + double slope_left = (value - prev_value) / (time - prev_time); + double slope_right = (next_value - value) / (next_time - time); + double delta = (time - prev_time) / (next_time - prev_time); + slope = slope * 0.5 + (slope_left * (1.0 - delta) + slope_right * delta) * 0.5; + + double bias_weight = ufbx_fabs(auto_bias) / 100.0; + if (bias_weight > 0.0001) { + double bias_target = auto_bias > 0.0 ? slope_right : slope_left; + double bias_delta = bias_target - slope; + slope = slope * (1.0 - bias_weight) + bias_target * bias_weight; + + // Auto bias larger than 500 (positive or negative) adds an absolute + // value to the slope, determined by `((bias-500) / 100)^2 * 40`. + double abs_bias_weight = bias_weight - 5.0; + if (abs_bias_weight > 0.0) { + double bias_sign = ufbx_fabs(bias_delta) > 0.00001 ? bias_delta : auto_bias; + bias_sign = bias_sign > 0.0 ? 1.0 : -1.0; + slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0; + } + } + } + + // Prevent overshooting by clamping the slope in case either + // tangent goes above/below the endpoints. + if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) { + // Split the slope to sign and a non-negative absolute value + double slope_sign = slope >= 0.0 ? 1.0 : -1.0; + double abs_slope = slope_sign * slope; + + // Find limits for the absolute value of the slope + double range_left = weight_left * (time - prev_time); + double range_right = weight_right * (next_time - time); + double max_left = range_left > 0.0 ? slope_sign * (value - prev_value) / range_left : 0.0; + double max_right = range_right > 0.0 ? slope_sign * (next_value - value) / range_right : 0.0; + + // Clamp negative values and NaNs to zero + if (!(max_left > 0.0)) max_left = 0.0; + if (!(max_right > 0.0)) max_right = 0.0; + + // Clamp the absolute slope from both sides + if (abs_slope > max_left) abs_slope = max_left; + if (abs_slope > max_right) abs_slope = max_right; + + slope = (slope_sign * abs_slope); + } + + return (float)slope; +} + +static float ufbxi_solve_auto_tangent_left(ufbxi_context *uc, double prev_time, double time, ufbx_real prev_value, ufbx_real value, float weight_left, float auto_bias, uint32_t flags) +{ + (void)weight_left; + if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) return 0.0f; + if (flags & UFBXI_KEY_CLAMP) { + if (ufbx_fabs(prev_value - value) <= uc->opts.key_clamp_threshold) { + return 0.0f; + } + } + + double slope = (value - prev_value) / (time - prev_time); + + if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) { + double abs_bias_weight = ufbx_fabs(auto_bias) / 100.0 - 5.0; + if (abs_bias_weight > 0.0) { + double bias_sign = auto_bias > 0.0 ? 1.0 : -1.0; + slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0; + } + } + + return (float)slope; +} + +static float ufbxi_solve_auto_tangent_right(ufbxi_context *uc, double time, double next_time, ufbx_real value, ufbx_real next_value, float weight_right, float auto_bias, uint32_t flags) +{ + (void)weight_right; + if (flags & UFBXI_KEY_CLAMP_PROGRESSIVE) return 0.0f; + if (flags & UFBXI_KEY_CLAMP) { + if (ufbx_fabs(next_value - value) <= uc->opts.key_clamp_threshold) { + return 0.0f; + } + } + + double slope = (next_value - value) / (next_time - time); + + if ((flags & UFBXI_KEY_TIME_INDEPENDENT) == 0) { + double abs_bias_weight = ufbx_fabs(auto_bias) / 100.0 - 5.0; + if (abs_bias_weight > 0.0) { + double bias_sign = auto_bias > 0.0 ? 1.0 : -1.0; + slope += abs_bias_weight * abs_bias_weight * bias_sign * 40.0; + } + } + + return (float)slope; +} + +static void ufbxi_solve_tcb(float *p_slope_left, float *p_slope_right, double tension, double continuity, double bias, double slope_left, double slope_right, bool edge) +{ + double factor = edge ? 1.0 : 0.5; + double d00 = factor * (1.0 - tension) * (1.0 + bias) * (1.0 - continuity); + double d01 = factor * (1.0 - tension) * (1.0 - bias) * (1.0 + continuity); + double d10 = factor * (1.0 - tension) * (1.0 + bias) * (1.0 + continuity); + double d11 = factor * (1.0 - tension) * (1.0 - bias) * (1.0 - continuity); + + *p_slope_left = (float)(d00 * slope_left + d01 * slope_right); + *p_slope_right = (float)(d10 * slope_left + d11 * slope_right); +} + +ufbxi_noinline static void ufbxi_read_extrapolation(ufbx_extrapolation *p_extrapolation, ufbxi_node *node, const char *name) +{ + ufbxi_node *child = ufbxi_find_child(node, name); + ufbx_extrapolation_mode mode = UFBX_EXTRAPOLATION_CONSTANT; + int32_t repeat_count = -1; + + if (child) { + int32_t mode_ch; + if (ufbxi_find_val1(child, ufbxi_Type, "I", &mode_ch)) { + + switch (mode_ch) { + case 'A': mode = UFBX_EXTRAPOLATION_REPEAT_RELATIVE; break; + case 'C': mode = UFBX_EXTRAPOLATION_CONSTANT; break; + case 'K': mode = UFBX_EXTRAPOLATION_SLOPE; break; + case 'M': mode = UFBX_EXTRAPOLATION_MIRROR; break; + case 'R': mode = UFBX_EXTRAPOLATION_REPEAT; break; + default: /* Unknown */ break; + } + if (ufbxi_find_val1(child, ufbxi_Repetition, "I", &repeat_count)) { + if (repeat_count < 0) { + repeat_count = -1; + } + } + } + } + + p_extrapolation->mode = mode; + p_extrapolation->repeat_count = repeat_count; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_animation_curve(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_anim_curve *curve = ufbxi_push_element(uc, info, ufbx_anim_curve, UFBX_ELEMENT_ANIM_CURVE); + ufbxi_check(curve); + + ufbxi_read_extrapolation(&curve->pre_extrapolation, node, ufbxi_Pre_Extrapolation); + ufbxi_read_extrapolation(&curve->post_extrapolation, node, ufbxi_Post_Extrapolation); + + if (uc->opts.ignore_animation) return 1; + + ufbxi_value_array *times, *values, *attr_flags, *attrs, *refs; + ufbxi_check(times = ufbxi_find_array(node, ufbxi_KeyTime, 'l')); + ufbxi_check(values = ufbxi_find_array(node, ufbxi_KeyValueFloat, 'r')); + ufbxi_check(attr_flags = ufbxi_find_array(node, ufbxi_KeyAttrFlags, 'i')); + ufbxi_check(attrs = ufbxi_find_array(node, ufbxi_KeyAttrDataFloat, '?')); + ufbxi_check(refs = ufbxi_find_array(node, ufbxi_KeyAttrRefCount, 'i')); + + // Time and value arrays that define the keyframes should be parallel + ufbxi_check(times->size == values->size); + + // Flags and attributes are run-length encoded where KeyAttrRefCount (refs) + // is an array that describes how many times to repeat a given flag/attribute. + // Attributes consist of 4 32-bit floating point values per key. + ufbxi_check(attr_flags->size == refs->size); + ufbxi_check(attrs->size == refs->size * 4u); + + size_t num_keys = times->size; + ufbx_keyframe *keys = ufbxi_push(&uc->result, ufbx_keyframe, num_keys); + ufbxi_check(keys); + + curve->keyframes.data = keys; + curve->keyframes.count = num_keys; + + int64_t *p_time = (int64_t*)times->data; + ufbx_real *p_value = (ufbx_real*)values->data; + int32_t *p_flag = (int32_t*)attr_flags->data; + float *p_attr = (float*)attrs->data; + int32_t *p_ref = (int32_t*)refs->data, *p_ref_end = p_ref + refs->size; + + // The previous key defines the weight/slope of the left tangent + float slope_left = 0.0f; + float weight_left = 0.333333f; + // float velocity_left = 0.0f; + + double prev_time = 0.0; + double next_time = 0.0; + + int32_t refs_left = 0; + if (num_keys > 0) { + next_time = (double)p_time[0] / uc->ktime_sec_double; + if (p_ref < p_ref_end) refs_left = *p_ref; + } + + for (size_t i = 0; i < num_keys; i++) { + ufbx_keyframe *key = &keys[i]; + ufbxi_check(refs_left > 0); + + ufbx_real value = *p_value; + if (i == 0) { + curve->min_value = value; + curve->max_value = value; + } else { + curve->min_value = ufbxi_min_real(curve->min_value, value); + curve->max_value = ufbxi_max_real(curve->max_value, value); + } + + key->time = next_time; + key->value = value; + + if (i + 1 < num_keys) { + next_time = (double)p_time[1] / uc->ktime_sec_double; + } + + uint32_t flags = (uint32_t)*p_flag; + + float slope_right = p_attr[0]; + float weight_right = 0.333333f; + //float velocity_right = 0.0f; + float next_slope_left = p_attr[1]; + float next_weight_left = 0.333333f; + // float next_velocity_left = 0.0f; + + if ((flags & (UFBXI_KEY_WEIGHTED_RIGHT|UFBXI_KEY_WEIGHTED_NEXT_LEFT)) != 0) { + // At least one of the tangents is weighted. The weights are encoded as + // two 0.4 _decimal_ fixed point values that are packed into 32 bits and + // interpreted as a 32-bit float. + uint32_t packed_weights; + memcpy(&packed_weights, &p_attr[2], sizeof(uint32_t)); + + if (flags & UFBXI_KEY_WEIGHTED_RIGHT) { + // Right tangent is weighted + weight_right = (float)(packed_weights & 0xffff) * 0.0001f; + } + + if (flags & UFBXI_KEY_WEIGHTED_NEXT_LEFT) { + // Next left tangent is weighted + next_weight_left = (float)(packed_weights >> 16) * 0.0001f; + } + } +#if 0 + if ((flags & (UFBXI_KEY_VELOCITY_RIGHT|UFBXI_KEY_VELOCITY_NEXT_LEFT)) != 0) { + // Velocities are encoded in the same way as weights, see above. + uint32_t packed_velocities; + memcpy(&packed_velocities, &p_attr[3], sizeof(uint32_t)); + + if (flags & UFBXI_KEY_VELOCITY_RIGHT) { + // Right tangent has velocity + velocity_right = (float)(int16_t)(packed_velocities & 0xffff) * 0.0001f; + } + + if (flags & UFBXI_KEY_VELOCITY_NEXT_LEFT) { + // Next left tangent has velocity + next_velocity_left = (float)(int16_t)(packed_velocities >> 16) * 0.0001f; + } + } +#endif + + if (flags & UFBXI_KEY_INTERPOLATION_CONSTANT) { + // Constant interpolation: Set cubic tangents to flat. + + if (flags & UFBXI_KEY_CONSTANT_NEXT) { + // Take constant value from next key + key->interpolation = UFBX_INTERPOLATION_CONSTANT_NEXT; + + } else { + // Take constant value from the previous key + key->interpolation = UFBX_INTERPOLATION_CONSTANT_PREV; + } + + weight_right = next_weight_left = 0.333333f; + slope_right = next_slope_left = 0.0f; + + } else if (flags & UFBXI_KEY_INTERPOLATION_CUBIC) { + // Cubic interpolation + key->interpolation = UFBX_INTERPOLATION_CUBIC; + + if (flags & UFBXI_KEY_TANGENT_TCB) { + double tcb_slope_left = 0.0; + double tcb_slope_right = 0.0; + bool tcb_edge = false; + if (i > 0 && key->time > prev_time) { + tcb_slope_left = (key->value - p_value[-1]) / (key->time - prev_time); + } else { + tcb_edge = true; + } + if (i + 1 < num_keys && next_time > key->time) { + tcb_slope_right = (p_value[1] - key->value) / (next_time - key->time); + } else { + tcb_edge = true; + } + + ufbxi_solve_tcb(&slope_left, &slope_right, p_attr[0], p_attr[1], p_attr[2], tcb_slope_left, tcb_slope_right, tcb_edge); + + // TODO: How to handle these? + next_slope_left = 0.0f; + next_weight_left = 0.333333f; + // next_velocity_left = 0.0f; + } else if (flags & UFBXI_KEY_TANGENT_USER) { + // User tangents + + if (flags & UFBXI_KEY_TANGENT_BROKEN) { + // Broken tangents: No need to modify slopes + } else { + // Unified tangents: Use right slope for both sides + // TODO: ??? slope_left = slope_right; + } + + } else { + // TODO: Auto break (0x800) + + if (i > 0 && i + 1 < num_keys && key->time > prev_time && next_time > key->time) { + if (ufbx_fabs(slope_left + slope_right) <= 0.0001f) { + slope_left = slope_right = ufbxi_solve_auto_tangent(uc, + prev_time, key->time, next_time, + p_value[-1], key->value, p_value[1], + weight_left, weight_right, slope_right, flags); + } else { + slope_left = ufbxi_solve_auto_tangent(uc, + prev_time, key->time, next_time, + p_value[-1], key->value, p_value[1], + weight_left, weight_right, -slope_left, flags); + slope_right = ufbxi_solve_auto_tangent(uc, + prev_time, key->time, next_time, + p_value[-1], key->value, p_value[1], + weight_left, weight_right, slope_right, flags); + } + } else if (i > 0 && key->time > prev_time) { + slope_left = slope_right = ufbxi_solve_auto_tangent_left(uc, + prev_time, key->time, + p_value[-1], key->value, + weight_left, -slope_left, flags); + } else if (i + 1 < num_keys && next_time > key->time) { + slope_left = slope_right = ufbxi_solve_auto_tangent_right(uc, + key->time, next_time, + key->value, p_value[1], + weight_right, slope_right, flags); + } else { + // Only / invalid keyframe: Set both slopes to zero + slope_left = slope_right = 0.0f; + } + + + // ??? Looks like at least MotionBuilder adjusts weight and auto bias to + // implement velocity and the velocity information in the file is purely + // for UI (?) If auto bias is not accounted for the velocity computation + // below results in the correct tangents, but with auto bias the velocity + // seems to be accounted for twice resulting in incorrect values... +#if 0 + if (weight_left >= UFBX_EPSILON) { + slope_left *= (float)(1.0 - ufbx_fmin(velocity_left / weight_left, 1.0)); + } + if (weight_right >= UFBX_EPSILON) { + slope_right *= (float)(1.0 - ufbx_fmin(velocity_right / weight_right, 1.0)); + } +#endif + } + + } else { + // Linear or unknown interpolation: Set cubic tangents to match + // the linear interpolation with weights of 1/3. + key->interpolation = UFBX_INTERPOLATION_LINEAR; + + weight_right = 0.333333f; + next_weight_left = 0.333333f; + + if (next_time > key->time) { + double delta_time = next_time - key->time; + if (delta_time > 0.0) { + double slope = (p_value[1] - key->value) / delta_time; + slope_right = next_slope_left = (float)slope; + } else { + slope_right = next_slope_left = 0.0f; + } + } else { + slope_right = next_slope_left = 0.0f; + } + } + + // Set the tangents based on weights (dx relative to the time difference + // between the previous/next key) and slope (simply d = slope * dx) + if (key->time > prev_time) { + double delta = key->time - prev_time; + key->left.dx = (float)(weight_left * delta); + key->left.dy = key->left.dx * slope_left; + } else { + key->left.dx = 0.0f; + key->left.dy = 0.0f; + } + + if (next_time > key->time) { + double delta = next_time - key->time; + key->right.dx = (float)(weight_right * delta); + key->right.dy = key->right.dx * slope_right; + } else { + key->right.dx = 0.0f; + key->right.dy = 0.0f; + } + + slope_left = next_slope_left; + weight_left = next_weight_left; + // velocity_left = next_velocity_left; + prev_time = key->time; + + // Decrement attribute refcount and potentially move to the next one. + if (--refs_left == 0) { + p_flag++; + p_attr += 4; + p_ref++; + if (p_ref < p_ref_end) refs_left = *p_ref; + } + p_time++; + p_value++; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_material(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_material *material = ufbxi_push_element(uc, info, ufbx_material, UFBX_ELEMENT_MATERIAL); + ufbxi_check(material); + + if (!ufbxi_find_val1(node, ufbxi_ShadingModel, "S", &material->shading_model_name)) { + material->shading_model_name = ufbx_empty_string; + } + + material->shader_prop_prefix = ufbx_empty_string; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_texture *texture = ufbxi_push_element(uc, info, ufbx_texture, UFBX_ELEMENT_TEXTURE); + ufbxi_check(texture); + + texture->type = UFBX_TEXTURE_FILE; + + texture->filename = ufbx_empty_string; + texture->absolute_filename = ufbx_empty_string; + texture->relative_filename = ufbx_empty_string; + + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "S", &texture->absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "S", &texture->absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "S", &texture->relative_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "S", &texture->relative_filename)); + + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "b", &texture->raw_absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "b", &texture->raw_absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "b", &texture->raw_relative_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "b", &texture->raw_relative_filename)); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_layered_texture(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_texture *texture = ufbxi_push_element(uc, info, ufbx_texture, UFBX_ELEMENT_TEXTURE); + ufbxi_check(texture); + + texture->type = UFBX_TEXTURE_LAYERED; + + texture->filename = ufbx_empty_string; + texture->absolute_filename = ufbx_empty_string; + texture->relative_filename = ufbx_empty_string; + + ufbxi_texture_extra *extra = ufbxi_push_element_extra(uc, texture->element.element_id, ufbxi_texture_extra); + ufbxi_check(extra); + + ufbxi_value_array *alphas = ufbxi_find_array(node, ufbxi_Alphas, 'r'); + if (alphas) { + extra->alphas = (ufbx_real*)alphas->data; + extra->num_alphas = alphas->size; + } + + ufbxi_value_array *blend_modes = ufbxi_find_array(node, ufbxi_BlendModes, 'i'); + if (blend_modes) { + extra->blend_modes = (int32_t*)blend_modes->data; + extra->num_blend_modes = blend_modes->size; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_video(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_video *video = ufbxi_push_element(uc, info, ufbx_video, UFBX_ELEMENT_VIDEO); + ufbxi_check(video); + + video->filename = ufbx_empty_string; + video->absolute_filename = ufbx_empty_string; + video->relative_filename = ufbx_empty_string; + + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "S", &video->absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "S", &video->absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "S", &video->relative_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "S", &video->relative_filename)); + + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_FileName, "b", &video->raw_absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Filename, "b", &video->raw_absolute_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFileName, "b", &video->raw_relative_filename)); + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_RelativeFilename, "b", &video->raw_relative_filename)); + + ufbxi_node *content_node = ufbxi_find_child(node, ufbxi_Content); + ufbxi_check(ufbxi_read_embedded_blob(uc, &video->content, content_node)); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_anim_stack(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + (void)node; + + ufbx_anim_stack *stack = ufbxi_push_element(uc, info, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK); + ufbxi_check(stack); + + uint32_t hash = ufbxi_hash_ptr(info->name.data); + ufbxi_tmp_anim_stack *entry = ufbxi_map_find(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &info->name.data); + if (!entry) { + entry = ufbxi_map_insert(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &info->name.data); + ufbxi_check(entry); + entry->name = info->name.data; + entry->stack = stack; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_pose(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, const char *sub_type) +{ + ufbx_pose *pose = ufbxi_push_element(uc, info, ufbx_pose, UFBX_ELEMENT_POSE); + ufbxi_check(pose); + + // TODO: What are the actual other types? + pose->is_bind_pose = sub_type == ufbxi_BindPose; + + size_t num_bones = 0; + ufbxi_for(ufbxi_node, n, node->children, node->num_children) { + if (n->name != ufbxi_PoseNode) continue; + + // Bones are linked with FBX names/IDs bypassing the connection system (!?) + uint64_t fbx_id = 0; + if (uc->version < 7000) { + char *name = NULL; + if (!ufbxi_find_val1(n, ufbxi_Node, "c", &name)) continue; + fbx_id = ufbxi_synthetic_id_from_string(uc, name); + ufbxi_check(fbx_id); + } else { + if (!ufbxi_find_val1(n, ufbxi_Node, "L", &fbx_id)) continue; + ufbxi_check(ufbxi_validate_fbx_id(uc, &fbx_id)); + } + + ufbxi_value_array *matrix = ufbxi_find_array(n, ufbxi_Matrix, 'r'); + if (!matrix) continue; + ufbxi_check(matrix->size >= 16); + + ufbxi_tmp_bone_pose *tmp_pose = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_bone_pose, 1); + ufbxi_check(tmp_pose); + + num_bones++; + tmp_pose->bone_fbx_id = fbx_id; + ufbxi_read_transform_matrix(&tmp_pose->bone_to_world, (ufbx_real*)matrix->data); + } + + // HACK: Transport `ufbxi_tmp_bone_pose` array through the `ufbx_bone_pose` pointer + pose->bone_poses.count = num_bones; + pose->bone_poses.data = (ufbx_bone_pose*)ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_tmp_bone_pose, num_bones); + ufbxi_check(pose->bone_poses.data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_shader_prop_bindings(ufbxi_context *uc, ufbx_shader_prop_binding *bindings, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_shader_prop_binding))); + ufbxi_macro_stable_sort(ufbx_shader_prop_binding, 32, bindings, uc->tmp_arr, count, + ( ufbxi_str_less(a->shader_prop, b->shader_prop) ) ); + return 1; +} + + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_binding_table(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_shader_binding *bindings = ufbxi_push_element(uc, info, ufbx_shader_binding, UFBX_ELEMENT_SHADER_BINDING); + ufbxi_check(bindings); + + size_t num_entries = 0; + ufbxi_for (ufbxi_node, n, node->children, node->num_children) { + if (n->name != ufbxi_Entry) continue; + + ufbx_string src, dst; + const char *src_type = NULL, *dst_type = NULL; + if (!ufbxi_get_val4(n, "SCSC", &src, (char**)&src_type, &dst, (char**)&dst_type)) { + continue; + } + + if (src_type == ufbxi_FbxPropertyEntry && dst_type == ufbxi_FbxSemanticEntry) { + ufbx_shader_prop_binding *bind = ufbxi_push(&uc->tmp_stack, ufbx_shader_prop_binding, 1); + ufbxi_check(bind); + bind->material_prop = src; + bind->shader_prop = dst; + num_entries++; + } else if (src_type == ufbxi_FbxSemanticEntry && dst_type == ufbxi_FbxPropertyEntry) { + ufbx_shader_prop_binding *bind = ufbxi_push(&uc->tmp_stack, ufbx_shader_prop_binding, 1); + ufbxi_check(bind); + bind->material_prop = dst; + bind->shader_prop = src; + num_entries++; + } + } + + bindings->prop_bindings.count = num_entries; + bindings->prop_bindings.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_shader_prop_binding, num_entries); + ufbxi_check(bindings->prop_bindings.data); + + ufbxi_check(ufbxi_sort_shader_prop_bindings(uc, bindings->prop_bindings.data, bindings->prop_bindings.count)); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_set(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + (void)node; + + ufbx_selection_set *set = ufbxi_push_element(uc, info, ufbx_selection_set, UFBX_ELEMENT_SELECTION_SET); + ufbxi_check(set); + + return 1; +} + +ufbxi_noinline static void ufbxi_find_uint32_list(ufbx_uint32_list *dst, ufbxi_node *node, const char *name) +{ + ufbxi_value_array *arr = ufbxi_find_array(node, name, 'i'); + if (arr) { + dst->data = (uint32_t*)arr->data; + dst->count = arr->size; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_selection_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_selection_node *sel = ufbxi_push_element(uc, info, ufbx_selection_node, UFBX_ELEMENT_SELECTION_NODE); + ufbxi_check(sel); + + int32_t in_set = 0; + if (ufbxi_find_val1(node, ufbxi_IsTheNodeInSet, "I", &in_set) && in_set) { + sel->include_node = true; + } + + ufbxi_find_uint32_list(&sel->vertices, node, ufbxi_VertexIndexArray); + ufbxi_find_uint32_list(&sel->edges, node, ufbxi_EdgeIndexArray); + ufbxi_find_uint32_list(&sel->faces, node, ufbxi_PolygonIndexArray); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_character(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + (void)node; + + ufbx_character *character = ufbxi_push_element(uc, info, ufbx_character, UFBX_ELEMENT_CHARACTER); + ufbxi_check(character); + + // TODO: There's some extremely cursed all-caps data in characters + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_audio_clip(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_audio_clip *audio = ufbxi_push_element(uc, info, ufbx_audio_clip, UFBX_ELEMENT_AUDIO_CLIP); + ufbxi_check(audio); + + audio->filename = ufbx_empty_string; + audio->absolute_filename = ufbx_empty_string; + audio->relative_filename = ufbx_empty_string; + + ufbxi_node *content_node = ufbxi_find_child(node, ufbxi_Content); + ufbxi_check(ufbxi_read_embedded_blob(uc, &audio->content, content_node)); + + return 1; +} + +typedef struct { + ufbx_constraint_type type; + const char *name; +} ufbxi_constraint_type; + +static const ufbxi_constraint_type ufbxi_constraint_types[] = { + { UFBX_CONSTRAINT_AIM, "Aim" }, + { UFBX_CONSTRAINT_PARENT, "Parent-Child" }, + { UFBX_CONSTRAINT_POSITION, "Position From Positions" }, + { UFBX_CONSTRAINT_ROTATION, "Rotation From Rotations" }, + { UFBX_CONSTRAINT_SCALE, "Scale From Scales" }, + { UFBX_CONSTRAINT_SINGLE_CHAIN_IK, "Single Chain IK" }, +}; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_constraint(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + (void)node; + + ufbx_constraint *constraint = ufbxi_push_element(uc, info, ufbx_constraint, UFBX_ELEMENT_CONSTRAINT); + ufbxi_check(constraint); + + if (!ufbxi_find_val1(node, ufbxi_Type, "S", &constraint->type_name)) { + constraint->type_name = ufbx_empty_string; + } + + ufbxi_for(const ufbxi_constraint_type, ctype, ufbxi_constraint_types, ufbxi_arraycount(ufbxi_constraint_types)) { + if (!strcmp(constraint->type_name.data, ctype->name)) { + constraint->type = ctype->type; + break; + } + } + + // TODO: There's some extremely cursed all-caps data in characters + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_synthetic_attribute(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info, ufbx_string type_str, const char *sub_type, const char *super_type) +{ + if ((sub_type == ufbxi_empty_char || sub_type == ufbxi_Model) && type_str.data == ufbxi_Model) { + // Plain model + return 1; + } + + ufbxi_element_info attrib_info = *info; + + attrib_info.fbx_id = ufbxi_push_synthetic_id(uc); + + // Use type and name from NodeAttributeName if it exists *uniquely* + ufbx_string type_and_name; + if (ufbxi_find_val1(node, ufbxi_NodeAttributeName, "s", &type_and_name)) { + ufbx_string attrib_type_str, attrib_name_str; + ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &attrib_type_str, &attrib_name_str)); + if (attrib_name_str.length > 0) { + attrib_info.name = attrib_name_str; + uint64_t attrib_id = ufbxi_synthetic_id_from_string(uc, type_and_name.data); + ufbxi_check(attrib_id); + if (info->fbx_id != attrib_id && !ufbxi_fbx_id_exists(uc, attrib_id)) { + attrib_info.fbx_id = attrib_id; + } + } + } + + // 6x00: Link the node to the node attribute so property connections can be + // redirected from connections if necessary. + ufbxi_check(ufbxi_insert_fbx_attr(uc, info->fbx_id, attrib_info.fbx_id)); + + // Split properties between the node and the attribute. + // Consider all user properties as node properties. + ufbx_prop *ps = info->props.props.data; + size_t dst = 0, src = 0, end = info->props.props.count; + while (src < end) { + if (!ufbxi_is_node_property_name(uc, ps[src].name.data) && (ps[src].flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) { + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, 1, &ps[src])); + src++; + } else if (dst != src) { + ps[dst++] = ps[src++]; + } else { + dst++; src++; + } + } + attrib_info.props.props.count = end - dst; + attrib_info.props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, attrib_info.props.props.count); + ufbxi_check(attrib_info.props.props.data); + info->props.props.count = dst; + + if (sub_type == ufbxi_Mesh) { + ufbxi_check(ufbxi_read_mesh(uc, node, &attrib_info)); + } else if (sub_type == ufbxi_Light) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_light), UFBX_ELEMENT_LIGHT)); + } else if (sub_type == ufbxi_Camera) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_camera), UFBX_ELEMENT_CAMERA)); + } else if (sub_type == ufbxi_LimbNode || sub_type == ufbxi_Limb || sub_type == ufbxi_Root) { + ufbxi_check(ufbxi_read_bone(uc, node, &attrib_info, sub_type)); + } else if (sub_type == ufbxi_Null || sub_type == ufbxi_Marker) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_empty), UFBX_ELEMENT_EMPTY)); + } else if (sub_type == ufbxi_NurbsCurve) { + if (!ufbxi_find_child(node, ufbxi_KnotVector)) return 1; + ufbxi_check(ufbxi_read_nurbs_curve(uc, node, &attrib_info)); + } else if (sub_type == ufbxi_NurbsSurface) { + if (!ufbxi_find_child(node, ufbxi_KnotVectorU)) return 1; + if (!ufbxi_find_child(node, ufbxi_KnotVectorV)) return 1; + ufbxi_check(ufbxi_read_nurbs_surface(uc, node, &attrib_info)); + } else if (sub_type == ufbxi_Line) { + if (!ufbxi_find_child(node, ufbxi_Points)) return 1; + if (!ufbxi_find_child(node, ufbxi_PointsIndex)) return 1; + ufbxi_check(ufbxi_read_line(uc, node, &attrib_info)); + } else if (sub_type == ufbxi_TrimNurbsSurface) { + if (!ufbxi_find_child(node, ufbxi_Layer)) return 1; + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_nurbs_trim_surface), UFBX_ELEMENT_NURBS_TRIM_SURFACE)); + } else if (sub_type == ufbxi_Boundary) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_nurbs_trim_boundary), UFBX_ELEMENT_NURBS_TRIM_BOUNDARY)); + } else if (sub_type == ufbxi_CameraStereo) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_stereo_camera), UFBX_ELEMENT_STEREO_CAMERA)); + } else if (sub_type == ufbxi_CameraSwitcher) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_camera_switcher), UFBX_ELEMENT_CAMERA_SWITCHER)); + } else if (sub_type == ufbxi_FKEffector) { + ufbxi_check(ufbxi_read_marker(uc, node, &attrib_info, sub_type, UFBX_MARKER_FK_EFFECTOR)); + } else if (sub_type == ufbxi_IKEffector) { + ufbxi_check(ufbxi_read_marker(uc, node, &attrib_info, sub_type, UFBX_MARKER_IK_EFFECTOR)); + } else if (sub_type == ufbxi_LodGroup) { + ufbxi_check(ufbxi_read_element(uc, node, &attrib_info, sizeof(ufbx_lod_group), UFBX_ELEMENT_LOD_GROUP)); + } else { + ufbx_string sub_type_str = { sub_type, strlen(sub_type) }; + ufbxi_check(ufbxi_read_unknown(uc, node, &attrib_info, type_str, sub_type_str, super_type)); + } + + ufbxi_check(ufbxi_connect_oo(uc, attrib_info.fbx_id, info->fbx_id)); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_global_settings(ufbxi_context *uc, ufbxi_node *node) +{ + ufbxi_check(ufbxi_read_properties(uc, node, &uc->scene.settings.props)); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_object(ufbxi_context *uc, ufbxi_node *node) +{ + ufbxi_element_info info = { 0 }; + info.dom_node = ufbxi_get_dom_node(uc, node); + + if (node->name == ufbxi_GlobalSettings) { + ufbxi_check(ufbxi_read_global_settings(uc, node)); + return 1; + } + + ufbx_string type_and_name, sub_type_str; + + // Failing to parse the object properties is not an error since + // there's some weird objects mixed in every now and then. + // FBX version 7000 and up uses 64-bit unique IDs per object, + // older FBX versions just use name/type pairs, which we can + // use as IDs since all strings are interned into a string pool. + if (uc->version >= 7000) { + if (!ufbxi_get_val3(node, "Lss", &info.fbx_id, &type_and_name, &sub_type_str)) return 1; + ufbxi_check(ufbxi_validate_fbx_id(uc, &info.fbx_id)); + } else { + if (!ufbxi_get_val2(node, "ss", &type_and_name, &sub_type_str)) return 1; + info.fbx_id = ufbxi_synthetic_id_from_string(uc, type_and_name.data); + ufbxi_check(info.fbx_id); + } + + // Remove the "Fbx" prefix from sub-types, remember to re-intern! + if (sub_type_str.length > 3 && !memcmp(sub_type_str.data, "Fbx", 3)) { + sub_type_str.data += 3; + sub_type_str.length -= 3; + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &sub_type_str, false)); + } + + ufbx_string type_str; + ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type_str, &info.name)); + + const char *name = node->name, *sub_type = sub_type_str.data; + ufbxi_check(ufbxi_read_properties(uc, node, &info.props)); + info.props.defaults = ufbxi_find_template(uc, name, sub_type); + + if (name == ufbxi_Model) { + if (uc->version < 7000) { + ufbxi_check(ufbxi_read_synthetic_attribute(uc, node, &info, type_str, sub_type, name)); + } + ufbxi_check(ufbxi_read_model(uc, node, &info)); + } else if (name == ufbxi_NodeAttribute) { + if (sub_type == ufbxi_Light) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_light), UFBX_ELEMENT_LIGHT)); + } else if (sub_type == ufbxi_Camera) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_camera), UFBX_ELEMENT_CAMERA)); + } else if (sub_type == ufbxi_LimbNode || sub_type == ufbxi_Limb || sub_type == ufbxi_Root) { + ufbxi_check(ufbxi_read_bone(uc, node, &info, sub_type)); + } else if (sub_type == ufbxi_Null || sub_type == ufbxi_Marker) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_empty), UFBX_ELEMENT_EMPTY)); + } else if (sub_type == ufbxi_CameraStereo) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_stereo_camera), UFBX_ELEMENT_STEREO_CAMERA)); + } else if (sub_type == ufbxi_CameraSwitcher) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_camera_switcher), UFBX_ELEMENT_CAMERA_SWITCHER)); + } else if (sub_type == ufbxi_FKEffector) { + ufbxi_check(ufbxi_read_marker(uc, node, &info, sub_type, UFBX_MARKER_FK_EFFECTOR)); + } else if (sub_type == ufbxi_IKEffector) { + ufbxi_check(ufbxi_read_marker(uc, node, &info, sub_type, UFBX_MARKER_IK_EFFECTOR)); + } else if (sub_type == ufbxi_LodGroup) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_lod_group), UFBX_ELEMENT_LOD_GROUP)); + } else { + ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name)); + } + } else if (name == ufbxi_Geometry) { + if (sub_type == ufbxi_Mesh) { + ufbxi_check(ufbxi_read_mesh(uc, node, &info)); + } else if (sub_type == ufbxi_Shape) { + ufbxi_check(ufbxi_read_shape(uc, node, &info)); + } else if (sub_type == ufbxi_NurbsCurve) { + ufbxi_check(ufbxi_read_nurbs_curve(uc, node, &info)); + } else if (sub_type == ufbxi_NurbsSurface) { + ufbxi_check(ufbxi_read_nurbs_surface(uc, node, &info)); + } else if (sub_type == ufbxi_Line) { + ufbxi_check(ufbxi_read_line(uc, node, &info)); + } else if (sub_type == ufbxi_TrimNurbsSurface) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_nurbs_trim_surface), UFBX_ELEMENT_NURBS_TRIM_SURFACE)); + } else if (sub_type == ufbxi_Boundary) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_nurbs_trim_boundary), UFBX_ELEMENT_NURBS_TRIM_BOUNDARY)); + } else { + ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name)); + } + } else if (name == ufbxi_Deformer) { + if (sub_type == ufbxi_Skin) { + ufbxi_check(ufbxi_read_skin(uc, node, &info)); + } else if (sub_type == ufbxi_Cluster) { + ufbxi_check(ufbxi_read_skin_cluster(uc, node, &info)); + } else if (sub_type == ufbxi_BlendShape) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_blend_deformer), UFBX_ELEMENT_BLEND_DEFORMER)); + } else if (sub_type == ufbxi_BlendShapeChannel) { + ufbxi_check(ufbxi_read_blend_channel(uc, node, &info)); + } else if (sub_type == ufbxi_VertexCacheDeformer) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_cache_deformer), UFBX_ELEMENT_CACHE_DEFORMER)); + } else { + ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name)); + } + } else if (name == ufbxi_Material) { + ufbxi_check(ufbxi_read_material(uc, node, &info)); + } else if (name == ufbxi_Texture) { + ufbxi_check(ufbxi_read_texture(uc, node, &info)); + } else if (name == ufbxi_LayeredTexture) { + ufbxi_check(ufbxi_read_layered_texture(uc, node, &info)); + } else if (name == ufbxi_Video) { + ufbxi_check(ufbxi_read_video(uc, node, &info)); + } else if (name == ufbxi_AnimationStack) { + ufbxi_check(ufbxi_read_anim_stack(uc, node, &info)); + } else if (name == ufbxi_AnimationLayer) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_anim_layer), UFBX_ELEMENT_ANIM_LAYER)); + } else if (name == ufbxi_AnimationCurveNode) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_anim_value), UFBX_ELEMENT_ANIM_VALUE)); + } else if (name == ufbxi_AnimationCurve) { + ufbxi_check(ufbxi_read_animation_curve(uc, node, &info)); + } else if (name == ufbxi_Pose) { + ufbxi_check(ufbxi_read_pose(uc, node, &info, sub_type)); + } else if (name == ufbxi_Implementation) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_shader), UFBX_ELEMENT_SHADER)); + } else if (name == ufbxi_BindingTable) { + ufbxi_check(ufbxi_read_binding_table(uc, node, &info)); + } else if (name == ufbxi_Collection) { + if (sub_type == ufbxi_SelectionSet) { + ufbxi_check(ufbxi_read_selection_set(uc, node, &info)); + } + } else if (name == ufbxi_CollectionExclusive) { + if (sub_type == ufbxi_DisplayLayer) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_display_layer), UFBX_ELEMENT_DISPLAY_LAYER)); + } + } else if (name == ufbxi_SelectionNode) { + ufbxi_check(ufbxi_read_selection_node(uc, node, &info)); + } else if (name == ufbxi_Constraint) { + if (sub_type == ufbxi_Character) { + ufbxi_check(ufbxi_read_character(uc, node, &info)); + } else { + ufbxi_check(ufbxi_read_constraint(uc, node, &info)); + } + } else if (name == ufbxi_SceneInfo) { + ufbxi_check(ufbxi_read_scene_info(uc, node)); + } else if (name == ufbxi_Cache) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_cache_file), UFBX_ELEMENT_CACHE_FILE)); + } else if (name == ufbxi_ObjectMetaData) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_metadata_object), UFBX_ELEMENT_METADATA_OBJECT)); + } else if (name == ufbxi_AudioLayer) { + ufbxi_check(ufbxi_read_element(uc, node, &info, sizeof(ufbx_audio_layer), UFBX_ELEMENT_AUDIO_LAYER)); + } else if (name == ufbxi_Audio) { + ufbxi_check(ufbxi_read_audio_clip(uc, node, &info)); + } else { + ufbxi_check(ufbxi_read_unknown(uc, node, &info, type_str, sub_type_str, name)); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects(ufbxi_context *uc) +{ + for (;;) { + // Push a deferred element ID for tagging warnings + uc->p_element_id = ufbxi_push(&uc->tmp_element_id, uint32_t, 1); + ufbxi_check(uc->p_element_id); + *uc->p_element_id = UFBX_NO_INDEX; + uc->warnings.deferred_element_id_plus_one = (uint32_t)uc->tmp_element_id.num_items; + + ufbxi_node *node; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL)); + if (!node) break; + + ufbxi_check(ufbxi_read_object(uc, node)); + + uc->warnings.deferred_element_id_plus_one = 0; + uc->p_element_id = NULL; + } + + return 1; +} + +typedef struct { + ufbxi_node **nodes; + size_t num_nodes; + uint32_t task_index; +} ufbxi_object_batch; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_objects_threaded(ufbxi_context *uc) +{ + uc->parse_threaded = true; + + bool parsed_to_end = false; + ufbxi_object_batch batches[UFBX_THREAD_GROUP_COUNT]; // ufbxi_uninit + memset(batches, 0, sizeof(batches)); + + size_t empty_count = 0; + size_t batch_index = 0; + while (empty_count < UFBX_THREAD_GROUP_COUNT) { + ufbxi_object_batch *batch = &batches[batch_index]; + + ufbxi_check(ufbxi_thread_pool_wait_group(&uc->thread_pool)); + + if (batch->num_nodes > 0) { + ufbxi_for_ptr(ufbxi_node, p_node, batch->nodes, batch->num_nodes) { + ufbxi_buf_clear(&uc->tmp_parse); + + // Push a deferred element ID for tagging warnings + uc->p_element_id = ufbxi_push(&uc->tmp_element_id, uint32_t, 1); + ufbxi_check(uc->p_element_id); + *uc->p_element_id = UFBX_NO_INDEX; + uc->warnings.deferred_element_id_plus_one = (uint32_t)uc->tmp_element_id.num_items; + + ufbxi_check(ufbxi_read_object(uc, *p_node)); + + uc->warnings.deferred_element_id_plus_one = 0; + uc->p_element_id = NULL; + } + batch->num_nodes = 0; + } + + ufbxi_buf *tmp_buf = &uc->tmp_thread_parse[batch_index]; + + // ASCII data may be in `tmp_buf`, so copy it to safety in case + if (uc->ascii.src_buf == tmp_buf) { + ufbxi_ascii *ua = &uc->ascii; + size_t size = ufbxi_to_size(ua->src_end - ua->src); + if (uc->read_buffer_size < size) { + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->read_buffer, &uc->read_buffer_size, size)); + } + memcpy(uc->read_buffer, ua->src, size); + uc->data = uc->data_begin = ua->src = uc->read_buffer; + ua->src_end = uc->read_buffer + size; + ua->src_is_retained = false; + ua->src_buf = NULL; + if (ufbxi_to_size(ua->src_end - ua->src) < uc->progress_interval) { + ua->src_yield = ua->src_end; + } else { + ua->src_yield = ua->src + uc->progress_interval; + } + uc->data = ua->src; + } + + ufbxi_buf_clear(tmp_buf); + + if (!parsed_to_end) { + size_t num_nodes = 0; + uint32_t task_start = uc->thread_pool.start_index; + uint32_t max_tasks = uc->thread_pool.num_tasks / UFBX_THREAD_GROUP_COUNT; + max_tasks = ufbxi_min32(max_tasks, ufbxi_thread_pool_available_tasks(&uc->thread_pool)); + size_t max_memory = uc->opts.thread_opts.memory_limit / UFBX_THREAD_GROUP_COUNT; + + for (;;) { + ufbxi_node *node = NULL; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, tmp_buf)); + if (!node) { + parsed_to_end = true; + break; + } + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbxi_node*, 1, &node)); + num_nodes++; + + uint32_t num_tasks = uc->thread_pool.start_index - task_start; + if (num_tasks >= max_tasks) break; + + size_t memory_used = tmp_buf->pushed_size + tmp_buf->pos; + if (memory_used >= max_memory) break; + } + + batch->num_nodes = num_nodes; + batch->nodes = ufbxi_push_pop(tmp_buf, &uc->tmp_stack, ufbxi_node*, num_nodes); + ufbxi_check(batch->nodes); + batch->task_index = uc->thread_pool.start_index; + + } + + // Not safe to refer to this buffer anymore + uc->ascii.src_is_retained = false; + + ufbxi_thread_pool_flush_group(&uc->thread_pool); + + if (batch->num_nodes == 0) { + empty_count += 1; + } + + batch_index = (batch_index + 1) % UFBX_THREAD_GROUP_COUNT; + } + + ufbxi_check(ufbxi_thread_pool_wait_all(&uc->thread_pool)); + + uc->parse_threaded = false; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_connections(ufbxi_context *uc) +{ + // Read the connections to the list first + for (;;) { + ufbxi_node *node; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL)); + if (!node) break; + + char *type; + + uint64_t src_id = 0, dst_id = 0; + ufbx_string src_prop = ufbx_empty_string, dst_prop = ufbx_empty_string; + + if (uc->version < 7000) { + char *src_name = NULL, *dst_name = NULL; + // Pre-7000 versions use Type::Name pairs as identifiers + + if (!ufbxi_get_val1(node, "c", &type)) continue; + + if (type == ufbxi_OO) { + if (!ufbxi_get_val3(node, "_cc", NULL, &src_name, &dst_name)) continue; + } else if (type == ufbxi_OP) { + if (!ufbxi_get_val4(node, "_ccs", NULL, &src_name, &dst_name, &dst_prop)) continue; + } else if (type == ufbxi_PO) { + if (!ufbxi_get_val4(node, "_csc", NULL, &src_name, &src_prop, &dst_name)) continue; + } else if (type == ufbxi_PP) { + if (!ufbxi_get_val5(node, "_cscs", NULL, &src_name, &src_prop, &dst_name, &dst_prop)) continue; + } else { + // TODO: Strict mode? + continue; + } + + if (src_prop.length > 0) { + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &src_prop, false)); + } + if (dst_prop.length > 0) { + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst_prop, false)); + } + + src_id = ufbxi_synthetic_id_from_string(uc, src_name); + dst_id = ufbxi_synthetic_id_from_string(uc, dst_name); + ufbxi_check(src_id && dst_id); + + } else { + // Post-7000 versions use proper unique 64-bit IDs + + if (!ufbxi_get_val1(node, "C", &type)) continue; + + if (type == ufbxi_OO) { + if (!ufbxi_get_val3(node, "_LL", NULL, &src_id, &dst_id)) continue; + } else if (type == ufbxi_OP) { + if (!ufbxi_get_val4(node, "_LLS", NULL, &src_id, &dst_id, &dst_prop)) continue; + } else if (type == ufbxi_PO) { + if (!ufbxi_get_val4(node, "_LSL", NULL, &src_id, &src_prop, &dst_id)) continue; + } else if (type == ufbxi_PP) { + if (!ufbxi_get_val5(node, "_LSLS", NULL, &src_id, &src_prop, &dst_id, &dst_prop)) continue; + } else { + // TODO: Strict mode? + continue; + } + + ufbxi_check(ufbxi_validate_fbx_id(uc, &src_id)); + ufbxi_check(ufbxi_validate_fbx_id(uc, &dst_id)); + } + + ufbxi_tmp_connection *conn = ufbxi_push(&uc->tmp_connections, ufbxi_tmp_connection, 1); + ufbxi_check(conn); + conn->src = src_id; + conn->dst = dst_id; + conn->src_prop = src_prop; + conn->dst_prop = dst_prop; + } + + return 1; +} + +// -- Pre-7000 "Take" based animation + +ufbxi_forceinline static char ufbxi_double_to_char(double value) +{ + if (value >= 0.0 && value <= 127.0) { + return (char)(int)value; + } else { + return 0; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_anim_channel(ufbxi_context *uc, ufbxi_node *node, uint64_t value_fbx_id, const char *name, ufbx_real *p_default) +{ + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Default, "R", p_default)); + + // Find the key array, early return with success if not found as we may have only a default + ufbxi_value_array *keys = ufbxi_find_array(node, ufbxi_Key, 'd'); + if (!keys) return 1; + + uint64_t curve_fbx_id = 0; + ufbx_anim_curve *curve = ufbxi_push_synthetic_element(uc, &curve_fbx_id, node, name, ufbx_anim_curve, UFBX_ELEMENT_ANIM_CURVE); + ufbxi_check(curve); + + ufbxi_check(ufbxi_connect_op(uc, curve_fbx_id, value_fbx_id, curve->name)); + + ufbxi_read_extrapolation(&curve->pre_extrapolation, node, ufbxi_Pre_Extrapolation); + ufbxi_read_extrapolation(&curve->post_extrapolation, node, ufbxi_Post_Extrapolation); + + if (uc->opts.ignore_animation) return 1; + + size_t num_keys = 0; + ufbxi_check(ufbxi_find_val1(node, ufbxi_KeyCount, "Z", &num_keys)); + curve->keyframes.data = ufbxi_push(&uc->result, ufbx_keyframe, num_keys); + curve->keyframes.count = num_keys; + ufbxi_check(curve->keyframes.data); + + float slope_left = 0.0f; + float weight_left = 0.333333f; + + double next_time = 0.0; + double next_value = 0.0; + double prev_time = 0.0; + + // The pre-7000 keyframe data is stored as a _heterogenous_ array containing 64-bit integers, + // floating point values, and _bare characters_. We cast all values to double and interpret them. + double *data = (double*)keys->data, *data_end = data + keys->size; + + if (num_keys > 0) { + ufbxi_check(data_end - data >= 2); + next_time = data[0] / uc->ktime_sec_double; + next_value = data[1]; + } + + for (size_t i = 0; i < num_keys; i++) { + ufbx_keyframe *key = &curve->keyframes.data[i]; + + if (i == 0) { + curve->min_value = (ufbx_real)next_value; + curve->max_value = (ufbx_real)next_value; + } else { + curve->min_value = ufbxi_min_real(curve->min_value, (ufbx_real)next_value); + curve->max_value = ufbxi_max_real(curve->max_value, (ufbx_real)next_value); + } + + // First three values: Time, Value, InterpolationMode + ufbxi_check(data_end - data >= 3); + key->time = next_time; + key->value = (ufbx_real)next_value; + char mode = ufbxi_double_to_char(data[2]); + data += 3; + + float slope_right = 0.0f; + float weight_right = 0.333333f; + float next_slope_left = 0.0f; + float next_weight_left = 0.333333f; + bool auto_slope = false; + + if (mode == 'U') { + // Cubic interpolation + key->interpolation = UFBX_INTERPOLATION_CUBIC; + + ufbxi_check(data_end - data >= 1); + char slope_mode = ufbxi_double_to_char(data[0]); + data += 1; + + size_t num_weights = 1; + if (slope_mode == 's' || slope_mode == 'b') { + // Slope mode 's'/'b' (standard? broken?) always have two explicit slopes + // TODO: `b` might actually be some kind of TCB curve + ufbxi_check(data_end - data >= 2); + slope_right = (float)data[0]; + next_slope_left = (float)data[1]; + data += 2; + } else if (slope_mode == 'a') { + // Parameterless slope mode 'a' seems to appear in baked animations. Let's just assume + // automatic tangents for now as they're the least likely to break with + // objectionable artifacts. We need to defer the automatic tangent resolve + // until we have read the next time/value. + // TODO: Solve what this is more thoroughly + auto_slope = true; + if (uc->version == 5000) { + num_weights = 0; + } + } else if (slope_mode == 'p') { + // TODO: What is this mode? It seems to have negative values sometimes? + // Also it seems to have _two_ trailing weights values, currently observed: + // `n,n` and `a,X,Y,n`... + // Ignore unknown values for now + ufbxi_check(data_end - data >= 2); + data += 2; + num_weights = 2; + } else if (slope_mode == 't') { + // TODO: What is this mode? It seems that it does not have any weights and the + // third value seems _tiny_ (around 1e-30?) + ufbxi_check(data_end - data >= 3); + data += 3; + num_weights = 0; + } else { + ufbxi_fail("Unknown slope mode"); + } + + for (; num_weights > 0; num_weights--) { + ufbxi_check(data_end - data >= 1); + char weight_mode = ufbxi_double_to_char(data[0]); + data += 1; + + if (weight_mode == 'n') { + // Automatic weights (0.3333...) + } else if (weight_mode == 'a') { + // Manual weights: RightWeight, NextLeftWeight + ufbxi_check(data_end - data >= 2); + weight_right = (float)data[0]; + next_weight_left = (float)data[1]; + data += 2; + } else if (weight_mode == 'l') { + // Next left tangent is weighted + ufbxi_check(data_end - data >= 1); + next_weight_left = (float)data[0]; + data += 1; + } else if (weight_mode == 'r') { + // Right tangent is weighted + ufbxi_check(data_end - data >= 1); + weight_right = (float)data[0]; + data += 1; + } else if (weight_mode == 'c') { + // TODO: What is this mode? At least it has no parameters so let's + // just assume automatic weights for the time being (0.3333...) + } else { + ufbxi_fail("Unknown weight mode"); + } + } + + } else if (mode == 'L') { + // Linear interpolation: No parameters + key->interpolation = UFBX_INTERPOLATION_LINEAR; + } else if (mode == 'C') { + // Constant interpolation: Single parameter (use prev/next) + ufbxi_check(data_end - data >= 1); + key->interpolation = ufbxi_double_to_char(data[0]) == 'n' ? UFBX_INTERPOLATION_CONSTANT_NEXT : UFBX_INTERPOLATION_CONSTANT_PREV; + data += 1; + } else { + ufbxi_fail("Unknown key mode"); + } + + // Retrieve next key and value + if (i + 1 < num_keys) { + ufbxi_check(data_end - data >= 2); + next_time = data[0] / uc->ktime_sec_double; + next_value = data[1]; + } + + if (auto_slope) { + if (i > 0) { + slope_left = slope_right = ufbxi_solve_auto_tangent(uc, + prev_time, key->time, next_time, + key[-1].value, key->value, (ufbx_real)next_value, + weight_left, weight_right, 0.0f, UFBXI_KEY_CLAMP_PROGRESSIVE|UFBXI_KEY_TIME_INDEPENDENT); + } else { + slope_left = slope_right = 0.0f; + } + } + + // Set up linear cubic tangents if necessary + if (key->interpolation == UFBX_INTERPOLATION_LINEAR) { + if (next_time > key->time) { + double slope = (next_value - key->value) / (next_time - key->time); + slope_right = next_slope_left = (float)slope; + } else { + slope_right = next_slope_left = 0.0f; + } + } + + if (key->time > prev_time) { + double delta = key->time - prev_time; + key->left.dx = (float)(weight_left * delta); + key->left.dy = key->left.dx * slope_left; + } else { + key->left.dx = 0.0f; + key->left.dy = 0.0f; + } + + if (next_time > key->time) { + double delta = next_time - key->time; + key->right.dx = (float)(weight_right * delta); + key->right.dy = key->right.dx * slope_right; + } else { + key->right.dx = 0.0f; + key->right.dy = 0.0f; + } + + slope_left = next_slope_left; + weight_left = next_weight_left; + prev_time = key->time; + } + + ufbxi_check(data == data_end); + + return 1; +} + +// Recursion limited as it is further called only for `name="T"/"R"/"S"` and +// cannot enter the `name=="Transform"` branch. +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_prop_channel(ufbxi_context *uc, ufbxi_node *node, uint64_t target_fbx_id, uint64_t layer_fbx_id, ufbx_string name) + ufbxi_recursive_function(int, ufbxi_read_take_prop_channel, (uc, node, target_fbx_id, layer_fbx_id, name), 2, + (ufbxi_context *uc, ufbxi_node *node, uint64_t target_fbx_id, uint64_t layer_fbx_id, ufbx_string name)) +{ + if (name.data == ufbxi_Transform) { + // Pre-7000 have transform keyframes in a deeply nested structure, + // flatten it to make it resemble post-7000 structure a bit closer: + // old: Model: { Channel: "Transform" { Channel: "T" { Channel "X": { ... } } } } + // new: Model: { Channel: "Lcl Translation" { Channel "X": { ... } } } + + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + if (child->name != ufbxi_Channel) continue; + + const char *old_name = NULL; + ufbxi_check(ufbxi_get_val1(child, "C", (char**)&old_name)); + + ufbx_string new_name; + if (old_name == ufbxi_T) { new_name.data = ufbxi_Lcl_Translation; new_name.length = sizeof(ufbxi_Lcl_Translation) - 1; } + else if (old_name == ufbxi_R) { new_name.data = ufbxi_Lcl_Rotation; new_name.length = sizeof(ufbxi_Lcl_Rotation) - 1; } + else if (old_name == ufbxi_S) { new_name.data = ufbxi_Lcl_Scaling; new_name.length = sizeof(ufbxi_Lcl_Scaling) - 1; } + else { + continue; + } + + // Read child as a top-level property channel + ufbxi_check(ufbxi_read_take_prop_channel(uc, child, target_fbx_id, layer_fbx_id, new_name)); + } + + } else { + + // Pre-6000 FBX files store blend shape keys with a " (Shape)" suffix + if (uc->version < 6000) { + const char *const suffix = " (Shape)"; + size_t suffix_len = strlen(suffix); + if (name.length > suffix_len && !memcmp(name.data + name.length - suffix_len, suffix, suffix_len)) { + name.length -= suffix_len; + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &name, false)); + } + } + + // Find 1-3 channel nodes that contain a `Key:` node + ufbxi_node *channel_nodes[3] = { 0 }; + const char *channel_names[3] = { 0 }; + size_t num_channel_nodes = 0; + + if (ufbxi_find_child(node, ufbxi_Key) || ufbxi_find_child(node, ufbxi_Default)) { + // Channel has only a single curve + channel_nodes[0] = node; + channel_names[0] = name.data; + num_channel_nodes = 1; + } else { + // Channel is a compound of multiple curves + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + if (child->name != ufbxi_Channel) continue; + if (!ufbxi_find_child(child, ufbxi_Key) && !ufbxi_find_child(child, ufbxi_Default)) continue; + if (!ufbxi_get_val1(child, "C", (char**)&channel_names[num_channel_nodes])) continue; + channel_nodes[num_channel_nodes] = child; + if (++num_channel_nodes == 3) break; + } + } + + // Early return: No valid channels found, not an error + if (num_channel_nodes == 0) return 1; + + uint64_t value_fbx_id = 0; + ufbx_anim_value *value = ufbxi_push_synthetic_element(uc, &value_fbx_id, node, name.data, ufbx_anim_value, UFBX_ELEMENT_ANIM_VALUE); + + // Add a "virtual" connection between the animated property and the layer/target + ufbxi_check(ufbxi_connect_oo(uc, value_fbx_id, layer_fbx_id)); + ufbxi_check(ufbxi_connect_op(uc, value_fbx_id, target_fbx_id, name)); + + for (size_t i = 0; i < num_channel_nodes; i++) { + ufbxi_check(ufbxi_read_take_anim_channel(uc, channel_nodes[i], value_fbx_id, channel_names[i], &value->default_value.v[i])); + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take_object(ufbxi_context *uc, ufbxi_node *node, uint64_t layer_fbx_id) +{ + // Takes are used only in pre-7000 FBX versions so objects are identified + // by their unique Type::Name pair that we use as unique IDs through the + // pooled interned string pointers. + const char *type_and_name = NULL; + ufbxi_check(ufbxi_get_val1(node, "c", (char**)&type_and_name)); + uint64_t target_fbx_id = ufbxi_synthetic_id_from_string(uc, type_and_name); + ufbxi_check(target_fbx_id); + + // Add all suitable Channels as animated properties + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + ufbx_string name; + if (child->name != ufbxi_Channel) continue; + if (!ufbxi_get_val1(child, "S", &name)) continue; + + ufbxi_check(ufbxi_read_take_prop_channel(uc, child, target_fbx_id, layer_fbx_id, name)); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_take(ufbxi_context *uc, ufbxi_node *node) +{ + ufbx_prop tmp_props[4]; + uint32_t num_props = 0; + memset(tmp_props, 0, sizeof(tmp_props)); + + int64_t start = 0, stop = 0; + if (ufbxi_find_val2(node, ufbxi_LocalTime, "LL", &start, &stop)) { + ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_LocalStart, start, UFBX_PROP_INTEGER); + ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_LocalStop, stop, UFBX_PROP_INTEGER); + } + if (ufbxi_find_val2(node, ufbxi_ReferenceTime, "LL", &start, &stop)) { + ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_ReferenceStart, start, UFBX_PROP_INTEGER); + ufbxi_init_synthetic_int_prop(&tmp_props[num_props++], ufbxi_ReferenceStop, stop, UFBX_PROP_INTEGER); + } + + const char *name; + ufbxi_check(ufbxi_get_val1(node, "C", (char**)&name)); + + // Hack: For post-7000 files we are only interested in the animation times + // for fallback in case the information is missing in the stacks. + if (uc->version >= 7000) { + uint32_t hash = ufbxi_hash_ptr(name); + ufbxi_tmp_anim_stack *entry = ufbxi_map_find(&uc->anim_stack_map, ufbxi_tmp_anim_stack, hash, &name); + + if (entry) { + ufbx_anim_stack *stack = entry->stack; + if (stack->props.props.count == 0) { + stack->props.props.count = num_props; + stack->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(stack->props.props.data); + } + } + + return 1; + } + + uint64_t stack_fbx_id = 0, layer_fbx_id = 0; + + // Treat the Take as a post-7000 version animation stack and layer. + ufbx_anim_stack *stack = ufbxi_push_synthetic_element(uc, &stack_fbx_id, node, name, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK); + ufbxi_check(stack); + + stack->props.props.count = num_props; + stack->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(stack->props.props.data); + + ufbx_anim_layer *layer = ufbxi_push_synthetic_element(uc, &layer_fbx_id, node, ufbxi_BaseLayer, ufbx_anim_layer, UFBX_ELEMENT_ANIM_LAYER); + ufbxi_check(layer); + + ufbxi_check(ufbxi_connect_oo(uc, layer_fbx_id, stack_fbx_id)); + + // Read all properties of objects included in the take + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + // TODO: Do some object types have another name? + if (child->name != ufbxi_Model) continue; + + ufbxi_check(ufbxi_read_take_object(uc, child, layer_fbx_id)); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_takes(ufbxi_context *uc) +{ + for (;;) { + ufbxi_node *node; + ufbxi_check(ufbxi_parse_toplevel_child(uc, &node, NULL)); + if (!node) break; + + if (node->name == ufbxi_Take) { + ufbxi_check(ufbxi_read_take(uc, node)); + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_settings(ufbxi_context *uc, ufbxi_node *node) +{ + if (uc->read_legacy_settings) return 1; + uc->read_legacy_settings = true; + + ufbx_prop tmp_props[2]; + uint32_t num_props = 0; + memset(tmp_props, 0, sizeof(tmp_props)); + + ufbxi_node *frame_rate = ufbxi_find_child_strcmp(node, "FrameRate"); + if (frame_rate) { + double fps = 0.0; + if (!ufbxi_get_val1(frame_rate, "D", &fps)) { + ufbx_string str; + if (ufbxi_get_val1(frame_rate, "S", &str)) { + char *end; + double val = ufbxi_parse_double(str.data, str.length, &end, uc->double_parse_flags); + if (end == str.data + str.length) { + fps = val; + } + } + } + if (fps > 0.0) { + ufbxi_init_synthetic_real_prop(&tmp_props[num_props++], ufbxi_CustomFrameRate, (ufbx_real)fps, UFBX_PROP_NUMBER); + ufbxi_init_synthetic_real_prop(&tmp_props[num_props++], ufbxi_TimeMode, UFBX_TIME_MODE_CUSTOM, UFBX_PROP_INTEGER); + } + } + + if (num_props > 0) { + ufbx_props *props = &uc->scene.settings.props; + size_t num_existing = props->props.count; + + size_t new_count = num_props + num_existing; + ufbx_prop *new_props = ufbxi_push(&uc->result, ufbx_prop, new_count); + ufbxi_check(new_props); + + memcpy(new_props, tmp_props, num_props * sizeof(ufbx_prop)); + if (num_existing > 0) { + memcpy(new_props + num_props, props->props.data, num_existing * sizeof(ufbx_prop)); + } + + ufbxi_check(ufbxi_sort_properties(uc, new_props, new_count)); + props->props.data = new_props; + props->props.count = new_count; + ufbxi_deduplicate_properties(&props->props); + + ufbxi_check(uc->scene.settings.props.props.data); + } + + return 1; +} + +ufbxi_noinline static ufbx_matrix ufbxi_unscaled_transform_to_matrix(const ufbx_transform *t) +{ + ufbx_transform transform = *t; + transform.scale.x = 1.0f; + transform.scale.y = 1.0f; + transform.scale.z = 1.0f; + return ufbx_transform_to_matrix(&transform); +} + +ufbxi_noinline static void ufbxi_setup_root_node(ufbxi_context *uc, ufbx_node *root) +{ + if (uc->opts.use_root_transform) { + root->local_transform = uc->opts.root_transform; + root->node_to_parent = ufbx_transform_to_matrix(&uc->opts.root_transform); + } else { + root->local_transform = ufbx_identity_transform; + root->node_to_parent = ufbx_identity_matrix; + } + root->is_root = true; +} + +static ufbxi_forceinline bool ufbxi_supports_version(uint32_t version) +{ + return version >= 3000 && version <= 7700; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_root(ufbxi_context *uc) +{ + // FBXHeaderExtension: Some metadata (optional) + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_FBXHeaderExtension)); + ufbxi_check(ufbxi_read_header_extension(uc)); + + // The ASCII exporter version is stored in top-level + if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) { + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Creator)); + if (uc->top_node) { + ufbxi_ignore(ufbxi_get_val1(uc->top_node, "S", &uc->scene.metadata.creator)); + } + } + + // Resolve the exporter before continuing + ufbxi_check(ufbxi_match_exporter(uc)); + if (uc->version < 7000) { + ufbxi_check(ufbxi_init_node_prop_names(uc)); + } + // Don't allow changing version from this point onwards + uc->ascii.found_version = true; + + // Document: Read root ID + if (uc->version >= 7000) { + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Documents)); + ufbxi_check(ufbxi_read_document(uc)); + } else { + // Pre-7000: Root node has a specific type-name pair "Model::Scene" + // (or reversed in binary). Use the interned name as ID as usual. + const char *root_name = uc->from_ascii ? "Model::Scene" : "Scene\x00\x01Model"; + root_name = ufbxi_push_string_imp(&uc->string_pool, root_name, 12, NULL, false, true); + ufbxi_check(root_name); + uc->root_id = ufbxi_synthetic_id_from_string(uc, root_name); + ufbxi_check(uc->root_id); + } + + // Add a nameless root node with the root ID + { + ufbxi_element_info root_info = { uc->root_id }; + root_info.name = ufbx_empty_string; + ufbx_node *root = ufbxi_push_element(uc, &root_info, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(root); + ufbxi_setup_root_node(uc, root); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id)); + } + + // Definitions: Object type counts and property templates (optional) + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Definitions)); + ufbxi_check(ufbxi_read_definitions(uc)); + + // Objects: Actual scene data + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Objects)); + if (!uc->sure_fbx) { + // If the file is a bit iffy about being a real FBX file reject it if + // even the objects are not found. + ufbxi_check_msg(uc->top_node, "Not an FBX file"); + } + if (uc->thread_pool.enabled) { + ufbxi_check(ufbxi_read_objects_threaded(uc)); + } else { + ufbxi_check(ufbxi_read_objects(uc)); + } + + // Connections: Relationships between nodes + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Connections)); + ufbxi_check(ufbxi_read_connections(uc)); + + // Takes: Pre-7000 animation data + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Takes)); + ufbxi_check(ufbxi_read_takes(uc)); + + // Check if there's a top-level GlobalSettings that we skimmed over + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_GlobalSettings)); + if (uc->top_node) { + ufbxi_check(ufbxi_read_global_settings(uc, uc->top_node)); + } + + // Version5: Pre-6000 settings + ufbxi_check(ufbxi_parse_toplevel(uc, ufbxi_Version5)); + if (uc->top_node) { + ufbxi_node *settings = ufbxi_find_child_strcmp(uc->top_node, "Settings"); + if (settings) { + ufbxi_check(ufbxi_read_legacy_settings(uc, settings)); + } + } + + // Force parsing all the nodes by parsing a toplevel that cannot be found + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_parse_toplevel(uc, NULL)); + } + + return 1; +} + +typedef struct { + const char *prop_name; + ufbx_prop_type prop_type; + const char *node_name; + const char *node_fmt; +} ufbxi_legacy_prop; + +// Must be alphabetically sorted! +static const ufbxi_legacy_prop ufbxi_legacy_light_props[] = { + { ufbxi_CastLight, UFBX_PROP_BOOLEAN, ufbxi_CastLight, "L" }, + { ufbxi_CastShadows, UFBX_PROP_BOOLEAN, ufbxi_CastShadows, "L" }, + { ufbxi_Color, UFBX_PROP_COLOR, ufbxi_Color, "RRR" }, + { ufbxi_ConeAngle, UFBX_PROP_NUMBER, ufbxi_ConeAngle, "R" }, + { ufbxi_HotSpot, UFBX_PROP_NUMBER, ufbxi_HotSpot, "R" }, + { ufbxi_Intensity, UFBX_PROP_NUMBER, ufbxi_Intensity, "R" }, + { ufbxi_LightType, UFBX_PROP_INTEGER, ufbxi_LightType, "L" }, +}; + +// Must be alphabetically sorted! +static const ufbxi_legacy_prop ufbxi_legacy_camera_props[] = { + { ufbxi_ApertureMode, UFBX_PROP_INTEGER, ufbxi_ApertureMode, "L" }, + { ufbxi_AspectH, UFBX_PROP_NUMBER, ufbxi_AspectH, "R" }, + { ufbxi_AspectRatioMode, UFBX_PROP_INTEGER, "AspectType", "L" }, + { ufbxi_AspectW, UFBX_PROP_NUMBER, ufbxi_AspectW, "R" }, + { ufbxi_FieldOfView, UFBX_PROP_NUMBER, "Aperture", "R" }, + { ufbxi_FieldOfViewX, UFBX_PROP_NUMBER, "FieldOfViewXProperty", "R" }, + { ufbxi_FieldOfViewY, UFBX_PROP_NUMBER, "FieldOfViewYProperty", "R" }, + { ufbxi_FilmHeight, UFBX_PROP_NUMBER, "CameraAperture", "_R" }, + { ufbxi_FilmSqueezeRatio, UFBX_PROP_NUMBER, "SqueezeRatio", "R" }, + { ufbxi_FilmWidth, UFBX_PROP_NUMBER, "CameraAperture", "R_" }, + { ufbxi_FocalLength, UFBX_PROP_NUMBER, ufbxi_FocalLength, "R" }, +}; + +// Must be alphabetically sorted! +static const ufbxi_legacy_prop ufbxi_legacy_bone_props[] = { + { ufbxi_Size, UFBX_PROP_NUMBER, ufbxi_Size, "R" }, +}; + +// Must be alphabetically sorted! +static const ufbxi_legacy_prop ufbxi_legacy_material_props[] = { + { ufbxi_AmbientColor, UFBX_PROP_COLOR, "Ambient", "RRR" }, + { ufbxi_DiffuseColor, UFBX_PROP_COLOR, "Diffuse", "RRR" }, + { ufbxi_EmissiveColor, UFBX_PROP_COLOR, "Emissive", "RRR" }, + { ufbxi_ShadingModel, UFBX_PROP_COLOR, ufbxi_ShadingModel, "S" }, + { ufbxi_Shininess, UFBX_PROP_NUMBER, "Shininess", "R" }, + { ufbxi_SpecularColor, UFBX_PROP_COLOR, "Specular", "RRR" }, +}; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_prop(ufbxi_node *node, ufbx_prop *prop, const ufbxi_legacy_prop *legacy_prop) +{ + size_t value_ix = 0; + uint32_t flags = 0; + + const char *fmt = legacy_prop->node_fmt; + for (size_t fmt_ix = 0; fmt[fmt_ix]; fmt_ix++) { + char c = fmt[fmt_ix]; + switch (c) { + case 'L': + ufbx_assert(value_ix == 0); + if (!ufbxi_get_val_at(node, fmt_ix, 'L', &prop->value_int)) return 0; + prop->value_real = (ufbx_real)prop->value_int; + prop->value_real_arr[1] = 0.0f; + prop->value_real_arr[2] = 0.0f; + prop->value_real_arr[3] = 0.0f; + prop->value_str = ufbx_empty_string; + prop->value_blob = ufbx_empty_blob; + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_INT; + value_ix++; + break; + case 'R': + ufbx_assert(value_ix < 4); + if (!ufbxi_get_val_at(node, fmt_ix, 'R', &prop->value_real_arr[value_ix])) return 0; + if (value_ix == 0) { + prop->value_int = ufbxi_f64_to_i64(prop->value_real); + prop->value_real_arr[1] = 0.0f; + prop->value_real_arr[2] = 0.0f; + prop->value_real_arr[3] = 0.0f; + prop->value_str = ufbx_empty_string; + prop->value_blob = ufbx_empty_blob; + } + flags &= ~(uint32_t)(UFBX_PROP_FLAG_VALUE_REAL|UFBX_PROP_FLAG_VALUE_VEC2|UFBX_PROP_FLAG_VALUE_VEC3|UFBX_PROP_FLAG_VALUE_VEC4); + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << value_ix; + value_ix++; + break; + case 'S': + ufbx_assert(value_ix == 0); + if (!ufbxi_get_val_at(node, fmt_ix, 'S', &prop->value_str)) return 0; + if (prop->value_str.length > 0) { + int found = ufbxi_get_val_at(node, fmt_ix, 'b', &prop->value_blob); + ufbxi_ignore(found); + ufbx_assert(found); + } else { + prop->value_blob = ufbx_empty_blob; + } + prop->value_real = 0.0f; + prop->value_real_arr[1] = 0.0f; + prop->value_real_arr[2] = 0.0f; + prop->value_real_arr[3] = 0.0f; + prop->value_int = 0; + flags |= (uint32_t)UFBX_PROP_FLAG_VALUE_STR; + value_ix++; + break; + case '_': + break; + default: + ufbxi_unreachable("Unhandled legacy fmt"); + } + } + + prop->flags = (ufbx_prop_flags)flags; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static size_t ufbxi_read_legacy_props(ufbxi_node *node, ufbx_prop *props, const ufbxi_legacy_prop *legacy_props, size_t num_legacy) +{ + size_t num_props = 0; + for (size_t legacy_ix = 0; legacy_ix < num_legacy; legacy_ix++) { + const ufbxi_legacy_prop *legacy_prop = &legacy_props[legacy_ix]; + ufbx_prop *prop = &props[num_props]; + + ufbxi_node *n = ufbxi_find_child_strcmp(node, legacy_prop->node_name); + if (!n) continue; + if (!ufbxi_read_legacy_prop(n, prop, legacy_prop)) continue; + + prop->name.data = legacy_prop->prop_name; + prop->name.length = strlen(legacy_prop->prop_name); + prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length); + prop->flags = (ufbx_prop_flags)0; + prop->type = legacy_prop->prop_type; + num_props++; + } + + return num_props; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_material(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name) +{ + ufbx_material *ufbxi_restrict material = ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, ufbx_material, UFBX_ELEMENT_MATERIAL); + ufbxi_check(material); + + ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_material_props)]; + size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_material_props, ufbxi_arraycount(ufbxi_legacy_material_props)); + + material->shading_model_name = ufbx_empty_string; + material->props.props.count = num_props; + material->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(material->props.props.data); + + material->shader_prop_prefix = ufbx_empty_string; + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_link(ufbxi_context *uc, ufbxi_node *node, uint64_t *p_fbx_id, const char *name) +{ + ufbx_skin_cluster *ufbxi_restrict cluster = ufbxi_push_synthetic_element(uc, p_fbx_id, node, name, ufbx_skin_cluster, UFBX_ELEMENT_SKIN_CLUSTER); + ufbxi_check(cluster); + + // TODO: Merge with ufbxi_read_skin_cluster(), at least partially? + ufbxi_value_array *indices = ufbxi_find_array(node, ufbxi_Indexes, 'i'); + ufbxi_value_array *weights = ufbxi_find_array(node, ufbxi_Weights, 'r'); + + if (indices && weights) { + ufbxi_check(indices->size == weights->size); + cluster->num_weights = indices->size; + cluster->vertices.data = (uint32_t*)indices->data; + cluster->weights.data = (ufbx_real*)weights->data; + cluster->vertices.count = cluster->num_weights; + cluster->weights.count = cluster->num_weights; + } + + ufbxi_value_array *transform = ufbxi_find_array(node, ufbxi_Transform, 'r'); + ufbxi_value_array *transform_link = ufbxi_find_array(node, ufbxi_TransformLink, 'r'); + if (transform && transform_link) { + ufbxi_check(transform->size >= 16); + ufbxi_check(transform_link->size >= 16); + + ufbxi_read_transform_matrix(&cluster->mesh_node_to_bone, (ufbx_real*)transform->data); + ufbxi_read_transform_matrix(&cluster->bind_to_world, (ufbx_real*)transform_link->data); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_light(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_light *ufbxi_restrict light = ufbxi_push_element(uc, info, ufbx_light, UFBX_ELEMENT_LIGHT); + ufbxi_check(light); + + ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_light_props)]; + size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_light_props, ufbxi_arraycount(ufbxi_legacy_light_props)); + + light->props.props.count = num_props; + light->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(light->props.props.data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_camera(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_camera *ufbxi_restrict camera = ufbxi_push_element(uc, info, ufbx_camera, UFBX_ELEMENT_CAMERA); + ufbxi_check(camera); + + ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_camera_props)]; + size_t num_props = ufbxi_read_legacy_props(node, tmp_props, ufbxi_legacy_camera_props, ufbxi_arraycount(ufbxi_legacy_camera_props)); + + camera->props.props.count = num_props; + camera->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(camera->props.props.data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_limb_node(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + ufbx_bone *ufbxi_restrict bone = ufbxi_push_element(uc, info, ufbx_bone, UFBX_ELEMENT_BONE); + ufbxi_check(bone); + + ufbx_prop tmp_props[ufbxi_arraycount(ufbxi_legacy_bone_props)]; + size_t num_props = 0; + + ufbxi_node *prop_node = ufbxi_find_child_strcmp(node, "Properties"); + if (prop_node) { + num_props = ufbxi_read_legacy_props(prop_node, tmp_props, ufbxi_legacy_bone_props, ufbxi_arraycount(ufbxi_legacy_bone_props)); + } + + bone->props.props.count = num_props; + bone->props.props.data = ufbxi_push_copy(&uc->result, ufbx_prop, num_props, tmp_props); + ufbxi_check(bone->props.props.data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_mesh(ufbxi_context *uc, ufbxi_node *node, ufbxi_element_info *info) +{ + // Only read polygon meshes, ignore eg. NURBS without error + ufbxi_node *node_vertices = ufbxi_find_child(node, ufbxi_Vertices); + ufbxi_node *node_indices = ufbxi_find_child(node, ufbxi_PolygonVertexIndex); + if (!node_vertices || !node_indices) return 1; + + ufbx_mesh *ufbxi_restrict mesh = ufbxi_push_element(uc, info, ufbx_mesh, UFBX_ELEMENT_MESH); + ufbxi_check(mesh); + + ufbxi_check(ufbxi_read_synthetic_blend_shapes(uc, node, info)); + + ufbxi_patch_mesh_reals(mesh); + + if (uc->opts.ignore_geometry) return 1; + + ufbxi_value_array *vertices = ufbxi_get_array(node_vertices, 'r'); + ufbxi_value_array *indices = ufbxi_get_array(node_indices, 'i'); + ufbxi_check(vertices && indices); + ufbxi_check(vertices->size % 3 == 0); + + mesh->num_vertices = vertices->size / 3; + mesh->num_indices = indices->size; + + uint32_t *index_data = (uint32_t*)indices->data; + + // Duplicate `index_data` for modification if we retain DOM + if (uc->opts.retain_dom) { + index_data = ufbxi_push_copy(&uc->result, uint32_t, indices->size, index_data); + ufbxi_check(index_data); + } + + mesh->vertices.data = (ufbx_vec3*)vertices->data; + mesh->vertex_indices.data = index_data; + mesh->vertices.count = mesh->num_vertices; + mesh->vertex_indices.count = mesh->num_indices; + + mesh->vertex_position.exists = true; + mesh->vertex_position.values.data = (ufbx_vec3*)vertices->data; + mesh->vertex_position.values.count = mesh->num_vertices; + mesh->vertex_position.indices.data = index_data; + mesh->vertex_position.indices.count = mesh->num_indices; + mesh->vertex_position.unique_per_vertex = true; + + // Check/make sure that the last index is negated (last of polygon) + if (mesh->num_indices > 0) { + if ((int32_t)index_data[mesh->num_indices - 1] >= 0) { + if (uc->opts.strict) ufbxi_fail("Non-negated last index"); + index_data[mesh->num_indices - 1] = ~index_data[mesh->num_indices - 1]; + } + } + + ufbxi_check(ufbxi_process_indices(uc, mesh, index_data)); + + // Normals are either per-vertex or per-index in legacy FBX files? + // If the version is 5000 prefer per-vertex, otherwise per-index... + ufbxi_value_array *normals = ufbxi_find_array(node, ufbxi_Normals, 'r'); + if (normals) { + size_t num_normals = normals->size / 3; + bool per_vertex = num_normals == mesh->num_vertices; + bool per_index = num_normals == mesh->num_indices; + if (per_vertex && (!per_index || uc->version == 5000)) { + mesh->vertex_normal.exists = true; + mesh->vertex_normal.values.count = num_normals; + mesh->vertex_normal.indices.count = mesh->num_indices; + mesh->vertex_normal.unique_per_vertex = true; + mesh->vertex_normal.values.data = (ufbx_vec3*)normals->data; + mesh->vertex_normal.indices.data = mesh->vertex_indices.data; + } else if (per_index) { + uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, mesh->num_indices); + mesh->vertex_normal.exists = true; + mesh->vertex_normal.values.count = num_normals; + mesh->vertex_normal.indices.count = mesh->num_indices; + mesh->vertex_normal.unique_per_vertex = false; + mesh->vertex_normal.values.data = (ufbx_vec3*)normals->data; + mesh->vertex_normal.indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive; + } + } + + // Optional UV values are stored pretty much like a modern vertex element + ufbxi_node *uv_info = ufbxi_find_child(node, ufbxi_GeometryUVInfo); + if (uv_info) { + ufbx_uv_set *set = ufbxi_push_zero(&uc->result, ufbx_uv_set, 1); + ufbxi_check(set); + set->index = 0; + set->name.data = ufbxi_empty_char; + ufbxi_check(ufbxi_read_vertex_element(uc, mesh, uv_info, (ufbx_vertex_attrib*)&set->vertex_uv, + ufbxi_TextureUV, ufbxi_TextureUVVerticeIndex, NULL, 'r', 2)); + + mesh->uv_sets.data = set; + mesh->uv_sets.count = 1; + mesh->vertex_uv = set->vertex_uv; + } + + // Material indices + { + const char *mapping = NULL; + ufbxi_check(ufbxi_find_val1(node, ufbxi_MaterialAssignation, "C", (char**)&mapping)); + if (mapping == ufbxi_ByPolygon) { + ufbxi_check(ufbxi_read_truncated_array(uc, &mesh->face_material.data, &mesh->face_material.count, node, ufbxi_Materials, 'i', mesh->num_faces)); + } else if (mapping == ufbxi_AllSame) { + ufbxi_value_array *arr = ufbxi_find_array(node, ufbxi_Materials, 'i'); + uint32_t material = 0; + if (arr && arr->size >= 1) { + material = ((uint32_t*)arr->data)[0]; + } + + mesh->face_material.count = mesh->num_faces; + if (material == 0) { + mesh->face_material.data = (uint32_t*)ufbxi_sentinel_index_zero; + } else { + mesh->face_material.data = ufbxi_push(&uc->result, uint32_t, mesh->num_faces); + ufbxi_check(mesh->face_material.data); + ufbxi_for_list(uint32_t, p_mat, mesh->face_material) { + *p_mat = material; + } + } + } + } + + uint64_t skin_fbx_id = 0; + ufbx_skin_deformer *skin = NULL; + + // Materials, Skin Clusters + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + if (child->name == ufbxi_Material) { + uint64_t fbx_id = 0; + ufbx_string type_and_name, type, name; + ufbxi_check(ufbxi_get_val1(child, "s", &type_and_name)); + ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name)); + ufbxi_check(ufbxi_read_legacy_material(uc, child, &fbx_id, name.data)); + ufbxi_check(ufbxi_connect_oo(uc, fbx_id, info->fbx_id)); + } else if (child->name == ufbxi_Link) { + uint64_t fbx_id = 0; + ufbx_string type_and_name, type, name; + ufbxi_check(ufbxi_get_val1(child, "s", &type_and_name)); + ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name)); + ufbxi_check(ufbxi_read_legacy_link(uc, child, &fbx_id, name.data)); + + uint64_t node_fbx_id = ufbxi_synthetic_id_from_string(uc, type_and_name.data); + ufbxi_check(node_fbx_id); + ufbxi_check(ufbxi_connect_oo(uc, node_fbx_id, fbx_id)); + if (!skin) { + skin = ufbxi_push_synthetic_element(uc, &skin_fbx_id, NULL, info->name.data, ufbx_skin_deformer, UFBX_ELEMENT_SKIN_DEFORMER); + ufbxi_check(skin); + ufbxi_check(ufbxi_connect_oo(uc, skin_fbx_id, info->fbx_id)); + } + ufbxi_check(ufbxi_connect_oo(uc, fbx_id, skin_fbx_id)); + } + } + + mesh->skinned_is_local = true; + mesh->skinned_position = mesh->vertex_position; + mesh->skinned_normal = mesh->vertex_normal; + + ufbxi_patch_mesh_reals(mesh); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_media(ufbxi_context *uc, ufbxi_node *node) +{ + ufbxi_node *videos = ufbxi_find_child(node, ufbxi_Video); + if (videos) { + ufbxi_for(ufbxi_node, child, videos->children, videos->num_children) { + ufbxi_element_info video_info = { 0 }; + ufbxi_check(ufbxi_get_val1(child, "S", &video_info.name)); + video_info.fbx_id = ufbxi_push_synthetic_id(uc); + video_info.dom_node = ufbxi_get_dom_node(uc, node); + + ufbxi_check(ufbxi_read_video(uc, child, &video_info)); + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_read_legacy_model(ufbxi_context *uc, ufbxi_node *node) +{ + ufbx_string type_and_name, type, name; + ufbxi_check(ufbxi_get_val1(node, "s", &type_and_name)); + ufbxi_check(ufbxi_split_type_and_name(uc, type_and_name, &type, &name)); + + ufbxi_element_info info = { 0 }; + info.fbx_id = ufbxi_synthetic_id_from_string(uc, type_and_name.data); + ufbxi_check(info.fbx_id); + info.name = name; + info.dom_node = ufbxi_get_dom_node(uc, node); + + ufbx_node *elem_node = ufbxi_push_element(uc, &info, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(elem_node); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &elem_node->element.element_id)); + + ufbxi_element_info attrib_info = { 0 }; + attrib_info.fbx_id = ufbxi_push_synthetic_id(uc); + attrib_info.name = name; + attrib_info.dom_node = info.dom_node; + + // If we make unused connections it doesn't matter.. + ufbxi_check(ufbxi_connect_oo(uc, attrib_info.fbx_id, info.fbx_id)); + + const char *attrib_type = ufbxi_empty_char; + ufbxi_ignore(ufbxi_find_val1(node, ufbxi_Type, "C", (char**)&attrib_type)); + + bool has_attrib = true; + if (attrib_type == ufbxi_Light) { + ufbxi_check(ufbxi_read_legacy_light(uc, node, &attrib_info)); + } else if (attrib_type == ufbxi_Camera) { + ufbxi_check(ufbxi_read_legacy_camera(uc, node, &attrib_info)); + } else if (attrib_type == ufbxi_LimbNode) { + ufbxi_check(ufbxi_read_legacy_limb_node(uc, node, &attrib_info)); + } else if (ufbxi_find_child(node, ufbxi_Vertices)) { + ufbxi_check(ufbxi_read_legacy_mesh(uc, node, &attrib_info)); + } else { + has_attrib = false; + } + + // Mark the node as having an attribute so property connections can be forwarded + if (has_attrib) { + ufbxi_check(ufbxi_insert_fbx_attr(uc, info.fbx_id, attrib_info.fbx_id)); + } + + // Children are represented as an array of strings + ufbxi_value_array *children = ufbxi_find_array(node, ufbxi_Children, 's'); + if (children) { + ufbx_string *names = (ufbx_string*)children->data; + for (size_t i = 0; i < children->size; i++) { + uint64_t child_fbx_id = ufbxi_synthetic_id_from_string(uc, names[i].data); + ufbxi_check(child_fbx_id); + ufbxi_check(ufbxi_connect_oo(uc, child_fbx_id, info.fbx_id)); + } + } + + // Non-take animation channels + ufbxi_for(ufbxi_node, child, node->children, node->num_children) { + if (child->name == ufbxi_Channel) { + ufbx_string channel_name; + if (ufbxi_get_val1(child, "S", &channel_name)) { + if (uc->legacy_implicit_anim_layer_id == 0) { + // Defer creation so we won't be the first animation stack.. + uc->legacy_implicit_anim_layer_id = ufbxi_push_synthetic_id(uc); + } + ufbxi_check(ufbxi_read_take_prop_channel(uc, child, info.fbx_id, uc->legacy_implicit_anim_layer_id, channel_name)); + } + } + } + + return 1; +} + +// Read a pre-6000 FBX file where everything is stored at the root level +ufbxi_nodiscard static ufbxi_noinline int ufbxi_read_legacy_root(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_init_node_prop_names(uc)); + + // Some legacy FBX files have an `Fbx_Root` node that could be used as the + // root node. However no other formats have root node with transforms so it + // might be better to leave it as-is and create an empty one. + { + ufbx_node *root = ufbxi_push_synthetic_element(uc, &uc->root_id, NULL, ufbxi_empty_char, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(root); + ufbxi_setup_root_node(uc, root); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id)); + } + + // NOTE: `ufbxi_read_header_extension()` is optional so use default KTime definition + uc->ktime_sec = 46186158000; + uc->ktime_sec_double = (double)uc->ktime_sec; + + for (;;) { + ufbxi_check(ufbxi_parse_legacy_toplevel(uc)); + if (!uc->top_node) break; + + ufbxi_node *node = uc->top_node; + if (node->name == ufbxi_FBXHeaderExtension) { + ufbxi_check(ufbxi_read_header_extension(uc)); + } else if (node->name == ufbxi_Media) { + ufbxi_check(ufbxi_read_legacy_media(uc, node)); + } else if (node->name == ufbxi_Takes) { + ufbxi_check(ufbxi_read_takes(uc)); + } else if (node->name == ufbxi_Model) { + ufbxi_check(ufbxi_read_legacy_model(uc, node)); + } else if (!strcmp(node->name, "Settings")) { + ufbxi_check(ufbxi_read_legacy_settings(uc, node)); + } + } + + if (uc->opts.retain_dom) { + ufbxi_check(ufbxi_retain_toplevel(uc, NULL)); + } + + // Create the implicit animation stack if necessary + if (uc->legacy_implicit_anim_layer_id) { + ufbxi_element_info layer_info = { 0 }; + layer_info.fbx_id = uc->legacy_implicit_anim_layer_id; + layer_info.name.data = "(internal)"; + layer_info.name.length = strlen(layer_info.name.data); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &layer_info.name, true)); + ufbx_anim_layer *layer = ufbxi_push_element(uc, &layer_info, ufbx_anim_layer, UFBX_ELEMENT_ANIM_LAYER); + ufbxi_check(layer); + + ufbxi_element_info stack_info = layer_info; + stack_info.fbx_id = ufbxi_push_synthetic_id(uc); + ufbx_anim_stack *stack = ufbxi_push_element(uc, &stack_info, ufbx_anim_stack, UFBX_ELEMENT_ANIM_STACK); + ufbxi_check(stack); + + ufbxi_check(ufbxi_connect_oo(uc, layer_info.fbx_id, stack_info.fbx_id)); + } + + return 1; +} + +// Filename manipulation + +ufbxi_nodiscard ufbxi_noinline static size_t ufbxi_trim_delimiters(ufbxi_context *uc, const char *data, size_t length) +{ + for (; length > 0; length--) { + char c = data[length - 1]; + bool is_separator = c == '/' || c == uc->opts.path_separator; + if (is_separator) { + length--; + break; + } + } + return length; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_init_file_paths(ufbxi_context *uc) +{ + if (uc->opts.filename.length > 0) { + uc->scene.metadata.filename = uc->opts.filename; + } else if (uc->opts.raw_filename.size > 0) { + uc->scene.metadata.filename.data = (const char*)uc->opts.raw_filename.data; + uc->scene.metadata.filename.length = uc->opts.raw_filename.size; + } + + if (uc->opts.raw_filename.size > 0) { + uc->scene.metadata.raw_filename = uc->opts.raw_filename; + } else if (uc->opts.filename.length > 0) { + uc->scene.metadata.raw_filename.data = uc->opts.filename.data; + uc->scene.metadata.raw_filename.size = uc->opts.filename.length; + } + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->scene.metadata.filename, false)); + ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &uc->scene.metadata.raw_filename, true)); + + uc->scene.metadata.relative_root.data = uc->scene.metadata.filename.data; + uc->scene.metadata.relative_root.length = ufbxi_trim_delimiters(uc, uc->scene.metadata.filename.data, uc->scene.metadata.filename.length); + + uc->scene.metadata.raw_relative_root.data = uc->scene.metadata.raw_filename.data; + uc->scene.metadata.raw_relative_root.size = ufbxi_trim_delimiters(uc, (const char*)uc->scene.metadata.raw_filename.data, uc->scene.metadata.raw_filename.size); + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->scene.metadata.relative_root, false)); + ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &uc->scene.metadata.raw_relative_root, true)); + + return 1; +} + +typedef union { + ufbx_string str; + ufbx_blob blob; +} ufbxi_strblob; + +static ufbxi_noinline void ufbxi_strblob_set(ufbxi_strblob *dst, const char *data, size_t length, bool raw) +{ + if (raw) { + dst->blob.data = data; + dst->blob.size = length; + } else { + dst->str.data = length == 0 ? ufbxi_empty_char : data; + dst->str.length = length; + } +} + +static ufbxi_forceinline const char *ufbxi_strblob_data(const ufbxi_strblob *strblob, bool raw) +{ + return raw ? (const char*)strblob->blob.data : strblob->str.data; +} + +static ufbxi_forceinline size_t ufbxi_strblob_length(const ufbxi_strblob *strblob, bool raw) +{ + return raw ? strblob->blob.size : strblob->str.length; +} + +ufbxi_nodiscard ufbxi_noinline static bool ufbxi_is_absolute_path(const char *path, size_t length) +{ + if (length > 0 && (path[0] == '/' || path[0] == '\\')) { + return true; + } else if (length > 2 && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) { + return true; + } + return false; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_relative_filename(ufbxi_context *uc, ufbxi_strblob *p_dst, const ufbxi_strblob *p_src, bool raw) +{ + const char *src = ufbxi_strblob_data(p_src, raw); + size_t src_length = ufbxi_strblob_length(p_src, raw); + + // Skip leading directory separators and early return if the relative path is empty + while (src_length > 0 && (src[0] == '/' || src[0] == '\\')) { + src++; + src_length--; + } + if (src_length == 0) { + ufbxi_strblob_set(p_dst, NULL, 0, raw); + return 1; + } + + const char *prefix_data; + size_t prefix_length; + if (raw) { + prefix_data = (const char*)uc->scene.metadata.raw_relative_root.data; + prefix_length = uc->scene.metadata.raw_relative_root.size; + } else { + prefix_data = (const char*)uc->scene.metadata.relative_root.data; + prefix_length = uc->scene.metadata.relative_root.length; + } + + // Retain absolute paths + if (ufbxi_is_absolute_path(src, src_length)) { + prefix_length = 0; + } + + // Undo directories from `prefix` for every `..` + while (prefix_length > 0 && src_length >= 3 && src[0] == '.' && src[1] == '.' && (src[2] == '/' || src[2] == '\\')) { + size_t part_start = prefix_length; + while (part_start > 0 && !(prefix_data[part_start - 1] == '/' || prefix_data[part_start - 1] == '\\')) { + part_start--; + } + size_t part_len = prefix_length - part_start; + + if (part_len == 2 && prefix_data[part_start] == '.' && prefix_data[part_start + 1] == '.') { + // Prefix itself ends in `..`, cannot cancel out a leading `../` + break; + } + + // Eat the leading '/' before the part segment + prefix_length = part_start > 0 ? part_start - 1 : 0; + + if (part_len == 1 && prefix_data[part_start] == '.') { + // Single '.' -> remove and continue without cancelling out a leading `../` + continue; + } + + src += 3; + src_length -= 3; + } + + size_t result_cap = prefix_length + src_length + 1; + char *result = ufbxi_push(&uc->tmp_stack, char, result_cap); + ufbxi_check(result); + char *ptr = result; + + // Copy prefix and suffix converting separators in the process + if (prefix_length > 0) { + memcpy(ptr, prefix_data, prefix_length); + ptr[prefix_length] = uc->opts.path_separator; + ptr += prefix_length + 1; + } + for (size_t i = 0; i < src_length; i++) { + char c = src[i]; + if (c == '/' || c == '\\') { + c = uc->opts.path_separator; + } + *ptr++ = c; + } + + // Intern the string and pop the temporary buffer + ufbx_string dst = { result, ufbxi_to_size(ptr - result) }; + ufbx_assert(dst.length <= result_cap); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &dst, raw)); + ufbxi_pop(&uc->tmp_stack, char, result_cap, NULL); + + ufbxi_strblob_set(p_dst, dst.data, dst.length, raw); + + return 1; +} + +// Open file utility + +static ufbxi_noinline bool ufbxi_open_file(const ufbx_open_file_cb *cb, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_blob *original_filename, ufbxi_allocator *ator, ufbx_open_file_type type) +{ + if (!cb || !cb->fn) return false; + + ufbx_open_file_info info; // ufbxi_uninit + info.context = (uintptr_t)ator; + if (original_filename) { + info.original_filename = *original_filename; + } else { + info.original_filename.data = path; + info.original_filename.size = path_len; + } + info.type = type; + + return cb->fn(cb->user, stream, path, path_len, &info); +} + +#define ufbxi_patch_zero(dst, src) do { \ + ufbx_assert((dst) == 0 || (dst) == (src)); \ + (dst) = (src); \ + } while (0) + +static void ufbxi_update_vertex_first_index(ufbx_mesh *mesh) +{ + ufbxi_for_list(uint32_t, p_vx_ix, mesh->vertex_first_index) { + *p_vx_ix = UFBX_NO_INDEX; + } + + uint32_t num_vertices = (uint32_t)mesh->num_vertices; + for (size_t ix = 0; ix < mesh->num_indices; ix++) { + uint32_t vx = mesh->vertex_indices.data[ix]; + if (vx < num_vertices && mesh->vertex_first_index.data[vx] == UFBX_NO_INDEX) { + mesh->vertex_first_index.data[vx] = (uint32_t)ix; + } + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh) +{ + if (mesh->vertices.count == 0) { + mesh->vertices = mesh->vertex_position.values; + } + if (mesh->vertex_indices.count == 0) { + mesh->vertex_indices = mesh->vertex_position.indices; + } + + ufbxi_patch_zero(mesh->num_vertices, mesh->vertices.count); + ufbxi_patch_zero(mesh->num_indices, mesh->vertex_indices.count); + ufbxi_patch_zero(mesh->num_faces, mesh->faces.count); + + if (mesh->num_triangles == 0 || mesh->max_face_triangles == 0) { + size_t num_triangles = 0; + size_t max_face_triangles = 0; + size_t num_bad_faces[3] = { 0 }; + ufbxi_nounroll ufbxi_for_list(ufbx_face, face, mesh->faces) { + if (face->num_indices >= 3) { + size_t tris = face->num_indices - 2; + num_triangles += tris; + max_face_triangles = ufbxi_max_sz(max_face_triangles, tris); + } else { + num_bad_faces[face->num_indices]++; + } + } + + ufbxi_patch_zero(mesh->num_triangles, num_triangles); + ufbxi_patch_zero(mesh->max_face_triangles, max_face_triangles); + ufbxi_patch_zero(mesh->num_empty_faces, num_bad_faces[0]); + ufbxi_patch_zero(mesh->num_point_faces, num_bad_faces[1]); + ufbxi_patch_zero(mesh->num_line_faces, num_bad_faces[2]); + } + + if (!mesh->skinned_position.exists) { + mesh->skinned_is_local = true; + mesh->skinned_position = mesh->vertex_position; + mesh->skinned_normal = mesh->vertex_normal; + } + + if (mesh->vertex_first_index.count == 0) { + mesh->vertex_first_index.count = mesh->num_vertices; + mesh->vertex_first_index.data = ufbxi_push(buf, uint32_t, mesh->num_vertices); + ufbxi_check_err(error, mesh->vertex_first_index.data); + ufbxi_update_vertex_first_index(mesh); + } + + if (mesh->uv_sets.count == 0 && mesh->vertex_uv.exists) { + ufbx_uv_set *uv_set = ufbxi_push_zero(buf, ufbx_uv_set, 1); + ufbxi_check_err(error, uv_set); + + uv_set->name.data = ufbxi_empty_char; + uv_set->vertex_uv = mesh->vertex_uv; + uv_set->vertex_tangent = mesh->vertex_tangent; + uv_set->vertex_bitangent = mesh->vertex_bitangent; + + mesh->uv_sets.data = uv_set; + mesh->uv_sets.count = 1; + } + + if (mesh->color_sets.count == 0 && mesh->vertex_color.exists) { + ufbx_color_set *color_set = ufbxi_push_zero(buf, ufbx_color_set, 1); + ufbxi_check_err(error, color_set); + + color_set->name.data = ufbxi_empty_char; + color_set->vertex_color = mesh->vertex_color; + + mesh->color_sets.data = color_set; + mesh->color_sets.count = 1; + } + + ufbxi_patch_mesh_reals(mesh); + + return 1; +} + +// -- .obj file + +#if UFBXI_FEATURE_FORMAT_OBJ + +static const uint8_t ufbxi_obj_attrib_stride[] = { + 3, 2, 3, 4, +}; + +ufbx_static_assert(obj_attrib_strides, ufbxi_arraycount(ufbxi_obj_attrib_stride) == UFBXI_OBJ_NUM_ATTRIBS_EXT); + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_props(ufbxi_context *uc, ufbx_prop_list *dst, size_t count) +{ + ufbx_prop_list props; // ufbxi_uninit + props.count = count; + props.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_props, ufbx_prop, count); + ufbxi_check(props.data); + + ufbxi_for_list(ufbx_prop, prop, props) { + prop->_internal_key = ufbxi_get_name_key(prop->name.data, prop->name.length); + if (prop->value_str.length == 0) { + prop->value_str.data = ufbxi_empty_char; + } + if (!prop->value_int) { + prop->value_int = ufbxi_f64_to_i64(prop->value_real); + } + if (prop->value_blob.size == 0 && prop->value_str.length > 0) { + prop->value_blob.data = prop->value_str.data; + prop->value_blob.size = prop->value_str.length; + } + } + + if (props.count > 1) { + ufbxi_check(ufbxi_sort_properties(uc, props.data, props.count)); + ufbxi_deduplicate_properties(&props); + } + + *dst = props; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_push_mesh(ufbxi_context *uc) +{ + ufbxi_obj_mesh *mesh = ufbxi_push_zero(&uc->obj.tmp_meshes, ufbxi_obj_mesh, 1); + ufbxi_check(mesh); + uc->obj.mesh = mesh; + + ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS; i++) { + mesh->vertex_range[i].min_ix = UINT64_MAX; + } + + const char *name = ""; + if (uc->opts.obj_split_groups && uc->obj.group.length > 0) { + name = uc->obj.group.data; + } else if (!uc->opts.obj_merge_objects && uc->obj.object.length > 0) { + name = uc->obj.object.data; + } else if (!uc->opts.obj_merge_groups && uc->obj.group.length > 0) { + name = uc->obj.group.data; + } + + mesh->fbx_node = ufbxi_push_synthetic_element(uc, &mesh->fbx_node_id, NULL, name, ufbx_node, UFBX_ELEMENT_NODE); + mesh->fbx_mesh = ufbxi_push_synthetic_element(uc, &mesh->fbx_mesh_id, NULL, name, ufbx_mesh, UFBX_ELEMENT_MESH); + ufbxi_check(mesh->fbx_node && mesh->fbx_mesh); + + mesh->fbx_mesh->vertex_position.unique_per_vertex = true; + + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &mesh->fbx_node->element_id)); + + uc->obj.face_material = UFBX_NO_INDEX; + uc->obj.face_group = 0; + uc->obj.face_group_dirty = true; + uc->obj.material_dirty = true; + + ufbxi_check(ufbxi_connect_oo(uc, mesh->fbx_mesh_id, mesh->fbx_node_id)); + ufbxi_check(ufbxi_connect_oo(uc, mesh->fbx_node_id, 0)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_flush_mesh(ufbxi_context *uc) +{ + if (!uc->obj.mesh) return 1; + + size_t num_props = uc->obj.tmp_props.num_items; + ufbxi_check(ufbxi_obj_pop_props(uc, &uc->obj.mesh->fbx_mesh->props.props, num_props)); + + size_t num_groups = uc->obj.tmp_face_group_infos.num_items; + ufbx_face_group *groups = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_group_infos, ufbx_face_group, num_groups); + ufbxi_check(groups); + + uc->obj.mesh->fbx_mesh->face_groups.data = groups; + uc->obj.mesh->fbx_mesh->face_groups.count = num_groups; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_init(ufbxi_context *uc) +{ + uc->from_ascii = true; + uc->obj.initialized = true; + + + ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS_EXT; i++) { + uc->obj.tmp_vertices[i].ator = &uc->ator_tmp; + uc->obj.tmp_indices[i].ator = &uc->ator_tmp; + } + uc->obj.tmp_color_valid.ator = &uc->ator_tmp; + uc->obj.tmp_faces.ator = &uc->ator_tmp; + uc->obj.tmp_face_material.ator = &uc->ator_tmp; + uc->obj.tmp_face_smoothing.ator = &uc->ator_tmp; + uc->obj.tmp_face_group.ator = &uc->ator_tmp; + uc->obj.tmp_face_group_infos.ator = &uc->ator_tmp; + uc->obj.tmp_meshes.ator = &uc->ator_tmp; + uc->obj.tmp_props.ator = &uc->ator_tmp; + + // .obj parsing does its own yield logic + uc->data_size += uc->yield_size; + + uc->obj.object.data = ufbxi_empty_char; + uc->obj.group.data = ufbxi_empty_char; + + ufbxi_map_init(&uc->obj.group_map, &uc->ator_tmp, ufbxi_map_cmp_const_char_ptr, NULL); + + // Add a nameless root node with the root ID + { + ufbxi_element_info root_info = { uc->root_id }; + root_info.name = ufbx_empty_string; + ufbx_node *root = ufbxi_push_element(uc, &root_info, ufbx_node, UFBX_ELEMENT_NODE); + ufbxi_check(root); + ufbxi_setup_root_node(uc, root); + ufbxi_check(ufbxi_push_copy(&uc->tmp_node_ids, uint32_t, 1, &root->element.element_id)); + } + + return 1; +} + +static ufbxi_noinline void ufbxi_obj_free(ufbxi_context *uc) +{ + if (!uc->obj.initialized) return; + + ufbxi_nounroll for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS_EXT; i++) { + ufbxi_buf_free(&uc->obj.tmp_vertices[i]); + ufbxi_buf_free(&uc->obj.tmp_indices[i]); + } + ufbxi_buf_free(&uc->obj.tmp_color_valid); + ufbxi_buf_free(&uc->obj.tmp_faces); + ufbxi_buf_free(&uc->obj.tmp_face_material); + ufbxi_buf_free(&uc->obj.tmp_face_smoothing); + ufbxi_buf_free(&uc->obj.tmp_face_group); + ufbxi_buf_free(&uc->obj.tmp_face_group_infos); + ufbxi_buf_free(&uc->obj.tmp_meshes); + ufbxi_buf_free(&uc->obj.tmp_props); + + ufbxi_map_free(&uc->obj.group_map); + + ufbxi_free(&uc->ator_tmp, ufbx_string, uc->obj.tokens, uc->obj.tokens_cap); + ufbxi_free(&uc->ator_tmp, ufbx_material*, uc->obj.tmp_materials, uc->obj.tmp_materials_cap); +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_read_line(ufbxi_context *uc) +{ + ufbxi_dev_assert(!uc->obj.eof); + + size_t offset = 0; + + for (;;) { + const char *begin = ufbxi_add_ptr(uc->data, offset); + const char *end = begin ? (const char*)memchr(begin, '\n', uc->data_size - offset) : NULL; + if (!end) { + if (uc->eof) { + offset = uc->data_size; + uc->obj.eof = true; + break; + } else { + size_t new_cap = ufbxi_max_sz(1, uc->data_size * 2); + ufbxi_check(ufbxi_refill(uc, new_cap, false)); + continue; + } + } + + offset += ufbxi_to_size(end - begin) + 1; + + // Handle line continuations + const char *esc = end; + if (esc > begin && esc[-1] == '\r') esc--; + if (esc > begin && esc[-1] == '\\') { + continue; + } + + break; + } + + size_t line_len = offset; + + uc->obj.line.data = uc->data; + uc->obj.line.length = line_len; + uc->data += line_len; + uc->data_size -= line_len; + + uc->obj.read_progress += line_len; + if (uc->obj.read_progress >= uc->progress_interval) { + ufbxi_check(ufbxi_report_progress(uc)); + uc->obj.read_progress %= uc->progress_interval; + } + + if (uc->obj.eof) { + char *new_data = ufbxi_push(&uc->tmp, char, line_len + 1); + ufbxi_check(new_data); + memcpy(new_data, uc->obj.line.data, line_len); + new_data[line_len] = '\n'; + uc->obj.line.data = new_data; + uc->obj.line.length++; + } + + return 1; +} + +static ufbxi_noinline ufbx_string ufbxi_obj_span_token(ufbxi_context *uc, size_t start_token, size_t end_token) +{ + ufbx_assert(start_token < uc->obj.num_tokens); + end_token = ufbxi_min_sz(end_token, uc->obj.num_tokens - 1); + + ufbx_assert(start_token <= end_token); + ufbx_string start = uc->obj.tokens[start_token]; + ufbx_string end = uc->obj.tokens[end_token]; + size_t num_between = ufbxi_to_size(end.data - start.data); + + ufbx_string result; + result.data = start.data; + result.length = num_between + end.length; + return result; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_tokenize(ufbxi_context *uc) +{ + const char *ptr = uc->obj.line.data, *end = ptr + uc->obj.line.length; + uc->obj.num_tokens = 0; + + for (;;) { + char c; + + // Skip whitespace + for (;;) { + c = *ptr; + if (c == ' ' || c == '\t' || c == '\r') { + ptr++; + continue; + } + + // Treat line continuations as whitespace + if (c == '\\') { + const char *p = ptr + 1; + if (*p == '\r') p++; + if (*p == '\n' && p < end - 1) { + ptr = p + 1; + continue; + } + } + + break; + } + + c = *ptr; + if (c == '\n') break; + if (c == '#' && uc->obj.num_tokens > 0) break; + + size_t index = uc->obj.num_tokens++; + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->obj.tokens, &uc->obj.tokens_cap, index + 1)); + + ufbx_string *tok = &uc->obj.tokens[index]; + tok->data = ptr; + + // Treat comment start as a single token + if (c == '#') { + ptr++; + tok->length = 1; + continue; + } + + for (;;) { + c = *++ptr; + + if (ufbxi_is_space(c)) { + break; + } + + if (c == '\\') { + const char *p = ptr + 1; + if (*p == '\r') p++; + if (*p == '\n' && p < end - 1) { + break; + } + } + } + + tok->length = ufbxi_to_size(ptr - tok->data); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_tokenize_line(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_obj_read_line(uc)); + ufbxi_check(ufbxi_obj_tokenize(uc)); + return 1; +} + +static ufbxi_noinline int ufbxi_obj_parse_vertex(ufbxi_context *uc, ufbxi_obj_attrib attrib, size_t offset) +{ + if (uc->opts.ignore_geometry) return 1; + + ufbxi_buf *dst = &uc->obj.tmp_vertices[attrib]; + size_t num_values = ufbxi_obj_attrib_stride[attrib]; + uc->obj.vertex_count[attrib]++; + + size_t read_values = num_values; + if (attrib == UFBXI_OBJ_ATTRIB_COLOR) { + if (offset + read_values > uc->obj.num_tokens) { + read_values = 3; + } + } + ufbxi_check(offset + read_values <= uc->obj.num_tokens); + + uint32_t parse_flags = uc->double_parse_flags; + ufbx_real *vals = ufbxi_push_fast(dst, ufbx_real, num_values); + ufbxi_check(vals); + for (size_t i = 0; i < read_values; i++) { + ufbx_string str = uc->obj.tokens[offset + i]; + char *end; // ufbxi_uninit + double val = ufbxi_parse_double(str.data, str.length, &end, parse_flags); + ufbxi_check(end == str.data + str.length); + vals[i] = (ufbx_real)val; + } + + if (read_values < num_values) { + ufbx_assert(read_values + 1 == num_values); + ufbx_assert(attrib == UFBXI_OBJ_ATTRIB_COLOR); + vals[read_values] = 1.0f; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_index(ufbxi_context *uc, ufbx_string *s, uint32_t attrib) +{ + const char *ptr = s->data, *end = ptr + s->length; + + bool negative = false; + if (*ptr == '-') { + negative = true; + ptr++; + } + + // As .obj indices are never zero we can detect missing indices + // by simply not writing to it. + uint64_t index = 0; + for (; ptr != end; ptr++) { + char c = *ptr; + if (c >= '0' && c <= '9') { + ufbxi_check(index < UINT64_MAX / 10 - 10); + index = index * 10 + (uint64_t)(c - '0'); + } else if (c == '/') { + ptr++; + break; + } + } + + if (negative) { + size_t count = uc->obj.vertex_count[attrib]; + index = index <= count ? count - index : UINT64_MAX; + } else { + // Corrects to zero based indices and wraps 0 to UINT64_MAX (missing) + index -= 1; + } + + ufbxi_obj_fast_indices *fast_indices = &uc->obj.fast_indices[attrib]; + if (fast_indices->num_left == 0) { + size_t num_push = 128; + uint64_t *dst = ufbxi_push(&uc->obj.tmp_indices[attrib], uint64_t, num_push); + ufbxi_check(dst); + uc->obj.fast_indices[attrib].indices = dst; + uc->obj.fast_indices[attrib].num_left = num_push; + } + + *fast_indices->indices++ = index; + fast_indices->num_left--; + + ufbxi_obj_mesh *mesh = uc->obj.mesh; + + if (index != UINT64_MAX) { + ufbxi_obj_index_range *range = &mesh->vertex_range[attrib]; + range->min_ix = ufbxi_min64(range->min_ix, index); + range->max_ix = ufbxi_max64(range->max_ix, index); + } + + s->data = ptr; + s->length = ufbxi_to_size(end - ptr); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_indices(ufbxi_context *uc, size_t token_begin, size_t num_tokens) +{ + bool flush_mesh = false; + if (uc->obj.object_dirty) { + if (!uc->opts.obj_merge_objects) { + flush_mesh = true; + } + uc->obj.object_dirty = false; + } + + if (uc->obj.group_dirty) { + if (((uc->obj.object.length == 0 || uc->opts.obj_merge_objects) && !uc->opts.obj_merge_groups) || uc->opts.obj_split_groups) { + flush_mesh = true; + } + uc->obj.group_dirty = false; + uc->obj.face_group_dirty = true; + } + + if (!uc->obj.mesh || flush_mesh) { + ufbxi_check(ufbxi_obj_flush_mesh(uc)); + ufbxi_check(ufbxi_obj_push_mesh(uc)); + } + ufbxi_obj_mesh *mesh = uc->obj.mesh; + + if (uc->obj.material_dirty) { + if (uc->obj.usemtl_fbx_id != 0) { + ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, uc->obj.usemtl_fbx_id); + ufbx_assert(entry); + if (mesh->usemtl_base == 0 || entry->user_id < mesh->usemtl_base) { + ufbxi_check(ufbxi_connect_oo(uc, uc->obj.usemtl_fbx_id, mesh->fbx_node_id)); + + uint32_t index = ++uc->obj.usemtl_index; + ufbxi_check(index < UINT32_MAX); + entry->user_id = index; + + if (mesh->usemtl_base == 0) { + mesh->usemtl_base = index; + } + uc->obj.face_material = index - mesh->usemtl_base; + } + uc->obj.face_material = entry->user_id - mesh->usemtl_base; + } else { + uc->obj.face_material = UFBX_NO_INDEX; + } + } + + // EARLY RETURN: Rest of the function should only be related to geometry! + if (uc->opts.ignore_geometry) return 1; + + if (num_tokens == 0 && !uc->opts.allow_empty_faces) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_EMPTY_FACE_REMOVED, "Empty face has been removed")); + return 1; + } + + if (uc->obj.face_group_dirty) { + ufbx_string name = ufbx_empty_string; + if (uc->obj.group.length > 0 && (uc->obj.object.length > 0 || uc->opts.obj_merge_groups) && !uc->opts.obj_split_groups) { + name = uc->obj.group; + } + + uint32_t hash = ufbxi_hash_ptr(name.data); + ufbxi_obj_group_entry *entry = ufbxi_map_find(&uc->obj.group_map, ufbxi_obj_group_entry, hash, &name.data); + if (!entry) { + entry = ufbxi_map_insert(&uc->obj.group_map, ufbxi_obj_group_entry, hash, &name.data); + ufbxi_check(entry); + entry->name = name.data; + entry->mesh_id = 0; + entry->local_id = 0; + } + + uint32_t mesh_id = mesh->fbx_mesh->element_id; + if (entry->mesh_id != mesh_id) { + uint32_t id = mesh->num_groups++; + entry->mesh_id = mesh_id; + entry->local_id = id; + + ufbx_face_group *group = ufbxi_push_zero(&uc->obj.tmp_face_group_infos, ufbx_face_group, 1); + ufbxi_check(group); + group->id = 0; + group->name = name; + } + + uc->obj.face_group = entry->local_id; + + if (!uc->obj.has_face_group) { + uc->obj.has_face_group = true; + ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_face_group, uint32_t, uc->obj.tmp_faces.num_items)); + } + + uc->obj.face_group_dirty = false; + } + + size_t num_indices = num_tokens; + ufbxi_check(UINT32_MAX - mesh->num_indices >= num_indices); + + ufbx_face *face = ufbxi_push_fast(&uc->obj.tmp_faces, ufbx_face, 1); + ufbxi_check(face); + + face->index_begin = (uint32_t)mesh->num_indices; + face->num_indices = (uint32_t)num_indices; + + mesh->num_faces++; + mesh->num_indices += num_indices; + + uint32_t *p_face_mat = ufbxi_push_fast(&uc->obj.tmp_face_material, uint32_t, 1); + ufbxi_check(p_face_mat); + *p_face_mat = uc->obj.face_material; + + if (uc->obj.has_face_smoothing) { + bool *p_face_smooth = ufbxi_push_fast(&uc->obj.tmp_face_smoothing, bool, 1); + ufbxi_check(p_face_smooth); + *p_face_smooth = uc->obj.face_smoothing; + } + + if (uc->obj.has_face_group) { + uint32_t *p_face_group = ufbxi_push_fast(&uc->obj.tmp_face_group, uint32_t, 1); + ufbxi_check(p_face_group); + *p_face_group = uc->obj.face_group; + } + + for (size_t ix = 0; ix < num_indices; ix++) { + ufbx_string tok = uc->obj.tokens[token_begin + ix]; + for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) { + ufbxi_check(ufbxi_obj_parse_index(uc, &tok, attrib)); + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_multi_indices(ufbxi_context *uc, size_t window) +{ + for (size_t begin = 1; begin + window <= uc->obj.num_tokens; begin++) { + ufbxi_check(ufbxi_obj_parse_indices(uc, begin, window)); + } + return 1; +} + +static ufbxi_noinline uint32_t ufbxi_parse_hex(const char *digits, size_t length) +{ + uint32_t value = 0; + + for (size_t i = 0; i < length; i++) { + char c = digits[i]; + uint32_t v = 0; + if (c >= '0' && c <= '9') { + v = (uint32_t)(c - '0'); + } else if (c >= 'A' && c <= 'F') { + v = (uint32_t)(c - 'A') + 10; + } else if (c >= 'a' && c <= 'f') { + v = (uint32_t)(c - 'a') + 10; + } + value = (value << 4) | v; + } + + return value; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_comment(ufbxi_context *uc) +{ + if (uc->obj.num_tokens >= 3 && ufbxi_str_equal(uc->obj.tokens[1], ufbxi_str_c("MRGB"))) { + size_t num_color = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR]; + + // Pop standard vertex colors and replace them with MRGB colors + if (num_color > uc->obj.mrgb_vertex_count) { + size_t num_pop = num_color - uc->obj.mrgb_vertex_count; + ufbxi_pop(&uc->obj.tmp_color_valid, bool, num_pop, NULL); + ufbxi_pop(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, num_pop * 4, NULL); + uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] -= num_pop; + } + + ufbx_string mrgb = uc->obj.tokens[2]; + for (size_t i = 0; i + 8 <= mrgb.length; i += 8) { + ufbx_real *p_rgba = ufbxi_push(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, 4); + bool *p_valid = ufbxi_push(&uc->obj.tmp_color_valid, bool, 1); + ufbxi_check(p_rgba && p_valid); + *p_valid = true; + + uint32_t hex = ufbxi_parse_hex(mrgb.data + i, 8); + p_rgba[0] = (ufbx_real)((hex >> 16u) & 0xff) / 255.0f; + p_rgba[1] = (ufbx_real)((hex >> 8u) & 0xff) / 255.0f; + p_rgba[2] = (ufbx_real)((hex >> 0u) & 0xff) / 255.0f; + p_rgba[3] = (ufbx_real)((hex >> 24u) & 0xff) / 255.0f; + } + + uc->obj.has_vertex_color = true; + } + + if (!uc->opts.disable_quirks) { + if (ufbxi_match(&uc->obj.line, "\\s*#\\s*File exported by ZBrush.*")) { + if (!uc->obj.mesh) { + uc->opts.obj_merge_groups = true; + } + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_material(ufbxi_context *uc) +{ + uc->obj.material_dirty = true; + + // Allow empty `usemtl` lines to specify "no material". + if (uc->obj.num_tokens < 2) { + uc->obj.usemtl_fbx_id = 0; + return 1; + } + + ufbx_string name = ufbxi_obj_span_token(uc, 1, SIZE_MAX); + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &name, false)); + + uint64_t fbx_id = ufbxi_synthetic_id_from_string(uc, name.data); + ufbxi_check(fbx_id); + + ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, fbx_id); + + uc->obj.usemtl_fbx_id = fbx_id; + + if (!entry) { + ufbxi_element_info info = { 0 }; + info.fbx_id = fbx_id; + info.name = name; + + ufbx_material *material = ufbxi_push_element(uc, &info, ufbx_material, UFBX_ELEMENT_MATERIAL); + ufbxi_check(material); + + material->shader_type = UFBX_SHADER_WAVEFRONT_MTL; + material->shading_model_name.data = ufbxi_empty_char; + material->shader_prop_prefix.data = ufbxi_empty_char; + + size_t id = material->element_id; + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->obj.tmp_materials, &uc->obj.tmp_materials_cap, id + 1)); + uc->obj.tmp_materials[id] = material; + } + + return 1; +} + +#define ufbxi_obj_cmd1(a) ((uint32_t)(a)<<24u) +#define ufbxi_obj_cmd2(a,b) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16) +#define ufbxi_obj_cmd3(a,b,c) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16 | (uint32_t)(c)<<8u) + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_vertices(ufbxi_context *uc, ufbx_real_list *dst, uint32_t attrib, uint64_t min_index) +{ + size_t stride = ufbxi_obj_attrib_stride[attrib]; + ufbxi_check(min_index < uc->obj.tmp_vertices[attrib].num_items / stride); + + size_t count = uc->obj.tmp_vertices[attrib].num_items - (size_t)min_index * stride; + ufbx_real *data = ufbxi_push(&uc->result, ufbx_real, count + 4); + ufbxi_check(data); + + data[0] = 0.0f; + data[1] = 0.0f; + data[2] = 0.0f; + data[3] = 0.0f; + data += 4; + + ufbxi_pop(&uc->obj.tmp_vertices[attrib], ufbx_real, count, data); + + dst->data = data; + dst->count = count; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_setup_attrib(ufbxi_context *uc, ufbxi_obj_mesh *mesh, uint64_t *tmp_indices, + ufbx_vertex_attrib *dst, const ufbx_real_list *p_data, uint32_t attrib, bool non_disjoint, bool required) +{ + ufbx_real_list data = *p_data; + + size_t num_indices = mesh->num_indices; + size_t stride = ufbxi_obj_attrib_stride[attrib]; + size_t num_values = data.count / stride; + + uint64_t mesh_min_ix = mesh->vertex_range[attrib].min_ix; + if (num_indices == 0 || num_values == 0 || mesh_min_ix == UINT64_MAX) { + ufbxi_check(num_indices == 0 || !required); + + // Pop indices without copying if the attribute is not used + ufbxi_pop(&uc->obj.tmp_indices[attrib], uint64_t, num_indices, NULL); + return 1; + } + + uint64_t min_index = non_disjoint ? 0 : mesh_min_ix; + + ufbxi_pop(&uc->obj.tmp_indices[attrib], uint64_t, num_indices, tmp_indices); + + uint32_t *dst_indices = ufbxi_push(&uc->result, uint32_t, num_indices); + ufbxi_check(dst_indices); + + dst->exists = true; + + dst->values.data = data.data; + dst->values.count = num_values; + + dst->indices.data = dst_indices; + dst->indices.count = num_indices; + + ufbxi_nounroll for (size_t i = 0; i < num_indices; i++) { + uint64_t ix = tmp_indices[i]; + if (ix != UINT64_MAX) { + ix -= min_index; + ufbxi_check(ix < UINT32_MAX); + } + if (ix < num_values) { + dst_indices[i] = (uint32_t)ix; + } else { + ufbxi_check(ufbxi_fix_index(uc, &dst_indices[i], (uint32_t)ix, num_values)); + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pad_colors(ufbxi_context *uc, size_t num_vertices) +{ + if (uc->opts.ignore_geometry) return 1; + + size_t num_colors = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR]; + if (num_vertices > num_colors) { + size_t num_pad = num_vertices - num_colors; + ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_vertices[UFBXI_OBJ_ATTRIB_COLOR], ufbx_real, num_pad * 4)); + ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_color_valid, bool, num_pad)); + uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] += num_pad; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_pop_meshes(ufbxi_context *uc) +{ + size_t num_meshes = uc->obj.tmp_meshes.num_items; + ufbxi_obj_mesh *meshes = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_meshes, ufbxi_obj_mesh, num_meshes); + ufbxi_check(meshes); + + if (uc->obj.has_vertex_color) { + ufbxi_check(ufbxi_obj_pad_colors(uc, uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_POSITION])); + } + + // Pop unused fast indices + for (size_t i = 0; i < UFBXI_OBJ_NUM_ATTRIBS; i++) { + ufbxi_pop(&uc->obj.tmp_indices[i], uint64_t, uc->obj.fast_indices[i].num_left, NULL); + } + + // Check if the file has disjoint vertices + bool non_disjoint[UFBXI_OBJ_NUM_ATTRIBS] = { 0 }; + uint64_t next_min[UFBXI_OBJ_NUM_ATTRIBS] = { 0 }; + ufbx_real_list vertices[UFBXI_OBJ_NUM_ATTRIBS_EXT] = { 0 }; + bool *color_valid = NULL; + + size_t max_indices = 0; + + for (size_t i = 0; i < num_meshes; i++) { + ufbxi_obj_mesh *mesh = &meshes[i]; + max_indices = ufbxi_max_sz(max_indices, mesh->num_indices); + ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) { + ufbxi_obj_index_range range = mesh->vertex_range[attrib]; + if (range.min_ix > range.max_ix) continue; + if (range.min_ix < next_min[attrib]) { + non_disjoint[attrib] = true; + } + next_min[attrib] = range.max_ix + 1; + } + } + + uint64_t *tmp_indices = ufbxi_push(&uc->tmp, uint64_t, max_indices); + ufbxi_check(tmp_indices); + + ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) { + if (!non_disjoint[attrib]) continue; + ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[attrib], attrib, 0)); + } + if (uc->obj.has_vertex_color && non_disjoint[UFBXI_OBJ_ATTRIB_POSITION]) { + ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[UFBXI_OBJ_ATTRIB_COLOR], UFBXI_OBJ_ATTRIB_COLOR, 0)); + color_valid = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_color_valid, bool, vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4); + ufbxi_check(color_valid); + } + + for (size_t i = num_meshes; i > 0; i--) { + ufbxi_obj_mesh *mesh = &meshes[i - 1]; + + ufbx_mesh *fbx_mesh = mesh->fbx_mesh; + + size_t num_faces = mesh->num_faces; + + if (!uc->opts.ignore_geometry) { + ufbxi_nounroll for (uint32_t attrib = 0; attrib < UFBXI_OBJ_NUM_ATTRIBS; attrib++) { + if (non_disjoint[attrib]) continue; + uint64_t min_ix = mesh->vertex_range[attrib].min_ix; + if (min_ix < UINT64_MAX) { + ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[attrib], attrib, min_ix)); + } + } + if (uc->obj.has_vertex_color && !non_disjoint[UFBXI_OBJ_ATTRIB_POSITION]) { + uint64_t min_ix = mesh->vertex_range[UFBXI_OBJ_ATTRIB_POSITION].min_ix; + ufbxi_check(min_ix < UINT64_MAX); + ufbxi_check(ufbxi_obj_pop_vertices(uc, &vertices[UFBXI_OBJ_ATTRIB_COLOR], UFBXI_OBJ_ATTRIB_COLOR, min_ix)); + color_valid = ufbxi_push_pop(&uc->tmp, &uc->obj.tmp_color_valid, bool, vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4); + ufbxi_check(color_valid); + } + + fbx_mesh->faces.count = num_faces; + fbx_mesh->face_material.count = num_faces; + + fbx_mesh->faces.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_faces, ufbx_face, num_faces); + fbx_mesh->face_material.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_material, uint32_t, num_faces); + + ufbxi_check(fbx_mesh->faces.data); + ufbxi_check(fbx_mesh->face_material.data); + + if (uc->obj.has_face_smoothing) { + fbx_mesh->face_smoothing.count = num_faces; + fbx_mesh->face_smoothing.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_smoothing, bool, num_faces); + ufbxi_check(fbx_mesh->face_smoothing.data); + } + + if (uc->obj.has_face_group) { + if (mesh->num_groups > 1) { + fbx_mesh->face_group.count = num_faces; + fbx_mesh->face_group.data = ufbxi_push_pop(&uc->result, &uc->obj.tmp_face_group, uint32_t, num_faces); + ufbxi_check(fbx_mesh->face_group.data); + } else { + ufbxi_pop(&uc->obj.tmp_face_group, uint32_t, num_faces, NULL); + } + } + + ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_position, + &vertices[UFBXI_OBJ_ATTRIB_POSITION], UFBXI_OBJ_ATTRIB_POSITION, non_disjoint[UFBXI_OBJ_ATTRIB_POSITION], true)); + + ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_uv, + &vertices[UFBXI_OBJ_ATTRIB_UV], UFBXI_OBJ_ATTRIB_UV, non_disjoint[UFBXI_OBJ_ATTRIB_UV], false)); + + ufbxi_check(ufbxi_obj_setup_attrib(uc, mesh, tmp_indices, (ufbx_vertex_attrib*)&fbx_mesh->vertex_normal, + &vertices[UFBXI_OBJ_ATTRIB_NORMAL], UFBXI_OBJ_ATTRIB_NORMAL, non_disjoint[UFBXI_OBJ_ATTRIB_NORMAL], false)); + + if (uc->obj.has_vertex_color) { + ufbx_assert(color_valid); + bool has_color = false; + bool all_valid = true; + size_t max_index = fbx_mesh->vertex_position.values.count; + ufbxi_for_list(uint32_t, p_ix, fbx_mesh->vertex_position.indices) { + if (*p_ix < max_index) { + if (color_valid[*p_ix]) { + has_color = true; + } else { + all_valid = false; + } + } + } + + if (has_color) { + fbx_mesh->vertex_color.exists = true; + fbx_mesh->vertex_color.values.data = (ufbx_vec4*)vertices[UFBXI_OBJ_ATTRIB_COLOR].data; + fbx_mesh->vertex_color.values.count = vertices[UFBXI_OBJ_ATTRIB_COLOR].count / 4; + fbx_mesh->vertex_color.indices = fbx_mesh->vertex_position.indices; + fbx_mesh->vertex_color.unique_per_vertex = true; + + if (!all_valid) { + uint32_t *indices = fbx_mesh->vertex_color.indices.data; + indices = ufbxi_push_copy(&uc->result, uint32_t, mesh->num_indices, indices); + ufbxi_check(indices); + + size_t num_values = fbx_mesh->vertex_color.values.count; + ufbxi_for(uint32_t, p_ix, indices, mesh->num_indices) { + if (*p_ix >= num_values || !color_valid[*p_ix]) { + ufbxi_check(ufbxi_fix_index(uc, p_ix, *p_ix, num_values)); + } + } + + fbx_mesh->vertex_color.indices.data = indices; + } + } + } + } + + ufbxi_check(ufbxi_finalize_mesh(&uc->result, &uc->error, fbx_mesh)); + + if (uc->retain_mesh_parts) { + fbx_mesh->face_group_parts.count = mesh->num_groups; + fbx_mesh->face_group_parts.data = ufbxi_push_zero(&uc->result, ufbx_mesh_part, mesh->num_groups); + ufbxi_check(fbx_mesh->face_group_parts.data); + } + + if (mesh->num_groups > 1) { + ufbxi_check(ufbxi_update_face_groups(&uc->result, &uc->error, fbx_mesh, false)); + } else if (mesh->num_groups == 1) { + fbx_mesh->face_group.data = (uint32_t*)ufbxi_sentinel_index_zero; + fbx_mesh->face_group.count = num_faces; + // NOTE: Consecutive and zero indices are always allocated so we can skip doing it here, + // see HACK(consecutiv-faces).. + if (fbx_mesh->face_group_parts.count > 0) { + ufbx_mesh_part *part = &fbx_mesh->face_group_parts.data[0]; + part->num_faces = fbx_mesh->num_faces; + part->num_faces = num_faces; + part->num_empty_faces = fbx_mesh->num_empty_faces; + part->num_point_faces = fbx_mesh->num_point_faces; + part->num_line_faces = fbx_mesh->num_line_faces; + part->num_triangles = fbx_mesh->num_triangles; + part->face_indices.data = (uint32_t*)ufbxi_sentinel_index_consecutive; + part->face_indices.count = num_faces; + } + } + + // HACK(consecutive-faces): Prepare for finalize to re-use a consecutive/zero + // index buffer for face materials.. + uc->max_zero_indices = ufbxi_max_sz(uc->max_zero_indices, num_faces); + uc->max_consecutive_indices = ufbxi_max_sz(uc->max_consecutive_indices, num_faces); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_file(ufbxi_context *uc) +{ + while (!uc->obj.eof) { + ufbxi_check(ufbxi_obj_tokenize_line(uc)); + size_t num_tokens = uc->obj.num_tokens; + if (num_tokens == 0) continue; + + ufbx_string cmd = uc->obj.tokens[0]; + uint32_t key = ufbxi_get_name_key(cmd.data, cmd.length); + if (key == ufbxi_obj_cmd1('v')) { + ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_POSITION, 1)); + if (num_tokens >= 7) { + size_t num_vertices = uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_POSITION]; + uc->obj.has_vertex_color = true; + ufbxi_check(ufbxi_obj_pad_colors(uc, num_vertices - 1)); + if (uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] < num_vertices) { + ufbx_assert(uc->obj.vertex_count[UFBXI_OBJ_ATTRIB_COLOR] == num_vertices - 1); + ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_COLOR, 4)); + bool *valid = ufbxi_push(&uc->obj.tmp_color_valid, bool, 1); + ufbxi_check(valid); + *valid = true; + } + } + } else if (key == ufbxi_obj_cmd2('v','t')) { + ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_UV, 1)); + } else if (key == ufbxi_obj_cmd2('v','n')) { + ufbxi_check(ufbxi_obj_parse_vertex(uc, UFBXI_OBJ_ATTRIB_NORMAL, 1)); + } else if (key == ufbxi_obj_cmd1('f')) { + ufbxi_check(ufbxi_obj_parse_indices(uc, 1, uc->obj.num_tokens - 1)); + } else if (key == ufbxi_obj_cmd1('p')) { + ufbxi_check(ufbxi_obj_parse_multi_indices(uc, 1)); + } else if (key == ufbxi_obj_cmd1('l')) { + ufbxi_check(ufbxi_obj_parse_multi_indices(uc, 2)); + } else if (key == ufbxi_obj_cmd1('s')) { + if (num_tokens >= 2) { + uc->obj.has_face_smoothing = true; + uc->obj.face_smoothing = !ufbxi_str_equal(uc->obj.tokens[1], ufbxi_str_c("off")); + + // Fill in previously missed face smoothing data + if (uc->obj.tmp_face_smoothing.num_items == 0 && uc->obj.tmp_faces.num_items > 0) { + ufbxi_check(ufbxi_push_zero(&uc->obj.tmp_face_smoothing, bool, uc->obj.tmp_faces.num_items)); + } + } + } else if (key == ufbxi_obj_cmd1('o')) { + if (num_tokens >= 2) { + uc->obj.object = ufbxi_obj_span_token(uc, 1, SIZE_MAX); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->obj.object, false)); + uc->obj.object_dirty = true; + } + } else if (key == ufbxi_obj_cmd1('g')) { + if (num_tokens >= 2) { + uc->obj.group = ufbxi_obj_span_token(uc, 1, SIZE_MAX); + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &uc->obj.group, false)); + uc->obj.group_dirty = true; + } else { + uc->obj.group = ufbx_empty_string; + uc->obj.group_dirty = true; + } + } else if (key == ufbxi_obj_cmd1('#')) { + ufbxi_check(ufbxi_obj_parse_comment(uc)); + } else if (ufbxi_str_equal(cmd, ufbxi_str_c("mtllib"))) { + ufbxi_check(uc->obj.num_tokens >= 2); + ufbx_string lib = ufbxi_obj_span_token(uc, 1, SIZE_MAX); + lib.data = ufbxi_push_copy(&uc->tmp, char, lib.length + 1, lib.data); + ufbxi_check(lib.data); + uc->obj.mtllib_relative_path.data = lib.data; + uc->obj.mtllib_relative_path.size = lib.length; + } else if (ufbxi_str_equal(cmd, ufbxi_str_c("usemtl"))) { + ufbxi_check(ufbxi_obj_parse_material(uc)); + } else if (!uc->opts.disable_quirks && key == 0) { + // ZBrush exporter seems to end the files with '\0', sometimes.. + } else { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_UNKNOWN_OBJ_DIRECTIVE, "Unknown .obj directive, skipped line")); + } + } + + ufbxi_check(ufbxi_obj_flush_mesh(uc)); + ufbxi_check(ufbxi_obj_pop_meshes(uc)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_flush_material(ufbxi_context *uc) +{ + if (uc->obj.usemtl_fbx_id == 0) return 1; + + ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, uc->obj.usemtl_fbx_id); + ufbx_assert(entry); + ufbx_material *material = uc->obj.tmp_materials[entry->element_id]; + + size_t num_props = uc->obj.tmp_props.num_items; + ufbxi_check(ufbxi_obj_pop_props(uc, &material->props.props, num_props)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_prop(ufbxi_context *uc, ufbx_string name, size_t start, bool include_rest, size_t *p_next) +{ + if (start >= uc->obj.num_tokens) { + if (p_next) { + *p_next = start; + } + return 1; + } + + ufbx_prop *prop = ufbxi_push_zero(&uc->obj.tmp_props, ufbx_prop, 1); + ufbxi_check(prop); + prop->name = name; + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop->name, false)); + + uint32_t flags = UFBX_PROP_FLAG_VALUE_STR; + + size_t num_reals = 0; + for (; num_reals < 4; num_reals++) { + if (start + num_reals >= uc->obj.num_tokens) break; + ufbx_string tok = uc->obj.tokens[start + num_reals]; + + char *end; // ufbxi_uninit + double val = ufbxi_parse_double(tok.data, tok.length, &end, uc->double_parse_flags); + if (end != tok.data + tok.length) break; + + prop->value_real_arr[num_reals] = (ufbx_real)val; + if (num_reals == 0) { + prop->value_int = ufbxi_f64_to_i64(val); + flags |= UFBX_PROP_FLAG_VALUE_INT; + } + } + + size_t num_args = 0; + if (!include_rest) { + for (; start + num_args < uc->obj.num_tokens - 1; num_args++) { + if (ufbxi_match(&uc->obj.tokens[start + num_args], "-[A-Za-z][\\-A-Za-z0-9_]*")) break; + } + } + + if (num_args > 0 || include_rest) { + ufbx_string span = ufbxi_obj_span_token(uc, start, include_rest ? SIZE_MAX : start + num_args - 1); + prop->value_str = span; + prop->value_blob.data = span.data; + prop->value_blob.size = span.length; + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop->value_str, false)); + ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &prop->value_blob, true)); + } else { + prop->value_str.data = ufbxi_empty_char; + } + + if (num_reals > 0) { + flags = (uint32_t)UFBX_PROP_FLAG_VALUE_REAL << (num_reals - 1); + } else { + if (!strcmp(prop->value_str.data, "on")) { + prop->value_int = 1; + prop->value_real = 1.0f; + flags |= UFBX_PROP_FLAG_VALUE_INT; + } else if (!strcmp(prop->value_str.data, "off")) { + prop->value_int = 0; + prop->value_real = 0.0f; + flags |= UFBX_PROP_FLAG_VALUE_INT; + } + } + + prop->flags = (ufbx_prop_flags)flags; + + if (p_next) { + *p_next = start + num_args; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_mtl_map(ufbxi_context *uc, size_t prefix_len) +{ + if (uc->obj.num_tokens < 2) return 1; + + size_t num_props = 1; + ufbxi_check(ufbxi_obj_parse_prop(uc, ufbxi_str_c("obj|args"), 1, true, NULL)); + + size_t start = 1; + for (; start + 1 < uc->obj.num_tokens; ) { + ufbx_string tok = uc->obj.tokens[start]; + if (ufbxi_match(&tok, "-[A-Za-z][\\-A-Za-z0-9_]*")) { + tok.data += 1; + tok.length -= 1; + ufbxi_check(ufbxi_obj_parse_prop(uc, tok, start + 1, false, &start)); + num_props++; + } else { + break; + } + } + + ufbx_string tex_str = ufbxi_obj_span_token(uc, start, SIZE_MAX); + ufbx_blob tex_raw = { tex_str.data, tex_str.length }; + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &tex_str, false)); + ufbxi_check(ufbxi_push_string_place_blob(&uc->string_pool, &tex_raw, true)); + + uint64_t fbx_id = 0; + ufbx_texture *texture = ufbxi_push_synthetic_element(uc, &fbx_id, NULL, "", ufbx_texture, UFBX_ELEMENT_TEXTURE); + ufbxi_check(texture); + + texture->filename.data = ufbxi_empty_char; + texture->absolute_filename.data = ufbxi_empty_char; + texture->uv_set.data = ufbxi_empty_char; + + texture->relative_filename = tex_str; + texture->raw_relative_filename = tex_raw; + + ufbxi_check(ufbxi_obj_pop_props(uc, &texture->props.props, num_props)); + + ufbx_string prop = uc->obj.tokens[0]; + ufbx_assert(prop.length >= prefix_len); + prop.data += prefix_len; + prop.length -= prefix_len; + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prop, false)); + + if (uc->obj.usemtl_fbx_id != 0) { + ufbxi_check(ufbxi_connect_op(uc, fbx_id, uc->obj.usemtl_fbx_id, prop)); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_parse_mtl(ufbxi_context *uc) +{ + uc->obj.mesh = NULL; + uc->obj.usemtl_fbx_id = 0; + + while (!uc->obj.eof) { + ufbxi_check(ufbxi_obj_tokenize_line(uc)); + size_t num_tokens = uc->obj.num_tokens; + if (num_tokens == 0) continue; + + ufbx_string cmd = uc->obj.tokens[0]; + if (ufbxi_str_equal(cmd, ufbxi_str_c("newmtl"))) { + // HACK: Reuse mesh material parsing, but don't allow for empty material name + ufbxi_check(uc->obj.num_tokens >= 2); + ufbxi_check(ufbxi_obj_flush_material(uc)); + ufbxi_check(ufbxi_obj_parse_material(uc)); + } else if (cmd.length > 4 && !memcmp(cmd.data, "map_", 4)) { + ufbxi_check(ufbxi_obj_parse_mtl_map(uc, 4)); + } else if (cmd.length == 4 && (!memcmp(cmd.data, "bump", 4) || !memcmp(cmd.data, "disp", 4) || !memcmp(cmd.data, "norm", 4))) { + ufbxi_check(ufbxi_obj_parse_mtl_map(uc, 0)); + } else if (cmd.length == 1 && cmd.data[0] == '#') { + // Implement .mtl magic comment handling here if necessary + } else { + ufbxi_check(ufbxi_obj_parse_prop(uc, uc->obj.tokens[0], 1, true, NULL)); + } + } + + ufbxi_check(ufbxi_obj_flush_material(uc)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_load_mtl(ufbxi_context *uc) +{ + // HACK: Reset everything and switch to loading the .mtl file globally + if (uc->close_fn) { + uc->close_fn(uc->read_user); + } + + uc->read_fn = NULL; + uc->close_fn = NULL; + uc->read_user = NULL; + uc->data_begin = NULL; + uc->data = NULL; + uc->data_size = 0; + uc->yield_size = 0; + uc->eof = false; + uc->obj.eof = false; + + if (uc->opts.obj_mtl_data.size > 0) { + uc->data_begin = uc->data = (const char*)uc->opts.obj_mtl_data.data; + uc->data_size = uc->opts.obj_mtl_data.size; + ufbxi_check(ufbxi_obj_parse_mtl(uc)); + return 1; + } + + ufbx_stream stream = { 0 }; + bool has_stream = false; + bool needs_stream = false; + ufbx_blob stream_path = { 0 }; + + if (uc->opts.open_file_cb.fn) { + if (uc->opts.obj_mtl_path.length > 0) { + has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, uc->opts.obj_mtl_path.data, uc->opts.obj_mtl_path.length, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL); + stream_path.data = uc->opts.obj_mtl_path.data; + stream_path.size = uc->opts.obj_mtl_path.length; + needs_stream = true; + if (!has_stream) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Could not open .mtl file: %s", uc->opts.obj_mtl_path.data)); + } + } + + if (!has_stream && uc->opts.load_external_files && uc->obj.mtllib_relative_path.size > 0) { + ufbx_blob dst; // ufbxi_uninit + ufbxi_check(ufbxi_resolve_relative_filename(uc, (ufbxi_strblob*)&dst, (const ufbxi_strblob*)&uc->obj.mtllib_relative_path, true)); + has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, (const char*)dst.data, dst.size, &uc->obj.mtllib_relative_path, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL); + stream_path = uc->obj.mtllib_relative_path; + needs_stream = true; + if (!has_stream) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Could not open .mtl file: %s", dst.data)); + } + } + + ufbx_string path = uc->scene.metadata.filename; + if (!has_stream && uc->opts.load_external_files && uc->opts.obj_search_mtl_by_filename && path.length > 4) { + ufbx_string ext = { path.data + path.length - 4, 4 }; + if (ufbxi_match(&ext, "\\c.obj")) { + ufbxi_analysis_assert(path.length < SIZE_MAX - 1); + char *copy = ufbxi_push_copy(&uc->tmp, char, path.length + 1, path.data); + ufbxi_check(copy); + copy[path.length - 3] = copy[path.length - 3] == 'O' ? 'M' : 'm'; + copy[path.length - 2] = copy[path.length - 2] == 'B' ? 'T' : 't'; + copy[path.length - 1] = copy[path.length - 1] == 'J' ? 'L' : 'l'; + has_stream = ufbxi_open_file(&uc->opts.open_file_cb, &stream, copy, path.length, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_OBJ_MTL); + if (has_stream) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_IMPLICIT_MTL, "Opened .mtl file derived from .obj filename: %s", copy)); + } + } + } + } + + if (has_stream) { + // Adopt `stream` to ufbx read callbacks + uc->read_fn = stream.read_fn; + uc->close_fn = stream.close_fn; + uc->read_user = stream.user; + + int ok = ufbxi_obj_parse_mtl(uc); + + if (uc->close_fn) { + uc->close_fn(uc->read_user); + } + uc->read_fn = NULL; + uc->close_fn = NULL; + uc->read_user = NULL; + + ufbxi_check(ok); + } else if (needs_stream && !uc->opts.ignore_missing_external_files) { + ufbxi_set_err_info(&uc->error, (const char*)stream_path.data, stream_path.size); + ufbxi_fail_msg("ufbxi_obj_load_mtl()", "External file not found"); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_obj_load(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_obj_init(uc)); + ufbxi_check(ufbxi_obj_parse_file(uc)); + ufbxi_check(ufbxi_init_file_paths(uc)); + ufbxi_check(ufbxi_obj_load_mtl(uc)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_mtl_load(ufbxi_context *uc) +{ + ufbxi_check(ufbxi_obj_init(uc)); + ufbxi_check(ufbxi_init_file_paths(uc)); + ufbxi_check(ufbxi_obj_parse_mtl(uc)); + + return 1; +} + +#else +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_obj_load(ufbxi_context *uc) +{ + ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_FORMAT_OBJ"); + ufbxi_fail_msg("UFBXI_FEATURE_FORMAT_OBJ", "Feature disabled"); +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_mtl_load(ufbxi_context *uc) +{ + ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_FORMAT_OBJ"); + ufbxi_fail_msg("UFBXI_FEATURE_FORMAT_OBJ", "Feature disabled"); +} + +static ufbxi_forceinline void ufbxi_obj_free(ufbxi_context *uc) +{ +} +#endif + +// -- Scene pre-processing + +typedef struct { + ufbx_element *src, *dst; +} ufbxi_pre_connection; + +typedef struct { + bool has_constant_scale; + bool has_recursive_scale_helper; + bool has_skin_deformer; + ufbx_vec3 constant_scale; + uint32_t element_id; + uint32_t first_child; + uint32_t next_child; + uint32_t parent; +} ufbxi_pre_node; + +typedef struct { + bool has_skin_deformer; +} ufbxi_pre_mesh; + +typedef struct { + bool has_constant_value; + ufbx_vec3 constant_value; +} ufbxi_pre_anim_value; + +static bool ufbxi_pivot_nonzero(ufbx_vec3 offset) +{ + // TODO: Expose this as a setting? + const double epsilon = 0.0009765625; + return ufbx_fabs(offset.x) >= epsilon || ufbx_fabs(offset.y) >= epsilon || ufbx_fabs(offset.z) >= epsilon; +} + +static ufbx_real ufbxi_pivot_div(ufbx_real offset, ufbx_real initial_scale) +{ + const double epsilon = 0.0078125; + if (ufbx_fabs(initial_scale) >= epsilon) { + return offset / initial_scale; + } else { + return offset; + } +} + +// Called between parsing and `ufbxi_finalize_scene()`. +// This is a very messy function reminiscent of the _old_ ufbx, where we do +// multiple passes over connections without having a proper scene graph. +// This, however gives us the advantage of allowing us to modify elements +// and connections. We can, for example, add new helper nodes and redirect +// animated properties from source nodes to the helpers. The rest of ufbx +// will treat these as if they were a part of the source file. +ufbxi_nodiscard ufbxi_noinline static int ufbxi_pre_finalize_scene(ufbxi_context *uc) +{ + bool required = false; + if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES || uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY) required = true; + if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_HELPER_NODES || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) required = true; + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT || uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT) required = true; +#if defined(UFBX_REGRESSION) + required = true; +#endif + + if (!required) return 1; + + uint32_t num_elements = uc->num_elements; + size_t num_nodes = uc->tmp_node_ids.num_items; + ufbx_element **elements = ufbxi_push_pop(&uc->tmp_parse, &uc->tmp_element_ptrs, ufbx_element*, num_elements); + ufbxi_check(elements); + + size_t num_connections = uc->tmp_connections.num_items; + ufbxi_tmp_connection *tmp_connections = ufbxi_push_peek(&uc->tmp_parse, &uc->tmp_connections, ufbxi_tmp_connection, num_connections); + ufbxi_check(tmp_connections); + + ufbxi_pre_connection *pre_connections = ufbxi_push(&uc->tmp_parse, ufbxi_pre_connection, num_connections); + ufbxi_check(pre_connections); + + uint32_t *instance_counts = ufbxi_push_zero(&uc->tmp_parse, uint32_t, num_elements); + ufbxi_check(instance_counts); + + bool *modify_not_supported = ufbxi_push_zero(&uc->tmp_parse, bool, num_elements); + ufbxi_check(modify_not_supported); + + ufbx_element_type *node_attrib_type = ufbxi_push_zero(&uc->tmp_parse, ufbx_element_type, num_nodes); + ufbxi_check(node_attrib_type); + + bool *has_unscaled_children = ufbxi_push_zero(&uc->tmp_parse, bool, num_nodes); + ufbxi_check(has_unscaled_children); + + bool *has_scale_animation = ufbxi_push_zero(&uc->tmp_parse, bool, num_nodes); + ufbxi_check(has_scale_animation); + + ufbxi_pre_node *pre_nodes = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_node, num_nodes); + ufbxi_check(pre_nodes); + + size_t num_meshes = uc->tmp_typed_element_offsets[UFBX_ELEMENT_MESH].num_items; + ufbxi_pre_mesh *pre_meshes = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_mesh, num_meshes); + ufbxi_check(pre_meshes); + + size_t num_anim_values = uc->tmp_typed_element_offsets[UFBX_ELEMENT_ANIM_VALUE].num_items; + ufbxi_pre_anim_value *pre_anim_values = ufbxi_push_zero(&uc->tmp_parse, ufbxi_pre_anim_value, num_anim_values); + ufbxi_check(pre_anim_values); + + uint64_t *fbx_ids = ufbxi_push_pop(&uc->tmp_parse, &uc->tmp_element_fbx_ids, uint64_t, num_elements); + ufbxi_check(fbx_ids); + + // TODO + const ufbx_real scale_epsilon = 0.001f; + const ufbx_real pivot_epsilon = 0.001f; + const ufbx_real compensate_epsilon = 0.01f; + + for (size_t i = 0; i < num_elements; i++) { + ufbx_element *element = elements[i]; + uint32_t id = element->typed_id; + + if (element->type == UFBX_ELEMENT_NODE) { + ufbxi_pre_node *pre_node = &pre_nodes[id]; + pre_node->has_constant_scale = true; + pre_node->constant_scale = ufbxi_find_vec3(&element->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); + pre_node->element_id = element->element_id; + pre_node->first_child = ~0u; + pre_node->next_child = ~0u; + pre_node->parent = ~0u; + } if (element->type == UFBX_ELEMENT_ANIM_VALUE) { + ufbxi_pre_anim_value *pre_value = &pre_anim_values[id]; + pre_value->has_constant_value = true; + pre_value->constant_value.x = ufbxi_find_real(&element->props, ufbxi_X, UFBX_NAN); + pre_value->constant_value.x = ufbxi_find_real(&element->props, ufbxi_d_X, pre_value->constant_value.x); + pre_value->constant_value.y = ufbxi_find_real(&element->props, ufbxi_Y, UFBX_NAN); + pre_value->constant_value.y = ufbxi_find_real(&element->props, ufbxi_d_Y, pre_value->constant_value.y); + pre_value->constant_value.z = ufbxi_find_real(&element->props, ufbxi_Z, UFBX_NAN); + pre_value->constant_value.z = ufbxi_find_real(&element->props, ufbxi_d_Z, pre_value->constant_value.z); + } + } + + for (size_t i = 0; i < num_connections; i++) { + ufbxi_tmp_connection *tmp = &tmp_connections[i]; + ufbxi_pre_connection *pre = &pre_connections[i]; + + ufbxi_fbx_id_entry *src_entry = ufbxi_find_fbx_id(uc, tmp->src); + ufbxi_fbx_id_entry *dst_entry = ufbxi_find_fbx_id(uc, tmp->dst); + + ufbx_element *src = src_entry ? elements[src_entry->element_id] : NULL; + ufbx_element *dst = dst_entry ? elements[dst_entry->element_id] : NULL; + pre->src = src; + pre->dst = dst; + if (!src || !dst) continue; + + if (tmp->src_prop.length == 0 && tmp->dst_prop.length == 0) { + // Count number of instances of each attribute + if (dst->type == UFBX_ELEMENT_NODE) { + ufbx_node *dst_node = (ufbx_node*)dst; + + if (src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) { + uint32_t count = ++instance_counts[src->element_id]; + node_attrib_type[dst->typed_id] = count == 1 ? src->type : UFBX_ELEMENT_UNKNOWN; + + // These must match what can be trasnsformed in `ufbxi_modify_geometry()` + switch (src->type) { + case UFBX_ELEMENT_MESH: + case UFBX_ELEMENT_LINE_CURVE: + case UFBX_ELEMENT_NURBS_CURVE: + case UFBX_ELEMENT_NURBS_SURFACE: + break; // Nop, supported + default: + modify_not_supported[dst->element_id] = true; + break; + } + } + + if (src->type == UFBX_ELEMENT_NODE) { + ufbx_node *src_node = (ufbx_node*)src; + ufbxi_pre_node *pre_dst = &pre_nodes[dst_node->typed_id]; + ufbxi_pre_node *pre_src = &pre_nodes[src_node->typed_id]; + + // Remember parent and add children into a linked list + if (pre_src->parent == ~0u) { + pre_src->parent = dst_node->typed_id; + pre_src->next_child = pre_dst->first_child; + pre_dst->first_child = src_node->typed_id; + } + + if (uc->opts.inherit_mode_handling != UFBX_INHERIT_MODE_HANDLING_PRESERVE) { + if (!dst_node->is_root && src_node->original_inherit_mode != UFBX_INHERIT_MODE_NORMAL) { + has_unscaled_children[dst->typed_id] = true; + } + } + } + } else if (dst->type == UFBX_ELEMENT_MESH) { + if (src->type == UFBX_ELEMENT_SKIN_DEFORMER) { + ufbxi_pre_mesh *pre_mesh = &pre_meshes[dst->typed_id]; + pre_mesh->has_skin_deformer = true; + } + } + } else if (tmp->src_prop.length == 0 && tmp->dst_prop.length != 0) { + const char *dst_prop = tmp->dst_prop.data; + if (dst->type == UFBX_ELEMENT_ANIM_VALUE && src->type == UFBX_ELEMENT_ANIM_CURVE) { + ufbx_anim_curve *src_curve = (ufbx_anim_curve*)src; + uint32_t index = 0; + if (dst_prop == ufbxi_Y || dst_prop == ufbxi_d_Y) { + index = 1; + } else if (dst_prop == ufbxi_Z || dst_prop == ufbxi_d_Z) { + index = 2; + } + + ufbxi_pre_anim_value *pre_value = &pre_anim_values[dst->typed_id]; + if (src_curve->max_value - src_curve->min_value >= scale_epsilon) { + pre_value->has_constant_value = false; + } else { + ufbx_real constant_value = (src_curve->min_value + src_curve->max_value) * 0.5f; + if (ufbx_isnan(pre_value->constant_value.v[index])) { + pre_value->constant_value.v[index] = constant_value; + } + if ((ufbx_real)ufbx_fabs(pre_value->constant_value.v[index] - constant_value) > scale_epsilon) { + pre_value->has_constant_value = false; + } + } + } + } + } + + for (size_t i = 0; i < num_connections; i++) { + ufbxi_tmp_connection *tmp = &tmp_connections[i]; + ufbxi_pre_connection *pre = &pre_connections[i]; + ufbx_element *src = pre->src, *dst = pre->dst; + if (!src || !dst) continue; + + if (tmp->src_prop.length == 0 && tmp->dst_prop.length == 0) { + // Count maximum number of instanced attributes in a node + if (dst->type == UFBX_ELEMENT_NODE) { + if (src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) { + instance_counts[dst->element_id] = ufbxi_max32(instance_counts[dst->element_id], instance_counts[src->element_id]); + if (src->type == UFBX_ELEMENT_MESH) { + ufbxi_pre_mesh *pre_mesh = &pre_meshes[src->typed_id]; + if (pre_mesh->has_skin_deformer) { + pre_nodes[dst->typed_id].has_skin_deformer = true; + } + } + } else if (src->type == UFBX_ELEMENT_SKIN_DEFORMER) { + pre_nodes[dst->typed_id].has_skin_deformer = true; + } + } + } else if (tmp->src_prop.length == 0 && tmp->dst_prop.length != 0) { + if (dst->type == UFBX_ELEMENT_NODE) { + if (src->type == UFBX_ELEMENT_ANIM_VALUE) { + if (tmp->dst_prop.data == ufbxi_Lcl_Scaling) { + ufbxi_pre_node *pre_node = &pre_nodes[dst->typed_id]; + if (pre_node->has_constant_scale) { + ufbxi_pre_anim_value *pre_value = &pre_anim_values[src->typed_id]; + if (!pre_value->has_constant_value) { + pre_node->has_constant_scale = false; + } else { + ufbx_real error = 0.0f; + error += (ufbx_real)ufbx_fabs(pre_value->constant_value.x - pre_node->constant_scale.x); + error += (ufbx_real)ufbx_fabs(pre_value->constant_value.y - pre_node->constant_scale.y); + error += (ufbx_real)ufbx_fabs(pre_value->constant_value.z - pre_node->constant_scale.z); + if (error >= scale_epsilon) { + pre_node->has_constant_scale = false; + } + } + } + } + } + } + } + } + + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT || uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT) { + for (size_t i = 0; i < num_nodes; i++) { + ufbxi_pre_node *pre_node = &pre_nodes[i]; + ufbx_node *node = (ufbx_node*)elements[pre_node->element_id]; + + ufbx_vec3 rotation_pivot = ufbxi_find_vec3(&node->props, ufbxi_RotationPivot, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling_pivot = ufbxi_find_vec3(&node->props, ufbxi_ScalingPivot, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling_offset = ufbxi_find_vec3(&node->props, ufbxi_ScalingOffset, 0.0f, 0.0f, 0.0f); + + bool should_modify_pivot = false; + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) { + should_modify_pivot = !ufbxi_is_vec3_zero(rotation_pivot); + } else if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT) { + should_modify_pivot = ufbxi_pivot_nonzero(rotation_pivot) || ufbxi_pivot_nonzero(scaling_pivot) || ufbxi_pivot_nonzero(scaling_offset); + } + + if (should_modify_pivot) { + bool skip_geometry_transform = false; + bool can_modify_geometry_transform = true; + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT) { + if (node_attrib_type[node->typed_id] == UFBX_ELEMENT_EMPTY) { + if (!uc->opts.pivot_handling_retain_empties) { + skip_geometry_transform = true; + } else { + can_modify_geometry_transform = false; + } + } + } + + if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK) { + if (instance_counts[node->element_id] > 1 || modify_not_supported[node->element_id]) { + can_modify_geometry_transform = false; + } + } + // Currently, geometry transform messes up skinning + if (pre_node->has_skin_deformer) { + can_modify_geometry_transform = false; + } + + bool can_modify_pivot = true; + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) { + ufbx_real err = 0.0f; + err += (ufbx_real)ufbx_fabs(rotation_pivot.x - scaling_pivot.x); + err += (ufbx_real)ufbx_fabs(rotation_pivot.y - scaling_pivot.y); + err += (ufbx_real)ufbx_fabs(rotation_pivot.z - scaling_pivot.z); + if (err > pivot_epsilon) { + can_modify_pivot = false; + } + } + + if (can_modify_pivot && (can_modify_geometry_transform || skip_geometry_transform)) { + ufbx_vec3 geometric_translation = ufbxi_find_vec3(&node->props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f); + + ufbx_vec3 child_offset = { 0.0f }; + ufbx_prop *new_props = NULL; + size_t num_props = node->props.props.count; + size_t new_prop_count = num_props; + if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT) { + ufbx_assert(!skip_geometry_transform); // not supporeted in legacy mode + child_offset = ufbxi_neg3(rotation_pivot); + geometric_translation = ufbxi_add3(geometric_translation, child_offset); + + new_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_props + 3); + ufbxi_check(new_props); + memcpy(new_props, node->props.props.data, num_props * sizeof(ufbx_prop)); + + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_RotationPivot, &ufbx_zero_vec3, UFBX_PROP_VECTOR); + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_ScalingPivot, &ufbx_zero_vec3, UFBX_PROP_VECTOR); + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_GeometricTranslation, &geometric_translation, UFBX_PROP_VECTOR); + } else if (uc->opts.pivot_handling == UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT) { + // We can eliminate the post-rotation translation and move it to the geometry/children as follows. + // Let Z be the initial value of S in the transform (aka `initial_scale`): + // + // (Rp-1+Soff+Sp) + S * (Sp-1) + // S * (Sp-1 + (Rp-1+Soff+Sp)/S) + // S * (Sp-1 + (Rp-1+Soff+Sp)/S - (Rp-1+Soff+Sp)/Z + (Rp-1+Soff+Sp)/Z) + // + // (Rp-1 + Soff + Sp) + S * (-(Rp-1 + Soff + Sp)/Z + (Sp-1 + (Rp-1 + Soff + Sp)/Z)) + // ^-scaled_offset--^ ^-unscaled_offset--^ ^-unscaled_offset--^ + // ^---------------- 0, when S=Z ----------------^ ^------- child_offset ------^ + // + // We need to be careful when doing this in case any component of Z is 0. Fortunately, + // the above holds for all `Z != 0`, it will just result in non-zero translation in the parent. + ufbx_vec3 initial_scale = ufbxi_find_vec3(&node->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); + ufbx_vec3 scaled_offset = ufbxi_sub3(ufbxi_add3(scaling_offset, scaling_pivot), rotation_pivot); + ufbx_vec3 unscaled_offset; + unscaled_offset.x = ufbxi_pivot_div(scaled_offset.x, initial_scale.x); + unscaled_offset.y = ufbxi_pivot_div(scaled_offset.y, initial_scale.y); + unscaled_offset.z = ufbxi_pivot_div(scaled_offset.z, initial_scale.z); + + // Convert `scaled_offset + S*unscaled_offset` to FBX scaling pivot and offset. + ufbx_vec3 new_scaling_pivot = unscaled_offset; + ufbx_vec3 new_scaling_offset = ufbxi_sub3(scaled_offset, new_scaling_pivot); + child_offset = ufbxi_sub3(unscaled_offset, scaling_pivot); + + new_props = ufbxi_push_zero(&uc->result, ufbx_prop, num_props + 4); + ufbxi_check(new_props); + memcpy(new_props, node->props.props.data, num_props * sizeof(ufbx_prop)); + + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_RotationPivot, &ufbx_zero_vec3, UFBX_PROP_VECTOR); + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_ScalingPivot, &new_scaling_pivot, UFBX_PROP_VECTOR); + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_ScalingOffset, &new_scaling_offset, UFBX_PROP_VECTOR); + if (!skip_geometry_transform) { + geometric_translation = ufbxi_add3(geometric_translation, child_offset); + ufbxi_init_synthetic_vec3_prop(&new_props[new_prop_count++], ufbxi_GeometricTranslation, &geometric_translation, UFBX_PROP_VECTOR); + } + } + + node->props.props.data = new_props; + node->props.props.count = new_prop_count; + ufbxi_check(ufbxi_sort_properties(uc, node->props.props.data, node->props.props.count)); + ufbxi_deduplicate_properties(&node->props.props); + + node->adjust_pre_translation = ufbxi_add3(node->adjust_pre_translation, rotation_pivot); + node->has_adjust_transform = true; + uint32_t ix = pre_node->first_child; + while (ix != ~0u) { + ufbxi_pre_node *pre_child = &pre_nodes[ix]; + ufbx_node *child = (ufbx_node*)elements[pre_child->element_id]; + + child->adjust_pre_translation = ufbxi_add3(child->adjust_pre_translation, child_offset); + child->has_adjust_transform = true; + + ix = pre_child->next_child; + } + } + } + } + } + + for (size_t i = 0; i < num_elements; i++) { + ufbx_element *element = elements[i]; + uint64_t fbx_id = fbx_ids[i]; + + if (element->type == UFBX_ELEMENT_NODE) { + ufbx_node *node = (ufbx_node*)element; + bool requires_helper_node = false; + if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES) { + requires_helper_node = true; + } else if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY) { + // Setup a geometry transform helper for nodes that have instanced attributes + requires_helper_node = instance_counts[i] > 1 || modify_not_supported[i]; + } + if (requires_helper_node) { + ufbxi_check(ufbxi_setup_geometry_transform_helper(uc, node, fbx_id)); + } + } + } + + for (size_t i = 0; i < num_elements; i++) { + ufbx_element *element = elements[i]; + uint64_t fbx_id = fbx_ids[i]; + + if (element->type == UFBX_ELEMENT_NODE) { + ufbx_node *node = (ufbx_node*)element; + if (has_unscaled_children[node->typed_id] && !node->scale_helper) { + ufbxi_pre_node *pre_node = &pre_nodes[node->typed_id]; + ufbx_real ref = uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE + ? pre_node->constant_scale.x : (ufbx_real)1.0f; + ufbx_vec3 scale = pre_node->constant_scale; + ufbx_real dx = (ufbx_real)ufbx_fabs(scale.x - ref); + ufbx_real dy = (ufbx_real)ufbx_fabs(scale.y - ref); + ufbx_real dz = (ufbx_real)ufbx_fabs(scale.z - ref); + if ((dx + dy + dz >= scale_epsilon || !pre_node->has_constant_scale || (ufbx_real)ufbx_fabs(scale.x) <= compensate_epsilon) + && uc->opts.inherit_mode_handling != UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) { + ufbxi_check(ufbxi_setup_scale_helper(uc, node, fbx_id)); + + // If we added a geometry transform helper that may scale further helpers + // recursively for all child nodes using `UFBX_INHERIT_MODE_COMPONENTWISE_SCALE` + // This is guaranteed to terminate as `ufbxi_pre_node` may only have one parent, + // meaning any cycles must contain `node` itself. + uint32_t ix = pre_node->first_child; + while (ix != ~0u && ix != node->typed_id) { + ufbxi_pre_node *pre_child = &pre_nodes[ix]; + ufbx_node *child = (ufbx_node*)elements[pre_child->element_id]; + + if (pre_child->parent != node->typed_id || child->original_inherit_mode == UFBX_INHERIT_MODE_COMPONENTWISE_SCALE) { + if (!pre_child->has_recursive_scale_helper && child->original_inherit_mode != UFBX_INHERIT_MODE_NORMAL) { + pre_child->has_recursive_scale_helper = true; + + uint64_t child_fbx_id = fbx_ids[pre_child->element_id]; + ufbxi_check(ufbxi_setup_scale_helper(uc, child, child_fbx_id)); + child->is_scale_compensate_parent = false; + + // Traverse to children if any + if (pre_child->first_child != ~0u) { + ix = pre_child->first_child; + continue; + } + } + } + + // Move to next child, popping parents until we find one + while (pre_child->next_child == ~0u) { + ix = pre_child->parent; + if (ix == node->typed_id) break; + pre_child = &pre_nodes[ix]; + } + if (ix != node->typed_id) { + ix = pre_child->next_child; + } + } + + } else if (uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE || uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK) { + if ((ufbx_real)ufbx_fabs(scale.x - 1.0f) >= scale_epsilon) { + node->is_scale_compensate_parent = true; + } + } + } + } + } + + return 1; +} + +// -- Scene processing + +static ufbxi_noinline ufbx_element *ufbxi_find_element_by_fbx_id(ufbxi_context *uc, uint64_t fbx_id) +{ + ufbxi_fbx_id_entry *entry = ufbxi_find_fbx_id(uc, fbx_id); + if (entry) { + return uc->scene.elements.data[entry->element_id]; + } + return NULL; +} + +ufbxi_forceinline static bool ufbxi_cmp_name_element_less(const ufbx_name_element *a, const ufbx_name_element *b) +{ + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + int cmp = strcmp(a->name.data, b->name.data); + if (cmp != 0) return cmp < 0; + return a->type < b->type; +} + +ufbxi_forceinline static bool ufbxi_cmp_name_element_less_ref(const ufbx_name_element *a, ufbx_string name, ufbx_element_type type, uint32_t key) +{ + if (a->_internal_key != key) return a->_internal_key < key; + int cmp = ufbxi_str_cmp(a->name, name); + if (cmp != 0) return cmp < 0; + return a->type < type; +} + +ufbxi_forceinline static bool ufbxi_cmp_prop_less_ref(const ufbx_prop *a, ufbx_string name, uint32_t key) +{ + if (a->_internal_key != key) return a->_internal_key < key; + return ufbxi_str_less(a->name, name); +} + +ufbxi_forceinline static bool ufbxi_cmp_prop_less_concat(const ufbx_prop *a, const ufbx_string *parts, size_t num_parts, uint32_t key) +{ + if (a->_internal_key != key) return a->_internal_key < key; + return ufbxi_concat_str_cmp(&a->name, parts, num_parts) < 0; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_name_elements(ufbxi_context *uc, ufbx_name_element *name_elems, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_name_element))); + ufbxi_macro_stable_sort(ufbx_name_element, 32, name_elems, uc->tmp_arr, count, + ( ufbxi_cmp_name_element_less(a, b) ) ); + return 1; +} + +ufbxi_noinline static bool ufbxi_cmp_node_less(ufbx_node *a, ufbx_node *b) +{ + if (a->node_depth != b->node_depth) return a->node_depth < b->node_depth; + if (a->parent && b->parent) { + uint32_t a_pid = a->parent->element.element_id, b_pid = b->parent->element.element_id; + if (a_pid != b_pid) return a_pid < b_pid; + } else { + ufbx_assert(a->parent == NULL && b->parent == NULL); + } + if (a->is_geometry_transform_helper != b->is_geometry_transform_helper) { + // Sort geometry transform helpers always before rest of the children. + return (unsigned)a->is_geometry_transform_helper > (unsigned)b->is_geometry_transform_helper; + } + if (a->is_scale_helper != b->is_scale_helper) { + // Sort scale helpers after geometry transform helpers. + return (unsigned)a->is_scale_helper > (unsigned)b->is_scale_helper; + } + return a->element.element_id < b->element.element_id; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_node_ptrs(ufbxi_context *uc, ufbx_node **nodes, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_node*))); + ufbxi_macro_stable_sort(ufbx_node*, 32, nodes, uc->tmp_arr, count, + ( ufbxi_cmp_node_less(*a, *b) ) ); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_cmp_tmp_material_texture_less(const ufbxi_tmp_material_texture *a, const ufbxi_tmp_material_texture *b) +{ + if (a->material_id != b->material_id) return a->material_id < b->material_id; + if (a->texture_id != b->texture_id) return a->texture_id < b->texture_id; + return ufbxi_str_less(a->prop_name, b->prop_name); +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_tmp_material_textures(ufbxi_context *uc, ufbxi_tmp_material_texture *mat_texs, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_tmp_material_texture))); + ufbxi_macro_stable_sort(ufbxi_tmp_material_texture, 32, mat_texs, uc->tmp_arr, count, + ( ufbxi_cmp_tmp_material_texture_less(a, b) )); + return 1; +} + +// We need to be able to assume no padding! +ufbx_static_assert(connection_size, sizeof(ufbx_connection) == sizeof(ufbx_element*)*2 + sizeof(ufbx_string)*2); + +ufbxi_forceinline static bool ufbxi_cmp_connection_less(ufbx_connection *a, ufbx_connection *b, size_t index) +{ + ufbx_element *a_elem = (&a->src)[index], *b_elem = (&b->src)[index]; + if (a_elem != b_elem) return a_elem < b_elem; + int cmp = strcmp((&a->src_prop)[index].data, (&b->src_prop)[index].data); + if (cmp != 0) return cmp < 0; + cmp = strcmp((&a->src_prop)[index ^ 1].data, (&b->src_prop)[index ^ 1].data); + return cmp < 0; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_connections(ufbxi_context *uc, ufbx_connection *connections, size_t count, size_t index) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_connection))); + ufbxi_macro_stable_sort(ufbx_connection, 32, connections, uc->tmp_arr, count, ( ufbxi_cmp_connection_less(a, b, index) )); + return 1; +} + +static uint64_t ufbxi_find_attribute_fbx_id(ufbxi_context *uc, uint64_t node_fbx_id) +{ + uint32_t hash = ufbxi_hash64(node_fbx_id); + ufbxi_fbx_attr_entry *entry = ufbxi_map_find(&uc->fbx_attr_map, ufbxi_fbx_attr_entry, hash, &node_fbx_id); + if (entry) { + return entry->attr_fbx_id; + } + return node_fbx_id; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_connections(ufbxi_context *uc) +{ + size_t num_connections = uc->tmp_connections.num_items; + ufbxi_tmp_connection *tmp_connections = ufbxi_push_pop(&uc->tmp, &uc->tmp_connections, ufbxi_tmp_connection, num_connections); + ufbxi_buf_free(&uc->tmp_connections); + ufbxi_check(tmp_connections); + + // NOTE: We truncate this array in case not all connections are resolved + uc->scene.connections_src.data = ufbxi_push(&uc->result, ufbx_connection, num_connections); + ufbxi_check(uc->scene.connections_src.data); + + // HACK: Translate property connections from node to attribute if the property name is not included + // in the known node properties and is not a property of the node. + if (uc->version > 0 && uc->version < 7000) { + ufbxi_for(ufbxi_tmp_connection, tmp_conn, tmp_connections, num_connections) { + if (tmp_conn->src_prop.length > 0 && !ufbxi_is_node_property_name(uc, tmp_conn->src_prop.data)) { + ufbx_element *src = ufbxi_find_element_by_fbx_id(uc, tmp_conn->src); + if (!src || !ufbx_find_prop_len(&src->props, tmp_conn->src_prop.data, tmp_conn->src_prop.length)) { + tmp_conn->src = ufbxi_find_attribute_fbx_id(uc, tmp_conn->src); + } + } + if (tmp_conn->dst_prop.length > 0 && !ufbxi_is_node_property_name(uc, tmp_conn->dst_prop.data)) { + ufbx_element *dst = ufbxi_find_element_by_fbx_id(uc, tmp_conn->dst); + if (!dst || !ufbx_find_prop_len(&dst->props, tmp_conn->dst_prop.data, tmp_conn->dst_prop.length)) { + tmp_conn->dst = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst); + } + } + } + } + + ufbxi_for(ufbxi_tmp_connection, tmp_conn, tmp_connections, num_connections) { + ufbx_element *src = ufbxi_find_element_by_fbx_id(uc, tmp_conn->src); + ufbx_element *dst = ufbxi_find_element_by_fbx_id(uc, tmp_conn->dst); + if (!src || !dst) continue; + + if (!uc->opts.disable_quirks) { + // Some exporters connect arbitrary non-nodes to root breaking further code, ignore those connections here! + if (dst->type == UFBX_ELEMENT_NODE && src->type != UFBX_ELEMENT_NODE && ((ufbx_node*)dst)->is_root) { + ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_BAD_ELEMENT_CONNECTED_TO_ROOT, src->element_id, "Non-node element connected to root")); + continue; + } + } + + // Remap connections to geometry transform helpers if necessary, see `ufbxi_setup_geometry_transform_helper()` for how these are setup. + if (uc->has_geometry_transform_nodes) { + if (dst->type == UFBX_ELEMENT_NODE && src->type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && src->type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB) { + ufbx_node *node = (ufbx_node*)dst; + if (node->has_geometry_transform) { + ufbxi_node_extra *extra = (ufbxi_node_extra*)ufbxi_get_element_extra(uc, node->element_id); + ufbx_assert(extra); + dst = uc->scene.elements.data[extra->geometry_helper_id]; + ufbx_assert(dst->type == UFBX_ELEMENT_NODE && ((ufbx_node*)dst)->is_geometry_transform_helper); + } + } + } + + // Remap connections to scale helpers if necessary, see `ufbxi_setup_scale_helper()` for how these are setup. + if (uc->has_scale_helper_nodes) { + if (dst->type == UFBX_ELEMENT_NODE) { + ufbx_node *dst_node = (ufbx_node*)dst; + if (dst_node->scale_helper) { + if (src->type == UFBX_ELEMENT_NODE) { + ufbx_node *src_node = (ufbx_node*)src; + if (!src_node->is_scale_helper && src_node->original_inherit_mode == UFBX_INHERIT_MODE_NORMAL) { + dst = &dst_node->scale_helper->element; + } + } else if (src->type == UFBX_ELEMENT_ANIM_VALUE) { + if (tmp_conn->dst_prop.data == ufbxi_Lcl_Scaling) { + dst = &dst_node->scale_helper->element; + } + } else { + dst = &dst_node->scale_helper->element; + } + } + } else if (src->type == UFBX_ELEMENT_NODE) { + ufbx_node *src_node = (ufbx_node*)src; + if (src_node->scale_helper) { + if (dst->type == UFBX_ELEMENT_SKIN_CLUSTER) { + src = &src_node->scale_helper->element; + } + } + } + } + + // Translate deformers to point to the geometry in 6100, we don't need to worry about + // blend shapes here as they're always connected synthetically in older files. + if (uc->version > 0 && uc->version < 7000 && dst->type == UFBX_ELEMENT_NODE) { + if (src->type == UFBX_ELEMENT_SKIN_DEFORMER || src->type == UFBX_ELEMENT_CACHE_DEFORMER) { + uint64_t dst_id = ufbxi_find_attribute_fbx_id(uc, tmp_conn->dst); + ufbx_element *dst_elem = ufbxi_find_element_by_fbx_id(uc, dst_id); + if (dst_elem) { + dst = dst_elem; + } + } + } + + ufbx_connection *conn = &uc->scene.connections_src.data[uc->scene.connections_src.count++]; + conn->src = src; + conn->dst = dst; + conn->src_prop = tmp_conn->src_prop; + conn->dst_prop = tmp_conn->dst_prop; + } + + uc->scene.connections_dst.count = uc->scene.connections_src.count; + uc->scene.connections_dst.data = ufbxi_push_copy(&uc->result, ufbx_connection, + uc->scene.connections_src.count, uc->scene.connections_src.data); + ufbxi_check(uc->scene.connections_dst.data); + + ufbxi_check(ufbxi_sort_connections(uc, uc->scene.connections_src.data, uc->scene.connections_src.count, 0)); + ufbxi_check(ufbxi_sort_connections(uc, uc->scene.connections_dst.data, uc->scene.connections_dst.count, 1)); + + // We don't need the temporary connections at this point anymore + ufbxi_buf_free(&uc->tmp_connections); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_connections_to_elements(ufbxi_context *uc) +{ + ufbx_connection *conn_src = uc->scene.connections_src.data; + ufbx_connection *conn_src_end = ufbxi_add_ptr(conn_src, uc->scene.connections_src.count); + ufbx_connection *conn_dst = uc->scene.connections_dst.data; + ufbx_connection *conn_dst_end = ufbxi_add_ptr(conn_dst, uc->scene.connections_dst.count); + + ufbxi_for_ptr(ufbx_element, p_elem, uc->scene.elements.data, uc->scene.elements.count) { + ufbx_element *elem = *p_elem; + uint32_t id = elem->element_id; + + while (conn_src < conn_src_end && conn_src->src->element_id < id) conn_src++; + while (conn_dst < conn_dst_end && conn_dst->dst->element_id < id) conn_dst++; + ufbx_connection *src_end = conn_src, *dst_end = conn_dst; + + while (src_end < conn_src_end && src_end->src->element_id == id) src_end++; + while (dst_end < conn_dst_end && dst_end->dst->element_id == id) dst_end++; + + elem->connections_src.data = conn_src; + elem->connections_src.count = ufbxi_to_size(src_end - conn_src); + elem->connections_dst.data = conn_dst; + elem->connections_dst.count = ufbxi_to_size(dst_end - conn_dst); + + // Setup animated properties + // TODO: It seems we're invalidating a lot of properties here actually, maybe they + // should be initially pushed to `tmp` instead of result if this happens so much.. + { + ufbx_prop *prop = elem->props.props.data, *prop_end = ufbxi_add_ptr(prop, elem->props.props.count); + ufbx_prop *copy_start = prop; + bool needs_copy = false; + size_t num_animated = 0, num_synthetic = 0; + + for (;;) { + // Scan to the next animation connection + for (; conn_dst < dst_end; conn_dst++) { + if (conn_dst->dst_prop.length == 0) continue; + if (conn_dst->src_prop.length > 0) break; + if (conn_dst->src->type == UFBX_ELEMENT_ANIM_VALUE) break; + } + + ufbx_string name = ufbx_empty_string; + if (conn_dst < dst_end) { + name = conn_dst->dst_prop; + } + if (name.length == 0) break; + + // NOTE: "Animated" properties also include connected ones as we need + // to resolve them during evaluation + num_animated++; + + ufbx_anim_value *anim_value = NULL; + uint32_t flags = 0; + for (; conn_dst < dst_end && conn_dst->dst_prop.data == name.data; conn_dst++) { + if (conn_dst->src_prop.length > 0) { + flags |= UFBX_PROP_FLAG_CONNECTED; + } else if (conn_dst->src->type == UFBX_ELEMENT_ANIM_VALUE) { + anim_value = (ufbx_anim_value*)conn_dst->src; + flags |= UFBX_PROP_FLAG_ANIMATED; + } + } + + uint32_t key = ufbxi_get_name_key(name.data, name.length); + while (prop != prop_end && ufbxi_name_key_less(prop, name.data, name.length, key)) prop++; + + if (prop != prop_end && prop->name.data == name.data) { + prop->flags = (ufbx_prop_flags)((uint32_t)prop->flags | flags); + } else { + // Animated property that is not in the element property list + // Copy the preceding properties to the stack, then push a + // synthetic property for the animated property. + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, ufbxi_to_size(prop - copy_start), copy_start)); + copy_start = prop; + needs_copy = true; + + // Let's hope we can find the property in the defaults at least + ufbx_prop anim_def_prop; + ufbx_prop *def_prop = NULL; + if (elem->props.defaults) { + def_prop = ufbxi_find_prop_with_key(elem->props.defaults, name.data, key); + } else if (anim_value) { + memset(&anim_def_prop, 0, sizeof(anim_def_prop)); + // Hack a couple of common types + ufbx_prop_type type = UFBX_PROP_UNKNOWN; + if (name.data == ufbxi_Lcl_Translation) type = UFBX_PROP_TRANSLATION; + else if (name.data == ufbxi_Lcl_Rotation) type = UFBX_PROP_ROTATION; + else if (name.data == ufbxi_Lcl_Scaling) { + type = UFBX_PROP_SCALING; + anim_def_prop.value_vec3.x = 1.0f; + anim_def_prop.value_vec3.y = 1.0f; + anim_def_prop.value_vec3.z = 1.0f; + } + // Property values are only defined in anim_props on legacy files + if (uc->version < 6000) { + anim_def_prop.value_vec3 = anim_value->default_value; + } + anim_def_prop.type = type; + def_prop = &anim_def_prop; + } else { + flags |= UFBX_PROP_FLAG_NO_VALUE; + } + + ufbx_prop *new_prop = ufbxi_push_zero(&uc->tmp_stack, ufbx_prop, 1); + ufbxi_check(new_prop); + if (def_prop) *new_prop = *def_prop; + flags |= (uint32_t)new_prop->flags; + new_prop->flags = (ufbx_prop_flags)(UFBX_PROP_FLAG_ANIMATABLE | UFBX_PROP_FLAG_SYNTHETIC | flags); + new_prop->name = name; + new_prop->_internal_key = key; + new_prop->value_str = ufbx_empty_string; + new_prop->value_blob = ufbx_empty_blob; + num_synthetic++; + } + } + + // Copy the properties if necessary + if (needs_copy) { + size_t num_new_props = elem->props.props.count + num_synthetic; + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_prop, ufbxi_to_size(prop_end - copy_start), copy_start)); + elem->props.props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_prop, num_new_props); + ufbxi_check(elem->props.props.data); + elem->props.props.count = num_new_props; + } + elem->props.num_animated = num_animated; + } + + conn_src = src_end; + conn_dst = dst_end; + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_linearize_nodes(ufbxi_context *uc) +{ + size_t num_nodes = uc->tmp_node_ids.num_items; + uint32_t *node_ids = ufbxi_push_pop(&uc->tmp, &uc->tmp_node_ids, uint32_t, num_nodes); + ufbxi_buf_free(&uc->tmp_node_ids); + ufbxi_check(node_ids); + + ufbx_node **node_ptrs = ufbxi_push(&uc->tmp_stack, ufbx_node*, num_nodes); + ufbxi_check(node_ptrs); + + // Fetch the node pointers + for (size_t i = 0; i < num_nodes; i++) { + node_ptrs[i] = (ufbx_node*)uc->scene.elements.data[node_ids[i]]; + ufbx_assert(node_ptrs[i]->element.type == UFBX_ELEMENT_NODE); + } + + uc->scene.root_node = node_ptrs[0]; + + size_t *node_offsets = ufbxi_push_pop(&uc->tmp_stack, &uc->tmp_typed_element_offsets[UFBX_ELEMENT_NODE], size_t, num_nodes); + ufbxi_check(node_offsets); + + // Hook up the parent nodes, we'll assume that there's no cycles at this point + ufbxi_for_ptr(ufbx_node, p_node, node_ptrs, num_nodes) { + ufbx_node *node = *p_node; + + // Pre-6000 files don't have any explicit root connections so they must always + // be connected to the root.. + if (node->parent == NULL && !(uc->opts.allow_nodes_out_of_root && uc->version >= 6000)) { + if (node != uc->scene.root_node) { + node->parent = uc->scene.root_node; + } + } + + ufbxi_for_list(ufbx_connection, conn, node->element.connections_dst) { + if (conn->src_prop.length > 0 || conn->dst_prop.length > 0) continue; + if (conn->src->type != UFBX_ELEMENT_NODE) continue; + ((ufbx_node*)conn->src)->parent = node; + } + } + + // Count the parent depths and child amounts + ufbxi_for_ptr(ufbx_node, p_node, node_ptrs, num_nodes) { + ufbx_node *node = *p_node; + uint32_t depth = 0; + + for (ufbx_node *p = node->parent; p; p = p->parent) { + depth += p->node_depth + 1; + if (p->node_depth > 0) break; + ufbxi_check_msg(depth <= num_nodes, "Cyclic node hierarchy"); + } + + if (uc->opts.node_depth_limit > 0) { + ufbxi_check_msg(depth <= uc->opts.node_depth_limit, "Node depth limit exceeded"); + } + node->node_depth = depth; + + // Second pass to cache the depths to avoid O(n^2) + for (ufbx_node *p = node->parent; p; p = p->parent) { + if (--depth <= p->node_depth) break; + p->node_depth = depth; + } + } + + ufbxi_check(ufbxi_sort_node_ptrs(uc, node_ptrs, num_nodes)); + + for (uint32_t i = 0; i < num_nodes; i++) { + size_t *p_offset = ufbxi_push(&uc->tmp_typed_element_offsets[UFBX_ELEMENT_NODE], size_t, 1); + ufbxi_check(p_offset); + ufbx_node *node = node_ptrs[i]; + + uint32_t original_id = node->element.typed_id; + node->element.typed_id = i; + *p_offset = node_offsets[original_id]; + } + + // Pop the temporary arrays + ufbxi_pop(&uc->tmp_stack, size_t, num_nodes, NULL); + ufbxi_pop(&uc->tmp_stack, ufbx_node*, num_nodes, NULL); + + return 1; +} + + +ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_dst_connections(ufbx_element *element, const char *prop) +{ + if (!prop) prop = ufbxi_empty_char; + + size_t begin = element->connections_dst.count, end = begin; + + ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &begin, + element->connections_dst.data, 0, element->connections_dst.count, + (strcmp(a->dst_prop.data, prop) < 0), + (a->dst_prop.data == prop && a->src_prop.length == 0)); + + ufbxi_macro_upper_bound_eq(ufbx_connection, 32, &end, + element->connections_dst.data, begin, element->connections_dst.count, + (a->dst_prop.data == prop && a->src_prop.length == 0)); + + ufbx_connection_list result = { element->connections_dst.data + begin, end - begin }; + return result; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_connection_list ufbxi_find_src_connections(ufbx_element *element, const char *prop) +{ + if (!prop) prop = ufbxi_empty_char; + + size_t begin = element->connections_src.count, end = begin; + + ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &begin, + element->connections_src.data, 0, element->connections_src.count, + (strcmp(a->src_prop.data, prop) < 0), + (a->src_prop.data == prop && a->dst_prop.length == 0)); + + ufbxi_macro_upper_bound_eq(ufbx_connection, 32, &end, + element->connections_src.data, begin, element->connections_src.count, + (a->src_prop.data == prop && a->dst_prop.length == 0)); + + ufbx_connection_list result = { element->connections_src.data + begin, end - begin }; + return result; +} + +ufbxi_nodiscard static ufbx_element *ufbxi_get_element_node(ufbx_element *element) +{ + if (!element) return NULL; + if (element->type == UFBX_ELEMENT_NODE) { + ufbx_node *node = (ufbx_node*)element; + if (node->is_geometry_transform_helper) return (ufbx_element*)node->parent; + return NULL; + } else { + return element->instances.count > 0 ? &element->instances.data[0]->element : NULL; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_dst_elements(ufbxi_context *uc, void *p_dst_list, ufbx_element *element, bool search_node, bool ignore_duplicates, const char *prop, ufbx_element_type src_type) +{ + size_t num_elements = 0; + + do { + ufbx_connection_list conns = ufbxi_find_dst_connections(element, prop); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->src->type == src_type) { + if (ignore_duplicates) { + uint32_t element_id = conn->src->element_id; + if (uc->tmp_element_flag[element_id]) { + ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_DUPLICATE_CONNECTION, element_id, "Duplicate connection to %u", element->element_id)); + continue; + } + uc->tmp_element_flag[element_id] = 1; + } + ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1); + ufbxi_check(p_elem); + *p_elem = conn->src; + num_elements++; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + ufbx_element_list *list = (ufbx_element_list*)p_dst_list; + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_elements); + list->count = num_elements; + ufbxi_check(list->data); + + if (ignore_duplicates) { + ufbxi_for_ptr_list(ufbx_element, p_elem, *list) { + uc->tmp_element_flag[(*p_elem)->element_id] = 0; + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_src_elements(ufbxi_context *uc, void *p_dst_list, ufbx_element *element, bool search_node, bool ignore_duplicates, const char *prop, ufbx_element_type dst_type) +{ + size_t num_elements = 0; + + do { + ufbx_connection_list conns = ufbxi_find_src_connections(element, prop); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->dst->type == dst_type) { + if (ignore_duplicates) { + uint32_t element_id = conn->dst->element_id; + if (uc->tmp_element_flag[element_id]) { + ufbxi_check(ufbxi_warnf_tag(UFBX_WARNING_DUPLICATE_CONNECTION, element_id, "Duplicate connection to %u", element->element_id)); + continue; + } + uc->tmp_element_flag[element_id] = 1; + } + ufbx_element **p_elem = ufbxi_push(&uc->tmp_stack, ufbx_element*, 1); + ufbxi_check(p_elem); + *p_elem = conn->dst; + num_elements++; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + ufbx_element_list *list = (ufbx_element_list*)p_dst_list; + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_elements); + list->count = num_elements; + ufbxi_check(list->data); + + if (ignore_duplicates) { + ufbxi_for_ptr_list(ufbx_element, p_elem, *list) { + uc->tmp_element_flag[(*p_elem)->element_id] = 0; + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_fetch_dst_element(ufbx_element *element, bool search_node, const char *prop, ufbx_element_type src_type) +{ + do { + ufbx_connection_list conns = ufbxi_find_dst_connections(element, prop); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->src->type == src_type) { + return conn->src; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + return NULL; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_element *ufbxi_fetch_src_element(ufbx_element *element, bool search_node, const char *prop, ufbx_element_type dst_type) +{ + do { + ufbx_connection_list conns = ufbxi_find_src_connections(element, prop); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->dst->type == dst_type) { + return conn->dst; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + return NULL; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_textures(ufbxi_context *uc, ufbx_material_texture_list *list, ufbx_element *element, bool search_node) +{ + size_t num_textures = 0; + + do { + ufbxi_for_list(ufbx_connection, conn, element->connections_dst) { + if (conn->src_prop.length > 0) continue; + if (conn->src->type == UFBX_ELEMENT_TEXTURE) { + ufbx_material_texture *tex = ufbxi_push(&uc->tmp_stack, ufbx_material_texture, 1); + ufbxi_check(tex); + tex->shader_prop = tex->material_prop = conn->dst_prop; + tex->texture = (ufbx_texture*)conn->src; + num_textures++; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material_texture, num_textures); + list->count = num_textures; + ufbxi_check(list->data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_mesh_materials(ufbxi_context *uc, ufbx_material_list *list, ufbx_element *element, bool search_node) +{ + size_t num_materials = 0; + + do { + ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->src->type == UFBX_ELEMENT_MATERIAL) { + ufbx_material *mat = (ufbx_material*)conn->src; + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_material*, 1, &mat)); + num_materials++; + } + } + + if (num_materials > 0) break; + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material*, num_materials); + list->count = num_materials; + ufbxi_check(list->data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_deformers(ufbxi_context *uc, ufbx_element_list *list, ufbx_element *element, bool search_node) +{ + size_t num_deformers = 0; + + do { + ufbxi_for_list(ufbx_connection, conn, element->connections_dst) { + if (conn->src_prop.length > 0) continue; + ufbx_element_type type = conn->src->type; + if (type == UFBX_ELEMENT_SKIN_DEFORMER || type == UFBX_ELEMENT_BLEND_DEFORMER || type == UFBX_ELEMENT_CACHE_DEFORMER) { + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &conn->src)); + num_deformers++; + } + } + } while (search_node && (element = ufbxi_get_element_node(element)) != NULL); + + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, num_deformers); + list->count = num_deformers; + ufbxi_check(list->data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe_list *list, ufbx_element *element) +{ + size_t num_keyframes = 0; + + ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->src->type == UFBX_ELEMENT_BLEND_SHAPE) { + ufbx_blend_keyframe key = { (ufbx_blend_shape*)conn->src }; + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_blend_keyframe, 1, &key)); + num_keyframes++; + } + } + + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_blend_keyframe, num_keyframes); + list->count = num_keyframes; + ufbxi_check(list->data); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_texture_layers(ufbxi_context *uc, ufbx_texture_layer_list *list, ufbx_element *element) +{ + size_t num_layers = 0; + + ufbx_connection_list conns = ufbxi_find_dst_connections(element, NULL); + ufbxi_for_list(ufbx_connection, conn, conns) { + if (conn->src->type == UFBX_ELEMENT_TEXTURE) { + ufbx_texture *texture = (ufbx_texture*)conn->src; + ufbx_texture_layer layer = { texture }; + layer.alpha = ufbxi_find_real(&texture->props, ufbxi_Texture_alpha, 1.0f); + layer.blend_mode = (ufbx_blend_mode)ufbxi_find_enum(&texture->props, ufbxi_BlendMode, UFBX_BLEND_REPLACE, UFBX_BLEND_OVERLAY); + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture_layer, 1, &layer)); + num_layers++; + } + } + + list->data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_texture_layer, num_layers); + list->count = num_layers; + ufbxi_check(list->data); + + return 1; +} + +static ufbxi_forceinline bool ufbxi_prop_connection_less(const ufbx_connection *a, const char *prop) +{ + int cmp = strcmp(a->dst_prop.data, prop); + if (cmp != 0) return cmp < 0; + return a->src_prop.length == 0; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_connection *ufbxi_find_prop_connection(const ufbx_element *element, const char *prop) +{ + if (!prop) prop = ufbxi_empty_char; + + size_t index = SIZE_MAX; + + ufbxi_macro_lower_bound_eq(ufbx_connection, 32, &index, + element->connections_dst.data, 0, element->connections_dst.count, + (ufbxi_prop_connection_less(a, prop)), + (a->dst_prop.data == prop && a->src_prop.length > 0)); + + return index < SIZE_MAX ? &element->connections_dst.data[index] : NULL; +} + +ufbxi_forceinline static void ufbxi_patch_index_pointer(ufbxi_context *uc, uint32_t **p_index) +{ + if (*p_index == ufbxi_sentinel_index_zero) { + *p_index = uc->zero_indices; + } else if (*p_index == ufbxi_sentinel_index_consecutive) { + *p_index = uc->consecutive_indices; + } +} + +ufbxi_nodiscard static bool ufbxi_cmp_anim_prop_less(const ufbx_anim_prop *a, const ufbx_anim_prop *b) +{ + if (a->element != b->element) return a->element < b->element; + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + return ufbxi_str_less(a->prop_name, b->prop_name); +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_anim_props(ufbxi_context *uc, ufbx_anim_prop *aprops, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_anim_prop))); + ufbxi_macro_stable_sort(ufbx_anim_prop, 32, aprops, uc->tmp_arr, count, ( ufbxi_cmp_anim_prop_less(a, b) )); + return 1; +} + +ufbxi_noinline static bool ufbxi_material_texture_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_material_texture *a = (const ufbx_material_texture*)va, *b = (const ufbx_material_texture*)vb; + return ufbxi_str_less(a->material_prop, b->material_prop); +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_material_textures(ufbxi_context *uc, ufbx_material_texture *textures, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_material_texture))); + ufbxi_stable_sort(sizeof(ufbx_material_texture), 32, textures, uc->tmp_arr, count, &ufbxi_material_texture_less, NULL); + return 1; +} + +static ufbxi_noinline bool ufbxi_bone_pose_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_bone_pose *a = (const ufbx_bone_pose *)va, *b = (const ufbx_bone_pose *)vb; + return a->bone_node->typed_id < b->bone_node->typed_id; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_anim_prop *ufbxi_find_anim_prop_start(ufbx_anim_layer *layer, const ufbx_element *element) +{ + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &index, layer->anim_props.data, 0, layer->anim_props.count, + (a->element < element), (a->element == element)); + return index != SIZE_MAX ? &layer->anim_props.data[index] : NULL; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_bone_poses(ufbxi_context *uc, ufbx_pose *pose) +{ + size_t count = pose->bone_poses.count; + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, pose->bone_poses.count * sizeof(ufbx_bone_pose))); + ufbxi_stable_sort(sizeof(ufbx_bone_pose), 16, pose->bone_poses.data, uc->tmp_arr, count, &ufbxi_bone_pose_less, NULL); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_skin_weights(ufbxi_context *uc, ufbx_skin_deformer *skin) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, skin->max_weights_per_vertex * sizeof(ufbx_skin_weight))); + + for (size_t i = 0; i < skin->vertices.count; i++) { + ufbx_skin_vertex v = skin->vertices.data[i]; + ufbxi_macro_stable_sort(ufbx_skin_weight, 32, skin->weights.data + v.weight_begin, uc->tmp_arr, v.num_weights, + ( a->weight > b->weight )); + } + + return 1; +} + +ufbxi_noinline static bool ufbxi_blend_keyframe_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_blend_keyframe *a = (const ufbx_blend_keyframe*)va, *b = (const ufbx_blend_keyframe*)vb; + return a->target_weight < b->target_weight; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_blend_keyframes(ufbxi_context *uc, ufbx_blend_keyframe *keyframes, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbx_blend_keyframe))); + ufbxi_stable_sort(sizeof(ufbx_blend_keyframe), 32, keyframes, uc->tmp_arr, count, &ufbxi_blend_keyframe_less, NULL); + return 1; +} + +// Material tables + +typedef void (*ufbxi_mat_transform_fn)(ufbx_vec4 *a); + +static void ufbxi_mat_transform_invert_x(ufbx_vec4 *v) { v->x = 1.0f - v->x; } +static void ufbxi_mat_transform_unknown_shininess(ufbx_vec4 *v) { if (v->x >= 0.0f) v->x = (ufbx_real)(1.0f - ufbx_sqrt(v->x) * (ufbx_real)0.1); if (!(v->x >= 0.0f)) v->x = 0.0f; } +static void ufbxi_mat_transform_blender_opacity(ufbx_vec4 *v) { v->x = 1.0f - v->x; } +static void ufbxi_mat_transform_blender_shininess(ufbx_vec4 *v) { if (v->x >= 0.0f) v->x = (ufbx_real)(1.0f - ufbx_sqrt(v->x) * (ufbx_real)0.1); if (!(v->x >= 0.0f)) v->x = 0.0f; } + +typedef enum { + UFBXI_MAT_TRANSFORM_IDENTITY, + UFBXI_MAT_TRANSFORM_INVERT_X, + UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, + UFBXI_MAT_TRANSFORM_BLENDER_OPACITY, + UFBXI_MAT_TRANSFORM_BLENDER_SHININESS, + + UFBXI_MAT_TRANSFORM_COUNT, +} ufbxi_mat_transform; + +typedef enum { + // Set `value_vec4.w` (usually alpha) to 1.0 if not defined by the property + UFBXI_SHADER_MAPPING_DEFAULT_W_1 = 0x1, + // Widen values to RGB if only a single value is present. + UFBXI_SHADER_MAPPING_WIDEN_TO_RGB = 0x2, + // Multiply the existing value. + UFBXI_SHADER_MAPPING_MULTIPLY_VALUE = 0x4, +} ufbxi_shader_mapping_flag; + +typedef enum { + // Invert the feature flag + UFBXI_SHADER_FEATURE_INVERTED = 0x1, + // Enable the feature if the given property exists + UFBXI_SHADER_FEATURE_IF_EXISTS = 0x2, + // Enable the feature if the given property has a texture + UFBXI_SHADER_FEATURE_IF_TEXTURE = 0x4, + // Enable if the feature is in [0.5, 1.5], (ie. 2 won't enable this feature) + UFBXI_SHADER_FEATURE_IF_AROUND_1 = 0x8, + + UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE = UFBXI_SHADER_FEATURE_IF_EXISTS|UFBXI_SHADER_FEATURE_IF_TEXTURE, +} ufbxi_shader_feature_flag; + +static const ufbxi_mat_transform_fn ufbxi_mat_transform_fns[] = { + NULL, + &ufbxi_mat_transform_invert_x, + &ufbxi_mat_transform_unknown_shininess, + &ufbxi_mat_transform_blender_opacity, + &ufbxi_mat_transform_blender_shininess, +}; + +ufbx_static_assert(transform_count, ufbxi_arraycount(ufbxi_mat_transform_fns) == UFBXI_MAT_TRANSFORM_COUNT); + +typedef struct { + uint8_t index; // < `ufbx_material_(fbx|pbr)_map` + uint8_t flags; // < Combination of `ufbxi_shader_mapping_flag` + uint8_t transform; // < `ufbxi_mat_transform` + uint8_t prop_len; // < Length of `prop` not including NULL terminator + const char *prop; // < Name of FBX material property or shader mapping +} ufbxi_shader_mapping; + +typedef struct { + const ufbxi_shader_mapping *data; + size_t count; + const ufbxi_shader_mapping *features; + size_t feature_count; + uint32_t default_features; + ufbx_string texture_prefix; + ufbx_string texture_suffix; + ufbx_string texture_enabled_prefix; + ufbx_string texture_enabled_suffix; +} ufbxi_shader_mapping_list; + +#define ufbxi_mat_string(str) sizeof(str) - 1, str + +static const ufbxi_shader_mapping ufbxi_base_fbx_mapping[] = { + { UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") }, + { UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") }, + { UFBX_MATERIAL_FBX_DIFFUSE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") }, + { UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") }, + { UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("SpecularColor") }, + { UFBX_MATERIAL_FBX_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("SpecularFactor") }, + { UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("Shininess") }, + { UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("ShininessExponent") }, + { UFBX_MATERIAL_FBX_REFLECTION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Reflection") }, + { UFBX_MATERIAL_FBX_REFLECTION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("ReflectionColor") }, + { UFBX_MATERIAL_FBX_REFLECTION_FACTOR, 0, 0, ufbxi_mat_string("ReflectionFactor") }, + { UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") }, + { UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") }, + { UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") }, + { UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") }, + { UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") }, + { UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") }, + { UFBX_MATERIAL_FBX_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") }, + { UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Ambient") }, + { UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("AmbientColor") }, + { UFBX_MATERIAL_FBX_AMBIENT_FACTOR, 0, 0, ufbxi_mat_string("AmbientFactor") }, + { UFBX_MATERIAL_FBX_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, + { UFBX_MATERIAL_FBX_BUMP, 0, 0, ufbxi_mat_string("Bump") }, + { UFBX_MATERIAL_FBX_BUMP_FACTOR, 0, 0, ufbxi_mat_string("BumpFactor") }, + { UFBX_MATERIAL_FBX_DISPLACEMENT, 0, 0, ufbxi_mat_string("Displacement") }, + { UFBX_MATERIAL_FBX_DISPLACEMENT_FACTOR, 0, 0, ufbxi_mat_string("DisplacementFactor") }, + { UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT, 0, 0, ufbxi_mat_string("VectorDisplacement") }, + { UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT_FACTOR, 0, 0, ufbxi_mat_string("VectorDisplacementFactor") }, +}; + +static const ufbxi_shader_mapping ufbxi_obj_fbx_mapping[] = { + { UFBX_MATERIAL_FBX_AMBIENT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ka") }, + { UFBX_MATERIAL_FBX_DIFFUSE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Kd") }, + { UFBX_MATERIAL_FBX_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ks") }, + { UFBX_MATERIAL_FBX_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ke") }, + { UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, 0, 0, ufbxi_mat_string("Ns") }, + { UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, 0, UFBXI_MAT_TRANSFORM_INVERT_X, ufbxi_mat_string("d") }, + { UFBX_MATERIAL_FBX_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") }, + { UFBX_MATERIAL_FBX_DISPLACEMENT, 0, 0, ufbxi_mat_string("disp") }, + { UFBX_MATERIAL_FBX_BUMP, 0, 0, ufbxi_mat_string("bump") }, + { UFBX_MATERIAL_FBX_BUMP, 0, 0, ufbxi_mat_string("Bump") }, +}; + +static const ufbxi_shader_mapping ufbxi_fbx_lambert_shader_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") }, + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, +}; + +static const ufbxi_shader_mapping ufbxi_fbx_phong_shader_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Diffuse") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") }, + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("DiffuseFactor") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("SpecularColor") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("SpecularFactor") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("Shininess") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("ShininessExponent") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Transparent") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("TransparentColor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparentFactor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("TransparencyFactor") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Emissive") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, +}; + +static const ufbxi_shader_mapping ufbxi_osl_standard_shader_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specular_roughness") }, + { UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diffuse_roughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular_color") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specular_IOR") }, + { UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specular_anisotropy") }, + { UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("specular_rotation") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmission_color") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmission_depth") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmission_scatter") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmission_scatter_anisotropy") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmission_dispersion") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_EXTRA_ROUGHNESS, 0, 0, ufbxi_mat_string("transmission_extra_roughness") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurface_color") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurface_radius") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurface_scale") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurface_anisotropy") }, + { UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("sheen") }, + { UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sheen_color") }, + { UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("sheen_roughness") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") }, + { UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") }, + { UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_IOR") }, + { UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coat_anisotropy") }, + { UFBX_MATERIAL_PBR_COAT_ROTATION, 0, 0, ufbxi_mat_string("coat_rotation") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_normal") }, + { UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_affect_color") }, + { UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_affect_roughness") }, + { UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thin_film_thickness") }, + { UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thin_film_IOR") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emission_color") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normalCamera") }, + { UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("tangent") }, +}; + +static const ufbxi_shader_mapping ufbxi_osl_standard_shader_features[] = { + { UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thin_walled") }, +}; + +static const ufbxi_shader_mapping ufbxi_arnold_shader_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specularRoughness") }, + { UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diffuseRoughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specularColor") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specularIOR") }, + { UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specularAnisotropy") }, + { UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("specularRotation") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmissionColor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmissionDepth") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmissionScatter") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmissionScatterAnisotropy") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmissionDispersion") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_EXTRA_ROUGHNESS, 0, 0, ufbxi_mat_string("transmissionExtraRoughness") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurfaceColor") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurfaceRadius") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurfaceScale") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurfaceAnisotropy") }, + { UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("sheen") }, + { UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sheenColor") }, + { UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("sheenRoughness") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") }, + { UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coatColor") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coatRoughness") }, + { UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coatIOR") }, + { UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coatAnisotropy") }, + { UFBX_MATERIAL_PBR_COAT_ROTATION, 0, 0, ufbxi_mat_string("coatRotation") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coatNormal") }, + { UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thinFilmThickness") }, + { UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thinFilmIOR") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emissionColor") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") }, + { UFBX_MATERIAL_PBR_INDIRECT_DIFFUSE, 0, 0, ufbxi_mat_string("indirectDiffuse") }, + { UFBX_MATERIAL_PBR_INDIRECT_SPECULAR, 0, 0, ufbxi_mat_string("indirectSpecular") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normalCamera") }, + { UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("tangent") }, + { UFBX_MATERIAL_PBR_MATTE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("aiMatteColor") }, + { UFBX_MATERIAL_PBR_MATTE_FACTOR, 0, 0, ufbxi_mat_string("aiMatteColorA") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_TYPE, 0, 0, ufbxi_mat_string("subsurfaceType") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_PRIORITY, 0, 0, ufbxi_mat_string("dielectricPriority") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_ENABLE_IN_AOV, 0, 0, ufbxi_mat_string("transmitAovs") }, +}; + +static const ufbxi_shader_mapping ufbxi_arnold_shader_features[] = { + { UFBX_MATERIAL_FEATURE_MATTE, 0, 0, ufbxi_mat_string("aiEnableMatte") }, + { UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thinWalled") }, + { UFBX_MATERIAL_FEATURE_CAUSTICS, 0, 0, ufbxi_mat_string("caustics") }, + { UFBX_MATERIAL_FEATURE_INTERNAL_REFLECTIONS, 0, 0, ufbxi_mat_string("internalReflections") }, + { UFBX_MATERIAL_FEATURE_EXIT_TO_BACKGROUND, 0, 0, ufbxi_mat_string("exitToBackground") }, +}; + +static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base_weight") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") }, + { UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diff_rough") }, + { UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("diff_roughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("reflectivity") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("refl_color") }, + { UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("anisotropy") }, + { UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("aniso_angle") }, + { UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("anisoangle") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("trans_ior") }, // NOTE: Not a typo, IOR is same for transparency/specular + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transparency") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("trans_color") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("trans_depth") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, 0, 0, ufbxi_mat_string("trans_rough") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, 0, 0, ufbxi_mat_string("trans_roughness") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("scattering") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_TINT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sss_color") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("sss_scatter_color") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("sss_depth") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("sss_scale") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coating") }, + { UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_rough") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") }, + { UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_ior") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("clearcoat_bump_map_amt") }, + { UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_affect_color") }, + { UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_affect_roughness") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("cutout") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump_map_amt") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_map_amt") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_TYPE, 0, 0, ufbxi_mat_string("subsurfaceType") }, +}; + +static const ufbxi_shader_mapping ufbxi_3ds_max_physical_material_features[] = { + { UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("thin_walled") }, + { UFBX_MATERIAL_FEATURE_SPECULAR, 0, 0, ufbxi_mat_string("material_mode") }, + { UFBX_MATERIAL_FEATURE_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("material_mode") }, + { UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS, UFBXI_SHADER_FEATURE_INVERTED, 0, ufbxi_mat_string("trans_roughness_lock") }, + { UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("roughness_inv") }, + { UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("trans_roughness_inv") }, + { UFBX_MATERIAL_FEATURE_COAT_ROUGHNESS_AS_GLOSSINESS, 0, 0, ufbxi_mat_string("coat_roughness_inv") }, +}; + +static const ufbxi_shader_mapping ufbxi_gltf_material_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|baseColor") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("main|roughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("main|metalness") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("main|normal") }, + { UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("main|ambientOcclusion") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|emission") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("main|emissionColor") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("main|Alpha") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("extension|clearcoat") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("extension|clearcoatRoughness") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("extension|clearcoatNormal") }, + { UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|sheenColor") }, + { UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("extension|sheenRoughness") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("extension|specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("extension|Specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|specularcolor") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("extension|specularColor") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("extension|transmission") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("extension|indexOfRefraction") }, +}; + +static const ufbxi_shader_mapping ufbxi_openpbr_material_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_FACTOR, 0, 0, ufbxi_mat_string("base_weight") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("specular_roughness") }, + { UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, 0, 0, ufbxi_mat_string("base_diffuse_roughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("base_metalness") }, + { UFBX_MATERIAL_PBR_SPECULAR_FACTOR, 0, 0, ufbxi_mat_string("specular_weight") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular_color") }, + { UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("specular_roughness_anisotropy") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("specular_ior") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, 0, 0, ufbxi_mat_string("transmission_weight") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("transmission_color") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, 0, 0, ufbxi_mat_string("transmission_depth") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("transmission_scatter") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, 0, 0, ufbxi_mat_string("transmission_scatter_anisotropy") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, 0, 0, ufbxi_mat_string("transmission_dispersion_scale") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, 0, 0, ufbxi_mat_string("subsurface_weight") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("subsurface_color") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("subsurface_radius_scale") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, 0, 0, ufbxi_mat_string("subsurface_radius") }, + { UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, 0, 0, ufbxi_mat_string("subsurface_scatter_anisotropy") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("coat_weight") }, + { UFBX_MATERIAL_PBR_COAT_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("coat_color") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("coat_roughness") }, + { UFBX_MATERIAL_PBR_COAT_ANISOTROPY, 0, 0, ufbxi_mat_string("coat_roughness_anisotropy") }, + { UFBX_MATERIAL_PBR_COAT_IOR, 0, 0, ufbxi_mat_string("coat_ior") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_normal_map") }, + { UFBX_MATERIAL_PBR_SHEEN_FACTOR, 0, 0, ufbxi_mat_string("fuzz_weight") }, + { UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("fuzz_color") }, + { UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, 0, 0, ufbxi_mat_string("fuzz_roughness") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emission_weight") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, UFBXI_SHADER_MAPPING_MULTIPLY_VALUE, 0, ufbxi_mat_string("emission_luminance") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emission_color") }, + { UFBX_MATERIAL_PBR_THIN_FILM_FACTOR, 0, 0, ufbxi_mat_string("thin_film_weight") }, + { UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, 0, 0, ufbxi_mat_string("thin_film_thickness") }, + { UFBX_MATERIAL_PBR_THIN_FILM_IOR, 0, 0, ufbxi_mat_string("thin_film_ior") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump_map_amt") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_map_amt") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump") }, + { UFBX_MATERIAL_PBR_COAT_NORMAL, 0, 0, ufbxi_mat_string("coat_bump_map_amt") }, + { UFBX_MATERIAL_PBR_TANGENT_MAP, 0, 0, ufbxi_mat_string("geometry_tangent_map") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("geometry_opacity") }, +}; + +static const ufbxi_shader_mapping ufbxi_openpbr_material_features[] = { + { UFBX_MATERIAL_FEATURE_THIN_WALLED, 0, 0, ufbxi_mat_string("geometry_thin_walled") }, +}; + +static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_metal_rough_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("Roughness_Map") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metalness") }, + { UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_amt") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") }, +}; + +static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("baseColor") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("Specular") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("specular") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("glossiness") }, + { UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emit_color") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("displacement_amt") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("opacity") }, +}; + +static const ufbxi_shader_mapping ufbxi_3ds_max_pbr_features[] = { + { UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, UFBXI_SHADER_FEATURE_IF_AROUND_1, 0, ufbxi_mat_string("useGlossiness") }, +}; + +static const ufbxi_shader_mapping ufbxi_gltf_material_features[] = { + { UFBX_MATERIAL_FEATURE_DOUBLE_SIDED, 0, 0, ufbxi_mat_string("main|DoubleSided") }, + { UFBX_MATERIAL_FEATURE_SHEEN, 0, 0, ufbxi_mat_string("extension|enableSheen") }, + { UFBX_MATERIAL_FEATURE_COAT, 0, 0, ufbxi_mat_string("extension|enableClearCoat") }, + { UFBX_MATERIAL_FEATURE_TRANSMISSION, 0, 0, ufbxi_mat_string("extension|enableTransmission") }, + { UFBX_MATERIAL_FEATURE_IOR, 0, 0, ufbxi_mat_string("extension|enableIndexOfRefraction") }, + { UFBX_MATERIAL_FEATURE_SPECULAR, 0, 0, ufbxi_mat_string("extension|enableSpecular") }, + { UFBX_MATERIAL_FEATURE_UNLIT, 0, 0, ufbxi_mat_string("extension|unlit") }, +}; + +// NOTE: These are just the names used by the standard PBS "preset". +// In _theory_ we could walk ShaderGraph but that's a bit out of scope for ufbx. +static const ufbxi_shader_mapping ufbxi_shaderfx_graph_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("color") }, + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("base_color") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("roughness") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("metallic") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("normal") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("emissive_intensity") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("emissive") }, + { UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, 0, 0, ufbxi_mat_string("ao") }, +}; + +static const ufbxi_shader_mapping ufbxi_blender_phong_shader_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("DiffuseColor") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, UFBXI_MAT_TRANSFORM_BLENDER_OPACITY, ufbxi_mat_string("TransparencyFactor") }, + { UFBX_MATERIAL_PBR_EMISSION_FACTOR, 0, 0, ufbxi_mat_string("EmissiveFactor") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1, 0, ufbxi_mat_string("EmissiveColor") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_BLENDER_SHININESS, ufbxi_mat_string("Shininess") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_BLENDER_SHININESS, ufbxi_mat_string("ShininessExponent") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("ReflectionFactor") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("NormalMap") }, +}; + +static const ufbxi_shader_mapping ufbxi_obj_pbr_mapping[] = { + { UFBX_MATERIAL_PBR_BASE_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Kd") }, + { UFBX_MATERIAL_PBR_SPECULAR_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ks") }, + { UFBX_MATERIAL_PBR_EMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ke") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, UFBXI_MAT_TRANSFORM_UNKNOWN_SHININESS, ufbxi_mat_string("Ns") }, + { UFBX_MATERIAL_PBR_ROUGHNESS, 0, 0, ufbxi_mat_string("Pr") }, + { UFBX_MATERIAL_PBR_SPECULAR_IOR, 0, 0, ufbxi_mat_string("Ni") }, + { UFBX_MATERIAL_PBR_METALNESS, 0, 0, ufbxi_mat_string("Pm") }, + { UFBX_MATERIAL_PBR_OPACITY, UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("d") }, + { UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Tf") }, + { UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, 0, 0, ufbxi_mat_string("disp") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("bump") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("Bump") }, + { UFBX_MATERIAL_PBR_NORMAL_MAP, 0, 0, ufbxi_mat_string("norm") }, + { UFBX_MATERIAL_PBR_SHEEN_COLOR, UFBXI_SHADER_MAPPING_DEFAULT_W_1|UFBXI_SHADER_MAPPING_WIDEN_TO_RGB, 0, ufbxi_mat_string("Ps") }, + { UFBX_MATERIAL_PBR_COAT_FACTOR, 0, 0, ufbxi_mat_string("Pc") }, + { UFBX_MATERIAL_PBR_COAT_ROUGHNESS, 0, 0, ufbxi_mat_string("Pcr") }, + { UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, 0, 0, ufbxi_mat_string("aniso") }, + { UFBX_MATERIAL_PBR_SPECULAR_ROTATION, 0, 0, ufbxi_mat_string("anisor") }, +}; + +static const ufbxi_shader_mapping ufbxi_obj_features[] = { + { UFBX_MATERIAL_FEATURE_PBR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pr") }, + { UFBX_MATERIAL_FEATURE_PBR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pm") }, + { UFBX_MATERIAL_FEATURE_SHEEN, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ps") }, + { UFBX_MATERIAL_FEATURE_COAT, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pc") }, + { UFBX_MATERIAL_FEATURE_METALNESS, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Pm") }, + { UFBX_MATERIAL_FEATURE_IOR, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ni") }, + { UFBX_MATERIAL_FEATURE_OPACITY, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("d") }, + { UFBX_MATERIAL_FEATURE_TRANSMISSION, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Tf") }, + { UFBX_MATERIAL_FEATURE_EMISSION, UFBXI_SHADER_FEATURE_IF_EXISTS_OR_TEXTURE, 0, ufbxi_mat_string("Ke") }, +}; + +enum { + UFBXI_MAT_PBR = 1 << UFBX_MATERIAL_FEATURE_PBR, + UFBXI_MAT_METALNESS = 1 << UFBX_MATERIAL_FEATURE_METALNESS, + UFBXI_MAT_DIFFUSE = 1 << UFBX_MATERIAL_FEATURE_DIFFUSE, + UFBXI_MAT_SPECULAR = 1 << UFBX_MATERIAL_FEATURE_SPECULAR, + UFBXI_MAT_EMISSION = 1 << UFBX_MATERIAL_FEATURE_EMISSION, + UFBXI_MAT_COAT = 1 << UFBX_MATERIAL_FEATURE_COAT, + UFBXI_MAT_SHEEN = 1 << UFBX_MATERIAL_FEATURE_SHEEN, + UFBXI_MAT_TRANSMISSION = 1 << UFBX_MATERIAL_FEATURE_TRANSMISSION, + UFBXI_MAT_OPACITY = 1 << UFBX_MATERIAL_FEATURE_OPACITY, + UFBXI_MAT_AMBIENT_OCCLUSION = 1 << UFBX_MATERIAL_FEATURE_AMBIENT_OCCLUSION, + UFBXI_MAT_MATTE = 1 << UFBX_MATERIAL_FEATURE_MATTE, + UFBXI_MAT_UNLIT = 1 << UFBX_MATERIAL_FEATURE_UNLIT, + UFBXI_MAT_IOR = 1 << UFBX_MATERIAL_FEATURE_IOR, + UFBXI_MAT_DIFFUSE_ROUGHNESS = 1 << UFBX_MATERIAL_FEATURE_DIFFUSE_ROUGHNESS, + UFBXI_MAT_TRANSMISSION_ROUGHNESS = 1 << UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS, + UFBXI_MAT_THIN_WALLED = 1 << UFBX_MATERIAL_FEATURE_THIN_WALLED, + UFBXI_MAT_CAUSTICS = 1 << UFBX_MATERIAL_FEATURE_CAUSTICS, + UFBXI_MAT_EXIT_TO_BACKGROUND = 1 << UFBX_MATERIAL_FEATURE_EXIT_TO_BACKGROUND, + UFBXI_MAT_INTERNAL_REFLECTIONS = 1 << UFBX_MATERIAL_FEATURE_INTERNAL_REFLECTIONS, + UFBXI_MAT_DOUBLE_SIDED = 1 << UFBX_MATERIAL_FEATURE_DOUBLE_SIDED, +}; + +static const ufbxi_shader_mapping_list ufbxi_shader_pbr_mappings[] = { + { // UFBX_SHADER_UNKNOWN + ufbxi_fbx_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_phong_shader_pbr_mapping), + NULL, 0, + (uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION), + }, + { // UFBX_SHADER_FBX_LAMBERT + ufbxi_fbx_lambert_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_lambert_shader_pbr_mapping), + NULL, 0, + (uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION), + }, + { // UFBX_SHADER_FBX_PHONG + ufbxi_fbx_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_fbx_phong_shader_pbr_mapping), + NULL, 0, + (uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_EMISSION | UFBXI_MAT_TRANSMISSION), + }, + { // UFBX_SHADER_OSL_STANDARD_SURFACE + ufbxi_osl_standard_shader_pbr_mapping, ufbxi_arraycount(ufbxi_osl_standard_shader_pbr_mapping), + ufbxi_osl_standard_shader_features, ufbxi_arraycount(ufbxi_osl_standard_shader_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT + | UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS), + }, + { // UFBX_SHADER_ARNOLD_STANDARD_SURFACE + ufbxi_arnold_shader_pbr_mapping, ufbxi_arraycount(ufbxi_arnold_shader_pbr_mapping), + ufbxi_arnold_shader_features, ufbxi_arraycount(ufbxi_arnold_shader_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT + | UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS), + }, + { // UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL + ufbxi_3ds_max_physical_material_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_physical_material_pbr_mapping), + ufbxi_3ds_max_physical_material_features, ufbxi_arraycount(ufbxi_3ds_max_physical_material_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_COAT + | UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR), + { NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix + { NULL, 0 }, ufbxi_string_literal("_map_on"), // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH + ufbxi_3ds_max_pbr_metal_rough_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_pbr_metal_rough_pbr_mapping), + ufbxi_3ds_max_pbr_features, ufbxi_arraycount(ufbxi_3ds_max_pbr_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_OPACITY), + { NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix + { NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS + ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping, ufbxi_arraycount(ufbxi_3ds_max_pbr_spec_gloss_pbr_mapping), + ufbxi_3ds_max_pbr_features, ufbxi_arraycount(ufbxi_3ds_max_pbr_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_SPECULAR | UFBXI_MAT_DIFFUSE | UFBXI_MAT_OPACITY), + { NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix + { NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_GLTF_MATERIAL + ufbxi_gltf_material_pbr_mapping, ufbxi_arraycount(ufbxi_gltf_material_pbr_mapping), + ufbxi_gltf_material_features, ufbxi_arraycount(ufbxi_gltf_material_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_AMBIENT_OCCLUSION), + { NULL, 0 }, ufbxi_string_literal("Map"), // texture_prefix/suffix + { NULL, 0 }, { NULL, 0 }, // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_OPENPBR_MATERIAL + ufbxi_openpbr_material_pbr_mapping, ufbxi_arraycount(ufbxi_openpbr_material_pbr_mapping), + ufbxi_openpbr_material_features, ufbxi_arraycount(ufbxi_openpbr_material_features), + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR | UFBXI_MAT_COAT + | UFBXI_MAT_SHEEN | UFBXI_MAT_TRANSMISSION | UFBXI_MAT_OPACITY | UFBXI_MAT_IOR | UFBXI_MAT_DIFFUSE_ROUGHNESS), + { NULL, 0 }, ufbxi_string_literal("_map"), // texture_prefix/suffix + { NULL, 0 }, ufbxi_string_literal("_map_on"), // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_SHADERFX_GRAPH + ufbxi_shaderfx_graph_pbr_mapping, ufbxi_arraycount(ufbxi_shaderfx_graph_pbr_mapping), + NULL, 0, + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION | UFBXI_MAT_AMBIENT_OCCLUSION), + ufbxi_string_literal("TEX_"), ufbxi_string_literal("_map"), // texture_prefix/suffix + ufbxi_string_literal("use_"), ufbxi_string_literal("_map"), // texture_enabled_prefix/suffix + }, + { // UFBX_SHADER_BLENDER_PHONG + ufbxi_blender_phong_shader_pbr_mapping, ufbxi_arraycount(ufbxi_blender_phong_shader_pbr_mapping), + NULL, 0, + (uint32_t)(UFBXI_MAT_PBR | UFBXI_MAT_METALNESS | UFBXI_MAT_DIFFUSE | UFBXI_MAT_EMISSION), + }, + { // UFBX_SHADER_WAVEFRONT_MTL + ufbxi_obj_pbr_mapping, ufbxi_arraycount(ufbxi_obj_pbr_mapping), + ufbxi_obj_features, ufbxi_arraycount(ufbxi_obj_features), + (uint32_t)(UFBXI_MAT_DIFFUSE | UFBXI_MAT_SPECULAR), + }, +}; + +ufbx_static_assert(shader_pbr_mapping_list, ufbxi_arraycount(ufbxi_shader_pbr_mappings) == UFBX_SHADER_TYPE_COUNT); + +enum { + UFBXI_MAPPING_FETCH_VALUE = 0x1, + UFBXI_MAPPING_FETCH_TEXTURE = 0x2, + UFBXI_MAPPING_FETCH_TEXTURE_ENABLED = 0x4, + UFBXI_MAPPING_FETCH_FEATURE = 0x8, +}; + +ufbxi_noinline static void ufbxi_fetch_mapping_maps(ufbx_material *material, ufbx_material_map *maps, ufbx_material_feature_info *features, + ufbx_shader *shader, const ufbxi_shader_mapping *mappings, size_t count, ufbx_string prefix, ufbx_string prefix2, ufbx_string suffix, uint32_t flags) +{ + char combined_name[512]; + ufbx_shader_prop_binding identity_binding; + + ufbxi_for(const ufbxi_shader_mapping, mapping, mappings, count) { + ufbx_string prop_name = { mapping->prop, mapping->prop_len }; + if (prefix.length > 0 || prefix2.length > 0 || suffix.length > 0) { + if (prop_name.length + prefix.length + prefix2.length + suffix.length <= sizeof(combined_name)) { + char *dst = combined_name; + + if (prefix.length > 0) { + memcpy(dst, prefix.data, prefix.length); + dst += prefix.length; + } + if (prefix2.length > 0) { + memcpy(dst, prefix2.data, prefix2.length); + dst += prefix2.length; + } + if (prop_name.length > 0) { + memcpy(dst, prop_name.data, prop_name.length); + dst += prop_name.length; + } + if (suffix.length > 0) { + memcpy(dst, suffix.data, suffix.length); + dst += suffix.length; + } + + prop_name.data = combined_name; + prop_name.length = ufbxi_to_size(dst - combined_name); + } + } + + ufbx_shader_prop_binding_list bindings = ufbx_find_shader_prop_bindings_len(shader, prop_name.data, prop_name.length); + if (bindings.count == 0) { + identity_binding.material_prop = prop_name; + identity_binding.shader_prop = ufbx_empty_string; + bindings.data = &identity_binding; + bindings.count = 1; + } + + uint32_t mapping_flags = mapping->flags; + ufbxi_for_list(ufbx_shader_prop_binding, binding, bindings) { + ufbx_string name = binding->material_prop; + + ufbx_prop *prop = ufbx_find_prop_len(&material->props, name.data, name.length); + if (flags & UFBXI_MAPPING_FETCH_FEATURE) { + ufbx_material_feature_info *feature = &features[mapping->index]; + if (prop && prop->type != UFBX_PROP_REFERENCE) { + feature->enabled = prop->value_int != 0; + feature->is_explicit = true; + if (mapping_flags & UFBXI_SHADER_FEATURE_IF_AROUND_1) { + feature->enabled = (prop->value_real >= 0.5f && prop->value_real <= 1.5f); + } + if (mapping_flags & UFBXI_SHADER_FEATURE_INVERTED) { + feature->enabled = !feature->enabled; + } + if (mapping_flags & UFBXI_SHADER_FEATURE_IF_EXISTS) { + feature->enabled = true; + } + } + if (mapping_flags & UFBXI_SHADER_FEATURE_IF_TEXTURE) { + ufbx_texture *texture = ufbx_find_prop_texture_len(material, name.data, name.length); + if (texture) { + feature->enabled = true; + } + } + continue; + } + + ufbx_material_map *map = &maps[mapping->index]; + + if (flags & UFBXI_MAPPING_FETCH_VALUE) { + if (prop && prop->type != UFBX_PROP_REFERENCE) { + if ((mapping->flags & UFBXI_SHADER_MAPPING_MULTIPLY_VALUE) != 0) { + map->value_vec4.x *= prop->value_vec4.x; + map->value_int = ufbxi_f64_to_i64(map->value_vec4.x); + } else { + map->value_vec4 = prop->value_vec4; + map->value_int = prop->value_int; + } + map->has_value = true; + if (mapping->transform) { + ufbxi_mat_transform_fn transform_fn = ufbxi_mat_transform_fns[mapping->transform]; + transform_fn(&map->value_vec4); + } + + uint32_t prop_flags = (uint32_t)prop->flags; + if ((mapping->flags & UFBXI_SHADER_MAPPING_DEFAULT_W_1) != 0 && (prop_flags & UFBX_PROP_FLAG_VALUE_VEC4) == 0) { + map->value_vec4.w = 1.0f; + } + if ((mapping->flags & UFBXI_SHADER_MAPPING_WIDEN_TO_RGB) != 0 && (prop_flags & UFBX_PROP_FLAG_VALUE_REAL) != 0) { + map->value_vec3.y = map->value_vec3.x; + map->value_vec3.z = map->value_vec3.x; + } + if ((prop_flags & UFBX_PROP_FLAG_VALUE_REAL) != 0) { + map->value_components = 1; + } else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC2) != 0) { + map->value_components = 2; + } else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC3) != 0) { + map->value_components = 3; + } else if ((prop_flags & UFBX_PROP_FLAG_VALUE_VEC4) != 0) { + map->value_components = 4; + } else { + map->value_components = 0; + } + } + } + + if (flags & UFBXI_MAPPING_FETCH_TEXTURE) { + ufbx_texture *texture = ufbx_find_prop_texture_len(material, name.data, name.length); + if (texture) { + map->texture = texture; + map->texture_enabled = true; + } + } + + if (flags & UFBXI_MAPPING_FETCH_TEXTURE_ENABLED) { + if (prop) { + map->texture_enabled = prop->value_int != 0; + } + } + } + } +} + +ufbxi_noinline static void ufbxi_update_factor(ufbx_material_map *factor_map, ufbx_material_map *color_map) +{ + if (!factor_map->has_value) { + if (color_map->has_value && !ufbxi_is_vec4_zero(color_map->value_vec4)) { + factor_map->value_real = 1.0f; + factor_map->value_int = 1; + } else { + factor_map->value_real = 0.0f; + factor_map->value_int = 0; + } + } +} + +// Some material modes have toggleable roughness/glossiness mode, we read it initially +// always as roughness and if a matching feature such as `roughness_as_glossiness` is set +// we transfer the data into the glossiness and invert the roughness. +typedef struct { + uint8_t feature; + uint8_t roughness_map; + uint8_t glossiness_map; +} ufbxi_glossiness_remap; + +static const ufbxi_glossiness_remap ufbxi_glossiness_remaps[] = { + { UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_ROUGHNESS, UFBX_MATERIAL_PBR_GLOSSINESS }, + { UFBX_MATERIAL_FEATURE_COAT_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_COAT_ROUGHNESS, UFBX_MATERIAL_PBR_COAT_GLOSSINESS }, + { UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS, UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, UFBX_MATERIAL_PBR_TRANSMISSION_GLOSSINESS }, +}; + +ufbxi_noinline static void ufbxi_fetch_maps(ufbx_scene *scene, ufbx_material *material) +{ + (void)scene; + + ufbx_shader *shader = material->shader; + ufbx_assert((uint32_t)material->shader_type < UFBX_SHADER_TYPE_COUNT); + + memset(&material->fbx, 0, sizeof(material->fbx)); + memset(&material->pbr, 0, sizeof(material->pbr)); + memset(&material->features, 0, sizeof(material->features)); + + const ufbxi_shader_mapping *base_mapping = ufbxi_base_fbx_mapping; + size_t num_base_mapping = ufbxi_arraycount(ufbxi_base_fbx_mapping); + + if (scene->metadata.file_format == UFBX_FILE_FORMAT_OBJ || scene->metadata.file_format == UFBX_FILE_FORMAT_MTL) { + base_mapping = ufbxi_obj_fbx_mapping; + num_base_mapping = ufbxi_arraycount(ufbxi_obj_fbx_mapping); + } + + ufbxi_fetch_mapping_maps(material, material->fbx.maps, NULL, NULL, + base_mapping, num_base_mapping, + ufbx_empty_string, ufbx_empty_string, ufbx_empty_string, + UFBXI_MAPPING_FETCH_VALUE | UFBXI_MAPPING_FETCH_TEXTURE); + + ufbxi_shader_mapping_list list = ufbxi_shader_pbr_mappings[material->shader_type]; + + for (uint32_t i = 0; i < UFBX_MATERIAL_FEATURE_COUNT; i++) { + if ((list.default_features & (1u << i)) != 0) { + material->features.features[i].enabled = true; + } + } + + ufbx_string prefix = ufbx_empty_string; + if (!shader) { + prefix = material->shader_prop_prefix; + } + + if (list.texture_prefix.length > 0 || list.texture_suffix.length > 0) { + ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader, + list.data, list.count, prefix, list.texture_prefix, list.texture_suffix, + UFBXI_MAPPING_FETCH_TEXTURE); + } + + ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader, + list.data, list.count, prefix, ufbx_empty_string, ufbx_empty_string, + UFBXI_MAPPING_FETCH_VALUE | UFBXI_MAPPING_FETCH_TEXTURE); + + if (list.texture_enabled_prefix.length > 0 || list.texture_enabled_suffix.length > 0) { + ufbxi_fetch_mapping_maps(material, material->pbr.maps, NULL, shader, + list.data, list.count, prefix, list.texture_enabled_prefix, list.texture_enabled_suffix, + UFBXI_MAPPING_FETCH_TEXTURE_ENABLED); + } + + ufbxi_fetch_mapping_maps(material, NULL, material->features.features, shader, + list.features, list.feature_count, prefix, ufbx_empty_string, ufbx_empty_string, + UFBXI_MAPPING_FETCH_FEATURE); + + ufbxi_update_factor(&material->fbx.diffuse_factor, &material->fbx.diffuse_color); + ufbxi_update_factor(&material->fbx.specular_factor, &material->fbx.specular_color); + ufbxi_update_factor(&material->fbx.reflection_factor, &material->fbx.reflection_color); + ufbxi_update_factor(&material->fbx.transparency_factor, &material->fbx.transparency_color); + ufbxi_update_factor(&material->fbx.emission_factor, &material->fbx.emission_color); + ufbxi_update_factor(&material->fbx.ambient_factor, &material->fbx.ambient_color); + + ufbxi_update_factor(&material->pbr.base_factor, &material->pbr.base_color); + ufbxi_update_factor(&material->pbr.specular_factor, &material->pbr.specular_color); + ufbxi_update_factor(&material->pbr.emission_factor, &material->pbr.emission_color); + ufbxi_update_factor(&material->pbr.sheen_factor, &material->pbr.sheen_color); + ufbxi_update_factor(&material->pbr.thin_film_factor, &material->pbr.thin_film_thickness); + ufbxi_update_factor(&material->pbr.transmission_factor, &material->pbr.transmission_color); + + // Patch transmission roughness if only extra roughness is defined + if (!material->pbr.transmission_roughness.has_value && material->pbr.roughness.has_value && material->pbr.transmission_extra_roughness.has_value) { + material->pbr.transmission_roughness.value_real = material->pbr.roughness.value_real + material->pbr.transmission_extra_roughness.value_real; + } + + // Map roughness to glossiness and vice versa + ufbxi_for(const ufbxi_glossiness_remap, remap, ufbxi_glossiness_remaps, ufbxi_arraycount(ufbxi_glossiness_remaps)) { + ufbx_material_map *roughness = &material->pbr.maps[remap->roughness_map]; + ufbx_material_map *glossiness = &material->pbr.maps[remap->glossiness_map]; + if (material->features.features[remap->feature].enabled) { + *glossiness = *roughness; + memset(roughness, 0, sizeof(ufbx_material_map)); + if (glossiness->has_value) { + roughness->value_real = 1.0f - glossiness->value_real; + } + } else { + if (roughness->has_value) { + glossiness->value_real = 1.0f - roughness->value_real; + } + } + } +} + +typedef enum { + UFBXI_CONSTRAINT_PROP_NODE, + UFBXI_CONSTRAINT_PROP_IK_EFFECTOR, + UFBXI_CONSTRAINT_PROP_IK_END_NODE, + UFBXI_CONSTRAINT_PROP_AIM_UP, + UFBXI_CONSTRAINT_PROP_TARGET, +} ufbxi_constraint_prop_type; + +typedef struct { + ufbxi_constraint_prop_type type; + const char *name; +} ufbxi_constraint_prop; + +static const ufbxi_constraint_prop ufbxi_constraint_props[] = { + { UFBXI_CONSTRAINT_PROP_NODE, "Constrained Object" }, + { UFBXI_CONSTRAINT_PROP_NODE, "Constrained object (Child)" }, + { UFBXI_CONSTRAINT_PROP_NODE, "First Joint" }, + { UFBXI_CONSTRAINT_PROP_TARGET, "Source" }, + { UFBXI_CONSTRAINT_PROP_TARGET, "Source (Parent)" }, + { UFBXI_CONSTRAINT_PROP_TARGET, "Aim At Object" }, + { UFBXI_CONSTRAINT_PROP_TARGET, "Pole Vector Object" }, + { UFBXI_CONSTRAINT_PROP_IK_EFFECTOR, "Effector" }, + { UFBXI_CONSTRAINT_PROP_IK_END_NODE, "End Joint" }, + { UFBXI_CONSTRAINT_PROP_AIM_UP, "World Up Object" }, +}; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_add_constraint_prop(ufbxi_context *uc, ufbx_constraint *constraint, ufbx_node *node, const char *prop) +{ + ufbxi_for(const ufbxi_constraint_prop, cprop, ufbxi_constraint_props, ufbxi_arraycount(ufbxi_constraint_props)) { + if (strcmp(cprop->name, prop) != 0) continue; + switch (cprop->type) { + case UFBXI_CONSTRAINT_PROP_NODE: constraint->node = node; break; + case UFBXI_CONSTRAINT_PROP_IK_EFFECTOR: constraint->ik_effector = node; break; + case UFBXI_CONSTRAINT_PROP_IK_END_NODE: constraint->ik_end_node = node; break; + case UFBXI_CONSTRAINT_PROP_AIM_UP: constraint->aim_up_node = node; break; + case UFBXI_CONSTRAINT_PROP_TARGET: { + ufbx_constraint_target *target = ufbxi_push_zero(&uc->tmp_stack, ufbx_constraint_target, 1); + ufbxi_check(target); + target->node = node; + target->weight = 1.0f; + target->transform = ufbx_identity_transform; + } break; + default: + ufbxi_unreachable("Unexpected constraint prop"); + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_nurbs_basis(ufbxi_context *uc, ufbx_nurbs_basis *basis) +{ + if (basis->topology == UFBX_NURBS_TOPOLOGY_CLOSED) { + basis->num_wrap_control_points = 1; + } else if (basis->topology == UFBX_NURBS_TOPOLOGY_PERIODIC) { + basis->num_wrap_control_points = basis->order - 1; + } else { + basis->num_wrap_control_points = 0; + } + + if (basis->order > 1) { + size_t degree = basis->order - 1; + ufbx_real_list knots = basis->knot_vector; + if (knots.count >= 2*degree + 1) { + basis->t_min = knots.data[degree]; + basis->t_max = knots.data[knots.count - degree - 1]; + + size_t max_spans = knots.count - 2*degree; + ufbx_real *spans = ufbxi_push(&uc->result, ufbx_real, max_spans); + ufbxi_check(spans); + + ufbx_real prev = -UFBX_INFINITY; + size_t num_spans = 0; + for (size_t i = 0; i < max_spans; i++) { + ufbx_real t = knots.data[degree + i]; + if (t != prev) { + spans[num_spans++] = t; + prev = t; + } + } + + basis->spans.data = spans; + basis->spans.count = num_spans; + basis->valid = true; + for (size_t i = 1; i < knots.count; i++) { + if (knots.data[i - 1] > knots.data[i]) { + basis->valid = false; + break; + } + } + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_lod_group(ufbxi_context *uc, ufbx_lod_group *lod) +{ + size_t num_levels = 0; + for (size_t i = 0; i < lod->instances.count; i++) { + num_levels = ufbxi_max_sz(num_levels, lod->instances.data[0]->children.count); + } + + char prop_name[64]; + for (size_t i = 0; ; i++) { + int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "Thresholds|Level%zu", i); + ufbx_prop *prop = ufbx_find_prop_len(&lod->props, prop_name, (size_t)len); + if (!prop) break; + num_levels = ufbxi_max_sz(num_levels, i + 1); + } + + ufbx_lod_level *levels = ufbxi_push_zero(&uc->result, ufbx_lod_level, num_levels); + ufbxi_check(levels); + + lod->relative_distances = ufbx_find_bool(&lod->props, "ThresholdsUsedAsPercentage", false); + lod->ignore_parent_transform = !ufbx_find_bool(&lod->props, "WorldSpace", true); + + lod->use_distance_limit = ufbx_find_bool(&lod->props, "MinMaxDistance", false); + lod->distance_limit_min = ufbx_find_real(&lod->props, "MinDistance", (ufbx_real)-100.0); + lod->distance_limit_max = ufbx_find_real(&lod->props, "MaxDistance", (ufbx_real)100.0); + + lod->lod_levels.data = levels; + lod->lod_levels.count = num_levels; + + for (size_t i = 0; i < num_levels; i++) { + ufbx_lod_level *level = &levels[i]; + + if (i > 0) { + int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "Thresholds|Level%zu", i - 1); + level->distance = ufbx_find_real_len(&lod->props, prop_name, (size_t)len, 0.0f); + } else if (lod->relative_distances) { + level->distance = (ufbx_real)100.0; + } + + { + int len = ufbxi_snprintf(prop_name, sizeof(prop_name), "DisplayLevels|Level%zu", i); + int64_t display = ufbx_find_int_len(&lod->props, prop_name, (size_t)len, 0); + if (display >= 0 && display <= 2) { + level->display = (ufbx_lod_display)display; + } + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_generate_normals(ufbxi_context *uc, ufbx_mesh *mesh) +{ + size_t num_indices = mesh->num_indices; + + mesh->generated_normals = true; + + ufbx_topo_edge *topo = ufbxi_push(&uc->tmp_stack, ufbx_topo_edge, num_indices); + ufbxi_check(topo); + + uint32_t *normal_indices = ufbxi_push(&uc->result, uint32_t, num_indices); + ufbxi_check(normal_indices); + + ufbx_compute_topology(mesh, topo, num_indices); + size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, num_indices, normal_indices, num_indices, false); + + if (num_normals == mesh->num_vertices) { + mesh->vertex_normal.unique_per_vertex = true; + } + + ufbx_vec3 *normal_data = ufbxi_push(&uc->result, ufbx_vec3, num_normals + 1); + ufbxi_check(normal_data); + + normal_data[0] = ufbx_zero_vec3; + normal_data++; + + ufbx_compute_normals(mesh, &mesh->vertex_position, normal_indices, num_indices, normal_data, num_normals); + + mesh->vertex_normal.exists = true; + mesh->vertex_normal.values.data = normal_data; + mesh->vertex_normal.values.count = num_normals; + mesh->vertex_normal.indices.data = normal_indices; + mesh->vertex_normal.indices.count = num_indices; + mesh->vertex_normal.value_reals = 3; + + mesh->skinned_normal = mesh->vertex_normal; + + ufbxi_pop(&uc->tmp_stack, ufbx_topo_edge, num_indices, NULL); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_prop_prefix(ufbxi_context *uc, ufbx_string *dst, ufbx_string prefix) +{ + size_t stack_size = 0; + if (prefix.length > 0 && prefix.data[prefix.length - 1] != '|') { + stack_size = prefix.length + 1; + char *copy = ufbxi_push(&uc->tmp_stack, char, stack_size); + ufbxi_check(copy); + memcpy(copy, prefix.data, prefix.length); + copy[prefix.length] = '|'; + + prefix.data = copy; + prefix.length += 1; + } + + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &prefix, false)); + *dst = prefix; + + if (stack_size > 0) { + ufbxi_pop(&uc->tmp_stack, char, stack_size, NULL); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_shader_texture_find_prefix(ufbxi_context *uc, ufbx_texture *texture, ufbx_shader_texture *shader) +{ + ufbx_string suffixes[3]; + size_t num_suffixes = 0; + + suffixes[num_suffixes++] = ufbxi_str_c(" Parameters/Connections"); + if (shader->shader_name.length > 0) { + suffixes[num_suffixes++] = shader->shader_name; + } + suffixes[num_suffixes++] = ufbxi_str_c("3dsMax|parameters"); + + ufbx_assert(num_suffixes <= ufbxi_arraycount(suffixes)); + + ufbxi_for(ufbx_string, p_suffix, suffixes, num_suffixes) { + ufbx_string suffix = *p_suffix; + + ufbxi_for_list(ufbx_prop, prop, texture->props.props) { + if (prop->type != UFBX_PROP_COMPOUND) continue; + if (ufbxi_ends_with(prop->name, suffix)) { + ufbxi_check(ufbxi_push_prop_prefix(uc, &shader->prop_prefix, prop->name)); + return 1; + } + } + } + + // Pre-7000 files don't have explicit Compound properties, so let's look for + // any property that has the suffix before the last `|` ... + ufbxi_for(ufbx_string, p_suffix, suffixes, num_suffixes) { + ufbx_string suffix = *p_suffix; + + ufbxi_for_list(ufbx_prop, prop, texture->props.props) { + ufbx_string name = prop->name; + while (name.length > 0) { + if (name.data[name.length - 1] == '|') { + break; + } + name.length--; + } + if (name.length <= 1) continue; + name.length--; + + if (ufbxi_ends_with(name, suffix)) { + ufbxi_check(ufbxi_push_prop_prefix(uc, &shader->prop_prefix, name)); + return 1; + } + } + } + + return 1; +} + +typedef struct { + uint64_t shader_id; + const char *shader_name; + const char *input_name; +} ufbxi_file_shader; + +// Known shaders that represent sampled images. +static const ufbxi_file_shader ufbxi_file_shaders[] = { + { UINT64_C(0x7e73161fad53b12a), "ai_image", "filename" }, + { 0, "OSLBitmap", ufbxi_Filename }, + { 0, "OSLBitmap2", ufbxi_Filename }, + { 0, "OSLBitmap3", ufbxi_Filename }, + { 0, "UberBitmap", ufbxi_Filename }, + { 0, "UberBitmap2", ufbxi_Filename }, +}; + +ufbxi_noinline static void ufbxi_update_shader_texture(ufbx_texture *texture, ufbx_shader_texture *shader) +{ + ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) { + ufbx_prop *prop = input->prop; + if (prop) { + input->prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length); + input->value_vec4 = prop->value_vec4; + input->value_int = prop->value_int; + input->value_str = prop->value_str; + input->value_blob = prop->value_blob; + input->texture = (ufbx_texture*)ufbx_get_prop_element(&texture->element, input->prop, UFBX_ELEMENT_TEXTURE); + } + + prop = input->texture_prop; + if (prop) { + input->texture_prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length); + ufbx_texture *tex = (ufbx_texture*)ufbx_get_prop_element(&texture->element, prop, UFBX_ELEMENT_TEXTURE); + if (tex) input->texture = tex; + } + + input->texture_enabled = input->texture != NULL; + prop = input->texture_enabled_prop; + if (prop) { + input->texture_enabled_prop = prop = ufbx_find_prop_len(&texture->props, prop->name.data, prop->name.length); + input->texture_enabled = prop->value_int != 0; + } + } + + if (shader->type == UFBX_SHADER_TEXTURE_SELECT_OUTPUT) { + ufbx_shader_texture_input *map = ufbx_find_shader_texture_input(shader, "sourceMap"); + ufbx_shader_texture_input *index = ufbx_find_shader_texture_input(shader, "outputChannelIndex"); + if (index) { + shader->main_texture_output_index = index->value_int; + } + if (map) { + shader->main_texture = map->texture; + map->texture_output_index = shader->main_texture_output_index; + } + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_shader_texture(ufbxi_context *uc, ufbx_texture *texture) +{ + uint32_t classid_a = (uint32_t)(uint64_t)ufbx_find_int(&texture->props, "3dsMax|ClassIDa", 0); + uint32_t classid_b = (uint32_t)(uint64_t)ufbx_find_int(&texture->props, "3dsMax|ClassIDb", 0); + uint64_t classid = (uint64_t)classid_a << 32u | classid_b; + + ufbx_string max_texture = ufbx_find_string(&texture->props, "3dsMax|MaxTexture", ufbx_empty_string); + + // Check first if the texture looks like it could be a shader. + ufbx_shader_texture_type type = (ufbx_shader_texture_type)UFBX_SHADER_TEXTURE_TYPE_COUNT; + + if (!strcmp(max_texture.data, "MULTIOUTPUT_TO_OSLMap") || classid == UINT64_C(0x896ef2fc44bd743f)) { + type = UFBX_SHADER_TEXTURE_SELECT_OUTPUT; + } else if (!strcmp(max_texture.data, "OSLMap") || classid == UINT64_C(0x7f9a7b9d6fcdf00d)) { + type = UFBX_SHADER_TEXTURE_OSL; + } else if (texture->type == UFBX_TEXTURE_FILE && texture->relative_filename.length == 0 && texture->absolute_filename.length == 0 && !texture->video) { + type = UFBX_SHADER_TEXTURE_UNKNOWN; + } + + if ((uint32_t)type == UFBX_SHADER_TEXTURE_TYPE_COUNT) return 1; + + ufbx_shader_texture *shader = ufbxi_push_zero(&uc->result, ufbx_shader_texture, 1); + ufbxi_check(shader); + + shader->type = type; + + static const char *const name_props[] = { + "3dsMax|params|OSLShaderName", + }; + + static const char *const source_props[] = { + "3dsMax|params|OSLCode", + }; + + shader->shader_source.data = ufbxi_empty_char; + shader->shader_name.data = ufbxi_empty_char; + + ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(name_props); i++) { + ufbx_prop *prop = ufbx_find_prop(&texture->props, name_props[i]); + if (prop) { + shader->shader_name = prop->value_str; + break; + } + } + + ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(source_props); i++) { + ufbx_prop *prop = ufbx_find_prop(&texture->props, source_props[i]); + if (prop) { + shader->shader_source = prop->value_str; + shader->raw_shader_source = prop->value_blob; + break; + } + } + + ufbxi_check(ufbxi_shader_texture_find_prefix(uc, texture, shader)); + + if (shader->shader_name.length == 0) { + ufbx_string name = shader->prop_prefix; + if (ufbxi_remove_suffix_c(&name, " Parameters/Connections|")) { + size_t begin = name.length; + while (begin > 0 && name.data[begin - 1] != '|') { + begin--; + } + + shader->shader_name.data = name.data + begin; + shader->shader_name.length = name.length - begin; + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, &shader->shader_name, false)); + } + } + + if (shader->shader_name.length == 0) { + if (max_texture.length > 0) { + shader->shader_name = max_texture; + } + } + + if (classid != 0) { + shader->shader_type_id = classid; + } + + if (shader->prop_prefix.length == 0) { + // If we not find any shader properties so we might have guessed wrong. + // We "leak" (freed with scene) the shader in this case but it's negligible. + return 1; + } + + ufbxi_for_list(ufbx_prop, prop, texture->props.props) { + + ufbx_string name = prop->name; + if (!ufbxi_remove_prefix_str(&name, shader->prop_prefix)) continue; + + // Check if this property is a modifier to an existing input. + ufbx_string base_name = name; + if (ufbxi_remove_suffix_c(&base_name, "_map") || ufbxi_remove_suffix_c(&base_name, ".shader")) { + ufbx_shader_texture_input *base = ufbx_find_shader_texture_input_len(shader, base_name.data, base_name.length); + if (base) { + base->texture_prop = prop; + continue; + } + } else if (ufbxi_remove_suffix_c(&base_name, ".connected") || ufbxi_remove_suffix_c(&base_name, "Enabled")) { + ufbx_shader_texture_input *base = ufbx_find_shader_texture_input_len(shader, base_name.data, base_name.length); + if (base) { + base->texture_enabled_prop = prop; + continue; + } + } + + // Use `uc->tmp_arr` to store the texture inputs so we can search them while we insert new ones. + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, + (shader->inputs.count + 1) * sizeof(ufbx_shader_texture_input))); + shader->inputs.data = (ufbx_shader_texture_input*)uc->tmp_arr; + + // Add a new property + ufbx_shader_texture_input *input = &shader->inputs.data[shader->inputs.count++]; + memset(input, 0, sizeof(ufbx_shader_texture_input)); + + // NOTE: This is a bit hackish, we are using a suffix of an interned string. It won't compare + // pointer equal to the same string but that shouldn't matter.. + input->name = name; + + // Connect the property only, values and textures etc are fetched in `ufbxi_update_shader_texture()`. + input->prop = prop; + } + + // Retain the shader inputs + shader->inputs.data = ufbxi_push_copy(&uc->result, ufbx_shader_texture_input, shader->inputs.count, shader->inputs.data); + ufbxi_check(shader->inputs.data); + + texture->shader = shader; + texture->type = UFBX_TEXTURE_SHADER; + uc->scene.metadata.num_shader_textures++; + + if (!uc->opts.disable_quirks) { + ufbxi_nounroll for (size_t i = 0; i < ufbxi_arraycount(ufbxi_file_shaders); i++) { + const ufbxi_file_shader *fs = &ufbxi_file_shaders[i]; + + if ((fs->shader_id && shader->shader_type_id == fs->shader_id) || !strcmp(shader->shader_name.data, fs->shader_name)) { + ufbx_shader_texture_input *input = ufbx_find_shader_texture_input(shader, fs->input_name); + if (input) { + // TODO: Support for specifying relative filename here if ever needed + ufbx_prop *prop = input->prop; + texture->absolute_filename = prop->value_str; + texture->raw_absolute_filename = prop->value_blob; + texture->type = UFBX_TEXTURE_FILE; + break; + } + } + } + } + + ufbxi_update_shader_texture(texture, shader); + + return 1; +} + +ufbxi_noinline static void ufbxi_propagate_main_textures(ufbx_scene *scene) +{ + // We need to do at least 2^(N-1) passes for N shader textures + size_t mask = scene->metadata.num_shader_textures; + while (mask) { + mask >>= 1; + + ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) { + ufbx_texture *texture = *p_texture; + ufbx_shader_texture *shader = texture->shader; + if (!shader) continue; + + ufbx_texture *main_tex = shader->main_texture; + if (!main_tex || shader->main_texture_output_index != 0) continue; + + ufbx_shader_texture *main_shader = main_tex->shader; + if (!main_shader || !main_shader->main_texture) continue; + + shader->main_texture = main_shader->main_texture; + shader->main_texture_output_index = main_shader->main_texture_output_index; + } + } + + // Remove cyclic main textures + ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) { + ufbx_texture *texture = *p_texture; + ufbx_shader_texture *shader = texture->shader; + if (!shader || !shader->main_texture || shader->main_texture_output_index != 0) continue; + ufbx_texture *main_tex = shader->main_texture; + if (main_tex && main_tex->shader && main_tex->shader->main_texture) { + // Should have been propagated to `texture` + shader->main_texture = NULL; + } + } + + ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) { + ufbx_texture *texture = *p_texture; + ufbx_shader_texture *shader = texture->shader; + if (!shader) continue; + + ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) { + if (!input->texture || !input->texture->shader) continue; + ufbx_shader_texture *input_shader = input->texture->shader; + if (input_shader->main_texture) { + input->texture = input_shader->main_texture; + input->texture_output_index = input_shader->main_texture_output_index; + } + } + } + + ufbxi_for_ptr_list(ufbx_material, p_material, scene->materials) { + ufbx_material *material = *p_material; + + ufbxi_for_list(ufbx_material_texture, tex, material->textures) { + ufbx_shader_texture *shader = tex->texture->shader; + if (shader && shader->main_texture && shader->main_texture_output_index == 0) { + tex->texture = shader->main_texture; + } + } + } +} + +#define ufbxi_patch_empty(m_dst, m_len, m_src) \ + do { if (!(m_dst).m_len) m_dst = m_src; } while (0) + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_insert_texture_file(ufbxi_context *uc, ufbx_texture *texture) +{ + texture->file_index = UFBX_NO_INDEX; + + const char *key = NULL; + + // HACK: Even the raw entries have a null terminator so we can offset the + // pointer by one for relative filenames. This guarantees that an overlapping + // absolute and relative filenames will get separate textures. + if (texture->raw_absolute_filename.size > 0) { + key = (const char*)texture->raw_absolute_filename.data; + } else if (texture->raw_relative_filename.size > 0) { + key = (const char*)texture->raw_relative_filename.data + 1; + } + + if (key == NULL) return 1; + uint32_t hash = ufbxi_hash_ptr(key); + ufbxi_texture_file_entry *entry = ufbxi_map_find(&uc->texture_file_map, ufbxi_texture_file_entry, hash, &key); + if (!entry) { + entry = ufbxi_map_insert(&uc->texture_file_map, ufbxi_texture_file_entry, hash, &key); + ufbxi_check(entry); + + ufbx_texture_file *file = ufbxi_push_zero(&uc->tmp, ufbx_texture_file, 1); + ufbxi_check(file); + + file->index = uc->texture_file_map.size - 1; + + entry->key = key; + entry->file = file; + } + + ufbx_texture_file *file = entry->file; + texture->file_index = file->index; + texture->has_file = true; + ufbxi_patch_empty(file->filename, length, texture->filename); + ufbxi_patch_empty(file->relative_filename, length, texture->relative_filename); + ufbxi_patch_empty(file->absolute_filename, length, texture->absolute_filename); + ufbxi_patch_empty(file->raw_filename, size, texture->raw_filename); + ufbxi_patch_empty(file->raw_relative_filename, size, texture->raw_relative_filename); + ufbxi_patch_empty(file->raw_absolute_filename, size, texture->raw_absolute_filename); + ufbxi_patch_empty(file->content, size, texture->content); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_pop_texture_files(ufbxi_context *uc) +{ + uint32_t num_files = uc->texture_file_map.size; + ufbx_texture_file *files = ufbxi_push(&uc->result, ufbx_texture_file, num_files); + ufbxi_check(files); + + uc->scene.texture_files.data = files; + uc->scene.texture_files.count = num_files; + + ufbxi_texture_file_entry *entries = (ufbxi_texture_file_entry*)uc->texture_file_map.items; + for (size_t i = 0; i < num_files; i++) { + memcpy(&files[i], entries[i].file, sizeof(ufbx_texture_file)); + } + + return 1; +} + +typedef struct { + ufbx_texture *texture; + size_t order; +} ufbxi_ordered_texture; + +ufbxi_noinline static bool ufbxi_ordered_texture_less_texture(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_ordered_texture *a = (const ufbxi_ordered_texture*)va, *b = (const ufbxi_ordered_texture*)vb; + return a->texture < b->texture; +} + +ufbxi_noinline static bool ufbxi_ordered_texture_less_order(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_ordered_texture *a = (const ufbxi_ordered_texture*)va, *b = (const ufbxi_ordered_texture*)vb; + return a->order < b->order; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_deduplicate_textures(ufbxi_context *uc, ufbxi_buf *dst_buf, ufbxi_ordered_texture **p_dst, size_t *p_dst_count, size_t count) +{ + ufbxi_ordered_texture *textures = ufbxi_push_pop(dst_buf, &uc->tmp_stack, ufbxi_ordered_texture, count); + ufbxi_check(textures); + + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_ordered_texture))); + + ufbxi_stable_sort(sizeof(ufbxi_ordered_texture), 16, textures, uc->tmp_arr, count, &ufbxi_ordered_texture_less_texture, NULL); + + // Remove adjacent duplicates + size_t dst_ix = 0; + for (size_t src_ix = 0; src_ix < count; src_ix++) { + if (src_ix > 0 && textures[src_ix - 1].texture == textures[src_ix].texture) { + continue; + } else { + if (src_ix != dst_ix) { + textures[dst_ix] = textures[src_ix]; + } + dst_ix++; + } + } + + size_t new_count = dst_ix; + ufbxi_stable_sort(sizeof(ufbxi_ordered_texture), 16, textures, uc->tmp_arr, new_count, &ufbxi_ordered_texture_less_order, NULL); + + *p_dst_count = new_count; + *p_dst = textures; + + return 1; +} + +typedef enum { + UFBXI_FILE_TEXTURE_FETCH_INITIAL, + UFBXI_FILE_TEXTURE_FETCH_STARTED, + UFBXI_FILE_TEXTURE_FETCH_FINISHED, +} ufbxi_file_texture_fetch_state; + +// Populate `ufbx_texture.file_textures[]` arrays. +ufbxi_nodiscard ufbxi_noinline static int ufbxi_fetch_file_textures(ufbxi_context *uc) +{ + // We keep pointers to `ufbx_texture` in `tmp_stack` as a working set, since we don't know + // how deep the shader graphs might be. + + // Start by pushing all the textures into the stack + size_t num_stack_textures = uc->scene.textures.count; + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, num_stack_textures, uc->scene.textures.data)); + + // Compressed `ufbxi_file_texture_fetch_state` + uint8_t *states = ufbxi_push_zero(&uc->tmp, uint8_t, uc->scene.textures.count); + ufbxi_check(states); + + while (num_stack_textures-- > 0) { + ufbx_texture *texture = NULL; + ufbxi_pop(&uc->tmp_stack, ufbx_texture*, 1, &texture); + + ufbxi_file_texture_fetch_state state = (ufbxi_file_texture_fetch_state)states[texture->typed_id]; + if (state == UFBXI_FILE_TEXTURE_FETCH_FINISHED) continue; + ufbx_shader_texture *shader = texture->shader; + + if (state == UFBXI_FILE_TEXTURE_FETCH_STARTED) { + states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_FINISHED; + + // HACK: Reuse `tmp_parse` for storing intermediate information as we can clear it. + ufbxi_buf_clear(&uc->tmp_parse); + + // Now all non-cyclical dependents should be processed. + size_t num_deps = 0; + + if (texture->type == UFBX_TEXTURE_FILE) { + ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1); + ufbxi_check(dst); + dst->texture = texture; + dst->order = num_deps++; + } + + ufbxi_for_list(ufbx_texture_layer, layer, texture->layers) { + ufbx_texture *dep_tex = layer->texture; + if (dep_tex->file_textures.count > 0) { + ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1); + ufbxi_check(dst); + dst->texture = dep_tex; + dst->order = num_deps++; + } + } + + if (shader) { + ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) { + ufbx_texture *dep_tex = input->texture; + if (dep_tex && dep_tex->file_textures.count > 0) { + ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1); + ufbxi_check(dst); + dst->texture = dep_tex; + dst->order = num_deps++; + } + } + } + + // Deduplicate the direct dependencies first + ufbxi_ordered_texture *deps; + ufbxi_check(ufbxi_deduplicate_textures(uc, &uc->tmp_parse, &deps, &num_deps, num_deps)); + + if (num_deps == 1) { + // If we have only a single dependency (that is not the same one) we can just copy the pointer + texture->file_textures = deps[0].texture->file_textures; + } else { + // Now collect all the file textures and deduplicate them + size_t num_files = 0; + ufbxi_for(ufbxi_ordered_texture, dep, deps, num_deps) { + ufbxi_for_ptr_list(ufbx_texture, p_tex, dep->texture->file_textures) { + ufbxi_ordered_texture *dst = ufbxi_push(&uc->tmp_stack, ufbxi_ordered_texture, 1); + ufbxi_check(dst); + dst->texture = *p_tex; + dst->order = num_files++; + } + } + + // Deduplicate the file textures + ufbxi_ordered_texture *files; + ufbxi_check(ufbxi_deduplicate_textures(uc, &uc->tmp_parse, &files, &num_files, num_files)); + + texture->file_textures.count = num_files; + texture->file_textures.data = ufbxi_push(&uc->result, ufbx_texture*, num_files); + ufbxi_check(texture->file_textures.data); + + for (size_t i = 0; i < num_files; i++) { + texture->file_textures.data[i] = files[i].texture; + } + } + + } else { + if (texture->type == UFBX_TEXTURE_FILE) { + // Simple case: Just point to self + texture->file_textures.count = 1; + texture->file_textures.data = ufbxi_push(&uc->result, ufbx_texture*, 1); + ufbxi_check(texture->file_textures.data); + texture->file_textures.data[0] = texture; + + // In simple cases we can quit here, for more complex file textures queue + // the texture in case there are other file textures as inputs. + if (!texture->shader) { + states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_FINISHED; + continue; + } + } + + // Complex: Process all dependencies first + states[texture->typed_id] = UFBXI_FILE_TEXTURE_FETCH_STARTED; + + // Push self first so we can return after processing dependencies + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &texture)); + num_stack_textures++; + + ufbxi_for_list(ufbx_texture_layer, layer, texture->layers) { + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &layer->texture)); + num_stack_textures++; + } + + if (shader) { + ufbxi_for_list(ufbx_shader_texture_input, input, shader->inputs) { + if (input->texture) { + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_texture*, 1, &input->texture)); + num_stack_textures++; + } + } + } + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static ufbx_node *ufbxi_get_geometry_transform_node(ufbx_element *element) +{ + if (element->instances.count == 1) { + ufbx_node *node = element->instances.data[0]; + if (node->has_geometry_transform) return node; + } + return NULL; +} + +ufbxi_noinline static void ufbxi_mirror_vec3_list(const void *v_list, ufbx_mirror_axis axis, size_t stride) +{ + const ufbx_void_list *list = (const ufbx_void_list*)v_list; + if (axis == UFBX_MIRROR_AXIS_NONE || !list || list->count == 0) return; + if (!stride) stride = sizeof(ufbx_vec3); + + void *ptr = (char*)list->data + (size_t)((int)axis - 1) * sizeof(ufbx_real); + void *end = (char*)ptr + list->count * stride; + while (ptr != end) { + ufbx_real *v = (ufbx_real*)ptr; + *v = -*v; + ptr = (char*)ptr + stride; + } +} + +ufbxi_noinline static void ufbxi_scale_vec3_list(const void *v_list, ufbx_real scale, size_t stride) +{ + const ufbx_void_list *list = (const ufbx_void_list*)v_list; + if (!list || list->count == 0) return; + if (!stride) stride = sizeof(ufbx_vec3); + + void *ptr = list->data, *end = (char*)ptr + list->count * stride; + while (ptr != end) { + ufbx_vec3 *v = (ufbx_vec3*)ptr; + v->x *= scale; + v->y *= scale; + v->z *= scale; + ptr = (char*)ptr + stride; + } +} + +ufbxi_noinline static void ufbxi_transform_vec3_list(const void *v_list, const ufbx_matrix *matrix, size_t stride) +{ + const ufbx_void_list *list = (const ufbx_void_list*)v_list; + if (!list || list->count == 0) return; + if (!stride) stride = sizeof(ufbx_vec3); + + void *ptr = list->data, *end = (char*)ptr + list->count * stride; + while (ptr != end) { + ufbx_vec3 *v = (ufbx_vec3*)ptr; + *v = ufbx_transform_position(matrix, *v); + ptr = (char*)ptr + stride; + } +} + +ufbxi_noinline static void ufbxi_normalize_vec3_list(const ufbx_vec3_list *list) +{ + ufbxi_nounroll ufbxi_for_list(ufbx_vec3, normal, *list) { + *normal = ufbxi_normalize3(*normal); + } +} + +// Forward declare as we're kind of preprocessing ata here that would usually happen later. +ufbxi_noinline static ufbx_transform ufbxi_get_geometry_transform(const ufbx_props *props, ufbx_node *node); + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_flip_attrib_winding(ufbxi_context *uc, ufbx_mesh *mesh, ufbx_uint32_list *indices, bool is_position) +{ + // All zero, no flipping needed + if (indices->data == uc->zero_indices || indices->count == 0) return 1; + + if (indices->data == mesh->vertex_position.indices.data && !is_position) { + // Sharing indices with vertex position, already flipped. + return 1; + } else if (indices->data == uc->consecutive_indices) { + // Need to duplicate consecutive indices, but we can cache the per mesh. + if (uc->tmp_mesh_consecutive_indices) { + indices->data = uc->tmp_mesh_consecutive_indices; + return 1; + } + indices->data = ufbxi_push_copy(&uc->result, uint32_t, indices->count, indices->data); + ufbxi_check(indices->data); + uc->tmp_mesh_consecutive_indices = indices->data; + } + + uint32_t *data = indices->data; + ufbxi_for_list(ufbx_face, face, mesh->faces) { + if (face->num_indices == 0) continue; + size_t begin = face->index_begin + 1; + size_t end = face->index_begin + face->num_indices - 1; + while (begin < end) { + uint32_t tmp = data[begin]; + data[begin] = data[end]; + data[end] = tmp; + begin++; + end--; + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_flip_winding(ufbxi_context *uc, ufbx_mesh *mesh) +{ + uc->tmp_mesh_consecutive_indices = NULL; + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_position.indices, true)); + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_normal.indices, false)); + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->vertex_crease.indices, false)); + if (mesh->uv_sets.count > 0) { + ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_uv.indices, false)); + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_tangent.indices, false)); + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_bitangent.indices, false)); + } + mesh->vertex_uv = mesh->uv_sets.data[0].vertex_uv; + mesh->vertex_bitangent = mesh->uv_sets.data[0].vertex_bitangent; + mesh->vertex_tangent = mesh->uv_sets.data[0].vertex_tangent; + } + if (mesh->color_sets.count > 0) { + ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) { + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &set->vertex_color.indices, false)); + } + mesh->vertex_color = mesh->color_sets.data[0].vertex_color; + } + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->skinned_position.indices, false)); + if (mesh->skinned_normal.indices.data != mesh->vertex_normal.indices.data) { + ufbxi_check(ufbxi_flip_attrib_winding(uc, mesh, &mesh->skinned_normal.indices, false)); + } + + ufbxi_update_vertex_first_index(mesh); + + // Mapping from old index values to flipped ones, reserve index -1 + // (aka `UFBX_NO_INDEX`) for itself. + if (mesh->edges.count > 0) { + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, (mesh->num_indices + 1) * sizeof(uint32_t))); + uint32_t *index_mapping = (uint32_t*)uc->tmp_arr + 1; + index_mapping[-1] = UFBX_NO_INDEX; + ufbxi_for_list(ufbx_face, face, mesh->faces) { + if (face->num_indices == 0) continue; + uint32_t begin = face->index_begin; + uint32_t count = face->num_indices - 1; + index_mapping[begin] = begin; + for (uint32_t i = 0; i < count; i++) { + index_mapping[begin + 1 + i] = begin + count - i; + } + } + + ufbxi_for_list(ufbx_edge, p_edge, mesh->edges) { + uint32_t a = index_mapping[(int32_t)p_edge->a]; + uint32_t b = index_mapping[(int32_t)p_edge->b]; + p_edge->a = b; + p_edge->b = a; + } + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_modify_geometry(ufbxi_context *uc) +{ + bool do_mirror = false; + bool do_winding = uc->opts.reverse_winding; + bool do_scale = false; + bool do_geometry_transforms = false; + if (uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY + || uc->opts.geometry_transform_handling == UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK) { + // Prefetch geometry transforms for processing, they will later be overwritten in `ufbxi_update_node()`. + ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) { + ufbx_node *node = *p_node; + if (node->is_root) continue; + + node->geometry_transform = ufbxi_get_geometry_transform(&node->props, node); + if (!ufbxi_is_transform_identity(&node->geometry_transform)) { + node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform); + node->has_geometry_transform = true; + } else { + node->geometry_to_node = ufbx_identity_matrix; + node->has_geometry_transform = false; + } + } + do_geometry_transforms = true; + } + if (uc->mirror_axis != 0) { + do_mirror = true; + } + if (uc->scene.metadata.geometry_scale != 1.0f) { + do_scale = true; + } + + ufbx_real geometry_scale = uc->scene.metadata.geometry_scale; + ufbx_mirror_axis mirror_axis = uc->mirror_axis; + + ufbxi_for_ptr_list(ufbx_blend_shape, p_shape, uc->scene.blend_shapes) { + ufbx_blend_shape *shape = *p_shape; + + if (do_scale) { + ufbxi_scale_vec3_list(&shape->position_offsets, geometry_scale, 0); + } + + if (do_mirror) { + ufbxi_mirror_vec3_list(&shape->position_offsets, mirror_axis, 0); + ufbxi_mirror_vec3_list(&shape->normal_offsets, mirror_axis, 0); + } + } + + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) { + ufbx_mesh *mesh = *p_mesh; + + if (do_scale) { + ufbxi_scale_vec3_list(&mesh->vertex_position.values, geometry_scale, 0); + } + + bool do_flip_winding = do_winding; + if (do_mirror) { + ufbxi_mirror_vec3_list(&mesh->vertex_position.values, mirror_axis, 0); + ufbxi_mirror_vec3_list(&mesh->vertex_normal.values, mirror_axis, 0); + ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + ufbxi_mirror_vec3_list(&set->vertex_tangent.values, mirror_axis, 0); + ufbxi_mirror_vec3_list(&set->vertex_bitangent.values, mirror_axis, 0); + } + if (!uc->opts.handedness_conversion_retain_winding) { + do_flip_winding = !do_flip_winding; + } + } + + // Flip face winding retaining the first vertex + if (do_flip_winding) { + mesh->reversed_winding = true; + ufbxi_check(ufbxi_flip_winding(uc, mesh)); + } + + ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&mesh->element); + if (do_geometry_transforms && geo_node) { + ufbx_matrix tangent_matrix = geo_node->geometry_to_node; + tangent_matrix.m03 = 0.0f; + tangent_matrix.m13 = 0.0f; + tangent_matrix.m23 = 0.0f; + ufbx_matrix normal_matrix = ufbx_matrix_for_normals(&geo_node->geometry_to_node); + + ufbxi_transform_vec3_list(&mesh->vertex_position.values, &geo_node->geometry_to_node, 0); + ufbxi_transform_vec3_list(&mesh->vertex_normal.values, &normal_matrix, 0); + ufbxi_normalize_vec3_list(&mesh->vertex_normal.values); + + ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + ufbxi_transform_vec3_list(&set->vertex_tangent.values, &tangent_matrix, 0); + ufbxi_transform_vec3_list(&set->vertex_bitangent.values, &tangent_matrix, 0); + ufbxi_normalize_vec3_list(&set->vertex_tangent.values); + ufbxi_normalize_vec3_list(&set->vertex_bitangent.values); + } + } + } + + ufbxi_for_ptr_list(ufbx_line_curve, p_curve, uc->scene.line_curves) { + ufbx_line_curve *curve = *p_curve; + + if (do_scale) { + ufbxi_scale_vec3_list(&curve->control_points, geometry_scale, 0); + } + + if (do_mirror) { + ufbxi_mirror_vec3_list(&curve->control_points, mirror_axis, 0); + } + + ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&curve->element); + if (do_geometry_transforms && geo_node) { + ufbxi_transform_vec3_list(&curve->control_points, &geo_node->geometry_to_node, 0); + } + } + + ufbxi_for_ptr_list(ufbx_nurbs_curve, p_curve, uc->scene.nurbs_curves) { + ufbx_nurbs_curve *curve = *p_curve; + + if (do_scale) { + ufbxi_scale_vec3_list(&curve->control_points, geometry_scale, sizeof(ufbx_vec4)); + } + + if (do_mirror) { + ufbxi_mirror_vec3_list(&curve->control_points, mirror_axis, sizeof(ufbx_vec4)); + } + + ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&curve->element); + if (do_geometry_transforms && geo_node) { + ufbxi_transform_vec3_list(&curve->control_points, &geo_node->geometry_to_node, sizeof(ufbx_vec4)); + } + } + + ufbxi_for_ptr_list(ufbx_nurbs_surface, p_surface, uc->scene.nurbs_surfaces) { + ufbx_nurbs_surface *surface = *p_surface; + + if (do_scale) { + ufbxi_scale_vec3_list(&surface->control_points, geometry_scale, sizeof(ufbx_vec4)); + } + + if (do_mirror) { + ufbxi_mirror_vec3_list(&surface->control_points, mirror_axis, sizeof(ufbx_vec4)); + } + + ufbx_node *geo_node = ufbxi_get_geometry_transform_node(&surface->element); + if (do_geometry_transforms && geo_node) { + ufbxi_transform_vec3_list(&surface->control_points, &geo_node->geometry_to_node, sizeof(ufbx_vec4)); + } + } + + if (uc->opts.geometry_transform_handling != UFBX_GEOMETRY_TRANSFORM_HANDLING_PRESERVE) { + // Reset all geometry transforms if we're not preserving them + ufbx_props *defaults = NULL; + ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) { + ufbx_node *node = *p_node; + if (!defaults) defaults = node->props.defaults; + + if (node->has_geometry_transform) { + ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricTranslation, 0.0f); + ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricRotation, 0.0f); + ufbxi_set_own_prop_vec3_uniform(&node->props, ufbxi_GeometricScaling, 1.0f); + } + } + + if (defaults) { + ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricTranslation, 0.0f); + ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricRotation, 0.0f); + ufbxi_set_own_prop_vec3_uniform(defaults, ufbxi_GeometricScaling, 1.0f); + } + } + + return 1; +} + +ufbxi_noinline static void ufbxi_postprocess_scene(ufbxi_context *uc) +{ + if (uc->opts.normalize_normals || uc->opts.normalize_tangents) { + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) { + ufbx_mesh *mesh = *p_mesh; + if (uc->opts.normalize_normals) { + ufbxi_normalize_vec3_list(&mesh->vertex_normal.values); + } + if (uc->opts.normalize_tangents) { + ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + ufbxi_normalize_vec3_list(&mesh->vertex_tangent.values); + ufbxi_normalize_vec3_list(&mesh->vertex_bitangent.values); + } + } + } + } + + if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) { + uc->scene.metadata.ortho_size_unit = 1.0f / uc->scene.metadata.geometry_scale; + } else { + uc->scene.metadata.ortho_size_unit = 30.0f; + } +} + +ufbxi_noinline static size_t ufbxi_next_path_segment(const char *data, size_t begin, size_t length) +{ + for (size_t i = begin; i < length; i++) { + if (data[i] == '/' || data[i] == '\\') { + return i; + } + } + return length; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_absolute_to_relative_path(ufbxi_context *uc, ufbxi_strblob *p_dst, const ufbxi_strblob *p_rel, const ufbxi_strblob *p_src, bool raw) +{ + const char *rel = ufbxi_strblob_data(p_rel, raw); + const char *src = ufbxi_strblob_data(p_src, raw); + size_t rel_length = ufbxi_strblob_length(p_rel, raw); + size_t src_length = ufbxi_strblob_length(p_src, raw); + + if (rel_length == 0 || src_length == 0) return 1; + + // Absolute paths must start with the same character (either drive or '/') + if (rel[0] != src[0]) return 1; + + // Find the last directory of the path we want to be relative to + while (rel_length > 0 && (rel[rel_length - 1] != '/' && rel[rel_length - 1] != '\\')) { + rel_length--; + } + + if (rel_length == 0) return 1; + char separator = rel[rel_length - 1]; + + size_t max_length = rel_length * 2 + src_length; + + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, max_length)); + char *tmp = uc->tmp_arr; + size_t tmp_length = 0; + + size_t rel_begin = 0; + size_t src_begin = 0; + while (rel_begin < rel_length && src_begin < src_length) { + size_t rel_end = ufbxi_next_path_segment(rel, rel_begin, rel_length); + size_t src_end = ufbxi_next_path_segment(src, src_begin, src_length); + if (rel_end != src_end || memcmp(rel + rel_begin, src + src_begin, src_end - src_begin) != 0) break; + + rel_begin = rel_end + 1; + src_begin = src_end + 1; + } + + while (rel_begin < rel_length) { + size_t rel_end = ufbxi_next_path_segment(rel, rel_begin, rel_length); + tmp[tmp_length++] = '.'; + tmp[tmp_length++] = '.'; + tmp[tmp_length++] = separator; + rel_begin = rel_end + 1; + } + + while (src_begin < src_length) { + size_t src_end = ufbxi_next_path_segment(src, src_begin, src_length); + size_t len = src_end - src_begin; + + memcpy(tmp + tmp_length, src + src_begin, len); + tmp_length += len; + + if (src_end < src_length) { + tmp[tmp_length++] = separator; + } + + src_begin = src_end + 1; + } + + ufbx_assert(tmp_length <= max_length); + + const char *dst = ufbxi_push_string(&uc->string_pool, tmp, tmp_length, NULL, true); + ufbxi_check(dst); + + ufbxi_strblob_set(p_dst, dst, tmp_length, raw); + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_filenames(ufbxi_context *uc, ufbxi_strblob *filename, ufbxi_strblob *absolute_filename, ufbxi_strblob *relative_filename, bool raw) +{ + if (ufbxi_strblob_length(relative_filename, raw) == 0) { + const ufbxi_strblob *original_file_path = raw + ? (const ufbxi_strblob*)&uc->scene.metadata.raw_original_file_path + : (const ufbxi_strblob*)&uc->scene.metadata.original_file_path; + + ufbxi_check(ufbxi_absolute_to_relative_path(uc, relative_filename, original_file_path, absolute_filename, raw)); + } + + ufbxi_check(ufbxi_resolve_relative_filename(uc, filename, relative_filename, raw)); + + return 1; +} + +ufbxi_noinline static bool ufbxi_file_content_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_file_content *a = (const ufbxi_file_content*)va, *b = (const ufbxi_file_content*)vb; + return ufbxi_str_less(a->absolute_filename, b->absolute_filename); +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_sort_file_contents(ufbxi_context *uc, ufbxi_file_content *content, size_t count) +{ + ufbxi_check(ufbxi_grow_array(&uc->ator_tmp, &uc->tmp_arr, &uc->tmp_arr_size, count * sizeof(ufbxi_file_content))); + ufbxi_stable_sort(sizeof(ufbxi_file_content), 32, content, uc->tmp_arr, count, &ufbxi_file_content_less, NULL); + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data) +{ + if (p_data->size == 0 || p_filename->length == 0) return 1; + ufbxi_file_content *content = ufbxi_push(&uc->tmp_stack, ufbxi_file_content, 1); + ufbxi_check(content); + + content->absolute_filename = *p_filename; + content->content = *p_data; + return 1; +} + +ufbxi_noinline static void ufbxi_fetch_file_content(ufbxi_context *uc, ufbx_string *p_filename, ufbx_blob *p_data) +{ + if (p_data->size > 0) return; + ufbx_string filename = *p_filename; + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbxi_file_content, 8, &index, uc->file_content, 0, uc->num_file_content, + ( ufbxi_str_less(a->absolute_filename, filename) ), + ( a->absolute_filename.data == filename.data )); + if (index != SIZE_MAX) { + *p_data = uc->file_content[index].content; + } +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_resolve_file_content(ufbxi_context *uc) +{ + size_t initial_stack = uc->tmp_stack.num_items; + + ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { + ufbx_video *video = *p_video; + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->filename, (ufbxi_strblob*)&video->absolute_filename, (ufbxi_strblob*)&video->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&video->raw_filename, (ufbxi_strblob*)&video->raw_absolute_filename, (ufbxi_strblob*)&video->raw_relative_filename, true)); + ufbxi_check(ufbxi_push_file_content(uc, &video->absolute_filename, &video->content)); + } + + ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) { + ufbx_audio_clip *clip = *p_clip; + clip->absolute_filename = ufbx_find_string(&clip->props, "Path", ufbx_empty_string); + clip->relative_filename = ufbx_find_string(&clip->props, "RelPath", ufbx_empty_string); + clip->raw_absolute_filename = ufbx_find_blob(&clip->props, "Path", ufbx_empty_blob); + clip->raw_relative_filename = ufbx_find_blob(&clip->props, "RelPath", ufbx_empty_blob); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->filename, (ufbxi_strblob*)&clip->absolute_filename, (ufbxi_strblob*)&clip->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&clip->raw_filename, (ufbxi_strblob*)&clip->raw_absolute_filename, (ufbxi_strblob*)&clip->raw_relative_filename, true)); + ufbxi_check(ufbxi_push_file_content(uc, &clip->absolute_filename, &clip->content)); + } + + uc->num_file_content = uc->tmp_stack.num_items - initial_stack; + uc->file_content = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_file_content, uc->num_file_content); + ufbxi_check(uc->file_content); + ufbxi_check(ufbxi_sort_file_contents(uc, uc->file_content, uc->num_file_content)); + + ufbxi_for_ptr_list(ufbx_video, p_video, uc->scene.videos) { + ufbx_video *video = *p_video; + ufbxi_fetch_file_content(uc, &video->absolute_filename, &video->content); + } + + ufbxi_for_ptr_list(ufbx_audio_clip, p_clip, uc->scene.audio_clips) { + ufbx_audio_clip *clip = *p_clip; + ufbxi_fetch_file_content(uc, &clip->absolute_filename, &clip->content); + } + + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_validate_indices(ufbxi_context *uc, ufbx_uint32_list *indices, size_t max_index) +{ + if (max_index == 0 && uc->opts.index_error_handling == UFBX_INDEX_ERROR_HANDLING_CLAMP) { + indices->data = NULL; + indices->count = 0; + return 1; + } + + ufbxi_nounroll ufbxi_for_list(uint32_t, p_ix, *indices) { + uint32_t ix = *p_ix; + if (ix >= max_index) { + ufbxi_check(ufbxi_fix_index(uc, p_ix, ix, max_index)); + } + } + + return 1; +} + +static bool ufbxi_material_part_usage_less(void *user, const void *va, const void *vb) +{ + ufbx_mesh_part *parts = (ufbx_mesh_part*)user; + uint32_t a = *(const uint32_t*)va, b = *(const uint32_t*)vb; + ufbx_mesh_part *pa = &parts[a]; + ufbx_mesh_part *pb = &parts[b]; + if (pa->face_indices.count == 0 || pb->face_indices.count == 0) { + if (pa->face_indices.count == pb->face_indices.count) return a < b; + return pa->face_indices.count > pb->face_indices.count; + } + return pa->face_indices.data[0] < pb->face_indices.data[0]; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_mesh_material(ufbxi_buf *buf, ufbx_error *error, ufbx_mesh *mesh) +{ + size_t num_materials = mesh->materials.count; + size_t num_parts = mesh->material_parts.count; + size_t num_faces = mesh->faces.count; + + ufbx_mesh_part *parts = mesh->material_parts.data; + ufbx_assert(!parts || (mesh->material_parts.count == num_materials) || (mesh->material_parts.count == 1 && num_materials == 0)); + + uint32_t *face_material = mesh->face_material.data; + + // Count the number of faces and triangles per material + ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) { + ufbx_face face = mesh->faces.data[i]; + uint32_t mat_ix = 0; + + if (face_material) { + mat_ix = face_material[i]; + if (mat_ix >= num_materials) { + face_material[i] = 0; + mat_ix = 0; + } + } + + if (parts) { + ufbxi_mesh_part_add_face(&parts[mat_ix], face.num_indices); + } + } + + if (parts) { + // Allocate per-material buffers (clear `num_faces` to 0 to re-use it as + // an index when fetching the face indices). + uint32_t part_index = 0; + ufbxi_for(ufbx_mesh_part, part, parts, num_parts) { + part->index = part_index++; + part->face_indices.count = part->num_faces; + part->face_indices.data = ufbxi_push(buf, uint32_t, part->num_faces); + ufbxi_check_err(error, part->face_indices.data); + part->num_faces = 0; + } + + // Fetch the per-material face indices + ufbxi_nounroll for (size_t i = 0; i < num_faces; i++) { + uint32_t mat_ix = face_material ? face_material[i] : 0; + if (mat_ix < num_parts) { + ufbx_mesh_part *part = &parts[mat_ix]; + part->face_indices.data[part->num_faces++] = (uint32_t)i; + } + } + + mesh->material_part_usage_order.count = num_parts; + mesh->material_part_usage_order.data = ufbxi_push(buf, uint32_t, num_parts); + ufbxi_check_err(error, mesh->material_part_usage_order.data); + for (size_t i = 0; i < num_parts; i++) { + mesh->material_part_usage_order.data[i] = (uint32_t)i; + } + ufbxi_unstable_sort(mesh->material_part_usage_order.data, num_parts, sizeof(uint32_t), &ufbxi_material_part_usage_less, parts); + } + + return 1; +} + +typedef struct { + ufbxi_refcount refcount; + ufbx_anim anim; + uint32_t magic; +} ufbxi_anim_imp; + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_push_anim(ufbxi_context *uc, ufbx_anim **p_anim, ufbx_anim_layer **layers, size_t num_layers) +{ + ufbx_anim *anim = ufbxi_push_zero(&uc->result, ufbx_anim, 1); + ufbxi_check(anim); + + anim->layers.data = layers; + anim->layers.count = num_layers; + + *p_anim = anim; + return 1; +} + +ufbxi_nodiscard ufbxi_noinline static int ufbxi_finalize_scene(ufbxi_context *uc) +{ + size_t num_elements = uc->num_elements; + + uc->scene.elements.count = num_elements; + uc->scene.elements.data = ufbxi_push(&uc->result, ufbx_element*, num_elements); + ufbxi_check(uc->scene.elements.data); + + uc->scene.metadata.element_buffer_size = uc->tmp_element_byte_offset; + char *element_data = (char*)ufbxi_push_pop(&uc->result, &uc->tmp_elements, uint64_t, uc->tmp_element_byte_offset/8); + ufbxi_check(element_data); + + size_t *element_offsets = ufbxi_push_pop(&uc->tmp, &uc->tmp_element_offsets, size_t, uc->tmp_element_offsets.num_items); + ufbxi_buf_free(&uc->tmp_element_offsets); + ufbxi_check(element_offsets); + for (size_t i = 0; i < num_elements; i++) { + ufbx_element *element = (ufbx_element*)(element_data + element_offsets[i]); + + if (element->type == UFBX_ELEMENT_NODE) { + ufbx_node *node = (ufbx_node*)element; + if (node->scale_helper) { + ufbxi_node_extra *extra = (ufbxi_node_extra*)ufbxi_get_element_extra(uc, node->element_id); + ufbx_assert(extra); + node->scale_helper = (ufbx_node*)(element_data + element_offsets[extra->scale_helper_id]); + } + } + + uc->scene.elements.data[i] = element; + } + + uc->scene.elements.count = num_elements; + ufbxi_buf_free(&uc->tmp_element_offsets); + ufbxi_buf_free(&uc->tmp_elements); + + uc->tmp_element_flag = ufbxi_push_zero(&uc->tmp, uint8_t, num_elements); + ufbxi_check(uc->tmp_element_flag); + + uc->scene.metadata.original_file_path = ufbx_find_string(&uc->scene.metadata.scene_props, "DocumentUrl", ufbx_empty_string); + uc->scene.metadata.raw_original_file_path = ufbx_find_blob(&uc->scene.metadata.scene_props, "DocumentUrl", ufbx_empty_blob); + + // Resolve and add the connections to elements + ufbxi_check(ufbxi_resolve_connections(uc)); + ufbxi_check(ufbxi_add_connections_to_elements(uc)); + ufbxi_check(ufbxi_linearize_nodes(uc)); + + for (size_t type = 0; type < UFBX_ELEMENT_TYPE_COUNT; type++) { + size_t num_typed = uc->tmp_typed_element_offsets[type].num_items; + size_t *typed_offsets = ufbxi_push_pop(&uc->tmp, &uc->tmp_typed_element_offsets[type], size_t, num_typed); + ufbxi_buf_free(&uc->tmp_typed_element_offsets[type]); + ufbxi_check(typed_offsets); + + ufbx_element_list *typed_elems = &uc->scene.elements_by_type[type]; + typed_elems->count = num_typed; + typed_elems->data = ufbxi_push(&uc->result, ufbx_element*, num_typed); + ufbxi_check(typed_elems->data); + + for (size_t i = 0; i < num_typed; i++) { + typed_elems->data[i] = (ufbx_element*)(element_data + typed_offsets[i]); + } + + ufbxi_buf_free(&uc->tmp_typed_element_offsets[type]); + } + + // Create named elements + uc->scene.elements_by_name.count = num_elements; + uc->scene.elements_by_name.data = ufbxi_push(&uc->result, ufbx_name_element, num_elements); + ufbxi_check(uc->scene.elements_by_name.data); + + for (size_t i = 0; i < num_elements; i++) { + + ufbx_element *elem = uc->scene.elements.data[i]; + ufbx_name_element *name_elem = &uc->scene.elements_by_name.data[i]; + + name_elem->name = elem->name; + name_elem->type = elem->type; + name_elem->_internal_key = ufbxi_get_name_key(elem->name.data, elem->name.length); + name_elem->element = elem; + } + + ufbxi_check(ufbxi_sort_name_elements(uc, uc->scene.elements_by_name.data, num_elements)); + + // Setup node children arrays and attribute pointers/lists + ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) { + ufbx_node *node = *p_node, *parent = node->parent; + if (parent) { + parent->children.count++; + if (parent->children.data == NULL) { + parent->children.data = p_node; + } + + if (node->is_geometry_transform_helper) { + parent->geometry_transform_helper = node; + } + + // Force top-level nodes to have `UFBX_INHERIT_MODE_NORMAL` to make unit scaling work. + if (parent->is_root && uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT && uc->opts.inherit_mode_handling == UFBX_INHERIT_MODE_HANDLING_PRESERVE) { + node->original_inherit_mode = UFBX_INHERIT_MODE_NORMAL; + node->inherit_mode = UFBX_INHERIT_MODE_NORMAL; + } + + // RrSs nodes inherit scale from their parent, Rrs ignore the scale of + // their _immediate_ parent, potentially multiple if chained. + if (node->original_inherit_mode == UFBX_INHERIT_MODE_COMPONENTWISE_SCALE) { + node->inherit_scale_node = parent; + } else if (node->original_inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) { + node->inherit_scale_node = parent->inherit_scale_node; + } + } + + ufbx_connection_list conns = ufbxi_find_dst_connections(&node->element, NULL); + + ufbxi_for_list(ufbx_connection, conn, conns) { + ufbx_element *elem = conn->src; + ufbx_element_type type = elem->type; + if (!(type >= UFBX_ELEMENT_TYPE_FIRST_ATTRIB && type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB)) continue; + + size_t index = node->all_attribs.count++; + if (index == 0) { + node->attrib = elem; + node->attrib_type = type; + } else { + if (index == 1) { + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &node->attrib)); + } + ufbxi_check(ufbxi_push_copy(&uc->tmp_stack, ufbx_element*, 1, &elem)); + } + + switch (elem->type) { + case UFBX_ELEMENT_MESH: node->mesh = (ufbx_mesh*)elem; break; + case UFBX_ELEMENT_LIGHT: node->light = (ufbx_light*)elem; break; + case UFBX_ELEMENT_CAMERA: node->camera = (ufbx_camera*)elem; break; + case UFBX_ELEMENT_BONE: node->bone = (ufbx_bone*)elem; break; + default: /* No shorthand */ break; + } + } + + if (node->all_attribs.count > 1) { + node->all_attribs.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_element*, node->all_attribs.count); + ufbxi_check(node->all_attribs.data); + } else if (node->all_attribs.count == 1) { + node->all_attribs.data = &node->attrib; + } + + ufbxi_check(ufbxi_fetch_dst_elements(uc, &node->materials, &node->element, false, false, NULL, UFBX_ELEMENT_MATERIAL)); + } + + // Resolve bind pose bones that don't use the normal connection system + ufbxi_for_ptr_list(ufbx_pose, p_pose, uc->scene.poses) { + ufbx_pose *pose = *p_pose; + + // HACK: Transport `ufbxi_tmp_bone_pose` array through the `ufbx_bone_pose` pointer + size_t num_bones = pose->bone_poses.count; + ufbxi_tmp_bone_pose *tmp_poses = (ufbxi_tmp_bone_pose*)pose->bone_poses.data; + pose->bone_poses.data = ufbxi_push(&uc->result, ufbx_bone_pose, num_bones); + ufbxi_check(pose->bone_poses.data); + + // Filter only found bones + pose->bone_poses.count = 0; + for (size_t i = 0; i < num_bones; i++) { + ufbx_element *elem = ufbxi_find_element_by_fbx_id(uc, tmp_poses[i].bone_fbx_id); + if (!elem || elem->type != UFBX_ELEMENT_NODE) continue; + + ufbx_node *node = (ufbx_node*)elem; + ufbx_bone_pose *bone = &pose->bone_poses.data[pose->bone_poses.count++]; + bone->bone_node = node; + bone->bone_to_world = tmp_poses[i].bone_to_world; + + if (pose->is_bind_pose) { + if (node->bind_pose == NULL) { + node->bind_pose = pose; + } + + ufbx_connection_list node_conns = ufbxi_find_src_connections(elem, NULL); + ufbxi_for_list(ufbx_connection, conn, node_conns) { + if (conn->dst->type != UFBX_ELEMENT_SKIN_CLUSTER) continue; + ufbx_skin_cluster *cluster = (ufbx_skin_cluster*)conn->dst; + if (ufbxi_matrix_all_zero(&cluster->bind_to_world)) { + cluster->bind_to_world = bone->bone_to_world; + } + } + } + } + ufbxi_check(ufbxi_sort_bone_poses(uc, pose)); + } + + // Fetch pointers that may break elements + + // Setup node attribute instances + for (int type = UFBX_ELEMENT_TYPE_FIRST_ATTRIB; type <= UFBX_ELEMENT_TYPE_LAST_ATTRIB; type++) { + ufbxi_for_ptr_list(ufbx_element, p_elem, uc->scene.elements_by_type[type]) { + ufbx_element *elem = *p_elem; + ufbxi_check(ufbxi_fetch_src_elements(uc, &elem->instances, elem, false, true, NULL, UFBX_ELEMENT_NODE)); + } + } + + bool search_node = uc->version < 7000; + + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, uc->scene.skin_clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + cluster->bone_node = (ufbx_node*)ufbxi_fetch_dst_element(&cluster->element, false, NULL, UFBX_ELEMENT_NODE); + } + + ufbxi_for_ptr_list(ufbx_skin_deformer, p_skin, uc->scene.skin_deformers) { + ufbx_skin_deformer *skin = *p_skin; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &skin->clusters, &skin->element, false, true, NULL, UFBX_ELEMENT_SKIN_CLUSTER)); + + // Remove clusters without a valid `bone` + if (!uc->opts.connect_broken_elements) { + size_t num_broken = 0; + for (size_t i = 0; i < skin->clusters.count; i++) { + if (!skin->clusters.data[i]->bone_node) { + num_broken++; + } else if (num_broken > 0) { + skin->clusters.data[i - num_broken] = skin->clusters.data[i]; + } + } + skin->clusters.count -= num_broken; + } + + size_t total_weights = 0; + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + ufbxi_check(SIZE_MAX - total_weights > cluster->num_weights); + total_weights += cluster->num_weights; + } + + size_t num_vertices = 0; + + // Iterate through meshes so we can pad the vertices to the largest one + { + ufbx_connection_list conns = ufbxi_find_src_connections(&skin->element, NULL); + ufbxi_for_list(ufbx_connection, conn, conns) { + ufbx_mesh *mesh = NULL; + if (conn->dst_prop.length > 0) continue; + if (conn->dst->type == UFBX_ELEMENT_MESH) { + mesh = (ufbx_mesh*)conn->dst; + } else if (conn->dst->type == UFBX_ELEMENT_NODE) { + ufbx_node *node = (ufbx_node*)conn->dst; + if (node->geometry_transform_helper) node = node->geometry_transform_helper; + mesh = node->mesh; + } + if (!mesh) continue; + num_vertices = ufbxi_max_sz(num_vertices, mesh->num_vertices); + } + } + + if (!uc->opts.skip_skin_vertices) { + skin->vertices.count = num_vertices; + skin->vertices.data = ufbxi_push_zero(&uc->result, ufbx_skin_vertex, num_vertices); + ufbxi_check(skin->vertices.data); + + skin->weights.count = total_weights; + skin->weights.data = ufbxi_push_zero(&uc->result, ufbx_skin_weight, total_weights); + ufbxi_check(skin->weights.data); + + bool retain_all = !uc->opts.clean_skin_weights; + + // Count the number of weights per vertex + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + for (size_t i = 0; i < cluster->num_weights; i++) { + uint32_t vertex = cluster->vertices.data[i]; + if (vertex < num_vertices && (retain_all || cluster->weights.data[i] > 0.0f)) { + skin->vertices.data[vertex].num_weights++; + } + } + } + + ufbx_real default_dq = skin->skinning_method == UFBX_SKINNING_METHOD_DUAL_QUATERNION ? 1.0f : 0.0f; + + // Prefix sum to assign the vertex weight offsets and set up default DQ values + uint32_t offset = 0; + uint32_t max_weights = 0; + for (size_t i = 0; i < num_vertices; i++) { + skin->vertices.data[i].weight_begin = offset; + skin->vertices.data[i].dq_weight = default_dq; + uint32_t num_weights = skin->vertices.data[i].num_weights; + offset += num_weights; + skin->vertices.data[i].num_weights = 0; + + if (num_weights > max_weights) max_weights = num_weights; + } + ufbx_assert(offset <= total_weights); + skin->max_weights_per_vertex = max_weights; + + // Copy the DQ weights to vertices + for (size_t i = 0; i < skin->num_dq_weights; i++) { + uint32_t vertex = skin->dq_vertices.data[i]; + if (vertex < num_vertices) { + skin->vertices.data[vertex].dq_weight = skin->dq_weights.data[i]; + } + } + + // Copy the weights to vertices + uint32_t cluster_index = 0; + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, skin->clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + for (size_t i = 0; i < cluster->num_weights; i++) { + uint32_t vertex = cluster->vertices.data[i]; + if (vertex < num_vertices && (retain_all || cluster->weights.data[i] > 0.0f)) { + uint32_t local_index = skin->vertices.data[vertex].num_weights++; + uint32_t index = skin->vertices.data[vertex].weight_begin + local_index; + skin->weights.data[index].cluster_index = cluster_index; + skin->weights.data[index].weight = cluster->weights.data[i]; + } + } + cluster_index++; + } + + // Sort the vertex weights by descending weight value + ufbxi_check(ufbxi_sort_skin_weights(uc, skin)); + } + } + + ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, uc->scene.blend_deformers) { + ufbx_blend_deformer *blend = *p_blend; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &blend->channels, &blend->element, false, true, NULL, UFBX_ELEMENT_BLEND_CHANNEL)); + } + + ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, uc->scene.cache_deformers) { + ufbx_cache_deformer *deformer = *p_deformer; + deformer->channel = ufbx_find_string(&deformer->props, "ChannelName", ufbx_empty_string); + deformer->file = (ufbx_cache_file*)ufbxi_fetch_dst_element(&deformer->element, false, NULL, UFBX_ELEMENT_CACHE_FILE); + } + + ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) { + ufbx_cache_file *cache = *p_cache; + + cache->absolute_filename = ufbx_find_string(&cache->props, "CacheAbsoluteFileName", ufbx_empty_string); + cache->relative_filename = ufbx_find_string(&cache->props, "CacheFileName", ufbx_empty_string); + + cache->raw_absolute_filename = ufbx_find_blob(&cache->props, "CacheAbsoluteFileName", ufbx_empty_blob); + cache->raw_relative_filename = ufbx_find_blob(&cache->props, "CacheFileName", ufbx_empty_blob); + + int64_t type = ufbx_find_int(&cache->props, "CacheFileType", 0); + if (type >= 0 && type <= UFBX_CACHE_FILE_FORMAT_MC) { + cache->format = (ufbx_cache_file_format)type; + } + + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&cache->filename, (ufbxi_strblob*)&cache->absolute_filename, (ufbxi_strblob*)&cache->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&cache->raw_filename, (ufbxi_strblob*)&cache->raw_absolute_filename, (ufbxi_strblob*)&cache->raw_relative_filename, true)); + } + + ufbx_assert(uc->tmp_full_weights.num_items == uc->scene.blend_channels.count); + ufbx_real_list *full_weights = ufbxi_push_pop(&uc->tmp, &uc->tmp_full_weights, ufbx_real_list, uc->tmp_full_weights.num_items); + ufbxi_buf_free(&uc->tmp_full_weights); + ufbxi_check(full_weights); + + ufbxi_for_ptr_list(ufbx_blend_channel, p_channel, uc->scene.blend_channels) { + ufbx_blend_channel *channel = *p_channel; + + ufbxi_check(ufbxi_fetch_blend_keyframes(uc, &channel->keyframes, &channel->element)); + + for (size_t i = 0; i < channel->keyframes.count; i++) { + ufbx_blend_keyframe *key = &channel->keyframes.data[i]; + key->target_weight = 1.0f; + if (i < full_weights->count) { + if (!uc->blender_full_weights) { + key->target_weight = full_weights->data[i] / (ufbx_real)100.0; + } else if (full_weights->count == key->shape->num_offsets) { + if (i == 0) { + // Duplicate `index_data` for modification if we retain DOM + if (uc->opts.retain_dom) { + full_weights->data = ufbxi_push_copy(&uc->result, ufbx_real, full_weights->count, full_weights->data); + ufbxi_check(full_weights->data); + } + ufbxi_for_list(ufbx_real, p_weight, *full_weights) { + *p_weight /= (ufbx_real)100.0; + } + } + key->shape->offset_weights = *full_weights; + } + } + } + + ufbxi_check(ufbxi_sort_blend_keyframes(uc, channel->keyframes.data, channel->keyframes.count)); + full_weights++; + + if (channel->keyframes.count > 0) { + channel->target_shape = channel->keyframes.data[channel->keyframes.count - 1].shape; + } + } + + { + // Generate and patch procedural index buffers + uint32_t *zero_indices = ufbxi_push(&uc->result, uint32_t, uc->max_zero_indices); + uint32_t *consecutive_indices = ufbxi_push(&uc->result, uint32_t, uc->max_consecutive_indices); + ufbxi_check(zero_indices && consecutive_indices); + + memset(zero_indices, 0, sizeof(uint32_t) * uc->max_zero_indices); + for (size_t i = 0; i < uc->max_consecutive_indices; i++) { + consecutive_indices[i] = (uint32_t)i; + } + + uc->zero_indices = zero_indices; + uc->consecutive_indices = consecutive_indices; + + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) { + ufbx_mesh *mesh = *p_mesh; + + ufbxi_patch_index_pointer(uc, &mesh->vertex_position.indices.data); + ufbxi_patch_index_pointer(uc, &mesh->vertex_normal.indices.data); + ufbxi_patch_index_pointer(uc, &mesh->vertex_color.indices.data); + ufbxi_patch_index_pointer(uc, &mesh->vertex_crease.indices.data); + ufbxi_patch_index_pointer(uc, &mesh->face_material.data); + ufbxi_patch_index_pointer(uc, &mesh->face_group.data); + + ufbxi_patch_index_pointer(uc, &mesh->skinned_position.indices.data); + ufbxi_patch_index_pointer(uc, &mesh->skinned_normal.indices.data); + + ufbxi_for_list(ufbx_uv_set, set, mesh->uv_sets) { + ufbxi_patch_index_pointer(uc, &set->vertex_uv.indices.data); + ufbxi_patch_index_pointer(uc, &set->vertex_bitangent.indices.data); + ufbxi_patch_index_pointer(uc, &set->vertex_tangent.indices.data); + } + + ufbxi_for_list(ufbx_color_set, set, mesh->color_sets) { + ufbxi_patch_index_pointer(uc, &set->vertex_color.indices.data); + } + + // Generate normals if necessary + if (!mesh->vertex_normal.exists && uc->opts.generate_missing_normals) { + ufbxi_check(ufbxi_generate_normals(uc, mesh)); + } + + // Assign first UV and color sets as the "canonical" ones + if (mesh->uv_sets.count > 0) { + mesh->vertex_uv = mesh->uv_sets.data[0].vertex_uv; + mesh->vertex_bitangent = mesh->uv_sets.data[0].vertex_bitangent; + mesh->vertex_tangent = mesh->uv_sets.data[0].vertex_tangent; + } + if (mesh->color_sets.count > 0) { + mesh->vertex_color = mesh->color_sets.data[0].vertex_color; + } + + if (mesh->face_group_parts.count == 1) { + ufbxi_patch_index_pointer(uc, &mesh->face_group_parts.data[0].face_indices.data); + } + + ufbxi_check(ufbxi_fetch_mesh_materials(uc, &mesh->materials, &mesh->element, true)); + + // Patch materials to instances if necessary + if (mesh->materials.count > 0) { + ufbxi_for_ptr_list(ufbx_node, p_node, mesh->instances) { + ufbx_node *node = *p_node; + if (node->materials.count < mesh->materials.count && mesh->materials.data[0] != NULL) { + ufbx_material **materials = ufbxi_push(&uc->result, ufbx_material*, mesh->materials.count); + ufbxi_check(materials); + ufbxi_nounroll for (size_t i = 0; i < node->materials.count; i++) { + materials[i] = node->materials.data[i]; + } + ufbxi_nounroll for (size_t i = node->materials.count; i < mesh->materials.count; i++) { + materials[i] = mesh->materials.data[i]; + } + node->materials.data = materials; + node->materials.count = mesh->materials.count; + } + } + } + + if (uc->retain_mesh_parts) { + size_t num_parts = ufbxi_max_sz(mesh->materials.count, 1); + mesh->material_parts.data = ufbxi_push_zero(&uc->result, ufbx_mesh_part, num_parts); + ufbxi_check(mesh->material_parts.data); + mesh->material_parts.count = num_parts; + } + + if (mesh->materials.count <= 1) { + // Use the shared consecutive index buffer for mesh faces if there's only one material + // See HACK(consecutive-faces) in `ufbxi_read_mesh()`. + if (mesh->material_parts.count > 0) { + ufbx_mesh_part *part = &mesh->material_parts.data[0]; + part->num_faces = mesh->num_faces; + part->num_triangles = mesh->num_triangles; + part->num_empty_faces = mesh->num_empty_faces; + part->num_point_faces = mesh->num_point_faces; + part->num_line_faces = mesh->num_line_faces; + part->face_indices.data = uc->consecutive_indices; + part->face_indices.count = mesh->num_faces; + mesh->material_part_usage_order.data = uc->zero_indices; + mesh->material_part_usage_order.count = 1; + } + + if (mesh->materials.count == 1) { + mesh->face_material.data = uc->zero_indices; + mesh->face_material.count = mesh->num_faces; + } else { + mesh->face_material.data = NULL; + mesh->face_material.count = 0; + } + } else if (mesh->materials.count > 0) { + ufbxi_check(ufbxi_finalize_mesh_material(&uc->result, &uc->error, mesh)); + } + + // Fetch deformers + ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->skin_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_SKIN_DEFORMER)); + ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->blend_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_BLEND_DEFORMER)); + ufbxi_check(ufbxi_fetch_dst_elements(uc, &mesh->cache_deformers, &mesh->element, search_node, true, NULL, UFBX_ELEMENT_CACHE_DEFORMER)); + ufbxi_check(ufbxi_fetch_deformers(uc, &mesh->all_deformers, &mesh->element, search_node)); + + // Vertex position must always exist if not explicitly allowed to be missing + if (!mesh->vertex_position.exists && !uc->opts.allow_missing_vertex_position) { + ufbxi_check(mesh->num_indices == 0); + mesh->vertex_position.exists = true; + mesh->vertex_position.unique_per_vertex = true; + mesh->skinned_position.exists = true; + mesh->skinned_position.unique_per_vertex = true; + } + + // Update metadata + if (mesh->max_face_triangles > uc->scene.metadata.max_face_triangles) { + uc->scene.metadata.max_face_triangles = mesh->max_face_triangles; + } + } + } + + ufbxi_for_ptr_list(ufbx_stereo_camera, p_stereo, uc->scene.stereo_cameras) { + ufbx_stereo_camera *stereo = *p_stereo; + stereo->left = (ufbx_camera*)ufbxi_fetch_dst_element(&stereo->element, search_node, ufbxi_LeftCamera, UFBX_ELEMENT_CAMERA); + stereo->right = (ufbx_camera*)ufbxi_fetch_dst_element(&stereo->element, search_node, ufbxi_RightCamera, UFBX_ELEMENT_CAMERA); + } + + ufbxi_for_ptr_list(ufbx_nurbs_curve, p_curve, uc->scene.nurbs_curves) { + ufbx_nurbs_curve *curve = *p_curve; + ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &curve->basis)); + } + + ufbxi_for_ptr_list(ufbx_nurbs_surface, p_surface, uc->scene.nurbs_surfaces) { + ufbx_nurbs_surface *surface = *p_surface; + ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &surface->basis_u)); + ufbxi_check(ufbxi_finalize_nurbs_basis(uc, &surface->basis_v)); + + surface->material = (ufbx_material*)ufbxi_fetch_dst_element(&surface->element, true, NULL, UFBX_ELEMENT_MATERIAL); + } + + ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, uc->scene.anim_stacks) { + ufbx_anim_stack *stack = *p_stack; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &stack->layers, &stack->element, false, true, NULL, UFBX_ELEMENT_ANIM_LAYER)); + + ufbxi_check(ufbxi_push_anim(uc, &stack->anim, stack->layers.data, stack->layers.count)); + } + + ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, uc->scene.anim_layers) { + ufbx_anim_layer *layer = *p_layer; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->anim_values, &layer->element, false, true, NULL, UFBX_ELEMENT_ANIM_VALUE)); + + ufbxi_check(ufbxi_push_anim(uc, &layer->anim, p_layer, 1)); + + uint32_t min_id = UINT32_MAX, max_id = 0; + + // Combine the animated properties with elements (potentially duplicates!) + size_t num_anim_props = 0; + ufbxi_for_ptr_list(ufbx_anim_value, p_value, layer->anim_values) { + ufbx_anim_value *value = *p_value; + ufbxi_for_list(ufbx_connection, ac, value->element.connections_src) { + if (ac->src_prop.length == 0 && ac->dst_prop.length > 0) { + ufbx_anim_prop *aprop = ufbxi_push(&uc->tmp_stack, ufbx_anim_prop, 1); + uint32_t id = ac->dst->element_id; + min_id = ufbxi_min32(min_id, id); + max_id = ufbxi_max32(max_id, id); + uint32_t id_mask = ufbxi_arraycount(layer->_element_id_bitmask) - 1; + layer->_element_id_bitmask[(id >> 5) & id_mask] |= 1u << (id & 31); + ufbxi_check(aprop); + aprop->anim_value = value; + aprop->element = ac->dst; + aprop->_internal_key = ufbxi_get_name_key(ac->dst_prop.data, ac->dst_prop.length); + aprop->prop_name = ac->dst_prop; + num_anim_props++; + } + } + } + + if (min_id != UINT32_MAX) { + layer->_min_element_id = min_id; + layer->_max_element_id = max_id; + } + + switch (ufbxi_find_int(&layer->props, ufbxi_BlendMode, 0)) { + case 0: // Additive + layer->blended = true; + layer->additive = true; + break; + case 1: // Override + layer->blended = false; + layer->additive = false; + break; + case 2: // Override Passthrough + layer->blended = true; + layer->additive = false; + break; + default: // Unknown + layer->blended = false; + layer->additive = false; + break; + } + + ufbx_prop *weight_prop = ufbxi_find_prop(&layer->props, ufbxi_Weight); + if (weight_prop) { + layer->weight = weight_prop->value_real / (ufbx_real)100.0; + if (layer->weight < 0.0f) layer->weight = 0.0f; + if (layer->weight > 0.99999f) layer->weight = 1.0f; + layer->weight_is_animated = (weight_prop->flags & UFBX_PROP_FLAG_ANIMATED) != 0; + } else { + layer->weight = 1.0f; + layer->weight_is_animated = false; + } + layer->compose_rotation = ufbxi_find_int(&layer->props, ufbxi_RotationAccumulationMode, 0) == 0; + layer->compose_scale = ufbxi_find_int(&layer->props, ufbxi_ScaleAccumulationMode, 0) == 0; + + // Add a dummy NULL element animated prop at the end so we can iterate + // animated props without worrying about boundary conditions.. + { + ufbx_anim_prop *aprop = ufbxi_push_zero(&uc->tmp_stack, ufbx_anim_prop, 1); + ufbxi_check(aprop); + } + + layer->anim_props.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_anim_prop, num_anim_props + 1); + ufbxi_check(layer->anim_props.data); + layer->anim_props.count = num_anim_props; + ufbxi_check(ufbxi_sort_anim_props(uc, layer->anim_props.data, layer->anim_props.count)); + } + + ufbxi_for_ptr_list(ufbx_anim_value, p_value, uc->scene.anim_values) { + ufbx_anim_value *value = *p_value; + + // TODO: Search for things like d|Visibility with a constructed name + value->default_value.x = ufbxi_find_real(&value->props, ufbxi_X, value->default_value.x); + value->default_value.x = ufbxi_find_real(&value->props, ufbxi_d_X, value->default_value.x); + value->default_value.y = ufbxi_find_real(&value->props, ufbxi_Y, value->default_value.y); + value->default_value.y = ufbxi_find_real(&value->props, ufbxi_d_Y, value->default_value.y); + value->default_value.z = ufbxi_find_real(&value->props, ufbxi_Z, value->default_value.z); + value->default_value.z = ufbxi_find_real(&value->props, ufbxi_d_Z, value->default_value.z); + + ufbxi_for_list(ufbx_connection, conn, value->element.connections_dst) { + if (conn->src->type == UFBX_ELEMENT_ANIM_CURVE && conn->src_prop.length == 0) { + ufbx_anim_curve *curve = (ufbx_anim_curve*)conn->src; + + uint32_t index = 0; + const char *name = conn->dst_prop.data; + if (name == ufbxi_Y || name == ufbxi_d_Y) index = 1; + if (name == ufbxi_Z || name == ufbxi_d_Z) index = 2; + + ufbx_prop *prop = ufbx_find_prop_len(&value->props, conn->dst_prop.data, conn->dst_prop.length); + if (prop) { + value->default_value.v[index] = prop->value_real; + } + value->curves[index] = curve; + } + } + } + + ufbxi_for_ptr_list(ufbx_anim_curve, p_curve, uc->scene.anim_curves) { + ufbx_anim_curve *curve = *p_curve; + if (curve->keyframes.count > 0) { + curve->min_time = curve->keyframes.data[0].time; + curve->max_time = curve->keyframes.data[curve->keyframes.count - 1].time; + } + } + + ufbxi_for_ptr_list(ufbx_shader, p_shader, uc->scene.shaders) { + ufbx_shader *shader = *p_shader; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &shader->bindings, &shader->element, false, false, NULL, UFBX_ELEMENT_SHADER_BINDING)); + + ufbx_prop *api = ufbx_find_prop(&shader->props, "RenderAPI"); + if (api) { + if (!strcmp(api->value_str.data, "ARNOLD_SHADER_ID")) { + shader->type = UFBX_SHADER_ARNOLD_STANDARD_SURFACE; + } else if (!strcmp(api->value_str.data, "OSL")) { + shader->type = UFBX_SHADER_OSL_STANDARD_SURFACE; + } else if (!strcmp(api->value_str.data, "SFX_PBS_SHADER")) { + shader->type = UFBX_SHADER_SHADERFX_GRAPH; + } + } + } + + ufbxi_for_ptr_list(ufbx_material, p_material, uc->scene.materials) { + ufbx_material *material = *p_material; + material->shader = (ufbx_shader*)ufbxi_fetch_src_element(&material->element, false, NULL, UFBX_ELEMENT_SHADER); + + if (!strcmp(material->shading_model_name.data, "lambert") || !strcmp(material->shading_model_name.data, "Lambert")) { + material->shader_type = UFBX_SHADER_FBX_LAMBERT; + } else if (!strcmp(material->shading_model_name.data, "phong") || !strcmp(material->shading_model_name.data, "Phong")) { + material->shader_type = UFBX_SHADER_FBX_PHONG; + } + + if (material->shader) { + material->shader_type = material->shader->type; + } else { + if (uc->opts.use_blender_pbr_material && uc->exporter == UFBX_EXPORTER_BLENDER_BINARY && uc->exporter_version >= ufbx_pack_version(4,12,0)) { + material->shader_type = UFBX_SHADER_BLENDER_PHONG; + } + + // TODO: Is this too strict? + if (material->shader_type == UFBX_SHADER_UNKNOWN) { + uint32_t classid_a = (uint32_t)(uint64_t)ufbx_find_int(&material->props, "3dsMax|ClassIDa", 0); + uint32_t classid_b = (uint32_t)(uint64_t)ufbx_find_int(&material->props, "3dsMax|ClassIDb", 0); + if (classid_a == 0x3d6b1cecu && classid_b == 0xdeadc001u) { + material->shader_type = UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL; + material->shader_prop_prefix.data = "3dsMax|Parameters|"; + material->shader_prop_prefix.length = strlen("3dsMax|Parameters|"); + } else if (classid_a == 0xf1551e33u && classid_b == 0x37fb1337u) { + material->shader_type = UFBX_SHADER_OPENPBR_MATERIAL; + material->shader_prop_prefix.data = "3dsMax|Parameters|"; + material->shader_prop_prefix.length = strlen("3dsMax|Parameters|"); + } else if (classid_a == 0x38420192u && classid_b == 0x45fe4e1bu) { + material->shader_type = UFBX_SHADER_GLTF_MATERIAL; + material->shader_prop_prefix.data = "3dsMax|"; + material->shader_prop_prefix.length = strlen("3dsMax|"); + } else if (classid_a == 0xd00f1e00u && classid_b == 0xbe77e500u) { + material->shader_type = UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH; + material->shader_prop_prefix.data = "3dsMax|main|"; + material->shader_prop_prefix.length = strlen("3dsMax|main|"); + } else if (classid_a == 0xd00f1e00u && classid_b == 0x01dbad33u) { + material->shader_type = UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS; + material->shader_prop_prefix.data = "3dsMax|main|"; + material->shader_prop_prefix.length = strlen("3dsMax|main|"); + } + } + } + + ufbxi_check(ufbxi_fetch_textures(uc, &material->textures, &material->element, false)); + } + + // Ugh.. Patch the textures from meshes for legacy LayerElement-style textures + { + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, uc->scene.meshes) { + ufbx_mesh *mesh = *p_mesh; + size_t num_materials = mesh->materials.count; + + ufbxi_mesh_extra *extra = (ufbxi_mesh_extra*)ufbxi_get_element_extra(uc, mesh->element.element_id); + if (!extra) continue; + if (num_materials == 0) continue; + + // TODO: This leaks currently to result, probably doesn't matter.. + ufbx_texture_list textures; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &textures, &mesh->element, true, false, NULL, UFBX_ELEMENT_TEXTURE)); + + size_t num_material_textures = 0; + ufbxi_for(ufbxi_tmp_mesh_texture, tex, extra->texture_arr, extra->texture_count) { + if (tex->all_same) { + int32_t texture_id = tex->num_faces > 0 ? (int32_t)tex->face_texture[0] : 0; + if (texture_id >= 0 && (size_t)texture_id < textures.count) { + ufbxi_tmp_material_texture *mat_texs = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, num_materials); + ufbxi_check(mat_texs); + num_material_textures += num_materials; + for (size_t i = 0; i < num_materials; i++) { + mat_texs[i].material_id = (int32_t)i; + mat_texs[i].texture_id = texture_id; + mat_texs[i].prop_name = tex->prop_name; + } + } + } else if (mesh->face_material.count) { + size_t num_faces = ufbxi_min_sz(tex->num_faces, mesh->num_faces); + int32_t prev_material = -1; + int32_t prev_texture = -1; + for (size_t i = 0; i < num_faces; i++) { + int32_t texture_id = (int32_t)tex->face_texture[i]; + int32_t material_id = (int32_t)mesh->face_material.data[i]; + if (texture_id < 0 || (size_t)texture_id >= textures.count) continue; + if (material_id < 0 || (size_t)material_id >= num_materials) continue; + if (material_id == prev_material && texture_id == prev_texture) continue; + prev_material = material_id; + prev_texture = texture_id; + + ufbxi_tmp_material_texture *mat_tex = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, 1); + ufbxi_check(mat_tex); + mat_tex->material_id = material_id; + mat_tex->texture_id = texture_id; + mat_tex->prop_name = tex->prop_name; + num_material_textures++; + } + } + } + + // Push a sentinel material texture to the end so we don't need to + // duplicate the material texture flushing code twice. + { + ufbxi_tmp_material_texture *mat_tex = ufbxi_push(&uc->tmp_stack, ufbxi_tmp_material_texture, 1); + ufbxi_check(mat_tex); + mat_tex->material_id = -1; + mat_tex->texture_id = -1; + mat_tex->prop_name = ufbx_empty_string; + } + + ufbxi_tmp_material_texture *mat_texs = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_tmp_material_texture, num_material_textures + 1); + ufbxi_check(mat_texs); + ufbxi_check(ufbxi_sort_tmp_material_textures(uc, mat_texs, num_material_textures)); + + int32_t prev_material = -2; + int32_t prev_texture = -2; + const char *prev_prop = NULL; + size_t num_textures_in_material = 0; + for (size_t i = 0; i < num_material_textures + 1; i++) { + ufbxi_tmp_material_texture mat_tex = mat_texs[i]; + if (mat_tex.material_id != prev_material) { + if (prev_material >= 0 && num_textures_in_material > 0) { + ufbx_material *mat = mesh->materials.data[prev_material]; + if (mat && mat->textures.count == 0) { + ufbx_material_texture *texs = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_material_texture, num_textures_in_material); + ufbxi_check(texs); + mat->textures.data = texs; + mat->textures.count = num_textures_in_material; + } else { + ufbxi_pop(&uc->tmp_stack, ufbx_material_texture, num_textures_in_material, NULL); + } + } + + if (mat_tex.material_id < 0) break; + prev_material = mat_tex.material_id; + prev_texture = -1; + prev_prop = NULL; + num_textures_in_material = 0; + } + if (mat_tex.texture_id == prev_texture && mat_tex.prop_name.data == prev_prop) continue; + prev_texture = mat_tex.texture_id; + prev_prop = mat_tex.prop_name.data; + + ufbx_material_texture *tex = ufbxi_push(&uc->tmp_stack, ufbx_material_texture, 1); + ufbxi_check(tex); + ufbx_assert(prev_texture >= 0 && (size_t)prev_texture < textures.count); + tex->texture = textures.data[prev_texture]; + tex->shader_prop = tex->material_prop = mat_tex.prop_name; + num_textures_in_material++; + } + } + } + + ufbxi_check(ufbxi_resolve_file_content(uc)); + + ufbxi_for_ptr_list(ufbx_texture, p_texture, uc->scene.textures) { + ufbx_texture *texture = *p_texture; + ufbxi_texture_extra *extra = (ufbxi_texture_extra*)ufbxi_get_element_extra(uc, texture->element.element_id); + + ufbx_prop *uv_set = ufbxi_find_prop(&texture->props, ufbxi_UVSet); + if (uv_set) { + texture->uv_set = uv_set->value_str; + } else { + texture->uv_set = ufbx_empty_string; + } + + texture->video = (ufbx_video*)ufbxi_fetch_dst_element(&texture->element, false, NULL, UFBX_ELEMENT_VIDEO); + if (texture->video) { + texture->content = texture->video->content; + } + + ufbxi_check(ufbxi_finalize_shader_texture(uc, texture)); + + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&texture->filename, (ufbxi_strblob*)&texture->absolute_filename, (ufbxi_strblob*)&texture->relative_filename, false)); + ufbxi_check(ufbxi_resolve_filenames(uc, (ufbxi_strblob*)&texture->raw_filename, (ufbxi_strblob*)&texture->raw_absolute_filename, (ufbxi_strblob*)&texture->raw_relative_filename, true)); + + // Fetch layered texture layers and patch alphas/blend modes + if (texture->type == UFBX_TEXTURE_LAYERED) { + ufbxi_check(ufbxi_fetch_texture_layers(uc, &texture->layers, &texture->element)); + if (extra) { + for (size_t i = 0, num = ufbxi_min_sz(extra->num_alphas, texture->layers.count); i < num; i++) { + texture->layers.data[i].alpha = extra->alphas[i]; + } + for (size_t i = 0, num = ufbxi_min_sz(extra->num_blend_modes, texture->layers.count); i < num; i++) { + int32_t mode = extra->blend_modes[i]; + if (mode >= 0 && mode < UFBX_BLEND_OVERLAY) { + texture->layers.data[i].blend_mode = (ufbx_blend_mode)mode; + } + } + } + } + + ufbxi_check(ufbxi_insert_texture_file(uc, texture)); + } + + ufbxi_propagate_main_textures(&uc->scene); + ufbxi_check(ufbxi_pop_texture_files(uc)); + + // Second pass to fetch material maps + ufbxi_for_ptr_list(ufbx_material, p_material, uc->scene.materials) { + ufbx_material *material = *p_material; + + ufbxi_check(ufbxi_sort_material_textures(uc, material->textures.data, material->textures.count)); + ufbxi_fetch_maps(&uc->scene, material); + + // Fetch `ufbx_material_texture.shader_prop` names + if (material->shader) { + ufbxi_for_ptr_list(ufbx_shader_binding, p_binding, material->shader->bindings) { + ufbx_shader_binding *binding = *p_binding; + + ufbxi_for_list(ufbx_shader_prop_binding, prop, binding->prop_bindings) { + ufbx_string name = prop->material_prop; + + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_material_texture, 4, &index, material->textures.data, 0, material->textures.count, + ( ufbxi_str_less(a->material_prop, name) ), ( a->material_prop.data == name.data )); + for (; index < material->textures.count && material->textures.data[index].shader_prop.data == name.data; index++) { + material->textures.data[index].shader_prop = prop->shader_prop; + } + } + } + } + } + + ufbxi_for_ptr_list(ufbx_display_layer, p_layer, uc->scene.display_layers) { + ufbx_display_layer *layer = *p_layer; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->nodes, &layer->element, false, true, NULL, UFBX_ELEMENT_NODE)); + } + + ufbxi_for_ptr_list(ufbx_selection_set, p_set, uc->scene.selection_sets) { + ufbx_selection_set *set = *p_set; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &set->nodes, &set->element, false, true, NULL, UFBX_ELEMENT_SELECTION_NODE)); + } + + ufbxi_for_ptr_list(ufbx_selection_node, p_node, uc->scene.selection_nodes) { + ufbx_selection_node *node = *p_node; + node->target_node = (ufbx_node*)ufbxi_fetch_dst_element(&node->element, false, NULL, UFBX_ELEMENT_NODE); + node->target_mesh = (ufbx_mesh*)ufbxi_fetch_dst_element(&node->element, false, NULL, UFBX_ELEMENT_MESH); + if (!node->target_mesh && node->target_node) { + node->target_mesh = node->target_node->mesh; + } else if (!node->target_node && node->target_mesh && node->target_mesh->instances.count > 0) { + node->target_node = node->target_mesh->instances.data[0]; + } + + ufbx_mesh *mesh = node->target_mesh; + if (mesh) { + ufbxi_check(ufbxi_validate_indices(uc, &node->vertices, mesh->num_vertices)); + ufbxi_check(ufbxi_validate_indices(uc, &node->edges, mesh->num_edges)); + ufbxi_check(ufbxi_validate_indices(uc, &node->faces, mesh->num_faces)); + } + } + + ufbxi_for_ptr_list(ufbx_constraint, p_constraint, uc->scene.constraints) { + ufbx_constraint *constraint = *p_constraint; + + size_t tmp_base = uc->tmp_stack.num_items; + + // Find property connections in _both_ src and dst connections as they are inconsistent + // in pre-7000 files. For example "Constrained Object" is a "PO" connection in 6100. + ufbxi_for_list(ufbx_connection, conn, constraint->element.connections_src) { + if (conn->src_prop.length == 0 || conn->dst->type != UFBX_ELEMENT_NODE) continue; + ufbxi_check(ufbxi_add_constraint_prop(uc, constraint, (ufbx_node*)conn->dst, conn->src_prop.data)); + } + ufbxi_for_list(ufbx_connection, conn, constraint->element.connections_dst) { + if (conn->dst_prop.length == 0 || conn->src->type != UFBX_ELEMENT_NODE) continue; + ufbxi_check(ufbxi_add_constraint_prop(uc, constraint, (ufbx_node*)conn->src, conn->dst_prop.data)); + } + + size_t num_targets = uc->tmp_stack.num_items - tmp_base; + constraint->targets.count = num_targets; + constraint->targets.data = ufbxi_push_pop(&uc->result, &uc->tmp_stack, ufbx_constraint_target, num_targets); + ufbxi_check(constraint->targets.data); + } + + ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, uc->scene.audio_layers) { + ufbx_audio_layer *layer = *p_layer; + ufbxi_check(ufbxi_fetch_dst_elements(uc, &layer->clips, &layer->element, false, true, NULL, UFBX_ELEMENT_AUDIO_CLIP)); + } + + ufbxi_for_ptr_list(ufbx_lod_group, p_lod, uc->scene.lod_groups) { + ufbxi_check(ufbxi_finalize_lod_group(uc, *p_lod)); + } + + ufbxi_check(ufbxi_fetch_file_textures(uc)); + + // NOTE: This will be patched over in `ufbxi_update_scene()` if there are `anim_layers` + if (uc->scene.anim_layers.count == 0) { + ufbxi_check(ufbxi_push_anim(uc, &uc->scene.anim, NULL, 0)); + } + + uc->scene.metadata.ktime_second = uc->ktime_sec; + + // Maya seems to use scale of 100/3, Blender binary uses exactly 33, ASCII has always value of 1.0 + if (uc->version < 6000) { + uc->scene.metadata.bone_prop_size_unit = 1.0f; + } else if (uc->exporter == UFBX_EXPORTER_BLENDER_BINARY) { + uc->scene.metadata.bone_prop_size_unit = 33.0f; + } else if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) { + uc->scene.metadata.bone_prop_size_unit = 1.0f; + } else { + uc->scene.metadata.bone_prop_size_unit = (ufbx_real)(100.0/3.0); + } + if (uc->exporter == UFBX_EXPORTER_BLENDER_ASCII) { + uc->scene.metadata.bone_prop_limb_length_relative = false; + } else { + uc->scene.metadata.bone_prop_limb_length_relative = true; + } + + return 1; +} + +// -- Interpret the read scene + +static ufbxi_forceinline void ufbxi_add_translate(ufbx_transform *t, ufbx_vec3 v) +{ + t->translation.x += v.x; + t->translation.y += v.y; + t->translation.z += v.z; +} + +static ufbxi_forceinline void ufbxi_sub_translate(ufbx_transform *t, ufbx_vec3 v) +{ + t->translation.x -= v.x; + t->translation.y -= v.y; + t->translation.z -= v.z; +} + +static ufbxi_forceinline void ufbxi_mul_scale(ufbx_transform *t, ufbx_vec3 v) +{ + t->translation.x *= v.x; + t->translation.y *= v.y; + t->translation.z *= v.z; + t->scale.x *= v.x; + t->scale.y *= v.y; + t->scale.z *= v.z; +} + +static ufbxi_forceinline void ufbxi_mul_scale_real(ufbx_transform *t, ufbx_real v) +{ + t->translation.x *= v; + t->translation.y *= v; + t->translation.z *= v; + t->scale.x *= v; + t->scale.y *= v; + t->scale.z *= v; +} + +static ufbxi_noinline ufbx_quat ufbxi_mul_quat(ufbx_quat a, ufbx_quat b) +{ + ufbx_quat r; + r.x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y; + r.y = a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x; + r.z = a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w; + r.w = a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z; + return r; +} + +static ufbxi_forceinline void ufbxi_add_weighted_vec3(ufbx_vec3 *r, ufbx_vec3 b, ufbx_real w) +{ + r->x += b.x * w; + r->y += b.y * w; + r->z += b.z * w; +} + +static ufbxi_forceinline void ufbxi_add_weighted_quat(ufbx_quat *r, ufbx_quat b, ufbx_real w) +{ + r->x += b.x * w; + r->y += b.y * w; + r->z += b.z * w; + r->w += b.w * w; +} + +static ufbxi_noinline void ufbxi_add_weighted_mat(ufbx_matrix *r, const ufbx_matrix *b, ufbx_real w) +{ + ufbxi_add_weighted_vec3(&r->cols[0], b->cols[0], w); + ufbxi_add_weighted_vec3(&r->cols[1], b->cols[1], w); + ufbxi_add_weighted_vec3(&r->cols[2], b->cols[2], w); + ufbxi_add_weighted_vec3(&r->cols[3], b->cols[3], w); +} + +static void ufbxi_mul_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order) +{ + if (ufbxi_is_vec3_zero(v)) return; + + ufbx_quat q = ufbx_euler_to_quat(v, order); + if (t->rotation.w != 1.0) { + t->rotation = ufbxi_mul_quat(q, t->rotation); + } else { + t->rotation = q; + } + + if (!ufbxi_is_vec3_zero(t->translation)) { + t->translation = ufbx_quat_rotate_vec3(q, t->translation); + } +} + +static void ufbxi_mul_rotate_quat(ufbx_transform *t, ufbx_quat q) +{ + if (ufbxi_is_quat_identity(q)) return; + + if (t->rotation.w != 1.0) { + t->rotation = ufbxi_mul_quat(q, t->rotation); + } else { + t->rotation = q; + } + + if (!ufbxi_is_vec3_zero(t->translation)) { + t->translation = ufbx_quat_rotate_vec3(q, t->translation); + } +} + +static void ufbxi_mul_inv_rotate(ufbx_transform *t, ufbx_vec3 v, ufbx_rotation_order order) +{ + if (ufbxi_is_vec3_zero(v)) return; + + ufbx_quat q = ufbx_euler_to_quat(v, order); + q.x = -q.x; q.y = -q.y; q.z = -q.z; + if (t->rotation.w != 1.0) { + t->rotation = ufbxi_mul_quat(q, t->rotation); + } else { + t->rotation = q; + } + + if (!ufbxi_is_vec3_zero(t->translation)) { + t->translation = ufbx_quat_rotate_vec3(q, t->translation); + } +} + +// -- Updating state from properties + +ufbxi_forceinline static void ufbxi_mirror_translation(ufbx_vec3 *p_vec, ufbx_mirror_axis axis) +{ + ufbxi_dev_assert(axis); + p_vec->v[axis - 1] = -p_vec->v[axis - 1]; +} + +ufbxi_forceinline static void ufbxi_mirror_rotation(ufbx_quat *p_quat, ufbx_mirror_axis axis) +{ + ufbxi_dev_assert(axis); + p_quat->v[axis % 3] = -p_quat->v[axis % 3]; + p_quat->v[(axis + 1) % 3] = -p_quat->v[(axis + 1) % 3]; +} + +ufbxi_noinline static ufbx_transform ufbxi_get_geometry_transform(const ufbx_props *props, ufbx_node *node) +{ + ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_GeometricTranslation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_GeometricRotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_GeometricScaling, 1.0f, 1.0f, 1.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + // WorldTransform = ParentWorldTransform * T * R * S * (OT * OR * OS) + + ufbxi_mul_scale(&t, scaling); + ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_add_translate(&t, translation); + + if (node->has_adjust_transform) { + t.translation.x *= node->adjust_translation_scale; + t.translation.y *= node->adjust_translation_scale; + t.translation.z *= node->adjust_translation_scale; + } + + if (node->adjust_mirror_axis) { + ufbxi_mirror_translation(&t.translation, node->adjust_mirror_axis); + ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis); + } + + return t; +} + +ufbxi_noinline static ufbx_transform ufbxi_get_transform(const ufbx_props *props, ufbx_rotation_order order, const ufbx_node *node, const ufbx_vec3 *translation_scale) +{ + ufbx_vec3 scale_pivot = ufbxi_find_vec3(props, ufbxi_ScalingPivot, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rot_pivot = ufbxi_find_vec3(props, ufbxi_RotationPivot, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scale_offset = ufbxi_find_vec3(props, ufbxi_ScalingOffset, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rot_offset = ufbxi_find_vec3(props, ufbxi_RotationOffset, 0.0f, 0.0f, 0.0f); + + ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Lcl_Translation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); + + ufbx_vec3 pre_rotation = ufbxi_find_vec3(props, ufbxi_PreRotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 post_rotation = ufbxi_find_vec3(props, ufbxi_PostRotation, 0.0f, 0.0f, 0.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + // WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1 + // NOTE: Rpost is inverted (!) after converting from PostRotation Euler angles + + if (translation_scale) { + translation.x *= translation_scale->x; + translation.y *= translation_scale->y; + translation.z *= translation_scale->z; + } + + if (node->has_adjust_transform) { + ufbxi_mul_rotate_quat(&t, node->adjust_post_rotation); + ufbxi_mul_scale_real(&t, node->adjust_post_scale); + } + + ufbxi_sub_translate(&t, scale_pivot); + ufbxi_mul_scale(&t, scaling); + ufbxi_add_translate(&t, scale_pivot); + + ufbxi_add_translate(&t, scale_offset); + + ufbxi_sub_translate(&t, rot_pivot); + ufbxi_mul_inv_rotate(&t, post_rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_mul_rotate(&t, rotation, order); + ufbxi_mul_rotate(&t, pre_rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_add_translate(&t, rot_pivot); + + ufbxi_add_translate(&t, rot_offset); + + ufbxi_add_translate(&t, translation); + + if (node->has_adjust_transform) { + ufbxi_add_translate(&t, node->adjust_pre_translation); + ufbxi_mul_rotate_quat(&t, node->adjust_pre_rotation); + ufbxi_mul_scale_real(&t, node->adjust_pre_scale); + t.translation.x *= node->adjust_translation_scale; + t.translation.y *= node->adjust_translation_scale; + t.translation.z *= node->adjust_translation_scale; + } + + if (node->adjust_mirror_axis) { + ufbxi_mirror_translation(&t.translation, node->adjust_mirror_axis); + ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis); + } + + return t; +} + +ufbxi_noinline static ufbx_quat ufbxi_get_rotation(const ufbx_props *props, ufbx_rotation_order order, const ufbx_node *node) +{ + ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 pre_rotation = ufbxi_find_vec3(props, ufbxi_PreRotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 post_rotation = ufbxi_find_vec3(props, ufbxi_PostRotation, 0.0f, 0.0f, 0.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + if (node->has_adjust_transform) { + ufbxi_mul_rotate_quat(&t, node->adjust_post_rotation); + } + + ufbxi_mul_inv_rotate(&t, post_rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_mul_rotate(&t, rotation, order); + ufbxi_mul_rotate(&t, pre_rotation, UFBX_ROTATION_ORDER_XYZ); + + if (node->has_adjust_transform) { + ufbxi_mul_rotate_quat(&t, node->adjust_pre_rotation); + } + + if (node->adjust_mirror_axis) { + ufbxi_mirror_rotation(&t.rotation, node->adjust_mirror_axis); + } + + return t.rotation; +} + +ufbxi_noinline static ufbx_vec3 ufbxi_get_scale(const ufbx_props *props, const ufbx_node *node) +{ + ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + if (node->has_adjust_transform) { + ufbxi_mul_scale_real(&t, node->adjust_post_scale); + } + + ufbxi_mul_scale(&t, scaling); + + if (node->has_adjust_transform) { + ufbxi_mul_scale_real(&t, node->adjust_pre_scale); + } + + return t.scale; +} + +ufbxi_noinline static ufbx_transform ufbxi_get_texture_transform(const ufbx_props *props) +{ + ufbx_vec3 scale_pivot = ufbxi_find_vec3(props, ufbxi_TextureScalingPivot, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rot_pivot = ufbxi_find_vec3(props, ufbxi_TextureRotationPivot, 0.0f, 0.0f, 0.0f); + + ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Translation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Rotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Scaling, 1.0f, 1.0f, 1.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + ufbxi_sub_translate(&t, scale_pivot); + ufbxi_mul_scale(&t, scaling); + ufbxi_add_translate(&t, scale_pivot); + + ufbxi_sub_translate(&t, rot_pivot); + ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_add_translate(&t, rot_pivot); + + ufbxi_add_translate(&t, translation); + + if (ufbxi_find_int(props, ufbxi_UVSwap, 0) != 0) { + const ufbx_vec3 swap_scale = { -1.0f, 0.0f, 0.0f }; + const ufbx_vec3 swap_rotate = { 0.0f, 0.0f, -90.0f }; + ufbxi_mul_scale(&t, swap_scale); + ufbxi_mul_rotate(&t, swap_rotate, UFBX_ROTATION_ORDER_XYZ); + } + + return t; +} + +ufbxi_noinline static ufbx_transform ufbxi_get_constraint_transform(const ufbx_props *props) +{ + ufbx_vec3 translation = ufbxi_find_vec3(props, ufbxi_Translation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rotation = ufbxi_find_vec3(props, ufbxi_Rotation, 0.0f, 0.0f, 0.0f); + ufbx_vec3 rotation_offset = ufbxi_find_vec3(props, ufbxi_RotationOffset, 0.0f, 0.0f, 0.0f); + ufbx_vec3 scaling = ufbxi_find_vec3(props, ufbxi_Scaling, 1.0f, 1.0f, 1.0f); + + ufbx_transform t = { { 0,0,0 }, { 0,0,0,1 }, { 1,1,1 }}; + + ufbxi_mul_scale(&t, scaling); + ufbxi_mul_rotate(&t, rotation, UFBX_ROTATION_ORDER_XYZ); + ufbxi_mul_rotate(&t, rotation_offset, UFBX_ROTATION_ORDER_XYZ); + ufbxi_add_translate(&t, translation); + + return t; +} + +ufbxi_noinline static void ufbxi_update_node(ufbx_node *node, const ufbx_transform_override *overrides, size_t num_overrides) +{ + node->rotation_order = (ufbx_rotation_order)ufbxi_find_enum(&node->props, ufbxi_RotationOrder, UFBX_ROTATION_ORDER_XYZ, UFBX_ROTATION_ORDER_SPHERIC); + node->euler_rotation = ufbxi_find_vec3(&node->props, ufbxi_Lcl_Rotation, 0.0f, 0.0f, 0.0f); + + if (!node->is_root) { + const ufbx_vec3 *transform_scale = NULL; + if (node->parent && node->parent->scale_helper) { + transform_scale = &node->parent->scale_helper->local_transform.scale; + } + node->local_transform = ufbxi_get_transform(&node->props, node->rotation_order, node, transform_scale); + if (node->is_scale_helper && node->parent && node->parent->inherit_scale_node) { + ufbx_node *scale_parent = node->parent->inherit_scale_node; + if (scale_parent->scale_helper) { + ufbx_vec3 inherit_scale = scale_parent->scale_helper->local_transform.scale; + node->local_transform.scale.x *= inherit_scale.x; + node->local_transform.scale.y *= inherit_scale.y; + node->local_transform.scale.z *= inherit_scale.z; + } + } + + if (num_overrides > 0) { + uint32_t typed_id = node->typed_id; + size_t override_ix = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_transform_override, 16, &override_ix, overrides, 0, num_overrides, + ( a->node_id < typed_id ), ( a->node_id == typed_id )); + if (override_ix != SIZE_MAX) { + node->local_transform = overrides[override_ix].transform; + } + } + node->node_to_parent = ufbx_transform_to_matrix(&node->local_transform); + node->geometry_transform = ufbxi_get_geometry_transform(&node->props, node); + } else { + node->geometry_transform = ufbx_identity_transform; + } + + ufbx_matrix unscaled_node_to_parent = ufbxi_unscaled_transform_to_matrix(&node->local_transform); + + node->inherit_scale = node->local_transform.scale; + + ufbx_node *parent = node->parent; + if (parent) { + if (node->inherit_mode == UFBX_INHERIT_MODE_NORMAL) { + node->node_to_world = ufbx_matrix_mul(&parent->node_to_world, &node->node_to_parent); + node->unscaled_node_to_world = ufbx_matrix_mul(&parent->node_to_world, &unscaled_node_to_parent); + } else { + ufbx_transform transform = node->local_transform; + + ufbx_vec3 parent_scale = ufbxi_one_vec3; + if (node->inherit_scale_node) { + parent_scale = node->inherit_scale_node->inherit_scale; + } + + transform.scale.x *= parent_scale.x; + transform.scale.y *= parent_scale.y; + transform.scale.z *= parent_scale.z; + transform.translation.x *= parent->inherit_scale.x; + transform.translation.y *= parent->inherit_scale.y; + transform.translation.z *= parent->inherit_scale.z; + + ufbx_matrix node_to_unscaled_parent = ufbx_transform_to_matrix(&transform); + ufbx_matrix unscaled_node_to_unscaled_parent = ufbxi_unscaled_transform_to_matrix(&transform); + + node->inherit_scale = transform.scale; + node->node_to_world = ufbx_matrix_mul(&parent->unscaled_node_to_world, &node_to_unscaled_parent); + node->unscaled_node_to_world = ufbx_matrix_mul(&parent->unscaled_node_to_world, &unscaled_node_to_unscaled_parent); + } + } else { + node->node_to_world = node->node_to_parent; + node->unscaled_node_to_world = unscaled_node_to_parent; + } + + if (!ufbxi_is_transform_identity(&node->geometry_transform)) { + node->geometry_to_node = ufbx_transform_to_matrix(&node->geometry_transform); + node->geometry_to_world = ufbx_matrix_mul(&node->node_to_world, &node->geometry_to_node); + node->has_geometry_transform = true; + } else { + node->geometry_to_node = ufbx_identity_matrix; + node->geometry_to_world = node->node_to_world; + node->has_geometry_transform = false; + } + + node->visible = ufbxi_find_int(&node->props, ufbxi_Visibility, 1) != 0; +} + +ufbxi_noinline static void ufbxi_update_light(ufbx_light *light) +{ + // NOTE: FBX seems to store intensities 100x of what's specified in at least + // Maya and Blender, should there be a quirks mode to not do this for specific + // exporters. Does the FBX SDK do this transparently as well? + light->intensity = ufbxi_find_real(&light->props, ufbxi_Intensity, (ufbx_real)100.0) / (ufbx_real)100.0; + + light->color = ufbxi_find_vec3(&light->props, ufbxi_Color, 1.0f, 1.0f, 1.0f); + light->type = (ufbx_light_type)ufbxi_find_enum(&light->props, ufbxi_LightType, 0, UFBX_LIGHT_VOLUME); + light->decay = (ufbx_light_decay)ufbxi_find_enum(&light->props, ufbxi_DecayType, UFBX_LIGHT_DECAY_NONE, UFBX_LIGHT_DECAY_CUBIC); + light->area_shape = (ufbx_light_area_shape)ufbxi_find_enum(&light->props, ufbxi_AreaLightShape, 0, UFBX_LIGHT_AREA_SHAPE_SPHERE); + light->inner_angle = ufbxi_find_real(&light->props, ufbxi_HotSpot, 0.0f); + light->inner_angle = ufbxi_find_real(&light->props, ufbxi_InnerAngle, light->inner_angle); + light->outer_angle = ufbxi_find_real(&light->props, ufbxi_Cone_angle, 0.0f); + light->outer_angle = ufbxi_find_real(&light->props, ufbxi_ConeAngle, light->outer_angle); + light->outer_angle = ufbxi_find_real(&light->props, ufbxi_OuterAngle, light->outer_angle); + light->cast_light = ufbxi_find_int(&light->props, ufbxi_CastLight, 1) != 0; + light->cast_shadows = ufbxi_find_int(&light->props, ufbxi_CastShadows, 0) != 0; +} + +typedef struct { + // 1/1000 decimal fixed point for size + uint16_t film_size_x, film_size_y; +} ufbxi_aperture_format; + +static const ufbxi_aperture_format ufbxi_aperture_formats[] = { + { 1000, 1000, }, // UFBX_APERTURE_FORMAT_CUSTOM + { 404, 295, }, // UFBX_APERTURE_FORMAT_16MM_THEATRICAL + { 493, 292, }, // UFBX_APERTURE_FORMAT_SUPER_16MM + { 864, 630, }, // UFBX_APERTURE_FORMAT_35MM_ACADEMY + { 816, 612, }, // UFBX_APERTURE_FORMAT_35MM_TV_PROJECTION + { 980, 735, }, // UFBX_APERTURE_FORMAT_35MM_FULL_APERTURE + { 825, 446, }, // UFBX_APERTURE_FORMAT_35MM_185_PROJECTION + { 864, 732, }, // UFBX_APERTURE_FORMAT_35MM_ANAMORPHIC + { 2066, 906, }, // UFBX_APERTURE_FORMAT_70MM_PROJECTION + { 1485, 991, }, // UFBX_APERTURE_FORMAT_VISTAVISION + { 2080, 1480, }, // UFBX_APERTURE_FORMAT_DYNAVISION + { 2772, 2072, }, // UFBX_APERTURE_FORMAT_IMAX +}; + +ufbxi_noinline static void ufbxi_update_camera(ufbx_scene *scene, ufbx_camera *camera) +{ + camera->projection_mode = (ufbx_projection_mode)ufbxi_find_enum(&camera->props, ufbxi_CameraProjectionType, 0, UFBX_PROJECTION_MODE_ORTHOGRAPHIC); + camera->aspect_mode = (ufbx_aspect_mode)ufbxi_find_enum(&camera->props, ufbxi_AspectRatioMode, 0, UFBX_ASPECT_MODE_FIXED_HEIGHT); + camera->aperture_mode = (ufbx_aperture_mode)ufbxi_find_enum(&camera->props, ufbxi_ApertureMode, UFBX_APERTURE_MODE_VERTICAL, UFBX_APERTURE_MODE_FOCAL_LENGTH); + camera->aperture_format = (ufbx_aperture_format)ufbxi_find_enum(&camera->props, ufbxi_ApertureFormat, UFBX_APERTURE_FORMAT_CUSTOM, UFBX_APERTURE_FORMAT_IMAX); + camera->gate_fit = (ufbx_gate_fit)ufbxi_find_enum(&camera->props, ufbxi_GateFit, 0, UFBX_GATE_FIT_STRETCH); + + camera->near_plane = ufbxi_find_real(&camera->props, ufbxi_NearPlane, 0.0f); + camera->far_plane = ufbxi_find_real(&camera->props, ufbxi_FarPlane, 0.0f); + + // Search both W/H and Width/Height but prefer the latter + ufbx_real aspect_x = ufbxi_find_real(&camera->props, ufbxi_AspectW, 0.0f); + ufbx_real aspect_y = ufbxi_find_real(&camera->props, ufbxi_AspectH, 0.0f); + aspect_x = ufbxi_find_real(&camera->props, ufbxi_AspectWidth, aspect_x); + aspect_y = ufbxi_find_real(&camera->props, ufbxi_AspectHeight, aspect_y); + + ufbx_real fov = ufbxi_find_real(&camera->props, ufbxi_FieldOfView, 0.0f); + ufbx_real fov_x = ufbxi_find_real(&camera->props, ufbxi_FieldOfViewX, 0.0f); + ufbx_real fov_y = ufbxi_find_real(&camera->props, ufbxi_FieldOfViewY, 0.0f); + + ufbx_real focal_length = ufbxi_find_real(&camera->props, ufbxi_FocalLength, 0.0f); + ufbx_real ortho_extent = scene->metadata.ortho_size_unit * ufbxi_find_real(&camera->props, ufbxi_OrthoZoom, 1.0f); + + ufbxi_aperture_format format = ufbxi_aperture_formats[camera->aperture_format]; + ufbx_vec2 film_size = { (ufbx_real)format.film_size_x * (ufbx_real)0.001, (ufbx_real)format.film_size_y * (ufbx_real)0.001 }; + ufbx_real squeeze_ratio = camera->aperture_format == UFBX_APERTURE_FORMAT_35MM_ANAMORPHIC ? 2.0f : 1.0f; + + film_size.x = ufbxi_find_real(&camera->props, ufbxi_FilmWidth, film_size.x); + film_size.y = ufbxi_find_real(&camera->props, ufbxi_FilmHeight, film_size.y); + squeeze_ratio = ufbxi_find_real(&camera->props, ufbxi_FilmSqueezeRatio, squeeze_ratio); + + if (aspect_x <= 0.0f && aspect_y <= 0.0f) { + aspect_x = film_size.x > 0.0f ? film_size.x : 1.0f; + aspect_y = film_size.y > 0.0f ? film_size.y : 1.0f; + } else if (aspect_x <= 0.0f) { + if (film_size.x > 0.0f && film_size.y > 0.0f) { + aspect_x = aspect_y / film_size.y * film_size.x; + } else { + aspect_x = aspect_y; + } + } else if (aspect_y <= 0.0f) { + if (film_size.x > 0.0f && film_size.y > 0.0f) { + aspect_y = aspect_x / film_size.x * film_size.y; + } else { + aspect_y = aspect_x; + } + } + + film_size.y *= squeeze_ratio; + + // TODO: Should this be done always? + ortho_extent *= scene->metadata.geometry_scale; + camera->near_plane *= scene->metadata.geometry_scale; + camera->far_plane *= scene->metadata.geometry_scale; + + camera->focal_length_mm = focal_length; + camera->film_size_inch = film_size; + camera->squeeze_ratio = squeeze_ratio; + camera->orthographic_extent = ortho_extent; + + switch (camera->aspect_mode) { + case UFBX_ASPECT_MODE_WINDOW_SIZE: + case UFBX_ASPECT_MODE_FIXED_RATIO: + camera->resolution_is_pixels = false; + camera->resolution.x = aspect_x; + camera->resolution.y = aspect_y; + break; + case UFBX_ASPECT_MODE_FIXED_RESOLUTION: + camera->resolution_is_pixels = true; + camera->resolution.x = aspect_x; + camera->resolution.y = aspect_y; + break; + case UFBX_ASPECT_MODE_FIXED_WIDTH: + camera->resolution_is_pixels = true; + camera->resolution.x = aspect_x; + camera->resolution.y = aspect_x * aspect_y; + break; + case UFBX_ASPECT_MODE_FIXED_HEIGHT: + camera->resolution_is_pixels = true; + camera->resolution.x = aspect_y * aspect_x; + camera->resolution.y = aspect_y; + break; + default: + ufbxi_unreachable("Unexpected aspect mode"); + } + + ufbx_real aspect_ratio = camera->resolution.x / camera->resolution.y; + ufbx_real film_ratio = film_size.x / film_size.y; + + camera->aspect_ratio = aspect_ratio; + + ufbx_gate_fit effective_fit = camera->gate_fit; + if (effective_fit == UFBX_GATE_FIT_FILL) { + effective_fit = aspect_ratio > film_ratio ? UFBX_GATE_FIT_HORIZONTAL : UFBX_GATE_FIT_VERTICAL; + } else if (effective_fit == UFBX_GATE_FIT_OVERSCAN) { + effective_fit = aspect_ratio < film_ratio ? UFBX_GATE_FIT_HORIZONTAL : UFBX_GATE_FIT_VERTICAL; + } + + switch (effective_fit) { + case UFBX_GATE_FIT_NONE: + camera->aperture_size_inch = camera->film_size_inch; + camera->orthographic_size.x = ortho_extent; + camera->orthographic_size.y = ortho_extent; + break; + case UFBX_GATE_FIT_VERTICAL: + camera->aperture_size_inch.x = camera->film_size_inch.y * aspect_ratio; + camera->aperture_size_inch.y = camera->film_size_inch.y; + camera->orthographic_size.x = ortho_extent * aspect_ratio; + camera->orthographic_size.y = ortho_extent; + break; + case UFBX_GATE_FIT_HORIZONTAL: + camera->aperture_size_inch.x = camera->film_size_inch.x; + camera->aperture_size_inch.y = camera->film_size_inch.x / aspect_ratio; + camera->orthographic_size.x = ortho_extent; + camera->orthographic_size.y = ortho_extent / aspect_ratio; + break; + case UFBX_GATE_FIT_FILL: + case UFBX_GATE_FIT_OVERSCAN: + camera->aperture_size_inch = camera->film_size_inch; + camera->orthographic_size.x = ortho_extent; + camera->orthographic_size.y = ortho_extent; + ufbxi_unreachable("Unreachable, set to vertical/horizontal above"); + break; + case UFBX_GATE_FIT_STRETCH: + camera->aperture_size_inch = camera->film_size_inch; + camera->orthographic_size.x = ortho_extent; + camera->orthographic_size.y = ortho_extent; + // TODO: Not sure what to do here... + break; + default: + ufbxi_unreachable("Unexpected gate fit"); + } + + switch (camera->aperture_mode) { + case UFBX_APERTURE_MODE_HORIZONTAL_AND_VERTICAL: + camera->field_of_view_deg.x = fov_x; + camera->field_of_view_deg.y = fov_y; + camera->field_of_view_tan.x = (ufbx_real)ufbx_tan((double)(fov_x * (UFBXI_DEG_TO_RAD * 0.5f))); + camera->field_of_view_tan.y = (ufbx_real)ufbx_tan((double)(fov_y * (UFBXI_DEG_TO_RAD * 0.5f))); + break; + case UFBX_APERTURE_MODE_HORIZONTAL: + camera->field_of_view_deg.x = fov; + camera->field_of_view_tan.x = (ufbx_real)ufbx_tan((double)(fov * (UFBXI_DEG_TO_RAD * 0.5f))); + camera->field_of_view_tan.y = camera->field_of_view_tan.x / aspect_ratio; + camera->field_of_view_deg.y = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.y) * UFBXI_RAD_TO_DEG * 2.0f; + break; + case UFBX_APERTURE_MODE_VERTICAL: + camera->field_of_view_deg.y = fov; + camera->field_of_view_tan.y = (ufbx_real)ufbx_tan((double)(fov * (UFBXI_DEG_TO_RAD * 0.5f))); + camera->field_of_view_tan.x = camera->field_of_view_tan.y * aspect_ratio; + camera->field_of_view_deg.x = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.x) * UFBXI_RAD_TO_DEG * 2.0f; + break; + case UFBX_APERTURE_MODE_FOCAL_LENGTH: + camera->field_of_view_tan.x = camera->aperture_size_inch.x / (camera->focal_length_mm * UFBXI_MM_TO_INCH) * 0.5f; + camera->field_of_view_tan.y = camera->aperture_size_inch.y / (camera->focal_length_mm * UFBXI_MM_TO_INCH) * 0.5f; + camera->field_of_view_deg.x = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.x) * UFBXI_RAD_TO_DEG * 2.0f; + camera->field_of_view_deg.y = (ufbx_real)ufbx_atan((double)camera->field_of_view_tan.y) * UFBXI_RAD_TO_DEG * 2.0f; + break; + default: + ufbxi_unreachable("Unexpected aperture mode"); + } + + if (camera->projection_mode == UFBX_PROJECTION_MODE_PERSPECTIVE) { + camera->projection_plane = camera->field_of_view_tan; + } else { + camera->projection_plane = camera->orthographic_size; + } +} + +ufbxi_noinline static void ufbxi_update_bone(ufbx_scene *scene, ufbx_bone *bone) +{ + ufbx_real unit = scene->metadata.bone_prop_size_unit; + + bone->radius = ufbxi_find_real(&bone->props, ufbxi_Size, unit) / unit; + if (scene->metadata.bone_prop_limb_length_relative) { + bone->relative_length = ufbxi_find_real(&bone->props, ufbxi_LimbLength, 1.0f); + } else { + bone->relative_length = 1.0f; + } +} + +ufbxi_noinline static void ufbxi_update_line_curve(ufbx_line_curve *line) +{ + line->color = ufbxi_find_vec3(&line->props, ufbxi_Color, 1.0f, 1.0f, 1.0f); +} + +ufbxi_noinline static void ufbxi_update_pose(ufbx_pose *pose) +{ + ufbxi_for_list(ufbx_bone_pose, bone, pose->bone_poses) { + ufbx_node *node = bone->bone_node; + + const ufbx_matrix *parent_to_world = &ufbx_identity_matrix; + ufbx_bone_pose *bone_pose = ufbx_get_bone_pose(pose, node->parent); + if (bone_pose) { + parent_to_world = &bone_pose->bone_to_world; + } else if (node->parent) { + parent_to_world = &node->parent->node_to_world; + } + + ufbx_matrix world_to_parent = ufbx_matrix_invert(parent_to_world); + bone->bone_to_parent = ufbx_matrix_mul(&world_to_parent, &bone->bone_to_world); + } +} + +ufbxi_noinline static void ufbxi_update_skin_cluster(ufbx_skin_cluster *cluster) +{ + if (cluster->bone_node) { + cluster->geometry_to_world = ufbx_matrix_mul(&cluster->bone_node->node_to_world, &cluster->geometry_to_bone); + } else { + cluster->geometry_to_world = ufbx_matrix_mul(&cluster->bind_to_world, &cluster->geometry_to_bone); + } + cluster->geometry_to_world_transform = ufbx_matrix_to_transform(&cluster->geometry_to_world); +} + +ufbxi_noinline static void ufbxi_update_blend_channel(ufbx_blend_channel *channel) +{ + ufbx_real weight = ufbxi_find_real(&channel->props, ufbxi_DeformPercent, 0.0f) * (ufbx_real)0.01; + channel->weight = weight; + + ptrdiff_t num_keys = (ptrdiff_t)channel->keyframes.count; + if (num_keys > 0) { + ufbx_blend_keyframe *keys = channel->keyframes.data; + + // Reset the effective weights to zero and find the split around zero + ptrdiff_t last_negative = -1; + for (ptrdiff_t i = 0; i < num_keys; i++) { + keys[i].effective_weight = (ufbx_real)0.0; + if (keys[i].target_weight < 0.0) last_negative = i; + } + + // Find either the next or last keyframe away from zero + ufbx_blend_keyframe zero_key = { NULL }; + ufbx_blend_keyframe *prev = &zero_key, *next = &zero_key; + if (weight > 0.0) { + if (last_negative >= 0) prev = &keys[last_negative]; + for (ptrdiff_t i = last_negative + 1; i < num_keys; i++) { + prev = next; + next = &keys[i]; + if (next->target_weight > weight) break; + } + } else { + if (last_negative + 1 < num_keys) prev = &keys[last_negative + 1]; + for (ptrdiff_t i = last_negative; i >= 0; i--) { + prev = next; + next = &keys[i]; + if (next->target_weight < weight) break; + } + } + + // Linearly interpolate between the endpoints with the weight + ufbx_real delta = next->target_weight - prev->target_weight; + if (delta != 0.0) { + ufbx_real t = (weight - prev->target_weight) / delta; + prev->effective_weight = 1.0f - t; + next->effective_weight = t; + } + } +} + +ufbxi_noinline static void ufbxi_update_material(ufbx_scene *scene, ufbx_material *material) +{ + if (material->props.num_animated > 0) { + ufbxi_fetch_maps(scene, material); + } +} + +ufbxi_noinline static void ufbxi_update_texture(ufbx_texture *texture) +{ + texture->uv_transform = ufbxi_get_texture_transform(&texture->props); + if (!ufbxi_is_transform_identity(&texture->uv_transform)) { + texture->has_uv_transform = true; + texture->texture_to_uv = ufbx_transform_to_matrix(&texture->uv_transform); + texture->uv_to_texture = ufbx_matrix_invert(&texture->texture_to_uv); + } else { + texture->has_uv_transform = false; + texture->texture_to_uv = ufbx_identity_matrix; + texture->uv_to_texture = ufbx_identity_matrix; + } + texture->wrap_u = (ufbx_wrap_mode)ufbxi_find_enum(&texture->props, ufbxi_WrapModeU, 0, UFBX_WRAP_CLAMP); + texture->wrap_v = (ufbx_wrap_mode)ufbxi_find_enum(&texture->props, ufbxi_WrapModeV, 0, UFBX_WRAP_CLAMP); + + if (texture->shader) { + ufbxi_update_shader_texture(texture, texture->shader); + } +} + +ufbxi_noinline static void ufbxi_update_anim_stack(ufbx_scene *scene, ufbx_anim_stack *stack) +{ + ufbx_prop *begin, *end; + begin = ufbxi_find_prop(&stack->props, ufbxi_LocalStart); + end = ufbxi_find_prop(&stack->props, ufbxi_LocalStop); + if (!begin || !end) { + begin = ufbxi_find_prop(&stack->props, ufbxi_ReferenceStart); + end = ufbxi_find_prop(&stack->props, ufbxi_ReferenceStop); + } + + if (begin && end) { + stack->time_begin = (double)begin->value_int / (double)scene->metadata.ktime_second; + stack->time_end = (double)end->value_int / (double)scene->metadata.ktime_second; + } + + stack->anim->time_begin = stack->time_begin; + stack->anim->time_end = stack->time_end; +} + +ufbxi_noinline static void ufbxi_update_display_layer(ufbx_display_layer *layer) +{ + layer->visible = ufbxi_find_int(&layer->props, ufbxi_Show, 1) != 0; + layer->frozen = ufbxi_find_int(&layer->props, ufbxi_Freeze, 1) != 0; + layer->ui_color = ufbxi_find_vec3(&layer->props, ufbxi_Color, 0.8f, 0.8f, 0.8f); +} + +ufbxi_noinline static void ufbxi_find_bool3(bool *dst, ufbx_props *props, const char *name, bool default_value) +{ + size_t name_len = strlen(name); + char local[64]; + ufbx_assert(name_len < sizeof(local) - 2); + memcpy(local, name, name_len); + + size_t local_len = name_len + 1; + local[local_len] = '\0'; + + int64_t def = default_value ? 1 : 0; + local[name_len] = 'X'; + dst[0] = ufbx_find_int_len(props, local, local_len, def) != 0; + local[name_len] = 'Y'; + dst[1] = ufbx_find_int_len(props, local, local_len, def) != 0; + local[name_len] = 'Z'; + dst[2] = ufbx_find_int_len(props, local, local_len, def) != 0; +} + +ufbxi_noinline static void ufbxi_update_constraint(ufbx_constraint *constraint) +{ + ufbx_props *props = &constraint->props; + ufbx_constraint_type constraint_type = constraint->type; + + constraint->transform_offset = ufbxi_get_constraint_transform(props); + + constraint->weight = ufbxi_find_real(props, ufbxi_Weight, (ufbx_real)100.0) / (ufbx_real)100.0; + + ufbxi_for_list(ufbx_constraint_target, target, constraint->targets) { + ufbx_node *node = target->node; + + ufbx_real weight_scale = (ufbx_real)100.0; + if (constraint_type == UFBX_CONSTRAINT_SINGLE_CHAIN_IK) { + // IK weights seem to be not scaled 100x? + weight_scale = (ufbx_real)1.0; + } + + ufbx_prop *prop; // ufbxi_uninit + ufbx_string parts[2]; // ufbxi_uninit + parts[0] = node->name; + parts[1] = ufbxi_str_c(".Weight"); + prop = ufbx_find_prop_concat(props, parts, 2); + target->weight = (prop ? prop->value_real : weight_scale) / weight_scale; + + if (constraint_type == UFBX_CONSTRAINT_PARENT) { + parts[1] = ufbxi_str_c(".Offset T"); + prop = ufbx_find_prop_concat(props, parts, 2); + ufbx_vec3 t = prop ? prop->value_vec3 : ufbx_zero_vec3; + parts[1] = ufbxi_str_c(".Offset R"); + prop = ufbx_find_prop_concat(props, parts, 2); + ufbx_vec3 r = prop ? prop->value_vec3 : ufbx_zero_vec3; + parts[1] = ufbxi_str_c(".Offset S"); + prop = ufbx_find_prop_concat(props, parts, 2); + ufbx_vec3 s = prop ? prop->value_vec3 : ufbxi_one_vec3; + + target->transform.translation = t; + target->transform.rotation = ufbx_euler_to_quat(r, UFBX_ROTATION_ORDER_XYZ); + target->transform.scale = s; + } + } + + constraint->active = ufbx_find_int(props, "Active", 1) != 0; + if (constraint_type == UFBX_CONSTRAINT_AIM) { + ufbxi_find_bool3(constraint->constrain_rotation, props, "Affect", 1); + + const ufbx_vec3 default_aim = { 1.0f, 0.0f, 0.0f }; + const ufbx_vec3 default_up = { 0.0f, 1.0f, 0.0f }; + + int64_t up_type = ufbx_find_int(props, "WorldUpType", 0); + if (up_type >= 0 && up_type < UFBX_CONSTRAINT_AIM_UP_NONE) { + constraint->aim_up_type = (ufbx_constraint_aim_up_type)up_type; + } + constraint->aim_vector = ufbx_find_vec3(props, "AimVector", default_aim); + constraint->aim_up_vector = ufbx_find_vec3(props, "UpVector", default_up); + + } else if (constraint_type == UFBX_CONSTRAINT_PARENT) { + ufbxi_find_bool3(constraint->constrain_translation, props, "AffectTranslation", 1); + ufbxi_find_bool3(constraint->constrain_rotation, props, "AffectRotation", 1); + ufbxi_find_bool3(constraint->constrain_scale, props, "AffectScale", 0); + } else if (constraint_type == UFBX_CONSTRAINT_POSITION) { + ufbxi_find_bool3(constraint->constrain_translation, props, "Affect", 1); + } else if (constraint_type == UFBX_CONSTRAINT_ROTATION) { + ufbxi_find_bool3(constraint->constrain_rotation, props, "Affect", 1); + } else if (constraint_type == UFBX_CONSTRAINT_SCALE) { + ufbxi_find_bool3(constraint->constrain_scale, props, "Affect", 1); + } else if (constraint_type == UFBX_CONSTRAINT_SINGLE_CHAIN_IK) { + constraint->constrain_rotation[0] = true; + constraint->constrain_rotation[1] = true; + constraint->constrain_rotation[2] = true; + constraint->ik_pole_vector = ufbx_find_vec3(props, "PoleVectorType", ufbx_zero_vec3); + } +} + +ufbxi_noinline static void ufbxi_update_anim(ufbx_scene *scene) +{ + if (scene->anim_stacks.count > 0) { + scene->anim = scene->anim_stacks.data[0]->anim; + } +} + +static ufbxi_forceinline void ufbxi_mirror_matrix_dst(ufbx_matrix *m, ufbx_mirror_axis axis) +{ + if (axis == 0) return; + int32_t ax = (int32_t)axis - 1; + m->cols[0].v[ax] = -m->cols[0].v[ax]; + m->cols[1].v[ax] = -m->cols[1].v[ax]; + m->cols[2].v[ax] = -m->cols[2].v[ax]; + m->cols[3].v[ax] = -m->cols[3].v[ax]; +} + +static ufbxi_forceinline void ufbxi_mirror_matrix_src(ufbx_matrix *m, ufbx_mirror_axis axis) +{ + if (axis == 0) return; + int32_t ax = (int32_t)axis - 1; + m->cols[ax].x = -m->cols[ax].x; + m->cols[ax].y = -m->cols[ax].y; + m->cols[ax].z = -m->cols[ax].z; +} + +static ufbxi_noinline void ufbxi_mirror_matrix(ufbx_matrix *m, ufbx_mirror_axis axis) +{ + if (axis == 0) return; + ufbxi_mirror_matrix_src(m, axis); + ufbxi_mirror_matrix_dst(m, axis); +} + +ufbxi_noinline static void ufbxi_update_initial_clusters(ufbx_scene *scene) +{ + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + cluster->geometry_to_bone = cluster->mesh_node_to_bone; + } + + ufbx_mirror_axis mirror_axis = scene->metadata.mirror_axis; + ufbx_real geometry_scale = scene->metadata.geometry_scale; + + // Space conversion for bind matrices + { + ufbx_matrix world_to_units; + ufbx_real translation_scale = 1.0f; + + if (scene->metadata.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT && scene->metadata.mirror_axis == UFBX_MIRROR_AXIS_NONE) { + world_to_units = scene->root_node->node_to_parent; + } else { + ufbx_transform root_transform; + root_transform.translation = ufbx_zero_vec3; + root_transform.rotation = scene->metadata.root_rotation; + root_transform.scale.x = scene->metadata.root_scale; + root_transform.scale.y = scene->metadata.root_scale; + root_transform.scale.z = scene->metadata.root_scale; + world_to_units = ufbx_transform_to_matrix(&root_transform); + translation_scale = scene->metadata.geometry_scale; + } + + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + cluster->bind_to_world = ufbx_matrix_mul(&world_to_units, &cluster->bind_to_world); + cluster->bind_to_world.cols[3].x *= translation_scale; + cluster->bind_to_world.cols[3].y *= translation_scale; + cluster->bind_to_world.cols[3].z *= translation_scale; + ufbxi_mirror_matrix(&cluster->bind_to_world, mirror_axis); + } + + ufbxi_for_ptr_list(ufbx_pose, p_pose, scene->poses) { + ufbxi_for_list(ufbx_bone_pose, pose, (*p_pose)->bone_poses) { + pose->bone_to_world = ufbx_matrix_mul(&world_to_units, &pose->bone_to_world); + pose->bone_to_world.cols[3].x *= translation_scale; + pose->bone_to_world.cols[3].y *= translation_scale; + pose->bone_to_world.cols[3].z *= translation_scale; + ufbxi_mirror_matrix(&pose->bone_to_world, mirror_axis); + } + } + } + + // Patch initial `mesh_node_to_bone` + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + + ufbx_skin_deformer *skin = (ufbx_skin_deformer*)ufbxi_fetch_src_element(&cluster->element, false, NULL, UFBX_ELEMENT_SKIN_DEFORMER); + if (!skin) continue; + + ufbx_node *node = (ufbx_node*)ufbxi_fetch_src_element(&skin->element, false, NULL, UFBX_ELEMENT_NODE); + if (!node) { + ufbx_mesh *mesh = (ufbx_mesh*)ufbxi_fetch_src_element(&skin->element, false, NULL, UFBX_ELEMENT_MESH); + if (mesh && mesh->instances.count > 0) { + node = mesh->instances.data[0]; + } + } + if (!node) continue; + + // Normalize to the non-helper node + if (node->is_geometry_transform_helper) { + node = node->parent; + } + + if (ufbxi_matrix_all_zero(&cluster->mesh_node_to_bone)) { + // If `mesh_node_to_bone` is not explicitly specified compute it from bind pose. + ufbx_matrix world_to_bind = ufbx_matrix_invert(&cluster->bind_to_world); + cluster->mesh_node_to_bone = ufbx_matrix_mul(&world_to_bind, &node->node_to_world); + } else { + // If `mesh_node_to_bone` is explicit, we may need to modify it for space conversion. + ufbxi_mirror_matrix(&cluster->mesh_node_to_bone, mirror_axis); + if (geometry_scale != 1.0f) { + cluster->mesh_node_to_bone.cols[3].x *= geometry_scale; + cluster->mesh_node_to_bone.cols[3].y *= geometry_scale; + cluster->mesh_node_to_bone.cols[3].z *= geometry_scale; + } + } + + // HACK: Account for geometry transforms by looking at the transform of the + // helper node if one is present. I don't think this is exactly how the skinning + // matrices are formed. + // TODO: Add a test with moving the skinned mesh root around. + if (node->geometry_transform_helper) { + ufbx_node *geo_node = node->geometry_transform_helper; + cluster->geometry_to_bone = ufbx_matrix_mul(&cluster->mesh_node_to_bone, &geo_node->node_to_parent); + } else if (node->has_geometry_transform) { + cluster->geometry_to_bone = ufbx_matrix_mul(&cluster->mesh_node_to_bone, &node->geometry_to_node); + } else { + cluster->geometry_to_bone = cluster->mesh_node_to_bone; + } + } +} + +ufbxi_noinline static ufbx_coordinate_axis ufbxi_find_axis(const ufbx_props *props, const char *axis_name, const char *sign_name) +{ + int64_t axis = ufbxi_find_int(props, axis_name, 3); + int64_t sign = ufbxi_find_int(props, sign_name, 2); + + switch (axis) { + case 0: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_X : UFBX_COORDINATE_AXIS_NEGATIVE_X; + case 1: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_Y : UFBX_COORDINATE_AXIS_NEGATIVE_Y; + case 2: return sign > 0 ? UFBX_COORDINATE_AXIS_POSITIVE_Z : UFBX_COORDINATE_AXIS_NEGATIVE_Z; + default: return UFBX_COORDINATE_AXIS_UNKNOWN; + } +} + +static const ufbx_real ufbxi_time_mode_fps[] = { + 30.0f, // UFBX_TIME_MODE_DEFAULT + 120.0f, // UFBX_TIME_MODE_120_FPS + 100.0f, // UFBX_TIME_MODE_100_FPS + 60.0f, // UFBX_TIME_MODE_60_FPS + 50.0f, // UFBX_TIME_MODE_50_FPS + 48.0f, // UFBX_TIME_MODE_48_FPS + 30.0f, // UFBX_TIME_MODE_30_FPS + 30.0f, // UFBX_TIME_MODE_30_FPS_DROP + 29.97f, // UFBX_TIME_MODE_NTSC_DROP_FRAME + 29.97f, // UFBX_TIME_MODE_NTSC_FULL_FRAME + 25.0f, // UFBX_TIME_MODE_PAL + 24.0f, // UFBX_TIME_MODE_24_FPS + 1000.0f, // UFBX_TIME_MODE_1000_FPS + 23.976f, // UFBX_TIME_MODE_FILM_FULL_FRAME + 24.0f, // UFBX_TIME_MODE_CUSTOM + 96.0f, // UFBX_TIME_MODE_96_FPS + 72.0f, // UFBX_TIME_MODE_72_FPS + 59.94f, // UFBX_TIME_MODE_59_94_FPS +}; + +// Returns whether a non-identity matrix was needed +static ufbxi_noinline bool ufbxi_axis_matrix(ufbx_matrix *mat, ufbx_coordinate_axes src, ufbx_coordinate_axes dst) +{ + uint32_t src_x = (uint32_t)src.right; + uint32_t dst_x = (uint32_t)dst.right; + uint32_t src_y = (uint32_t)src.up; + uint32_t dst_y = (uint32_t)dst.up; + uint32_t src_z = (uint32_t)src.front; + uint32_t dst_z = (uint32_t)dst.front; + + if (src_x == dst_x && src_y == dst_y && src_z == dst_z) return false; + + // Remap axes (axis enum divided by 2) potentially flipping if the signs (enum parity) doesn't match + memset(mat, 0, sizeof(ufbx_matrix)); + mat->cols[src_x >> 1].v[dst_x >> 1] = ((src_x ^ dst_x) & 1) == 0 ? 1.0f : -1.0f; + mat->cols[src_y >> 1].v[dst_y >> 1] = ((src_y ^ dst_y) & 1) == 0 ? 1.0f : -1.0f; + mat->cols[src_z >> 1].v[dst_z >> 1] = ((src_z ^ dst_z) & 1) == 0 ? 1.0f : -1.0f; + + return true; +} + +ufbxi_noinline static void ufbxi_update_adjust_transforms(ufbxi_context *uc, ufbx_scene *scene) +{ + ufbx_transform root_transform = ufbx_identity_transform; + if (!ufbxi_matrix_all_zero(&uc->axis_matrix)) { + root_transform = ufbx_matrix_to_transform(&uc->axis_matrix); + } + root_transform.scale.x *= uc->unit_scale; + root_transform.scale.y *= uc->unit_scale; + root_transform.scale.z *= uc->unit_scale; + + ufbx_space_conversion conversion = uc->opts.space_conversion; + + ufbx_quat light_post_rotation = ufbx_identity_quat; + ufbx_quat camera_post_rotation = ufbx_identity_quat; + ufbx_vec3 light_direction = { 0.0f, -1.0f, 0.0f }; + bool has_light_transform = false; + bool has_camera_transform = false; + + if (ufbx_coordinate_axes_valid(uc->opts.target_light_axes)) { + ufbx_matrix mat; // ufbxi_uninit + ufbx_coordinate_axes light_axes = { + UFBX_COORDINATE_AXIS_POSITIVE_X, + UFBX_COORDINATE_AXIS_NEGATIVE_Z, + UFBX_COORDINATE_AXIS_POSITIVE_Y, + }; + if (ufbxi_axis_matrix(&mat, uc->opts.target_light_axes, light_axes)) { + light_post_rotation = ufbx_matrix_to_transform(&mat).rotation; + + ufbx_matrix inv = ufbx_matrix_invert(&mat); + light_direction = ufbx_transform_direction(&inv, light_direction); + has_light_transform = true; + } + } + + if (ufbx_coordinate_axes_valid(uc->opts.target_camera_axes)) { + ufbx_matrix mat; // ufbxi_uninit + ufbx_coordinate_axes camera_axes = { + UFBX_COORDINATE_AXIS_POSITIVE_Z, + UFBX_COORDINATE_AXIS_POSITIVE_Y, + UFBX_COORDINATE_AXIS_NEGATIVE_X, + }; + if (ufbxi_axis_matrix(&mat, uc->opts.target_camera_axes, camera_axes)) { + camera_post_rotation = ufbx_matrix_to_transform(&mat).rotation; + has_camera_transform = true; + } + } + + ufbxi_for_ptr_list(ufbx_light, p_light, scene->lights) { + ufbx_light *light = *p_light; + light->local_direction.x = 0.0f; + light->local_direction.y = -1.0f; + light->local_direction.z = 0.0f; + } + + scene->metadata.space_conversion = conversion; + scene->metadata.geometry_transform_handling = uc->opts.geometry_transform_handling; + scene->metadata.inherit_mode_handling = uc->opts.inherit_mode_handling; + scene->metadata.pivot_handling = uc->opts.pivot_handling; + scene->metadata.handedness_conversion_axis = uc->opts.handedness_conversion_axis; + + ufbx_real root_scale = ufbxi_min3(root_transform.scale); + if (conversion == UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY) { + scene->metadata.geometry_scale = root_scale; + scene->metadata.root_scale = 1.0f; + } else { + scene->metadata.geometry_scale = 1.0f; + scene->metadata.root_scale = root_scale; + } + scene->metadata.root_rotation = root_transform.rotation; + + ufbxi_for_ptr_list(ufbx_node, p_node, scene->nodes) { + ufbx_node *node = *p_node; + + node->adjust_post_rotation = ufbx_identity_quat; + node->adjust_pre_rotation = ufbx_identity_quat; + node->adjust_pre_scale = 1.0f; + node->adjust_post_scale = 1.0f; + node->adjust_translation_scale = 1.0f; + + if (conversion == UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS) { + if (node->node_depth <= 1 && !node->is_root) { + node->adjust_pre_rotation = root_transform.rotation; + node->adjust_pre_scale = root_scale; + node->has_adjust_transform = true; + node->has_root_adjust_transform = true; + } + } else if (conversion == UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY) { + if (!node->is_root) { + if (node->node_depth <= 1) { + node->adjust_pre_rotation = root_transform.rotation; + } + node->adjust_translation_scale = root_scale; + node->has_adjust_transform = true; + } + } + + if (node->parent) { + // We are not inheriting local scale, so propagate root scale manually and + // apply scale compensation if necessary. + ufbx_node *parent = node->parent; + if (parent->has_root_adjust_transform && node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) { + node->adjust_post_scale *= root_scale; + node->has_adjust_transform = true; + node->has_root_adjust_transform = true; + } + if (parent->is_scale_compensate_parent && node->original_inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) { + ufbx_vec3 scale = ufbxi_find_vec3(&parent->props, ufbxi_Lcl_Scaling, 1.0f, 1.0f, 1.0f); + ufbx_real size = scale.x; + if (ufbx_fabs(scale.y - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.y; + if (ufbx_fabs(scale.z - 1.0f) < ufbx_fabs(size - 1.0f)) size = scale.z; + node->adjust_post_scale *= 1.0f / size; + node->has_adjust_transform = true; + } + } + + if (node->all_attribs.count == 1) { + if (has_light_transform && node->light) { + node->adjust_post_rotation = light_post_rotation; + node->light->local_direction = light_direction; + node->has_adjust_transform = true; + } + if (has_camera_transform && node->camera) { + node->adjust_post_rotation = camera_post_rotation; + node->camera->projection_axes = uc->opts.target_camera_axes; + node->has_adjust_transform = true; + } + } + } +} + +ufbxi_noinline static void ufbxi_update_scene(ufbx_scene *scene, bool initial, const ufbx_transform_override *transform_overrides, size_t num_transform_overrides) +{ + ufbxi_for_ptr_list(ufbx_node, p_node, scene->nodes) { + ufbxi_update_node(*p_node, transform_overrides, num_transform_overrides); + } + + ufbxi_for_ptr_list(ufbx_light, p_light, scene->lights) { + ufbxi_update_light(*p_light); + } + + ufbxi_for_ptr_list(ufbx_camera, p_camera, scene->cameras) { + ufbxi_update_camera(scene, *p_camera); + } + + ufbxi_for_ptr_list(ufbx_bone, p_bone, scene->bones) { + ufbxi_update_bone(scene, *p_bone); + } + + ufbxi_for_ptr_list(ufbx_line_curve, p_line, scene->line_curves) { + ufbxi_update_line_curve(*p_line); + } + + if (initial) { + ufbxi_update_initial_clusters(scene); + + ufbxi_for_ptr_list(ufbx_pose, p_pose, scene->poses) { + ufbxi_update_pose(*p_pose); + } + } + + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, scene->skin_clusters) { + ufbxi_update_skin_cluster(*p_cluster); + } + + ufbxi_for_ptr_list(ufbx_blend_channel, p_channel, scene->blend_channels) { + ufbxi_update_blend_channel(*p_channel); + } + + ufbxi_for_ptr_list(ufbx_texture, p_texture, scene->textures) { + ufbxi_update_texture(*p_texture); + } + + ufbxi_propagate_main_textures(scene); + + ufbxi_for_ptr_list(ufbx_material, p_material, scene->materials) { + ufbxi_update_material(scene, *p_material); + } + + ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, scene->anim_stacks) { + ufbxi_update_anim_stack(scene, *p_stack); + } + + ufbxi_for_ptr_list(ufbx_display_layer, p_layer, scene->display_layers) { + ufbxi_update_display_layer(*p_layer); + } + + ufbxi_for_ptr_list(ufbx_constraint, p_constraint, scene->constraints) { + ufbxi_update_constraint(*p_constraint); + } + + ufbxi_update_anim(scene); +} + +static ufbxi_noinline void ufbxi_update_scene_metadata(ufbx_metadata *metadata) +{ + ufbx_props *props = &metadata->scene_props; + metadata->original_application.vendor = ufbx_find_string(props, "Original|ApplicationVendor", ufbx_empty_string); + metadata->original_application.name = ufbx_find_string(props, "Original|ApplicationName", ufbx_empty_string); + metadata->original_application.version = ufbx_find_string(props, "Original|ApplicationVersion", ufbx_empty_string); + metadata->latest_application.vendor = ufbx_find_string(props, "LastSaved|ApplicationVendor", ufbx_empty_string); + metadata->latest_application.name = ufbx_find_string(props, "LastSaved|ApplicationName", ufbx_empty_string); + metadata->latest_application.version = ufbx_find_string(props, "LastSaved|ApplicationVersion", ufbx_empty_string); +} + +static const ufbx_real ufbxi_pow10_targets[] = { + 0.0f, + (ufbx_real)1e-8, (ufbx_real)1e-7, (ufbx_real)1e-6, (ufbx_real)1e-5, + (ufbx_real)1e-4, (ufbx_real)1e-3, (ufbx_real)1e-2, (ufbx_real)1e-1, + (ufbx_real)1e+0, (ufbx_real)1e+1, (ufbx_real)1e+2, (ufbx_real)1e+3, + (ufbx_real)1e+4, (ufbx_real)1e+5, (ufbx_real)1e+6, (ufbx_real)1e+7, + (ufbx_real)1e+8, (ufbx_real)1e+9, +}; + +static ufbxi_noinline ufbx_real ufbxi_round_if_near(const ufbx_real *targets, size_t num_targets, ufbx_real value) +{ + for (size_t i = 0; i < num_targets; i++) { + double target = targets[i]; + double error = target * 9.5367431640625e-7; + if (error < 0.0) error = -error; + if (error < 7.52316384526264005e-37) error = 7.52316384526264005e-37; + if (value >= target - error && value <= target + error) { + return (ufbx_real)target; + } + } + return value; +} + +static ufbxi_noinline void ufbxi_update_scene_settings(ufbx_scene_settings *settings) +{ + ufbx_real unit_scale_factor = ufbxi_find_real(&settings->props, ufbxi_UnitScaleFactor, 1.0f); + ufbx_real original_unit_scale_factor = ufbxi_find_real(&settings->props, ufbxi_OriginalUnitScaleFactor, unit_scale_factor); + + settings->axes.up = ufbxi_find_axis(&settings->props, ufbxi_UpAxis, ufbxi_UpAxisSign); + settings->axes.front = ufbxi_find_axis(&settings->props, ufbxi_FrontAxis, ufbxi_FrontAxisSign); + settings->axes.right = ufbxi_find_axis(&settings->props, ufbxi_CoordAxis, ufbxi_CoordAxisSign); + settings->unit_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), unit_scale_factor * (ufbx_real)0.01); + settings->original_unit_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), original_unit_scale_factor * (ufbx_real)0.01); + settings->frames_per_second = ufbxi_find_real(&settings->props, ufbxi_CustomFrameRate, 24.0f); + settings->ambient_color = ufbxi_find_vec3(&settings->props, ufbxi_AmbientColor, 0.0f, 0.0f, 0.0f); + settings->original_axis_up = ufbxi_find_axis(&settings->props, ufbxi_OriginalUpAxis, ufbxi_OriginalUpAxisSign); + + ufbx_prop *default_camera = ufbxi_find_prop(&settings->props, ufbxi_DefaultCamera); + if (default_camera) { + settings->default_camera = default_camera->value_str; + } else { + settings->default_camera = ufbx_empty_string; + } + + settings->time_mode = (ufbx_time_mode)ufbxi_find_enum(&settings->props, ufbxi_TimeMode, UFBX_TIME_MODE_24_FPS, UFBX_TIME_MODE_59_94_FPS); + settings->time_protocol = (ufbx_time_protocol)ufbxi_find_enum(&settings->props, ufbxi_TimeProtocol, UFBX_TIME_PROTOCOL_DEFAULT, UFBX_TIME_PROTOCOL_DEFAULT); + settings->snap_mode = (ufbx_snap_mode)ufbxi_find_enum(&settings->props, ufbxi_SnapOnFrameMode, UFBX_SNAP_MODE_NONE, UFBX_SNAP_MODE_SNAP_AND_PLAY); + + if (settings->time_mode != UFBX_TIME_MODE_CUSTOM) { + settings->frames_per_second = ufbxi_time_mode_fps[settings->time_mode]; + } +} + +static ufbxi_noinline void ufbxi_update_scene_settings_obj(ufbxi_context *uc) +{ + ufbx_scene_settings *settings = &uc->scene.settings; + settings->original_unit_meters = settings->unit_meters = uc->opts.obj_unit_meters; + if (ufbx_coordinate_axes_valid(uc->opts.obj_axes)) { + settings->axes = uc->opts.obj_axes; + } else { + settings->axes.right = UFBX_COORDINATE_AXIS_UNKNOWN; + settings->axes.up = UFBX_COORDINATE_AXIS_UNKNOWN; + settings->axes.front = UFBX_COORDINATE_AXIS_UNKNOWN; + } +} + +// -- Geometry caches + +#if UFBXI_FEATURE_GEOMETRY_CACHE + +typedef struct { + ufbxi_refcount refcount; + ufbx_geometry_cache cache; + uint32_t magic; + bool owned_by_scene; + + ufbxi_buf string_buf; +} ufbxi_geometry_cache_imp; + +ufbx_static_assert(geometry_cache_imp_offset, offsetof(ufbxi_geometry_cache_imp, cache) == sizeof(ufbxi_refcount)); + +typedef struct { + ufbx_string name; + ufbx_string interpretation; + uint32_t sample_rate; + uint32_t start_time; + uint32_t end_time; + uint32_t current_time; + uint32_t consecutive_fails; + bool try_load; +} ufbxi_cache_tmp_channel; + +typedef enum { + UFBXI_CACHE_XML_TYPE_NONE, + UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME, + UFBXI_CACHE_XML_TYPE_SINGLE_FILE, +} ufbxi_cache_xml_type; + +typedef enum { + UFBXI_CACHE_XML_FORMAT_NONE, + UFBXI_CACHE_XML_FORMAT_MCC, + UFBXI_CACHE_XML_FORMAT_MCX, +} ufbxi_cache_xml_format; + +typedef struct { + ufbx_error error; + ufbx_string filename; + bool owned_by_scene; + bool ignore_if_not_found; + + ufbx_geometry_cache_opts opts; + + ufbxi_allocator *ator_tmp; + ufbxi_allocator ator_result; + + ufbxi_buf result; + ufbxi_buf tmp; + ufbxi_buf tmp_stack; + + ufbxi_cache_tmp_channel *channels; + size_t num_channels; + + // Temporary array + char *tmp_arr; + size_t tmp_arr_size; + + ufbxi_string_pool string_pool; + + ufbx_open_file_cb open_file_cb; + + double frames_per_second; + + ufbx_string stream_filename; + ufbx_stream stream; + + bool mc_for8; + + ufbx_string xml_filename; + uint32_t xml_ticks_per_frame; + ufbxi_cache_xml_type xml_type; + ufbxi_cache_xml_format xml_format; + + ufbx_string channel_name; + + char *name_buf; + size_t name_cap; + + uint64_t file_offset; + const char *pos, *pos_end; + + ufbx_geometry_cache cache; + ufbxi_geometry_cache_imp *imp; + + char buffer[128]; +} ufbxi_cache_context; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_read(ufbxi_cache_context *cc, void *dst, size_t size, bool allow_eof) +{ + size_t buffered = ufbxi_min_sz(ufbxi_to_size(cc->pos_end - cc->pos), size); + memcpy(dst, cc->pos, buffered); + cc->pos += buffered; + size -= buffered; + cc->file_offset += buffered; + if (size == 0) return 1; + dst = (char*)dst + buffered; + + if (size >= sizeof(cc->buffer)) { + size_t num_read = cc->stream.read_fn(cc->stream.user, dst, size); + ufbxi_check_err_msg(&cc->error, num_read <= size, "IO error"); + if (!allow_eof) { + ufbxi_check_err_msg(&cc->error, num_read == size, "Truncated file"); + } + cc->file_offset += num_read; + size -= num_read; + dst = (char*)dst + num_read; + } else { + size_t num_read = cc->stream.read_fn(cc->stream.user, cc->buffer, sizeof(cc->buffer)); + ufbxi_check_err_msg(&cc->error, num_read <= sizeof(cc->buffer), "IO error"); + if (!allow_eof) { + ufbxi_check_err_msg(&cc->error, num_read >= size, "Truncated file"); + } + cc->pos = cc->buffer; + cc->pos_end = cc->buffer + sizeof(cc->buffer); + + memcpy(dst, cc->pos, size); + cc->pos += size; + cc->file_offset += size; + + size_t num_written = ufbxi_min_sz(size, num_read); + size -= num_written; + dst = (char*)dst + num_written; + } + + if (size > 0) { + memset(dst, 0, size); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_skip(ufbxi_cache_context *cc, uint64_t size) +{ + cc->file_offset += size; + + uint64_t buffered = ufbxi_min64((uint64_t)(cc->pos_end - cc->pos), size); + cc->pos += buffered; + size -= buffered; + + if (cc->stream.skip_fn) { + while (size >= UFBXI_MAX_SKIP_SIZE) { + size -= UFBXI_MAX_SKIP_SIZE; + ufbxi_check_err_msg(&cc->error, cc->stream.skip_fn(cc->stream.user, UFBXI_MAX_SKIP_SIZE - 1), "Truncated file"); + + // Check that we can read at least one byte in case the file is broken + // and causes us to seek indefinitely forwards as `fseek()` does not + // report if we hit EOF... + char single_byte[1]; // ufbxi_uninit + size_t num_read = cc->stream.read_fn(cc->stream.user, single_byte, 1); + ufbxi_check_err_msg(&cc->error, num_read <= 1, "IO error"); + ufbxi_check_err_msg(&cc->error, num_read == 1, "Truncated file"); + } + + if (size > 0) { + ufbxi_check_err_msg(&cc->error, cc->stream.skip_fn(cc->stream.user, (size_t)size), "Truncated file"); + } + + } else { + char skip_buf[2048]; // ufbxi_uninit + while (size > 0) { + size_t to_skip = (size_t)ufbxi_min64(size, sizeof(skip_buf)); + size -= to_skip; + ufbxi_check_err_msg(&cc->error, cc->stream.read_fn(cc->stream.user, skip_buf, to_skip), "Truncated file"); + } + } + + return 1; +} + +#define ufbxi_cache_mc_tag(a,b,c,d) ((uint32_t)(a)<<24u | (uint32_t)(b)<<16 | (uint32_t)(c)<<8u | (uint32_t)(d)) + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_tag(ufbxi_cache_context *cc, uint32_t *p_tag) +{ + char buf[4]; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, true)); + *p_tag = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3]; + if (*p_tag == ufbxi_cache_mc_tag('F','O','R','8')) { + cc->mc_for8 = true; + } + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_u32(ufbxi_cache_context *cc, uint32_t *p_value) +{ + char buf[4]; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, false)); + *p_value = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3]; + if (cc->mc_for8) { + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 4, false)); + } + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_mc_read_u64(ufbxi_cache_context *cc, uint64_t *p_value) +{ + if (!cc->mc_for8) { + uint32_t v32; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &v32)); + *p_value = v32; + } else { + char buf[8]; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, buf, 8, false)); + uint32_t hi = (uint32_t)(uint8_t)buf[0]<<24u | (uint32_t)(uint8_t)buf[1]<<16 | (uint32_t)(uint8_t)buf[2]<<8u | (uint32_t)(uint8_t)buf[3]; + uint32_t lo = (uint32_t)(uint8_t)buf[4]<<24u | (uint32_t)(uint8_t)buf[5]<<16 | (uint32_t)(uint8_t)buf[6]<<8u | (uint32_t)(uint8_t)buf[7]; + *p_value = (uint64_t)hi << 32u | (uint64_t)lo; + } + return 1; +} + +static const uint8_t ufbxi_cache_data_format_size[] = { + 0, 4, 12, 8, 24, +}; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_mc(ufbxi_cache_context *cc) +{ + uint32_t version = 0, time_start = 0, time_end = 0; + uint32_t count = 0, time = 0; + char skip_buf[8]; // ufbxi_uninit + + for (;;) { + uint32_t tag; // ufbxi_uninit + uint64_t size; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_tag(cc, &tag)); + if (tag == 0) break; + + if (tag == ufbxi_cache_mc_tag('C','A','C','H') || tag == ufbxi_cache_mc_tag('M','Y','C','H')) { + continue; + } + if (cc->mc_for8) { + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, skip_buf, 4, false)); + } + + ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u64(cc, &size)); + uint64_t begin = cc->file_offset; + + size_t alignment = cc->mc_for8 ? 8 : 4; + + ufbx_cache_data_format format = UFBX_CACHE_DATA_FORMAT_UNKNOWN; + switch (tag) { + case ufbxi_cache_mc_tag('F','O','R','4'): cc->mc_for8 = false; break; + case ufbxi_cache_mc_tag('F','O','R','8'): cc->mc_for8 = true; break; + case ufbxi_cache_mc_tag('V','R','S','N'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &version)); break; + case ufbxi_cache_mc_tag('S','T','I','M'): + ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time_start)); + time = time_start; + break; + case ufbxi_cache_mc_tag('E','T','I','M'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time_end)); break; + case ufbxi_cache_mc_tag('T','I','M','E'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &time)); break; + case ufbxi_cache_mc_tag('C','H','N','M'): { + ufbxi_check_err(&cc->error, size > 0 && size < SIZE_MAX); + size_t length = (size_t)size - 1; + size_t padded_length = ((size_t)size + alignment - 1) & ~(alignment - 1); + ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->name_buf, &cc->name_cap, padded_length)); + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, cc->name_buf, padded_length, false)); + cc->channel_name.data = cc->name_buf; + cc->channel_name.length = length; + ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &cc->channel_name, false)); + } break; + case ufbxi_cache_mc_tag('S','I','Z','E'): ufbxi_check_err(&cc->error, ufbxi_cache_mc_read_u32(cc, &count)); break; + case ufbxi_cache_mc_tag('F','V','C','A'): format = UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT; break; + case ufbxi_cache_mc_tag('D','V','C','A'): format = UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE; break; + case ufbxi_cache_mc_tag('F','B','C','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_FLOAT; break; + case ufbxi_cache_mc_tag('D','B','C','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE; break; + case ufbxi_cache_mc_tag('D','B','L','A'): format = UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE; break; + default: ufbxi_fail_err(&cc->error, "Unknown tag"); + } + + if (format != UFBX_CACHE_DATA_FORMAT_UNKNOWN) { + ufbx_cache_frame *frame = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_frame, 1); + ufbxi_check_err(&cc->error, frame); + + uint32_t elem_size = ufbxi_cache_data_format_size[format]; + uint64_t total_size = (uint64_t)elem_size * (uint64_t)count; + ufbxi_check_err(&cc->error, size >= elem_size * count); + + frame->channel = cc->channel_name; + frame->time = (double)time * (1.0/6000.0); + frame->filename = cc->stream_filename; + frame->data_format = format; + frame->data_encoding = UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN; + frame->data_offset = cc->file_offset; + frame->data_count = count; + frame->data_element_bytes = elem_size; + frame->data_total_bytes = total_size; + frame->file_format = UFBX_CACHE_FILE_FORMAT_MC; + + uint64_t end = begin + ((size + alignment - 1) & ~(uint64_t)(alignment - 1)); + ufbxi_check_err(&cc->error, end >= cc->file_offset); + uint64_t left = end - cc->file_offset; + ufbxi_check_err(&cc->error, ufbxi_cache_skip(cc, left)); + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_pc2(ufbxi_cache_context *cc) +{ + char header[32]; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, header, sizeof(header), false)); + + uint32_t version = ufbxi_read_u32(header + 12); + uint32_t num_points = ufbxi_read_u32(header + 16); + double start_frame = ufbxi_read_f32(header + 20); + double frames_per_sample = ufbxi_read_f32(header + 24); + uint32_t num_samples = ufbxi_read_u32(header + 28); + + (void)version; + + ufbx_cache_frame *frames = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_frame, num_samples); + ufbxi_check_err(&cc->error, frames); + + uint64_t total_points = (uint64_t)num_points * (uint64_t)num_samples; + ufbxi_check_err(&cc->error, total_points < UINT64_MAX / 12); + + uint64_t offset = cc->file_offset; + + // Skip almost to the end of the data and try to read one byte as there's + // nothing after the data so we can't detect EOF.. + if (total_points > 0) { + char last_byte[1]; // ufbxi_uninit + ufbxi_check_err(&cc->error, ufbxi_cache_skip(cc, total_points * 12 - 1)); + ufbxi_check_err(&cc->error, ufbxi_cache_read(cc, last_byte, 1, false)); + } + + for (uint32_t i = 0; i < num_samples; i++) { + ufbx_cache_frame *frame = &frames[i]; + + double sample_frame = start_frame + (double)i * frames_per_sample; + frame->channel = cc->channel_name; + frame->time = sample_frame / cc->frames_per_second; + frame->filename = cc->stream_filename; + frame->data_format = UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT; + frame->data_encoding = UFBX_CACHE_DATA_ENCODING_LITTLE_ENDIAN; + frame->data_offset = offset; + frame->data_count = num_points; + frame->data_element_bytes = 12; + frame->data_total_bytes = num_points * 12; + frame->file_format = UFBX_CACHE_FILE_FORMAT_PC2; + offset += num_points * 12; + } + + return 1; +} + +static ufbxi_noinline bool ufbxi_tmp_channel_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_cache_tmp_channel *a = (const ufbxi_cache_tmp_channel *)va, *b = (const ufbxi_cache_tmp_channel *)vb; + return ufbxi_str_less(a->name, b->name); +} + +static ufbxi_noinline int ufbxi_cache_sort_tmp_channels(ufbxi_cache_context *cc, ufbxi_cache_tmp_channel *channels, size_t count) +{ + ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->tmp_arr, &cc->tmp_arr_size, count * sizeof(ufbxi_cache_tmp_channel))); + ufbxi_stable_sort(sizeof(ufbxi_cache_tmp_channel), 16, channels, cc->tmp_arr, count, &ufbxi_tmp_channel_less, NULL); + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_xml_imp(ufbxi_cache_context *cc, ufbxi_xml_document *doc) +{ + cc->xml_ticks_per_frame = 250; + cc->xml_filename = cc->stream_filename; + + ufbxi_xml_tag *tag_root = ufbxi_xml_find_child(doc->root, "Autodesk_Cache_File"); + if (tag_root) { + ufbxi_xml_tag *tag_type = ufbxi_xml_find_child(tag_root, "cacheType"); + ufbxi_xml_tag *tag_fps = ufbxi_xml_find_child(tag_root, "cacheTimePerFrame"); + ufbxi_xml_tag *tag_channels = ufbxi_xml_find_child(tag_root, "Channels"); + + size_t num_extra = 0; + ufbxi_for(ufbxi_xml_tag, tag, tag_root->children, tag_root->num_children) { + if (tag->num_children != 1) continue; + if (strcmp(tag->name.data, "extra") != 0) continue; + ufbx_string *extra = ufbxi_push(&cc->tmp_stack, ufbx_string, 1); + ufbxi_check_err(&cc->error, extra); + *extra = tag->children[0].text; + ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, extra, false)); + num_extra++; + } + cc->cache.extra_info.count = num_extra; + cc->cache.extra_info.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_string, num_extra); + ufbxi_check_err(&cc->error, cc->cache.extra_info.data); + + if (tag_type) { + ufbxi_xml_attrib *type = ufbxi_xml_find_attrib(tag_type, "Type"); + ufbxi_xml_attrib *format = ufbxi_xml_find_attrib(tag_type, "Format"); + if (type) { + if (!strcmp(type->value.data, "OneFilePerFrame")) { + cc->xml_type = UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME; + } else if (!strcmp(type->value.data, "OneFile")) { + cc->xml_type = UFBXI_CACHE_XML_TYPE_SINGLE_FILE; + } + } + if (format) { + if (!strcmp(format->value.data, "mcc")) { + cc->xml_format = UFBXI_CACHE_XML_FORMAT_MCC; + } else if (!strcmp(format->value.data, "mcx")) { + cc->xml_format = UFBXI_CACHE_XML_FORMAT_MCX; + } + } + } + + if (tag_fps) { + ufbxi_xml_attrib *fps = ufbxi_xml_find_attrib(tag_fps, "TimePerFrame"); + if (fps) { + uint32_t value = ufbxi_parse_uint32_radix(fps->value.data, 10); + if (value > 0) { + cc->xml_ticks_per_frame = value; + } + } + } + + if (tag_channels) { + cc->channels = ufbxi_push_zero(&cc->tmp, ufbxi_cache_tmp_channel, tag_channels->num_children); + ufbxi_check_err(&cc->error, cc->channels); + + ufbxi_for(ufbxi_xml_tag, tag, tag_channels->children, tag_channels->num_children) { + ufbxi_xml_attrib *name = ufbxi_xml_find_attrib(tag, "ChannelName"); + ufbxi_xml_attrib *type = ufbxi_xml_find_attrib(tag, "ChannelType"); + ufbxi_xml_attrib *interpretation = ufbxi_xml_find_attrib(tag, "ChannelInterpretation"); + if (!(name && type && interpretation)) continue; + + ufbxi_cache_tmp_channel *channel = &cc->channels[cc->num_channels++]; + channel->name = name->value; + channel->interpretation = interpretation->value; + ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &channel->name, false)); + ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &channel->interpretation, false)); + + ufbxi_xml_attrib *sampling_rate = ufbxi_xml_find_attrib(tag, "SamplingRate"); + ufbxi_xml_attrib *start_time = ufbxi_xml_find_attrib(tag, "StartTime"); + ufbxi_xml_attrib *end_time = ufbxi_xml_find_attrib(tag, "EndTime"); + if (sampling_rate && start_time && end_time) { + channel->sample_rate = ufbxi_parse_uint32_radix(sampling_rate->value.data, 10); + channel->start_time = ufbxi_parse_uint32_radix(start_time->value.data, 10); + channel->end_time = ufbxi_parse_uint32_radix(end_time->value.data, 10); + channel->current_time = channel->start_time; + channel->try_load = true; + } + } + } + } + + ufbxi_check_err(&cc->error, ufbxi_cache_sort_tmp_channels(cc, cc->channels, cc->num_channels)); + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_xml(ufbxi_cache_context *cc) +{ + ufbxi_xml_load_opts opts = { 0 }; + opts.ator = cc->ator_tmp; + opts.read_fn = cc->stream.read_fn; + opts.read_user = cc->stream.user; + opts.prefix = cc->pos; + opts.prefix_length = ufbxi_to_size(cc->pos_end - cc->pos); + ufbxi_xml_document *doc = ufbxi_load_xml(&opts, &cc->error); + ufbxi_check_err(&cc->error, doc); + + int xml_ok = ufbxi_cache_load_xml_imp(cc, doc); + ufbxi_free_xml(doc); + ufbxi_check_err(&cc->error, xml_ok); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_file(ufbxi_cache_context *cc, ufbx_string filename) +{ + cc->stream_filename = filename; + ufbxi_check_err(&cc->error, ufbxi_push_string_place_str(&cc->string_pool, &cc->stream_filename, false)); + + // Assume all files have at least 16 bytes of header + size_t magic_len = cc->stream.read_fn(cc->stream.user, cc->buffer, 16); + ufbxi_check_err_msg(&cc->error, magic_len <= 16, "IO error"); + ufbxi_check_err_msg(&cc->error, magic_len == 16, "Truncated file"); + cc->pos = cc->buffer; + cc->pos_end = cc->buffer + 16; + + cc->file_offset = 0; + + if (!memcmp(cc->buffer, "POINTCACHE2", 11)) { + ufbxi_check_err(&cc->error, ufbxi_cache_load_pc2(cc)); + } else if (!memcmp(cc->buffer, "FOR4", 4) || !memcmp(cc->buffer, "FOR8", 4)) { + ufbxi_check_err(&cc->error, ufbxi_cache_load_mc(cc)); + } else { + ufbxi_check_err(&cc->error, ufbxi_cache_load_xml(cc)); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_try_open_file(ufbxi_cache_context *cc, ufbx_string filename, const ufbx_blob *original_filename, bool *p_found) +{ + memset(&cc->stream, 0, sizeof(cc->stream)); + ufbxi_regression_assert(strlen(filename.data) == filename.length); + if (!ufbxi_open_file(&cc->open_file_cb, &cc->stream, filename.data, filename.length, original_filename, cc->ator_tmp, UFBX_OPEN_FILE_GEOMETRY_CACHE)) { + return 1; + } + + int ok = ufbxi_cache_load_file(cc, filename); + *p_found = true; + + if (cc->stream.close_fn) { + cc->stream.close_fn(cc->stream.user); + } + + return ok; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_cache_load_frame_files(ufbxi_cache_context *cc) +{ + if (cc->xml_filename.length == 0) return 1; + + const char *extension = NULL; + switch (cc->xml_format) { + case UFBXI_CACHE_XML_FORMAT_MCC: extension = "mc"; break; + case UFBXI_CACHE_XML_FORMAT_MCX: extension = "mcx"; break; + default: return 1; + } + + // Ensure worst case space for `path/filenameFrame123Tick456.mcx` + size_t name_buf_len = cc->xml_filename.length + 64; + char *name_buf = ufbxi_push(&cc->tmp, char, name_buf_len); + ufbxi_check_err(&cc->error, name_buf); + + // Find the prefix before `.xml` + size_t prefix_len = cc->xml_filename.length; + for (size_t i = prefix_len; i > 0; --i) { + if (cc->xml_filename.data[i - 1] == '.') { + prefix_len = i - 1; + break; + } + } + memcpy(name_buf, cc->xml_filename.data, prefix_len); + + char *suffix_data = name_buf + prefix_len; + size_t suffix_len = name_buf_len - prefix_len; + + ufbx_string filename; + filename.data = name_buf; + + if (cc->xml_type == UFBXI_CACHE_XML_TYPE_SINGLE_FILE) { + filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, ".%s", extension); + bool found = false; + ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename, NULL, &found)); + } else if (cc->xml_type == UFBXI_CACHE_XML_TYPE_FILE_PER_FRAME) { + uint32_t lowest_time = 0; + for (;;) { + // Find the first `time >= lowest_time` value that has data in some channel + uint32_t time = UINT32_MAX; + ufbxi_for(ufbxi_cache_tmp_channel, chan, cc->channels, cc->num_channels) { + if (!chan->try_load || chan->consecutive_fails > 10) continue; + uint32_t sample_rate = chan->sample_rate ? chan->sample_rate : cc->xml_ticks_per_frame; + if (chan->current_time < lowest_time) { + uint32_t delta = (lowest_time - chan->current_time - 1) / sample_rate; + chan->current_time += delta * sample_rate; + if (UINT32_MAX - chan->current_time >= sample_rate) { + chan->current_time += sample_rate; + } else { + chan->try_load = false; + continue; + } + } + if (chan->current_time <= chan->end_time) { + time = ufbxi_min32(time, chan->current_time); + } + } + if (time == UINT32_MAX) break; + + // Try to load a file at the specified frame/tick + uint32_t frame = time / cc->xml_ticks_per_frame; + uint32_t tick = time % cc->xml_ticks_per_frame; + if (tick == 0) { + filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, "Frame%u.%s", frame, extension); + } else { + filename.length = prefix_len + (size_t)ufbxi_snprintf(suffix_data, suffix_len, "Frame%uTick%u.%s", frame, tick, extension); + } + bool found = false; + ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename, NULL, &found)); + + // Update channel status + ufbxi_for(ufbxi_cache_tmp_channel, chan, cc->channels, cc->num_channels) { + if (chan->current_time == time) { + chan->consecutive_fails = found ? 0 : chan->consecutive_fails + 1; + } + } + + lowest_time = time + 1; + } + } + + return 1; +} + +static ufbxi_noinline bool ufbxi_cmp_cache_frame_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_cache_frame *a = (const ufbx_cache_frame *)va, *b = (const ufbx_cache_frame *)vb; + if (a->channel.data != b->channel.data) { + // Channel names should be interned + ufbxi_regression_assert(!ufbxi_str_equal(a->channel, b->channel)); + return ufbxi_str_less(a->channel, b->channel); + } + return a->time < b->time; +} + +static ufbxi_noinline int ufbxi_cache_sort_frames(ufbxi_cache_context *cc, ufbx_cache_frame *frames, size_t count) +{ + ufbxi_check_err(&cc->error, ufbxi_grow_array(cc->ator_tmp, &cc->tmp_arr, &cc->tmp_arr_size, count * sizeof(ufbx_cache_frame))); + ufbxi_stable_sort(sizeof(ufbx_cache_frame), 16, frames, cc->tmp_arr, count, &ufbxi_cmp_cache_frame_less, NULL); + return 1; +} + +typedef struct { + ufbx_cache_interpretation interpretation; + const char *pattern; +} ufbxi_cache_interpretation_name; + +static const ufbxi_cache_interpretation_name ufbxi_cache_interpretation_names[] = { + { UFBX_CACHE_INTERPRETATION_POINTS, "\\cpoints?" }, + { UFBX_CACHE_INTERPRETATION_VERTEX_POSITION, "\\cpositions?" }, + { UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL, "\\cnormals?" }, +}; + +static ufbxi_noinline int ufbxi_cache_setup_channels(ufbxi_cache_context *cc) +{ + ufbxi_cache_tmp_channel *tmp_chan = cc->channels, *tmp_end = ufbxi_add_ptr(tmp_chan, cc->num_channels); + + size_t begin = 0, num_channels = 0; + while (begin < cc->cache.frames.count) { + ufbx_cache_frame *frame = &cc->cache.frames.data[begin]; + size_t end = begin + 1; + while (end < cc->cache.frames.count && cc->cache.frames.data[end].channel.data == frame->channel.data) { + end++; + } + + ufbx_cache_channel *chan = ufbxi_push_zero(&cc->tmp_stack, ufbx_cache_channel, 1); + ufbxi_check_err(&cc->error, chan); + + chan->name = frame->channel; + chan->interpretation_name = ufbx_empty_string; + chan->frames.data = frame; + chan->frames.count = end - begin; + + while (tmp_chan < tmp_end && ufbxi_str_less(tmp_chan->name, chan->name)) { + tmp_chan++; + } + if (tmp_chan < tmp_end && ufbxi_str_equal(tmp_chan->name, chan->name)) { + chan->interpretation_name = tmp_chan->interpretation; + } + + if (frame->file_format == UFBX_CACHE_FILE_FORMAT_PC2) { + chan->interpretation = UFBX_CACHE_INTERPRETATION_VERTEX_POSITION; + } else { + ufbxi_for(const ufbxi_cache_interpretation_name, name, ufbxi_cache_interpretation_names, ufbxi_arraycount(ufbxi_cache_interpretation_names)) { + if (ufbxi_match(&chan->interpretation_name, name->pattern)) { + chan->interpretation = name->interpretation; + break; + } + } + } + + ufbx_mirror_axis mirror_axis = UFBX_MIRROR_AXIS_NONE; + ufbx_real scale_factor = 1.0f; + if (chan->interpretation != UFBX_CACHE_INTERPRETATION_UNKNOWN) { + mirror_axis = cc->opts.mirror_axis; + if (cc->opts.use_scale_factor) { + scale_factor = cc->opts.scale_factor; + } + } + chan->mirror_axis = mirror_axis; + chan->scale_factor = scale_factor; + ufbxi_for_list(ufbx_cache_frame, f, chan->frames) { + f->mirror_axis = mirror_axis; + f->scale_factor = scale_factor; + } + + num_channels++; + begin = end; + } + + cc->cache.channels.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_cache_channel, num_channels); + ufbxi_check_err(&cc->error, cc->cache.channels.data); + cc->cache.channels.count = num_channels; + + return 1; +} + + +static ufbxi_noinline int ufbxi_cache_load_imp(ufbxi_cache_context *cc, ufbx_string filename) +{ + cc->tmp.ator = cc->ator_tmp; + cc->tmp_stack.ator = cc->ator_tmp; + + cc->channel_name.data = ufbxi_empty_char; + + if (!cc->open_file_cb.fn) { + cc->open_file_cb.fn = ufbx_default_open_file; + } + + // Make sure the filename we pass to `open_file_fn()` is NULL-terminated + char *filename_data = ufbxi_push(&cc->tmp, char, filename.length + 1); + ufbxi_check_err(&cc->error, filename_data); + memcpy(filename_data, filename.data, filename.length); + filename_data[filename.length] = '\0'; + ufbx_string filename_copy = { filename_data, filename.length }; + + // TODO: NULL termination! + bool found = false; + ufbxi_check_err(&cc->error, ufbxi_cache_try_open_file(cc, filename_copy, NULL, &found)); + if (!found) { + ufbxi_set_err_info(&cc->error, filename.data, filename.length); + ufbxi_fail_err_msg(&cc->error, "open_file_fn()", "File not found"); + } + + cc->cache.root_filename = cc->stream_filename; + + ufbxi_check_err(&cc->error, ufbxi_cache_load_frame_files(cc)); + + size_t num_frames = cc->tmp_stack.num_items; + cc->cache.frames.count = num_frames; + cc->cache.frames.data = ufbxi_push_pop(&cc->result, &cc->tmp_stack, ufbx_cache_frame, num_frames); + ufbxi_check_err(&cc->error, cc->cache.frames.data); + + ufbxi_check_err(&cc->error, ufbxi_cache_sort_frames(cc, cc->cache.frames.data, cc->cache.frames.count)); + ufbxi_check_err(&cc->error, ufbxi_cache_setup_channels(cc)); + + // Must be last allocation! + cc->imp = ufbxi_push(&cc->result, ufbxi_geometry_cache_imp, 1); + ufbxi_check_err(&cc->error, cc->imp); + + ufbxi_init_ref(&cc->imp->refcount, UFBXI_CACHE_IMP_MAGIC, NULL); + + cc->imp->cache = cc->cache; + cc->imp->magic = UFBXI_CACHE_IMP_MAGIC; + cc->imp->owned_by_scene = cc->owned_by_scene; + cc->imp->refcount.ator = cc->ator_result; + cc->imp->refcount.buf = cc->result; + cc->imp->refcount.buf.ator = &cc->imp->refcount.ator; + cc->imp->string_buf = cc->string_pool.buf; + cc->imp->string_buf.ator = &cc->imp->refcount.ator; + + return 1; +} + +ufbxi_noinline static ufbx_geometry_cache *ufbxi_cache_load(ufbxi_cache_context *cc, ufbx_string filename) +{ + int ok = ufbxi_cache_load_imp(cc, filename); + + ufbxi_buf_free(&cc->tmp); + ufbxi_buf_free(&cc->tmp_stack); + ufbxi_free(cc->ator_tmp, char, cc->name_buf, cc->name_cap); + ufbxi_free(cc->ator_tmp, char, cc->tmp_arr, cc->tmp_arr_size); + if (!cc->owned_by_scene) { + ufbxi_string_pool_temp_free(&cc->string_pool); + ufbxi_free_ator(cc->ator_tmp); + } + + if (ok) { + return &cc->imp->cache; + } else { + ufbxi_fix_error_type(&cc->error, "Failed to load geometry cache", NULL); + if (!cc->owned_by_scene) { + ufbxi_buf_free(&cc->string_pool.buf); + ufbxi_free_ator(&cc->ator_result); + } + return NULL; + } +} + +ufbxi_noinline static ufbx_geometry_cache *ufbxi_load_geometry_cache(ufbx_string filename, const ufbx_geometry_cache_opts *user_opts, ufbx_error *p_error) +{ + ufbx_geometry_cache_opts opts; // ufbxi_uninit + if (user_opts) { + opts = *user_opts; + } else { + memset(&opts, 0, sizeof(opts)); + } + + ufbxi_cache_context cc = { UFBX_ERROR_NONE }; + ufbxi_allocator ator_tmp = { 0 }; + ufbxi_init_ator(&cc.error, &ator_tmp, &opts.temp_allocator, "temp"); + ufbxi_init_ator(&cc.error, &cc.ator_result, &opts.result_allocator, "result"); + cc.ator_tmp = &ator_tmp; + + cc.opts = opts; + + cc.open_file_cb = opts.open_file_cb; + + cc.string_pool.error = &cc.error; + ufbxi_map_init(&cc.string_pool.map, cc.ator_tmp, &ufbxi_map_cmp_string, NULL); + cc.string_pool.buf.ator = &cc.ator_result; + cc.string_pool.buf.unordered = true; + cc.string_pool.initial_size = 64; + cc.result.ator = &cc.ator_result; + + cc.frames_per_second = opts.frames_per_second > 0.0 ? opts.frames_per_second : 30.0; + + ufbx_geometry_cache *cache = ufbxi_cache_load(&cc, filename); + if (p_error) { + if (cache) { + ufbxi_clear_error(p_error); + } else { + *p_error = cc.error; + } + } + return cache; +} + +static ufbxi_noinline void ufbxi_free_geometry_cache_imp(ufbxi_geometry_cache_imp *imp) +{ + ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC); + ufbxi_buf_free(&imp->string_buf); +} + +#else + +typedef struct { + ufbxi_refcount refcount; + uint32_t magic; + bool owned_by_scene; +} ufbxi_geometry_cache_imp; + +static ufbxi_noinline ufbx_geometry_cache *ufbxi_load_geometry_cache(ufbx_string filename, const ufbx_geometry_cache_opts *user_opts, ufbx_error *p_error) +{ + if (p_error) { + memset(p_error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(p_error, "UFBX_ENABLE_GEOMETRY_CACHE"); + ufbxi_report_err_msg(p_error, "UFBXI_FEATURE_GEOMETRY_CACHE", "Feature disabled"); + } + return NULL; +} + +static ufbxi_forceinline void ufbxi_free_geometry_cache_imp(ufbxi_geometry_cache_imp *imp) +{ +} + +#endif + +// -- External files + +typedef enum { + UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE, +} ufbxi_external_file_type; + +typedef struct { + ufbxi_external_file_type type; + ufbx_string filename; + ufbx_string absolute_filename; + size_t index; + void *data; + size_t data_size; +} ufbxi_external_file; + +static bool ufbxi_less_external_file(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_external_file *a = (const ufbxi_external_file*)va, *b = (const ufbxi_external_file*)vb; + if (a->type != b->type) return a->type < b->type; + int cmp = ufbxi_str_cmp(a->filename, b->filename); + if (cmp != 0) return cmp < 0; + if (a->index != b->index) return a->index < b->index; + return false; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_cache(ufbxi_context *uc, ufbxi_external_file *file) +{ +#if UFBXI_FEATURE_GEOMETRY_CACHE + ufbxi_cache_context cc = { UFBX_ERROR_NONE }; + cc.owned_by_scene = true; + + cc.open_file_cb = uc->opts.open_file_cb; + cc.frames_per_second = uc->scene.settings.frames_per_second; + + // Temporarily "borrow" allocators for the geometry cache + cc.ator_tmp = &uc->ator_tmp; + cc.string_pool = uc->string_pool; + cc.result = uc->result; + + cc.opts.mirror_axis = uc->mirror_axis; + cc.opts.use_scale_factor = true; + cc.opts.scale_factor = uc->scene.metadata.geometry_scale; + + ufbx_geometry_cache *cache = ufbxi_cache_load(&cc, file->filename); + if (!cache) { + if (cc.error.type == UFBX_ERROR_FILE_NOT_FOUND) { + memset(&cc.error, 0, sizeof(cc.error)); + cache = ufbxi_cache_load(&cc, file->absolute_filename); + } + } + + // Return the "borrowed" allocators + uc->string_pool = cc.string_pool; + uc->result = cc.result; + + if (!cache) { + if (cc.error.type == UFBX_ERROR_FILE_NOT_FOUND) { + if (uc->opts.ignore_missing_external_files) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_MISSING_EXTERNAL_FILE, "Failed to open geometry cache: %s", file->filename.data)); + return 1; + } else { + cc.error.type = UFBX_ERROR_EXTERNAL_FILE_NOT_FOUND; + cc.error.description.data = "External file not found"; + cc.error.description.length = strlen("External file not found"); + } + } + + uc->error = cc.error; + return 0; + } + + file->data = cache; + return 1; +#else + if (uc->opts.ignore_missing_external_files) return 1; + + ufbxi_fmt_err_info(&uc->error, "UFBX_ENABLE_GEOMETRY_CACHE"); + ufbxi_fail_msg("UFBXI_FEATURE_GEOMETRY_CACHE", "Feature disabled"); +#endif +} + +static ufbxi_noinline ufbxi_external_file *ufbxi_find_external_file(ufbxi_external_file *files, size_t num_files, ufbxi_external_file_type type, const char *name) +{ + size_t ix = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbxi_external_file, 32, &ix, files, 0, num_files, + ( type != a->type ? type < a->type : strcmp(a->filename.data, name) < 0 ), + ( a->type == type && a->filename.data == name )); + return ix != SIZE_MAX ? &files[ix] : NULL; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_external_files(ufbxi_context *uc) +{ + size_t num_files = 0; + + // Gather external files to deduplicate them + ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) { + ufbx_cache_file *cache = *p_cache; + if (cache->filename.length > 0) { + ufbxi_external_file *file = ufbxi_push_zero(&uc->tmp_stack, ufbxi_external_file, 1); + ufbxi_check(file); + file->index = num_files++; + file->type = UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE; + file->filename = cache->filename; + file->absolute_filename = cache->absolute_filename; + } + } + + // Sort and load the external files + ufbxi_external_file *files = ufbxi_push_pop(&uc->tmp, &uc->tmp_stack, ufbxi_external_file, num_files); + ufbxi_check(files); + ufbxi_unstable_sort(files, num_files, sizeof(ufbxi_external_file), &ufbxi_less_external_file, NULL); + + ufbxi_external_file_type prev_type = UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE; + const char *prev_name = NULL; + ufbxi_for(ufbxi_external_file, file, files, num_files) { + if (file->filename.data == prev_name && file->type == prev_type) continue; + if (file->type == UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE) { + ufbxi_check(ufbxi_load_external_cache(uc, file)); + } + prev_name = file->filename.data; + prev_type = file->type; + } + + // Patch the loaded files + ufbxi_for_ptr_list(ufbx_cache_file, p_cache, uc->scene.cache_files) { + ufbx_cache_file *cache = *p_cache; + ufbxi_external_file *file = ufbxi_find_external_file(files, num_files, + UFBXI_EXTERNAL_FILE_GEOMETRY_CACHE, cache->filename.data); + if (file && file->data) { + cache->external_cache = (ufbx_geometry_cache*)file->data; + } + } + + // Patch the geometry deformers + ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, uc->scene.cache_deformers) { + ufbx_cache_deformer *deformer = *p_deformer; + if (!deformer->file || !deformer->file->external_cache) continue; + ufbx_geometry_cache *cache = deformer->file->external_cache; + deformer->external_cache = cache; + + // HACK: It seems like channels may be connected even if the name is wrong + // and they work when exporting from Marvelous to Maya... + if (cache->channels.count == 1) { + deformer->external_channel = &cache->channels.data[0]; + } else { + ufbx_string channel = deformer->channel; + size_t ix = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_cache_channel, 16, &ix, cache->channels.data, 0, cache->channels.count, + ( ufbxi_str_less(a->name, channel) ), ( a->name.data == channel.data )); + if (ix != SIZE_MAX) { + deformer->external_channel = &cache->channels.data[ix]; + } + } + } + + return 1; +} + +static ufbxi_noinline void ufbxi_transform_to_axes(ufbxi_context *uc, ufbx_coordinate_axes dst_axes) +{ + if (!ufbx_coordinate_axes_valid(uc->scene.settings.axes)) return; + if (!ufbxi_axis_matrix(&uc->axis_matrix, uc->scene.settings.axes, dst_axes)) return; + + if (ufbx_matrix_determinant(&uc->axis_matrix) < 0.0f) { + if (uc->opts.handedness_conversion_axis != UFBX_MIRROR_AXIS_NONE) { + ufbx_mirror_axis mirror_axis = uc->opts.handedness_conversion_axis; + uc->mirror_axis = mirror_axis; + uc->scene.metadata.mirror_axis = uc->mirror_axis; + + ufbxi_mirror_matrix_dst(&uc->axis_matrix, uc->mirror_axis); + ufbxi_dev_assert(ufbx_matrix_determinant(&uc->axis_matrix) >= 0.0f); + + ufbxi_for_ptr_list(ufbx_node, p_node, uc->scene.nodes) { + ufbx_node *node = *p_node; + if (!node->is_root) { + node->adjust_mirror_axis = mirror_axis; + } + } + } + } + + if (uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT) { + ufbx_matrix axis_mat = uc->axis_matrix; + if (!ufbxi_is_transform_identity(&uc->scene.root_node->local_transform)) { + ufbx_matrix root_mat = ufbx_transform_to_matrix(&uc->scene.root_node->local_transform); + axis_mat = ufbx_matrix_mul(&root_mat, &axis_mat); + } + + ufbxi_mirror_matrix(&axis_mat, uc->mirror_axis); + + uc->scene.root_node->local_transform = ufbx_matrix_to_transform(&axis_mat); + uc->scene.root_node->node_to_parent = axis_mat; + } +} + +static ufbxi_noinline int ufbxi_scale_units(ufbxi_context *uc, ufbx_real target_meters) +{ + if (uc->scene.settings.unit_meters <= 0.0f) return 1; + target_meters = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), target_meters); + + ufbx_real ratio = uc->scene.settings.unit_meters / target_meters; + ratio = ufbxi_round_if_near(ufbxi_pow10_targets, ufbxi_arraycount(ufbxi_pow10_targets), ratio); + if (ratio == 1.0f) return 1; + + uc->unit_scale = ratio; + + if (uc->opts.space_conversion == UFBX_SPACE_CONVERSION_TRANSFORM_ROOT) { + uc->scene.root_node->local_transform.scale.x *= ratio; + uc->scene.root_node->local_transform.scale.y *= ratio; + uc->scene.root_node->local_transform.scale.z *= ratio; + uc->scene.root_node->node_to_parent.m00 *= ratio; + uc->scene.root_node->node_to_parent.m01 *= ratio; + uc->scene.root_node->node_to_parent.m02 *= ratio; + uc->scene.root_node->node_to_parent.m10 *= ratio; + uc->scene.root_node->node_to_parent.m11 *= ratio; + uc->scene.root_node->node_to_parent.m12 *= ratio; + uc->scene.root_node->node_to_parent.m20 *= ratio; + uc->scene.root_node->node_to_parent.m21 *= ratio; + uc->scene.root_node->node_to_parent.m22 *= ratio; + } + + return 1; +} + +// -- Curve evaluation + +static ufbxi_forceinline double ufbxi_find_cubic_bezier_t(double p1, double p2, double x0) +{ + double p1_3 = p1 * 3.0, p2_3 = p2 * 3.0; + double a = p1_3 - p2_3 + 1.0; + double b = p2_3 - p1_3 - p1_3; + double c = p1_3; + + double a_3 = 3.0*a, b_2 = 2.0*b; + double t = x0; + double x1, t2, t3; + + // Manually unroll three iterations of Newton-Raphson, this is enough + // for most tangents + t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0; + t -= x1 / (a_3*t2 + b_2*t + c); + + t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0; + t -= x1 / (a_3*t2 + b_2*t + c); + + t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0; + t -= x1 / (a_3*t2 + b_2*t + c); + + // 4 ULP from 1.0 + const double eps = 8.881784197001252e-16; + if (ufbx_fabs(x1) <= eps) return t; + + // Perform more iterations until we reach desired accuracy + for (size_t i = 0; i < 4; i++) { + + t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0; + t -= x1 / (a_3*t2 + b_2*t + c); + + t2 = t*t; t3 = t2*t; x1 = a*t3 + b*t2 + c*t - x0; + t -= x1 / (a_3*t2 + b_2*t + c); + + if (ufbx_fabs(x1) <= eps) return t; + } + + return t; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_skinning(ufbx_scene *scene, ufbx_error *error, ufbxi_buf *buf_result, ufbxi_buf *buf_tmp, + double time, bool load_caches, ufbx_geometry_cache_data_opts *cache_opts) +{ +#if UFBXI_FEATURE_SKINNING_EVALUATION + size_t max_skinned_indices = 0; + + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, scene->meshes) { + ufbx_mesh *mesh = *p_mesh; + if (mesh->blend_deformers.count == 0 && mesh->skin_deformers.count == 0 && (mesh->cache_deformers.count == 0 || !load_caches)) continue; + max_skinned_indices = ufbxi_max_sz(max_skinned_indices, mesh->num_indices); + } + + ufbx_topo_edge *topo = ufbxi_push(buf_tmp, ufbx_topo_edge, max_skinned_indices); + ufbxi_check_err(error, topo); + + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, scene->meshes) { + ufbx_mesh *mesh = *p_mesh; + if (mesh->blend_deformers.count == 0 && mesh->skin_deformers.count == 0 && (mesh->cache_deformers.count == 0 || !load_caches)) continue; + if (mesh->num_vertices == 0) continue; + + size_t num_vertices = mesh->num_vertices; + ufbx_vec3 *result_pos = ufbxi_push(buf_result, ufbx_vec3, num_vertices + 1); + ufbxi_check_err(error, result_pos); + + result_pos[0] = ufbx_zero_vec3; + result_pos++; + + bool cached_position = false, cached_normals = false; + if (load_caches && mesh->cache_deformers.count > 0) { + ufbxi_for_ptr_list(ufbx_cache_deformer, p_cache, mesh->cache_deformers) { + ufbx_cache_channel *channel = (*p_cache)->external_channel; + if (!channel) continue; + + if ((channel->interpretation == UFBX_CACHE_INTERPRETATION_VERTEX_POSITION || channel->interpretation == UFBX_CACHE_INTERPRETATION_POINTS) && !cached_position) { + size_t num_read = ufbx_sample_geometry_cache_vec3(channel, time, result_pos, num_vertices, cache_opts); + if (num_read == num_vertices) { + mesh->skinned_is_local = true; + cached_position = true; + } + } else if (channel->interpretation == UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL && !cached_normals) { + // TODO: Is this right at all? + size_t num_normals = mesh->skinned_normal.values.count; + ufbx_vec3 *normal_data = ufbxi_push(buf_result, ufbx_vec3, num_normals + 1); + ufbxi_check_err(error, normal_data); + normal_data[0] = ufbx_zero_vec3; + normal_data++; + + size_t num_read = ufbx_sample_geometry_cache_vec3(channel, time, normal_data, num_normals, cache_opts); + if (num_read == num_normals) { + cached_normals = true; + mesh->skinned_normal.values.data = normal_data; + } + } + } + } + + if (!cached_position) { + memcpy(result_pos, mesh->vertices.data, num_vertices * sizeof(ufbx_vec3)); + + ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, mesh->blend_deformers) { + ufbx_add_blend_vertex_offsets(*p_blend, result_pos, num_vertices, 1.0f); + } + + // TODO: What should we do about multiple skins?? + if (mesh->skin_deformers.count > 0) { + ufbx_matrix *fallback = mesh->instances.count > 0 ? &mesh->instances.data[0]->geometry_to_world : NULL; + ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; + for (size_t i = 0; i < num_vertices; i++) { + ufbx_matrix mat = ufbx_get_skin_vertex_matrix(skin, i, fallback); + result_pos[i] = ufbx_transform_position(&mat, result_pos[i]); + } + + mesh->skinned_is_local = false; + } + } + + mesh->skinned_position.values.data = result_pos; + + if (!cached_normals) { + size_t num_indices = mesh->num_indices; + uint32_t *normal_indices = ufbxi_push(buf_result, uint32_t, num_indices); + ufbxi_check_err(error, normal_indices); + + ufbx_compute_topology(mesh, topo, num_indices); + size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, num_indices, normal_indices, num_indices, false); + + if (num_normals == mesh->num_vertices) { + mesh->skinned_normal.unique_per_vertex = true; + } + + ufbx_vec3 *normal_data = ufbxi_push(buf_result, ufbx_vec3, num_normals + 1); + ufbxi_check_err(error, normal_data); + + normal_data[0] = ufbx_zero_vec3; + normal_data++; + + ufbx_compute_normals(mesh, &mesh->skinned_position, normal_indices, num_indices, normal_data, num_normals); + + mesh->generated_normals = true; + mesh->skinned_normal.exists = true; + mesh->skinned_normal.values.data = normal_data; + mesh->skinned_normal.values.count = num_normals; + mesh->skinned_normal.indices.data = normal_indices; + mesh->skinned_normal.indices.count = num_indices; + mesh->skinned_normal.value_reals = 3; + } + } + + return 1; +#else + ufbxi_fmt_err_info(error, "UFBX_ENABLE_SKINNING_EVALUATION"); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_SKINNING_EVALUATION", "Feature disabled"); + return 0; +#endif +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_fixup_opts_string(ufbxi_context *uc, ufbx_string *str, bool push) +{ + if (str->length > 0) { + if (str->length == SIZE_MAX) { + str->length = str->data ? strlen(str->data) : 0; + } + if (push) { + ufbxi_check(ufbxi_push_string_place_str(&uc->string_pool, str, false)); + } + } else { + str->data = ufbxi_empty_char; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_resolve_warning_elements(ufbxi_context *uc) +{ + size_t num_elements = uc->tmp_element_id.num_items; + uint32_t *element_ids = ufbxi_push_pop(&uc->tmp, &uc->tmp_element_id, uint32_t, num_elements); + ufbxi_check(element_ids); + + ufbxi_for_list(ufbx_warning, warning, uc->scene.metadata.warnings) { + uint32_t element_id = warning->element_id; + // Decode `element_id`, see HACK(warning-element) in `ufbxi_vwarnf_imp()` for the encoding. + if ((element_id & 0x80000000u) != 0 && element_id != ~0u) { + warning->element_id = element_ids[element_id & ~0x80000000u]; + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_load_imp(ufbxi_context *uc) +{ + // Check for deferred failure + if (uc->deferred_failure) return 0; + if (uc->deferred_load) { + ufbx_stream stream = { 0 }; + ufbx_open_file_opts opts = { 0 }; + const char *filename = uc->load_filename; + size_t filename_len = uc->load_filename_len; + bool ok = false; + if (filename_len == SIZE_MAX) { + opts.filename_null_terminated = true; + filename_len = strlen(filename); + } + if (uc->opts.filename.length == 0 || uc->opts.filename.data == NULL) { + uc->opts.filename.data = filename; + uc->opts.filename.length = filename_len; + } + ufbx_error error; + error.type = UFBX_ERROR_NONE; + if (uc->opts.open_main_file_with_default || uc->opts.open_file_cb.fn == &ufbx_default_open_file) { + ufbx_open_file_context ctx = (ufbx_open_file_context)&uc->ator_tmp; + ok = ufbx_open_file_ctx(&stream, ctx, filename, filename_len, &opts, &error); + } else { + ok = ufbxi_open_file(&uc->opts.open_file_cb, &stream, uc->load_filename, filename_len, NULL, &uc->ator_tmp, UFBX_OPEN_FILE_MAIN_MODEL); + } + if (!ok) { + if (error.type != UFBX_ERROR_NONE) { + // cppcheck-suppress uninitStructMember + uc->error = error; + } else { + ufbxi_set_err_info(&uc->error, filename, filename_len); + } + ufbxi_fail_msg("open_file_fn()", "File not found"); + } + uc->read_fn = stream.read_fn; + uc->skip_fn = stream.skip_fn; + uc->size_fn = stream.size_fn; + uc->close_fn = stream.close_fn; + uc->read_user = stream.user; + } + + if (uc->opts.progress_cb.fn && uc->progress_bytes_total == 0 && uc->size_fn) { + uint64_t total = uc->size_fn(uc->read_user); + ufbxi_check(total != UINT64_MAX); + uc->progress_bytes_total = total; + } + + ufbxi_check(uc->opts.path_separator >= 0x20 && uc->opts.path_separator <= 0x7e); + + ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.filename, false)); + ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.obj_mtl_path, true)); + ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.geometry_transform_helper_name, true)); + ufbxi_check(ufbxi_fixup_opts_string(uc, &uc->opts.scale_helper_name, true)); + + ufbxi_check(ufbxi_thread_pool_init(&uc->thread_pool, &uc->error, &uc->ator_tmp, &uc->opts.thread_opts)); + + if (!uc->opts.allow_unsafe) { + ufbxi_check_msg(uc->opts.index_error_handling != UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE, "Unsafe options"); + ufbxi_check_msg(uc->opts.unicode_error_handling != UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE, "Unsafe options"); + } else { + uc->scene.metadata.is_unsafe = true; + } + + if (uc->opts.index_error_handling == UFBX_INDEX_ERROR_HANDLING_NO_INDEX) { + uc->scene.metadata.may_contain_no_index = true; + } + + uc->retain_mesh_parts = !uc->opts.ignore_geometry && !uc->opts.skip_mesh_parts; + uc->scene.metadata.may_contain_missing_vertex_position = uc->opts.allow_missing_vertex_position; + uc->scene.metadata.may_contain_broken_elements = uc->opts.connect_broken_elements; + + uc->scene.metadata.creator.data = ufbxi_empty_char; + + uc->unit_scale = 1.0f; + if (uc->data == NULL) { + ufbxi_dev_assert(uc->data_begin == NULL); + uc->data_begin = uc->data = ufbxi_zero_size_buffer; + } + + uc->retain_vertex_w = (uc->opts.retain_dom || uc->opts.retain_vertex_attrib_w) && !uc->opts.ignore_geometry; + + ufbxi_check(ufbxi_load_strings(uc)); + ufbxi_check(ufbxi_load_maps(uc)); + ufbxi_check(ufbxi_determine_format(uc)); + + ufbx_file_format format = uc->scene.metadata.file_format; + + if (format == UFBX_FILE_FORMAT_FBX) { + ufbxi_check(ufbxi_begin_parse(uc)); + if (uc->version < 6000) { + ufbxi_check(ufbxi_read_legacy_root(uc)); + } else { + ufbxi_check(ufbxi_read_root(uc)); + } + if (!ufbxi_supports_version(uc->version)) { + ufbxi_check(ufbxi_warnf(UFBX_WARNING_UNSUPPORTED_VERSION, "Unsupported FBX version (%u)", uc->version)); + } + ufbxi_update_scene_metadata(&uc->scene.metadata); + ufbxi_check(ufbxi_init_file_paths(uc)); + } else if (format == UFBX_FILE_FORMAT_OBJ) { + ufbxi_check(ufbxi_obj_load(uc)); + ufbxi_update_scene_metadata(&uc->scene.metadata); + } else if (format == UFBX_FILE_FORMAT_MTL) { + ufbxi_check(ufbxi_mtl_load(uc)); + ufbxi_update_scene_metadata(&uc->scene.metadata); + } + + // Fake DOM root if necessary + if (uc->opts.retain_dom && !uc->scene.dom_root) { + ufbx_dom_node *dom_root = ufbxi_push_zero(&uc->result, ufbx_dom_node, 1); + ufbxi_check(dom_root); + dom_root->name.data = ufbxi_empty_char; + uc->scene.dom_root = dom_root; + } + + ufbxi_check(ufbxi_pre_finalize_scene(uc)); + + // We can free `tmp_parse` already here as all parsing is done by now. + ufbxi_buf_free(&uc->tmp_parse); + + ufbxi_check(ufbxi_finalize_scene(uc)); + + ufbxi_update_scene_settings(&uc->scene.settings); + if (uc->scene.metadata.file_format == UFBX_FILE_FORMAT_OBJ) { + ufbxi_update_scene_settings_obj(uc); + } + + // Axis conversion + if (ufbx_coordinate_axes_valid(uc->opts.target_axes)) { + ufbxi_transform_to_axes(uc, uc->opts.target_axes); + } + + // Unit conversion + if (uc->opts.target_unit_meters > 0.0f) { + ufbxi_check(ufbxi_scale_units(uc, uc->opts.target_unit_meters)); + } + + // TODO: This could be done in evaluate as well with refactoring + ufbxi_update_adjust_transforms(uc, &uc->scene); + + ufbxi_check(ufbxi_modify_geometry(uc)); + ufbxi_postprocess_scene(uc); + + ufbxi_update_scene(&uc->scene, true, NULL, 0); + + // Force a non-NULL anim pointer + if (!uc->scene.anim) { + uc->scene.anim = ufbxi_push_zero(&uc->result, ufbx_anim, 1); + } + + if (uc->opts.load_external_files) { + ufbxi_check(ufbxi_load_external_files(uc)); + } + + // Evaluate skinning if requested + if (uc->opts.evaluate_skinning) { + ufbx_geometry_cache_data_opts cache_opts = { 0 }; + cache_opts.open_file_cb = uc->opts.open_file_cb; + ufbxi_check(ufbxi_evaluate_skinning(&uc->scene, &uc->error, &uc->result, &uc->tmp, + 0.0, uc->opts.load_external_files && uc->opts.evaluate_caches, &cache_opts)); + } + + // Pop warnings to metadata + ufbxi_check(ufbxi_pop_warnings(&uc->warnings, &uc->scene.metadata.warnings, uc->scene.metadata.has_warning)); + ufbxi_check(ufbxi_resolve_warning_elements(uc)); + + // Copy local data to the scene + uc->scene.metadata.version = uc->version; + uc->scene.metadata.ascii = uc->from_ascii; + uc->scene.metadata.big_endian = uc->file_big_endian; + uc->scene.metadata.geometry_ignored = uc->opts.ignore_geometry; + uc->scene.metadata.animation_ignored = uc->opts.ignore_animation; + uc->scene.metadata.embedded_ignored = uc->opts.ignore_embedded; + + // Retain the scene, this must be the final allocation as we copy + // `ator_result` to `ufbx_scene_imp`. + ufbxi_scene_imp *imp = ufbxi_push(&uc->result, ufbxi_scene_imp, 1); + ufbxi_check(imp); + + ufbxi_init_ref(&imp->refcount, UFBXI_SCENE_IMP_MAGIC, NULL); + + imp->magic = UFBXI_SCENE_IMP_MAGIC; + imp->scene = uc->scene; + imp->refcount.ator = uc->ator_result; + imp->refcount.ator.error = NULL; + + // Copy retained buffers and translate the allocator struct to the one + // contained within `ufbxi_scene_imp` + imp->refcount.buf = uc->result; + imp->refcount.buf.ator = &imp->refcount.ator; + imp->string_buf = uc->string_pool.buf; + imp->string_buf.ator = &imp->refcount.ator; + + imp->scene.metadata.result_memory_used = imp->refcount.ator.current_size; + imp->scene.metadata.temp_memory_used = uc->ator_tmp.current_size; + imp->scene.metadata.result_allocs = imp->refcount.ator.num_allocs; + imp->scene.metadata.temp_allocs = uc->ator_tmp.num_allocs; + + ufbxi_for_ptr_list(ufbx_element, p_elem, imp->scene.elements) { + (*p_elem)->scene = &imp->scene; + } + + uc->scene_imp = imp; + + return 1; +} + +static ufbxi_noinline void ufbxi_free_temp(ufbxi_context *uc) +{ + ufbxi_thread_pool_free(&uc->thread_pool); + + ufbxi_string_pool_temp_free(&uc->string_pool); + ufbxi_buf_free(&uc->warnings.tmp_stack); + + ufbxi_map_free(&uc->prop_type_map); + ufbxi_map_free(&uc->fbx_id_map); + ufbxi_map_free(&uc->ptr_fbx_id_map); + ufbxi_map_free(&uc->texture_file_map); + ufbxi_map_free(&uc->anim_stack_map); + ufbxi_map_free(&uc->fbx_attr_map); + ufbxi_map_free(&uc->node_prop_set); + ufbxi_map_free(&uc->dom_node_map); + + ufbxi_buf_free(&uc->tmp); + ufbxi_buf_free(&uc->tmp_parse); + for (size_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) { + ufbxi_buf_free(&uc->tmp_thread_parse[i]); + } + ufbxi_buf_free(&uc->tmp_stack); + ufbxi_buf_free(&uc->tmp_connections); + ufbxi_buf_free(&uc->tmp_node_ids); + ufbxi_buf_free(&uc->tmp_elements); + ufbxi_buf_free(&uc->tmp_element_offsets); + ufbxi_buf_free(&uc->tmp_element_fbx_ids); + ufbxi_buf_free(&uc->tmp_element_ptrs); + for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) { + ufbxi_buf_free(&uc->tmp_typed_element_offsets[i]); + } + ufbxi_buf_free(&uc->tmp_mesh_textures); + ufbxi_buf_free(&uc->tmp_full_weights); + ufbxi_buf_free(&uc->tmp_dom_nodes); + ufbxi_buf_free(&uc->tmp_element_id); + ufbxi_buf_free(&uc->tmp_ascii_spans); + + ufbxi_free(&uc->ator_tmp, ufbxi_node, uc->top_nodes, uc->top_nodes_cap); + ufbxi_free(&uc->ator_tmp, void*, uc->element_extra_arr, uc->element_extra_cap); + + ufbxi_free(&uc->ator_tmp, char, uc->ascii.token.str_data, uc->ascii.token.str_cap); + ufbxi_free(&uc->ator_tmp, char, uc->ascii.prev_token.str_data, uc->ascii.prev_token.str_cap); + + ufbxi_free(&uc->ator_tmp, char, uc->read_buffer, uc->read_buffer_size); + ufbxi_free(&uc->ator_tmp, char, uc->tmp_arr, uc->tmp_arr_size); + ufbxi_free(&uc->ator_tmp, char, uc->swap_arr, uc->swap_arr_size); + + ufbxi_obj_free(uc); + + ufbxi_free_ator(&uc->ator_tmp); +} + +static ufbxi_noinline void ufbxi_free_result(ufbxi_context *uc) +{ + ufbxi_buf_free(&uc->result); + ufbxi_buf_free(&uc->string_pool.buf); + + ufbxi_free_ator(&uc->ator_result); +} + +static ufbxi_noinline ufbx_scene *ufbxi_load(ufbxi_context *uc, const ufbx_load_opts *user_opts, ufbx_error *p_error) +{ + // Test endianness + { + uint8_t buf[2]; + uint16_t val = 0xbbaa; + memcpy(buf, &val, 2); + uc->local_big_endian = buf[0] == 0xbb; + } + + uc->double_parse_flags = ufbxi_parse_double_init_flags(); + + if (user_opts) { + uc->opts = *user_opts; + } else { + memset(&uc->opts, 0, sizeof(uc->opts)); + } + + if (uc->opts.file_size_estimate) { + uc->progress_bytes_total = uc->opts.file_size_estimate; + } + + if (uc->opts.ignore_all_content) { + uc->opts.ignore_geometry = true; + uc->opts.ignore_animation = true; + uc->opts.ignore_embedded = true; + } + + ufbx_inflate_retain inflate_retain; + inflate_retain.initialized = false; + + ufbxi_init_ator(&uc->error, &uc->ator_tmp, &uc->opts.temp_allocator, "temp"); + ufbxi_init_ator(&uc->error, &uc->ator_result, &uc->opts.result_allocator, "result"); + + if (uc->opts.read_buffer_size == 0) { + uc->opts.read_buffer_size = 0x4000; + } + if (uc->opts.read_buffer_size <= 32) { + uc->opts.read_buffer_size = 32; + } + + if (uc->opts.file_format_lookahead == 0) { + uc->opts.file_format_lookahead = 0x4000; + } else if (uc->opts.file_format_lookahead < UFBXI_MIN_FILE_FORMAT_LOOKAHEAD) { + uc->opts.file_format_lookahead = UFBXI_MIN_FILE_FORMAT_LOOKAHEAD; + } + + if (!uc->opts.path_separator) { + uc->opts.path_separator = UFBX_PATH_SEPARATOR; + } + + if (!uc->opts.progress_cb.fn || uc->opts.progress_interval_hint >= SIZE_MAX) { + uc->progress_interval = SIZE_MAX; + } else if (uc->opts.progress_interval_hint > 0) { + uc->progress_interval = (size_t)uc->opts.progress_interval_hint; + } else { + uc->progress_interval = 0x4000; + } + + if (!uc->opts.open_file_cb.fn) { + uc->opts.open_file_cb.fn = &ufbx_default_open_file; + } + + if (!uc->opts.thread_opts.memory_limit) { + uc->opts.thread_opts.memory_limit = 32*1024*1024; + } + + uc->synthetic_id_counter = UFBXI_SYNTHETIC_ID_START; + + uc->string_pool.error = &uc->error; + ufbxi_map_init(&uc->string_pool.map, &uc->ator_tmp, &ufbxi_map_cmp_string, NULL); + uc->string_pool.buf.ator = &uc->ator_result; + uc->string_pool.buf.unordered = true; + uc->string_pool.initial_size = 1024; + uc->string_pool.error_handling = uc->opts.unicode_error_handling; + + ufbxi_map_init(&uc->prop_type_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL); + ufbxi_map_init(&uc->fbx_id_map, &uc->ator_tmp, &ufbxi_map_cmp_uint64, NULL); + ufbxi_map_init(&uc->ptr_fbx_id_map, &uc->ator_tmp, &ufbxi_map_cmp_ptr_id, NULL); + ufbxi_map_init(&uc->texture_file_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL); + ufbxi_map_init(&uc->anim_stack_map, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL); + ufbxi_map_init(&uc->fbx_attr_map, &uc->ator_tmp, &ufbxi_map_cmp_uint64, NULL); + ufbxi_map_init(&uc->node_prop_set, &uc->ator_tmp, &ufbxi_map_cmp_const_char_ptr, NULL); + ufbxi_map_init(&uc->dom_node_map, &uc->ator_tmp, &ufbxi_map_cmp_uintptr, NULL); + + uc->tmp.ator = &uc->ator_tmp; + uc->tmp_parse.ator = &uc->ator_tmp; + uc->tmp_stack.ator = &uc->ator_tmp; + uc->tmp_connections.ator = &uc->ator_tmp; + uc->tmp_node_ids.ator = &uc->ator_tmp; + uc->tmp_elements.ator = &uc->ator_tmp; + uc->tmp_element_offsets.ator = &uc->ator_tmp; + uc->tmp_element_fbx_ids.ator = &uc->ator_tmp; + uc->tmp_element_ptrs.ator = &uc->ator_tmp; + for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) { + uc->tmp_typed_element_offsets[i].ator = &uc->ator_tmp; + } + uc->tmp_mesh_textures.ator = &uc->ator_tmp; + uc->tmp_full_weights.ator = &uc->ator_tmp; + uc->tmp_dom_nodes.ator = &uc->ator_tmp; + uc->tmp_element_id.ator = &uc->ator_tmp; + uc->tmp_ascii_spans.ator = &uc->ator_tmp; + + for (size_t i = 0; i < UFBX_THREAD_GROUP_COUNT; i++) { + uc->tmp_thread_parse[i].ator = &uc->ator_tmp; + uc->tmp_thread_parse[i].unordered = true; + uc->tmp_thread_parse[i].clearable = true; + } + + uc->result.ator = &uc->ator_result; + + uc->tmp.unordered = true; + uc->tmp_parse.unordered = true; + uc->tmp_parse.clearable = true; + uc->result.unordered = true; + + uc->warnings.error = &uc->error; + uc->warnings.result = &uc->result; + uc->warnings.tmp_stack.ator = &uc->ator_tmp; + uc->string_pool.warnings = &uc->warnings; + + // Set zero size `swap_arr` to a non-NULL buffer so we can tell the difference between empty + // array and an allocation failure. + uc->swap_arr = (char*)ufbxi_zero_size_buffer; + + // NOTE: Though `inflate_retain` leaks out of the scope we don't use it outside this function. + // cppcheck-suppress autoVariables + uc->inflate_retain = &inflate_retain; + + int ok = ufbxi_load_imp(uc); + + if (uc->close_fn) { + uc->close_fn(uc->read_user); + } + + ufbxi_free_temp(uc); + + if (ok) { + if (p_error) { + ufbxi_clear_error(p_error); + } + return &uc->scene_imp->scene; + } else { + ufbxi_fix_error_type(&uc->error, "Failed to load", p_error); + if (p_error && p_error->type == UFBX_ERROR_UNKNOWN && uc->scene.metadata.file_format == UFBX_FILE_FORMAT_FBX && !ufbxi_supports_version(uc->version)) { + p_error->description.data = "Unsupported version"; + p_error->description.length = strlen("Unsupported version"); + p_error->type = UFBX_ERROR_UNSUPPORTED_VERSION; + ufbxi_fmt_err_info(p_error, "%u", uc->version); + } + ufbxi_free_result(uc); + return NULL; + } +} + +// -- Animation evaluation + +static ufbxi_forceinline bool ufbxi_override_less_than_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop) +{ + if (over->element_id != element_id) return over->element_id < element_id; + if (over->_internal_key != prop->_internal_key) return over->_internal_key < prop->_internal_key; + return strcmp(over->prop_name.data, prop->name.data); +} + +static ufbxi_forceinline bool ufbxi_override_equals_to_prop(const ufbx_prop_override *over, uint32_t element_id, const ufbx_prop *prop) +{ + if (over->element_id != element_id) return false; + if (over->_internal_key != prop->_internal_key) return false; + return !strcmp(over->prop_name.data, prop->name.data); +} + +static ufbxi_noinline bool ufbxi_find_prop_override(const ufbx_prop_override_list *overrides, uint32_t element_id, ufbx_prop *prop) +{ + size_t ix = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_prop_override, 16, &ix, overrides->data, 0, overrides->count, + ( ufbxi_override_less_than_prop(a, element_id, prop) ), + ( ufbxi_override_equals_to_prop(a, element_id, prop) )); + + if (ix != SIZE_MAX) { + const ufbx_prop_override *over = &overrides->data[ix]; + const uint32_t clear_flags = UFBX_PROP_FLAG_NO_VALUE | UFBX_PROP_FLAG_NOT_FOUND; + prop->flags = (ufbx_prop_flags)(((uint32_t)prop->flags & ~clear_flags) | UFBX_PROP_FLAG_OVERRIDDEN); + prop->value_vec4 = over->value; + prop->value_real_arr[3] = 0.0f; + prop->value_int = over->value_int; + prop->value_str = over->value_str; + prop->value_blob.data = prop->value_str.data; + prop->value_blob.size = prop->value_str.length; + return true; + } else { + return false; + } +} + +static ufbxi_noinline ufbx_prop_override_list ufbxi_find_element_prop_overrides(const ufbx_prop_override_list *overrides, uint32_t element_id) +{ + size_t begin = overrides->count, end = begin; + + ufbxi_macro_lower_bound_eq(ufbx_prop_override, 32, &begin, overrides->data, 0, overrides->count, + (a->element_id < element_id), + (a->element_id == element_id)); + + ufbxi_macro_upper_bound_eq(ufbx_prop_override, 32, &end, overrides->data, begin, overrides->count, + (a->element_id == element_id)); + + ufbx_prop_override_list result = { overrides->data + begin, end - begin }; + return result; +} + +typedef struct ufbxi_anim_layer_combine_ctx { + const ufbx_anim *anim; + const ufbx_element *element; + double time; + ufbx_rotation_order rotation_order; + bool has_rotation_order; +} ufbxi_anim_layer_combine_ctx; + +static ufbxi_noinline double ufbxi_pow_abs(double v, double e) +{ + if (e <= 0.0) return 1.0; + if (e >= 1.0) return v; + double sign = v < 0.0 ? -1.0 : 1.0; + return sign * ufbx_pow(v * sign, e); +} + +// Recursion is limited by the fact that we recurse only when the property name is "Lcl Rotation" +// and when recursing we always evaluate the property "RotationOrder" +static ufbxi_noinline void ufbxi_combine_anim_layer(ufbxi_anim_layer_combine_ctx *ctx, ufbx_anim_layer *layer, ufbx_real weight, const char *prop_name, ufbx_vec3 *result, const ufbx_vec3 *value) + ufbxi_recursive_function_void(ufbxi_combine_anim_layer, (ctx, layer, weight, prop_name, result, value), 2, + (ufbxi_anim_layer_combine_ctx *ctx, ufbx_anim_layer *layer, ufbx_real weight, const char *prop_name, ufbx_vec3 *result, const ufbx_vec3 *value)) +{ + if (layer->compose_rotation && layer->blended && prop_name == ufbxi_Lcl_Rotation && !ctx->has_rotation_order) { + ufbx_prop rp = ufbx_evaluate_prop_len(ctx->anim, ctx->element, ufbxi_RotationOrder, sizeof(ufbxi_RotationOrder) - 1, ctx->time); + // NOTE: Defaults to 0 (UFBX_ROTATION_XYZ) gracefully if property is not found + if (rp.value_int >= 0 && rp.value_int <= UFBX_ROTATION_ORDER_SPHERIC) { + ctx->rotation_order = (ufbx_rotation_order)rp.value_int; + } else { + ctx->rotation_order = UFBX_ROTATION_ORDER_XYZ; + } + ctx->has_rotation_order = true; + } + + if (layer->additive) { + if (layer->compose_scale && prop_name == ufbxi_Lcl_Scaling) { + result->x *= (ufbx_real)ufbxi_pow_abs(value->x, weight); + result->y *= (ufbx_real)ufbxi_pow_abs(value->y, weight); + result->z *= (ufbx_real)ufbxi_pow_abs(value->z, weight); + } else if (layer->compose_rotation && prop_name == ufbxi_Lcl_Rotation) { + ufbx_quat a = ufbx_euler_to_quat(*result, ctx->rotation_order); + ufbx_quat b = ufbx_euler_to_quat(*value, ctx->rotation_order); + b = ufbx_quat_slerp(ufbx_identity_quat, b, weight); + ufbx_quat res = ufbxi_mul_quat(a, b); + *result = ufbx_quat_to_euler(res, ctx->rotation_order); + } else { + result->x += value->x * weight; + result->y += value->y * weight; + result->z += value->z * weight; + } + } else if (layer->blended) { + ufbx_real res_weight = 1.0f - weight; + if (layer->compose_scale && prop_name == ufbxi_Lcl_Scaling) { + result->x = (ufbx_real)(ufbxi_pow_abs(result->x, res_weight) * ufbxi_pow_abs(value->x, weight)); + result->y = (ufbx_real)(ufbxi_pow_abs(result->y, res_weight) * ufbxi_pow_abs(value->y, weight)); + result->z = (ufbx_real)(ufbxi_pow_abs(result->z, res_weight) * ufbxi_pow_abs(value->z, weight)); + } else if (layer->compose_rotation && prop_name == ufbxi_Lcl_Rotation) { + ufbx_quat a = ufbx_euler_to_quat(*result, ctx->rotation_order); + ufbx_quat b = ufbx_euler_to_quat(*value, ctx->rotation_order); + ufbx_quat res = ufbx_quat_slerp(a, b, weight); + *result = ufbx_quat_to_euler(res, ctx->rotation_order); + } else { + result->x = result->x * res_weight + value->x * weight; + result->y = result->y * res_weight + value->y * weight; + result->z = result->z * res_weight + value->z * weight; + } + } else { + *result = *value; + } +} + +static ufbxi_forceinline bool ufbxi_anim_layer_might_contain_id(const ufbx_anim_layer *layer, uint32_t id) +{ + uint32_t id_mask = ufbxi_arraycount(layer->_element_id_bitmask) - 1; + bool ok = id - layer->_min_element_id <= (layer->_max_element_id - layer->_min_element_id); + ok &= (layer->_element_id_bitmask[(id >> 5) & id_mask] & (1u << (id & 31))) != 0; + return ok; +} + +static ufbxi_noinline void ufbxi_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, size_t num_props, uint32_t flags) +{ + ufbxi_anim_layer_combine_ctx combine_ctx = { anim, element, time }; + + uint32_t element_id = element->element_id; + size_t num_layers = anim->layers.count; + for (size_t layer_ix = 0; layer_ix < num_layers; layer_ix++) { + ufbx_anim_layer *layer = anim->layers.data[layer_ix]; + if (!ufbxi_anim_layer_might_contain_id(layer, element_id)) continue; + + // Find the weight for the current layer + // TODO: Should this be searched from multiple layers? + ufbx_real weight = layer_ix < anim->override_layer_weights.count ? anim->override_layer_weights.data[layer_ix] : layer->weight; + if (layer->weight_is_animated && layer->blended) { + ufbx_anim_prop *weight_aprop = ufbxi_find_anim_prop_start(layer, &layer->element); + if (weight_aprop) { + weight = ufbx_evaluate_anim_value_real_flags(weight_aprop->anim_value, time, flags) / (ufbx_real)100.0; + if (weight < 0.0f) weight = 0.0f; + if (weight > 0.99999f) weight = 1.0f; + } + } + + ufbx_anim_prop *aprop = ufbxi_find_anim_prop_start(layer, element); + if (!aprop) continue; + + for (size_t i = 0; i < num_props; i++) { + ufbx_prop *prop = &props[i]; + + // Don't evaluate on top of overridden properties + if ((prop->flags & UFBX_PROP_FLAG_OVERRIDDEN) != 0) continue; + + // Connections override animation by default + if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) continue; + + // Skip until we reach `aprop >= prop` + // NOTE: No need to check for end as `anim_props` is terminated with a NULL sentinel. + while (aprop->element == element && aprop->_internal_key < prop->_internal_key) aprop++; + if (aprop->prop_name.data != prop->name.data) { + while (aprop->element == element && strcmp(aprop->prop_name.data, prop->name.data) < 0) aprop++; + } + + // TODO: Should we skip the blending for the first layer _per property_ + // This could be done by having `UFBX_PROP_FLAG_ANIMATION_EVALUATED` + // that gets set for the first layer of animation that is applied. + if (aprop->prop_name.data == prop->name.data) { + ufbx_vec3 v = ufbx_evaluate_anim_value_vec3_flags(aprop->anim_value, time, flags); + if (layer_ix == 0) { + prop->value_vec3 = v; + } else { + ufbxi_combine_anim_layer(&combine_ctx, layer, weight, prop->name.data, &prop->value_vec3, &v); + } + } + } + } + + ufbxi_for(ufbx_prop, prop, props, num_props) { + if (prop->flags & UFBX_PROP_FLAG_OVERRIDDEN) continue; + prop->value_int = ufbxi_f64_to_i64(prop->value_real); + } +} + +// Recursion limited by not calling `ufbx_evaluate_prop_len()` with a connected property, +// meaning it will never call `ufbxi_evaluate_connected_prop()` again indirectly. +static ufbxi_noinline void ufbxi_evaluate_connected_prop(ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags) + ufbxi_recursive_function_void(ufbxi_evaluate_connected_prop, (prop, anim, element, name, time, flags), 3, + (ufbx_prop *prop, const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags)) +{ + ufbx_connection *conn = ufbxi_find_prop_connection(element, name); + + for (size_t i = 0; i < 1000 && conn; i++) { + ufbx_connection *next_conn = ufbxi_find_prop_connection(conn->src, conn->src_prop.data); + if (!next_conn) break; + conn = next_conn; + } + + // Found a non-cyclic connection + if (conn && !ufbxi_find_prop_connection(conn->src, conn->src_prop.data)) { + ufbx_prop ep = ufbx_evaluate_prop_flags_len(anim, conn->src, conn->src_prop.data, conn->src_prop.length, time, flags); + prop->value_vec4 = ep.value_vec4; + prop->value_int = ep.value_int; + prop->value_str = ep.value_str; + prop->value_blob = ep.value_blob; + } else { + // Connection not found, maybe it's animated? + prop->flags = (ufbx_prop_flags)((uint32_t)prop->flags & ~(uint32_t)UFBX_PROP_FLAG_CONNECTED); + } +} + +typedef struct { + const ufbx_prop *prop, *prop_end; + const ufbx_prop_override *over, *over_end; + ufbx_prop tmp; +} ufbxi_prop_iter; + +static ufbxi_noinline void ufbxi_init_prop_iter_slow(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element) +{ + iter->prop = element->props.props.data; + iter->prop_end = element->props.props.data + element->props.props.count; + + ufbx_prop_override_list over = ufbxi_find_element_prop_overrides(&anim->prop_overrides, element->element_id); + iter->over = over.data; + iter->over_end = over.data + over.count; + if (over.count > 0) { + memset(&iter->tmp, 0, sizeof(ufbx_prop)); + } +} + +static ufbxi_forceinline void ufbxi_init_prop_iter(ufbxi_prop_iter *iter, const ufbx_anim *anim, const ufbx_element *element) +{ + iter->prop = element->props.props.data; + iter->prop_end = ufbxi_add_ptr(element->props.props.data, element->props.props.count); + iter->over = iter->over_end = NULL; + if (anim->prop_overrides.count > 0) { + ufbxi_init_prop_iter_slow(iter, anim, element); + } +} + +static ufbxi_noinline const ufbx_prop *ufbxi_next_prop_slow(ufbxi_prop_iter *iter) +{ + const ufbx_prop *prop = iter->prop; + const ufbx_prop_override *over = iter->over; + if (prop == iter->prop_end && over == iter->over_end) return NULL; + + // We can use `UINT32_MAX` as a terminating key (aka prefix) as prop names must + // be valid UTF-8 and the byte sequence "\xff\xff\xff\xff" is not valid. + uint32_t prop_key = prop != iter->prop_end ? prop->_internal_key : UINT32_MAX; + uint32_t over_key = over != iter->over_end ? over->_internal_key : UINT32_MAX; + + int cmp = 0; + if (prop_key != over_key) { + cmp = prop_key < over_key ? -1 : 1; + } else { + cmp = strcmp(prop->name.data, over->prop_name.data); + } + + if (cmp >= 0) { + ufbx_prop *dst = &iter->tmp; + dst->name = over->prop_name; + dst->_internal_key = over->_internal_key; + dst->type = UFBX_PROP_UNKNOWN; + dst->flags = UFBX_PROP_FLAG_OVERRIDDEN; + dst->value_str = over->value_str; + dst->value_blob.data = dst->value_str.data; + dst->value_blob.size = dst->value_str.length; + dst->value_int = over->value_int; + dst->value_vec4 = over->value; + iter->over = over + 1; + if (cmp == 0) { + iter->prop = prop + 1; + } + return dst; + } else { + iter->prop = prop + 1; + return prop; + } +} + +static ufbxi_forceinline const ufbx_prop *ufbxi_next_prop(ufbxi_prop_iter *iter) +{ + if (iter->over == iter->over_end) { + if (iter->prop == iter->prop_end) return NULL; + return iter->prop++; + } else { + return ufbxi_next_prop_slow(iter); + } +} + +static ufbxi_noinline ufbx_props ufbxi_evaluate_selected_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *props, const char *const *prop_names, size_t max_props, uint32_t flags) +{ + const char *name = prop_names[0]; + uint32_t key = ufbxi_get_name_key_c(name); + size_t num_props = 0; + +#if defined(UFBX_REGRESSION) + for (size_t i = 1; i < max_props; i++) { + ufbx_assert(strcmp(prop_names[i - 1], prop_names[i]) < 0); + } +#endif + + size_t name_ix = 0; + + ufbxi_prop_iter iter; // ufbxi_uninit + ufbxi_init_prop_iter(&iter, anim, element); + const ufbx_prop *prop = NULL; + while ((prop = ufbxi_next_prop(&iter)) != NULL) { + while (name_ix < max_props) { + if (key > prop->_internal_key) break; + if (name == prop->name.data) { + if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) { + ufbx_prop *dst = &props[num_props++]; + *dst = *prop; + ufbxi_evaluate_connected_prop(dst, anim, element, name, time, flags); + } else if ((prop->flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_OVERRIDDEN)) != 0) { + props[num_props++] = *prop; + } + break; + } else if (strcmp(name, prop->name.data) < 0) { + name_ix++; + if (name_ix < max_props) { + name = prop_names[name_ix]; + key = ufbxi_get_name_key_c(name); + } + } else { + break; + } + } + } + + ufbxi_evaluate_props(anim, element, time, props, num_props, flags); + + ufbx_props prop_list; + prop_list.props.data = props; + prop_list.props.count = prop_list.num_animated = num_props; + prop_list.defaults = (ufbx_props*)&element->props; + return prop_list; +} + +// Recursion limited by not calling `ufbx_evaluate_curve()` with `UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION`. +static ufbxi_noinline ufbx_real ufbxi_extrapolate_curve(const ufbx_anim_curve *curve, double real_time, uint32_t flags) + ufbxi_recursive_function(ufbx_real, ufbxi_extrapolate_curve, (curve, real_time, flags), 3, + (const ufbx_anim_curve *curve, double real_time, uint32_t flags)) +{ + bool pre = real_time < curve->min_time; + const ufbx_keyframe *key; + ufbx_extrapolation ext; + if (pre) { + key = &curve->keyframes.data[0]; + ext = curve->pre_extrapolation; + } else { + key = &curve->keyframes.data[curve->keyframes.count - 1]; + ext = curve->post_extrapolation; + } + + if (ext.mode == UFBX_EXTRAPOLATION_CONSTANT) { + return key->value; + } else if (ext.mode == UFBX_EXTRAPOLATION_SLOPE) { + ufbx_tangent tangent = *(pre ? &key->right : &key->left); + return key->value + (ufbx_real)(tangent.dy * ((real_time - key->time) / tangent.dx)); + } else if (ext.repeat_count == 0) { + return key->value; + } + + // Perform all operations in KTime ticks to be frame perfect + double scale = (double)curve->element.scene->metadata.ktime_second; + double min_time = ufbx_rint(curve->min_time * scale); + double max_time = ufbx_rint(curve->max_time * scale); + double time = real_time * scale; + + double delta = pre ? min_time - time : time - max_time; + double duration = max_time - min_time; + + // Require at least one KTime unit + if (!(duration >= 1.0)) return key->value; + + double rep = delta / duration; + double rep_n = ufbx_floor(rep); + double rep_d = delta - rep_n * duration; + + if (ext.repeat_count > 0 && rep_n >= (double)ext.repeat_count) { + // Clamp to the repeat count to handle mirroring + rep_n = (double)(ext.repeat_count - 1); + rep_d = duration; + } + + if (ext.mode == UFBX_EXTRAPOLATION_MIRROR) { + double rep_parity = rep_n*0.5 - ufbx_floor(rep_n*0.5); + if (rep_parity <= 0.25) { + rep_d = duration - rep_d; + } + } + + if (pre) rep_d = duration - rep_d; + double new_time = (min_time + rep_d) / scale; + + ufbx_real value = ufbx_evaluate_curve_flags(curve, new_time, key->value, flags | UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION); + + if (ext.mode == UFBX_EXTRAPOLATION_REPEAT_RELATIVE) { + ufbx_real val_delta = curve->keyframes.data[curve->keyframes.count - 1].value - curve->keyframes.data[0].value; + if (pre) val_delta = -val_delta; + value += val_delta * (ufbx_real)(rep_n + 1.0); + } + + return value; +} + +#if UFBXI_FEATURE_SCENE_EVALUATION + +typedef struct { + char *src_element; + char *dst_element; + + ufbxi_scene_imp *src_imp; + ufbx_scene src_scene; + ufbx_evaluate_opts opts; + ufbx_anim *anim; + double time; + + ufbx_error error; + + // Allocators + ufbxi_allocator ator_result; + ufbxi_allocator ator_tmp; + + ufbxi_buf result; + ufbxi_buf tmp; + + ufbx_scene scene; + + ufbxi_scene_imp *scene_imp; +} ufbxi_eval_context; + +static ufbxi_forceinline ufbx_element *ufbxi_translate_element(ufbxi_eval_context *ec, void *elem) +{ + return elem ? (ufbx_element*)(ec->dst_element + ((char*)elem - ec->src_element)) : NULL; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_element_list(ufbxi_eval_context *ec, void *p_list) +{ + ufbx_element_list *list = (ufbx_element_list*)p_list; + size_t count = list->count; + ufbx_element **src = list->data; + ufbx_element **dst = ufbxi_push(&ec->result, ufbx_element*, count); + ufbxi_check_err(&ec->error, dst); + list->data = dst; + for (size_t i = 0; i < count; i++) { + dst[i] = ufbxi_translate_element(ec, src[i]); + } + return 1; +} + +static ufbxi_noinline void ufbxi_translate_maps(ufbxi_eval_context *ec, ufbx_material_map *maps, size_t count) +{ + ufbxi_nounroll ufbxi_for(ufbx_material_map, map, maps, count) { + map->texture = (ufbx_texture*)ufbxi_translate_element(ec, map->texture); + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_translate_anim(ufbxi_eval_context *ec, ufbx_anim **p_anim) +{ + ufbx_anim *anim = ufbxi_push_copy(&ec->result, ufbx_anim, 1, *p_anim); + ufbxi_check_err(&ec->error, anim); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &anim->layers)); + *p_anim = anim; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_evaluate_imp(ufbxi_eval_context *ec) +{ + ec->scene = ec->src_scene; + size_t num_elements = ec->scene.elements.count; + + char *element_data = (char*)ufbxi_push(&ec->result, uint64_t, ec->scene.metadata.element_buffer_size/8); + ufbxi_check_err(&ec->error, element_data); + + ec->scene.elements.data = ufbxi_push(&ec->result, ufbx_element*, num_elements); + ufbxi_check_err(&ec->error, ec->scene.elements.data); + + ec->src_element = (char*)ec->src_scene.elements.data[0]; + ec->dst_element = element_data; + + for (size_t i = 0; i < UFBX_ELEMENT_TYPE_COUNT; i++) { + ec->scene.elements_by_type[i].data = ufbxi_push(&ec->result, ufbx_element*, ec->scene.elements_by_type[i].count); + ufbxi_check_err(&ec->error, ec->scene.elements_by_type[i].data); + } + + size_t num_connections = ec->scene.connections_dst.count; + ec->scene.connections_src.data = ufbxi_push(&ec->result, ufbx_connection, num_connections); + ec->scene.connections_dst.data = ufbxi_push(&ec->result, ufbx_connection, num_connections); + ufbxi_check_err(&ec->error, ec->scene.connections_src.data); + ufbxi_check_err(&ec->error, ec->scene.connections_dst.data); + for (size_t i = 0; i < num_connections; i++) { + ufbx_connection *src = &ec->scene.connections_src.data[i]; + ufbx_connection *dst = &ec->scene.connections_dst.data[i]; + *src = ec->src_scene.connections_src.data[i]; + *dst = ec->src_scene.connections_dst.data[i]; + src->src = ufbxi_translate_element(ec, src->src); + src->dst = ufbxi_translate_element(ec, src->dst); + dst->src = ufbxi_translate_element(ec, dst->src); + dst->dst = ufbxi_translate_element(ec, dst->dst); + } + + ec->scene.elements_by_name.data = ufbxi_push(&ec->result, ufbx_name_element, num_elements); + ufbxi_check_err(&ec->error, ec->scene.elements_by_name.data); + + ec->scene.root_node = (ufbx_node*)ufbxi_translate_element(ec, ec->scene.root_node); + ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &ec->scene.anim)); + + for (size_t i = 0; i < num_elements; i++) { + ufbx_element *src = ec->src_scene.elements.data[i]; + ufbx_element *dst = ufbxi_translate_element(ec, src); + size_t size = ufbx_element_type_size[src->type]; + ufbx_assert(size > 0); + memcpy(dst, src, size); + + ec->scene.elements.data[i] = dst; + ec->scene.elements_by_type[src->type].data[src->typed_id] = dst; + + dst->connections_src.data = ec->scene.connections_src.data + (dst->connections_src.data - ec->src_scene.connections_src.data); + dst->connections_dst.data = ec->scene.connections_dst.data + (dst->connections_dst.data - ec->src_scene.connections_dst.data); + if (dst->instances.count > 0) { + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &dst->instances)); + } + + ufbx_name_element named = ec->src_scene.elements_by_name.data[i]; + named.element = ufbxi_translate_element(ec, named.element); + ec->scene.elements_by_name.data[i] = named; + } + + ufbxi_for_ptr_list(ufbx_node, p_node, ec->scene.nodes) { + ufbx_node *node = *p_node; + node->parent = (ufbx_node*)ufbxi_translate_element(ec, node->parent); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->children)); + + node->attrib = ufbxi_translate_element(ec, node->attrib); + node->mesh = (ufbx_mesh*)ufbxi_translate_element(ec, node->mesh); + node->light = (ufbx_light*)ufbxi_translate_element(ec, node->light); + node->camera = (ufbx_camera*)ufbxi_translate_element(ec, node->camera); + node->bone = (ufbx_bone*)ufbxi_translate_element(ec, node->bone); + node->inherit_scale_node = (ufbx_node*)ufbxi_translate_element(ec, node->inherit_scale_node); + node->scale_helper = (ufbx_node*)ufbxi_translate_element(ec, node->scale_helper); + node->bind_pose = (ufbx_pose*)ufbxi_translate_element(ec, node->bind_pose); + + if (node->all_attribs.count > 1) { + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->all_attribs)); + } else if (node->all_attribs.count == 1) { + node->all_attribs.data = &node->attrib; + } + + node->geometry_transform_helper = (ufbx_node*)ufbxi_translate_element(ec, node->geometry_transform_helper); + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &node->materials)); + } + + ufbxi_for_ptr_list(ufbx_mesh, p_mesh, ec->scene.meshes) { + ufbx_mesh *mesh = *p_mesh; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->materials)); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->skin_deformers)); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->blend_deformers)); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->cache_deformers)); + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &mesh->all_deformers)); + } + + ufbxi_for_ptr_list(ufbx_stereo_camera, p_stereo, ec->scene.stereo_cameras) { + ufbx_stereo_camera *stereo = *p_stereo; + stereo->left = (ufbx_camera*)ufbxi_translate_element(ec, stereo->left); + stereo->right = (ufbx_camera*)ufbxi_translate_element(ec, stereo->right); + } + + ufbxi_for_ptr_list(ufbx_skin_deformer, p_skin, ec->scene.skin_deformers) { + ufbx_skin_deformer *skin = *p_skin; + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &skin->clusters)); + } + + ufbxi_for_ptr_list(ufbx_skin_cluster, p_cluster, ec->scene.skin_clusters) { + ufbx_skin_cluster *cluster = *p_cluster; + cluster->bone_node = (ufbx_node*)ufbxi_translate_element(ec, cluster->bone_node); + } + + ufbxi_for_ptr_list(ufbx_blend_deformer, p_blend, ec->scene.blend_deformers) { + ufbx_blend_deformer *blend = *p_blend; + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &blend->channels)); + } + + ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, ec->scene.blend_channels) { + ufbx_blend_channel *chan = *p_chan; + + ufbx_blend_keyframe *keys = ufbxi_push(&ec->result, ufbx_blend_keyframe, chan->keyframes.count); + ufbxi_check_err(&ec->error, keys); + for (size_t i = 0; i < chan->keyframes.count; i++) { + keys[i] = chan->keyframes.data[i]; + keys[i].shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, keys[i].shape); + } + chan->keyframes.data = keys; + chan->target_shape = (ufbx_blend_shape*)ufbxi_translate_element(ec, chan->target_shape); + } + + ufbxi_for_ptr_list(ufbx_cache_deformer, p_deformer, ec->scene.cache_deformers) { + ufbx_cache_deformer *deformer = *p_deformer; + deformer->file = (ufbx_cache_file*)ufbxi_translate_element(ec, deformer->file); + } + + ufbxi_for_ptr_list(ufbx_material, p_material, ec->scene.materials) { + ufbx_material *material = *p_material; + + material->shader = (ufbx_shader*)ufbxi_translate_element(ec, material->shader); + ufbxi_translate_maps(ec, material->fbx.maps, UFBX_MATERIAL_FBX_MAP_COUNT); + ufbxi_translate_maps(ec, material->pbr.maps, UFBX_MATERIAL_PBR_MAP_COUNT); + + ufbx_material_texture *textures = ufbxi_push(&ec->result, ufbx_material_texture, material->textures.count); + ufbxi_check_err(&ec->error, textures); + for (size_t i = 0; i < material->textures.count; i++) { + textures[i] = material->textures.data[i]; + textures[i].texture = (ufbx_texture*)ufbxi_translate_element(ec, textures[i].texture); + } + material->textures.data = textures; + } + + ufbxi_for_ptr_list(ufbx_texture, p_texture, ec->scene.textures) { + ufbx_texture *texture = *p_texture; + texture->video = (ufbx_video*)ufbxi_translate_element(ec, texture->video); + + ufbx_texture_layer *layers = ufbxi_push(&ec->result, ufbx_texture_layer, texture->layers.count); + ufbxi_check_err(&ec->error, layers); + for (size_t i = 0; i < texture->layers.count; i++) { + layers[i] = texture->layers.data[i]; + layers[i].texture = (ufbx_texture*)ufbxi_translate_element(ec, layers[i].texture); + } + texture->layers.data = layers; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &texture->file_textures)); + + if (texture->shader) { + ufbx_shader_texture *shader = texture->shader; + shader = ufbxi_push_copy(&ec->result, ufbx_shader_texture, 1, shader); + ufbxi_check_err(&ec->error, shader); + texture->shader = shader; + + ufbx_shader_texture_input *inputs = ufbxi_push_copy(&ec->result, ufbx_shader_texture_input, shader->inputs.count, shader->inputs.data); + ufbxi_check_err(&ec->error, inputs); + shader->inputs.data = inputs; + } + } + + ufbxi_for_ptr_list(ufbx_shader, p_shader, ec->scene.shaders) { + ufbx_shader *shader = *p_shader; + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &shader->bindings)); + } + + ufbxi_for_ptr_list(ufbx_display_layer, p_layer, ec->scene.display_layers) { + ufbx_display_layer *layer = *p_layer; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->nodes)); + } + + ufbxi_for_ptr_list(ufbx_selection_set, p_set, ec->scene.selection_sets) { + ufbx_selection_set *set = *p_set; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &set->nodes)); + } + + ufbxi_for_ptr_list(ufbx_selection_node, p_node, ec->scene.selection_nodes) { + ufbx_selection_node *node = *p_node; + + node->target_node = (ufbx_node*)ufbxi_translate_element(ec, node->target_node); + node->target_mesh = (ufbx_mesh*)ufbxi_translate_element(ec, node->target_mesh); + } + + ufbxi_for_ptr_list(ufbx_constraint, p_constraint, ec->scene.constraints) { + ufbx_constraint *constraint = *p_constraint; + + constraint->node = (ufbx_node*)ufbxi_translate_element(ec, constraint->node); + constraint->aim_up_node = (ufbx_node*)ufbxi_translate_element(ec, constraint->aim_up_node); + constraint->ik_effector = (ufbx_node*)ufbxi_translate_element(ec, constraint->ik_effector); + constraint->ik_end_node = (ufbx_node*)ufbxi_translate_element(ec, constraint->ik_end_node); + + ufbx_constraint_target *targets = ufbxi_push(&ec->result, ufbx_constraint_target, constraint->targets.count); + ufbxi_check_err(&ec->error, targets); + for (size_t i = 0; i < constraint->targets.count; i++) { + targets[i] = constraint->targets.data[i]; + targets[i].node = (ufbx_node*)ufbxi_translate_element(ec, targets[i].node); + } + constraint->targets.data = targets; + } + + ufbxi_for_ptr_list(ufbx_audio_layer, p_layer, ec->scene.audio_layers) { + ufbx_audio_layer *layer = *p_layer; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->clips)); + } + + ufbxi_for_ptr_list(ufbx_anim_stack, p_stack, ec->scene.anim_stacks) { + ufbx_anim_stack *stack = *p_stack; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &stack->layers)); + ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &stack->anim)); + } + + ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, ec->scene.anim_layers) { + ufbx_anim_layer *layer = *p_layer; + + ufbxi_check_err(&ec->error, ufbxi_translate_element_list(ec, &layer->anim_values)); + ufbx_anim_prop *props = ufbxi_push(&ec->result, ufbx_anim_prop, layer->anim_props.count + 1); + ufbxi_check_err(&ec->error, props); + for (size_t i = 0; i < layer->anim_props.count; i++) { + props[i] = layer->anim_props.data[i]; + props[i].element = ufbxi_translate_element(ec, props[i].element); + props[i].anim_value = (ufbx_anim_value*)ufbxi_translate_element(ec, props[i].anim_value); + } + // Maintain NULL sentinel + memset(props + layer->anim_props.count, 0, sizeof(ufbx_anim_prop)); + layer->anim_props.data = props; + } + + ufbxi_for_ptr_list(ufbx_pose, p_pose, ec->scene.poses) { + ufbx_pose *pose = *p_pose; + + ufbx_bone_pose *bones = ufbxi_push(&ec->result, ufbx_bone_pose, pose->bone_poses.count); + ufbxi_check_err(&ec->error, bones); + for (size_t i = 0; i < pose->bone_poses.count; i++) { + bones[i] = pose->bone_poses.data[i]; + bones[i].bone_node = (ufbx_node*)ufbxi_translate_element(ec, bones[i].bone_node); + } + pose->bone_poses.data = bones; + } + + ufbxi_check_err(&ec->error, ufbxi_translate_anim(ec, &ec->anim)); + + ufbxi_for_ptr_list(ufbx_anim_value, p_value, ec->scene.anim_values) { + ufbx_anim_value *value = *p_value; + value->curves[0] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[0]); + value->curves[1] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[1]); + value->curves[2] = (ufbx_anim_curve*)ufbxi_translate_element(ec, value->curves[2]); + } + + ufbx_anim anim = *ec->anim; + ufbx_prop_override *over = anim.prop_overrides.data, *over_end = ufbxi_add_ptr(over, anim.prop_overrides.count); + + // Evaluate the properties + ufbxi_for_ptr_list(ufbx_element, p_elem, ec->scene.elements) { + ufbx_element *elem = *p_elem; + size_t num_animated = elem->props.num_animated; + size_t num_override = 0; + + // Setup the overrides for this element if found + while (over != over_end && over->element_id == elem->element_id) { + num_override++; + over++; + } + + num_animated += num_override; + if (num_animated == 0) continue; + + anim.prop_overrides.data = ufbxi_sub_ptr(over, num_override); + anim.prop_overrides.count = num_override; + + ufbx_prop *props = ufbxi_push(&ec->result, ufbx_prop, num_animated); + ufbxi_check_err(&ec->error, props); + + elem->props = ufbx_evaluate_props_flags(&anim, elem, ec->time, props, num_animated, ec->opts.evaluate_flags); + elem->props.defaults = &ec->src_scene.elements.data[elem->element_id]->props; + } + + // Update all derived values + ufbxi_update_scene(&ec->scene, false, anim.transform_overrides.data, anim.transform_overrides.count); + + // Evaluate skinning if requested + if (ec->opts.evaluate_skinning) { + ufbx_geometry_cache_data_opts cache_opts = { 0 }; + cache_opts.open_file_cb = ec->opts.open_file_cb; + ufbxi_check_err(&ec->error, ufbxi_evaluate_skinning(&ec->scene, &ec->error, &ec->result, &ec->tmp, + ec->time, ec->opts.load_external_files && ec->opts.evaluate_caches, &cache_opts)); + } + + // Retain the scene, this must be the final allocation as we copy + // `ator_result` to `ufbx_scene_imp`. + ufbxi_scene_imp *imp = ufbxi_push_zero(&ec->result, ufbxi_scene_imp, 1); + ufbxi_check_err(&ec->error, imp); + + ufbx_assert(ec->src_imp->magic == UFBXI_SCENE_IMP_MAGIC); + ufbxi_init_ref(&imp->refcount, UFBXI_SCENE_IMP_MAGIC, &ec->src_imp->refcount); + + imp->magic = UFBXI_SCENE_IMP_MAGIC; + imp->scene = ec->scene; + imp->refcount.ator = ec->ator_result; + imp->refcount.ator.error = NULL; + + // Copy retained buffers and translate the allocator struct to the one + // contained within `ufbxi_scene_imp` + imp->refcount.buf = ec->result; + imp->refcount.buf.ator = &imp->refcount.ator; + + imp->scene.metadata.result_memory_used = imp->refcount.ator.current_size; + imp->scene.metadata.temp_memory_used = ec->ator_tmp.current_size; + imp->scene.metadata.result_allocs = imp->refcount.ator.num_allocs; + imp->scene.metadata.temp_allocs = ec->ator_tmp.num_allocs; + + ufbxi_for_ptr_list(ufbx_element, p_elem, imp->scene.elements) { + (*p_elem)->scene = &imp->scene; + } + + ec->scene_imp = imp; + ec->result.ator = &ec->ator_result; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline ufbx_scene *ufbxi_evaluate_scene(ufbxi_eval_context *ec, ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *user_opts, ufbx_error *p_error) +{ + if (user_opts) { + ec->opts = *user_opts; + } else { + memset(&ec->opts, 0, sizeof(ec->opts)); + } + + ec->src_imp = ufbxi_get_imp(ufbxi_scene_imp, scene); + ec->src_scene = *scene; + ec->anim = anim ? (ufbx_anim*)anim : scene->anim; + ec->time = time; + + ufbxi_init_ator(&ec->error, &ec->ator_tmp, &ec->opts.temp_allocator, "temp"); + ufbxi_init_ator(&ec->error, &ec->ator_result, &ec->opts.result_allocator, "result"); + + ec->result.ator = &ec->ator_result; + ec->tmp.ator = &ec->ator_tmp; + + ec->result.unordered = true; + ec->tmp.unordered = true; + + if (ufbxi_evaluate_imp(ec)) { + ufbxi_buf_free(&ec->tmp); + ufbxi_free_ator(&ec->ator_tmp); + if (p_error) { + ufbxi_clear_error(p_error); + } + return &ec->scene_imp->scene; + } else { + ufbxi_fix_error_type(&ec->error, "Failed to evaluate", p_error); + ufbxi_buf_free(&ec->tmp); + ufbxi_buf_free(&ec->result); + ufbxi_free_ator(&ec->ator_tmp); + ufbxi_free_ator(&ec->ator_result); + return NULL; + } +} + +#endif + +typedef struct { + ufbx_error error; + ufbxi_allocator ator_result; + ufbxi_buf result; + const ufbx_scene *scene; + ufbx_anim_opts opts; + + ufbx_anim anim; + ufbxi_anim_imp *imp; +} ufbxi_create_anim_context; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_check_string(ufbx_error *error, ufbx_string *dst, const ufbx_string *src) +{ + size_t length = src->length != SIZE_MAX ? src->length : strlen(src->data); + const char *data = length != 0 ? src->data : ufbxi_empty_char; + if (length > 0) { + size_t valid_length = ufbxi_utf8_valid_length(data, length); + ufbxi_check_err_msg(error, valid_length == length, "Invalid UTF-8"); + } + + dst->data = data; + dst->length = length; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_anim_string(ufbxi_create_anim_context *ac, ufbx_string *str) +{ + size_t length = str->length; + if (length > 0) { + char *copy = ufbxi_push(&ac->result, char, length + 1); + ufbxi_check_err(&ac->error, copy); + memcpy(copy, str->data, length); + copy[str->length] = '\0'; + str->data = copy; + } else { + ufbx_assert(str->data == ufbxi_empty_char); + } + + return 1; +} + +static bool ufbxi_prop_override_prop_name_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb; + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + return ufbxi_str_less(a->prop_name, b->prop_name); +} + +static bool ufbxi_prop_override_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_prop_override *a = (const ufbx_prop_override*)va, *b = (const ufbx_prop_override*)vb; + if (a->element_id != b->element_id) return a->element_id < b->element_id; + if (a->_internal_key != b->_internal_key) return a->_internal_key < b->_internal_key; + return strcmp(a->prop_name.data, b->prop_name.data) < 0; +} + +static bool ufbxi_transform_override_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_transform_override *a = (const ufbx_transform_override*)va, *b = (const ufbx_transform_override*)vb; + return a->node_id < b->node_id; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_create_anim_imp(ufbxi_create_anim_context *ac) +{ + const ufbx_scene *scene = ac->scene; + ufbx_anim *anim = &ac->anim; + + ufbxi_init_ator(&ac->error, &ac->ator_result, &ac->opts.result_allocator, "result"); + ac->result.unordered = true; + ac->result.ator = &ac->ator_result; + + anim->ignore_connections = ac->opts.ignore_connections; + anim->custom = true; + + size_t num_layers = ac->opts.layer_ids.count; + anim->layers.count = num_layers; + anim->layers.data = ufbxi_push_zero(&ac->result, ufbx_anim_layer*, num_layers); + ufbxi_check_err(&ac->error, anim->layers.data); + + if (ac->opts.override_layer_weights.count > 0) { + ufbxi_check_err_msg(&ac->error, ac->opts.override_layer_weights.count == num_layers, "override_layer_weights[] count must match layer_ids[] count"); + anim->override_layer_weights.data = ufbxi_push_copy(&ac->result, ufbx_real, num_layers, ac->opts.override_layer_weights.data); + ufbxi_check_err(&ac->error, anim->override_layer_weights.data); + anim->override_layer_weights.count = num_layers; + } + + for (size_t i = 0; i < num_layers; i++) { + uint32_t index = ac->opts.layer_ids.data[i]; + ufbxi_check_err_msg(&ac->error, index < scene->anim_layers.count, "layer_ids out of bounds"); + anim->layers.data[i] = ac->scene->anim_layers.data[index]; + } + + ufbx_const_prop_override_desc_list prop_overrides = ac->opts.prop_overrides; + if (prop_overrides.count > 0) { + anim->prop_overrides.count = prop_overrides.count; + anim->prop_overrides.data = ufbxi_push_zero(&ac->result, ufbx_prop_override, prop_overrides.count); + ufbxi_check_err(&ac->error, anim->prop_overrides.data); + + for (size_t i = 0; i < prop_overrides.count; i++) { + const ufbx_prop_override_desc *src = &prop_overrides.data[i]; + ufbx_prop_override *dst = &anim->prop_overrides.data[i]; + + dst->element_id = src->element_id; + dst->value = src->value; + dst->value_int = src->value_int; + + if (dst->value.x != 0.0f && dst->value_int == 0) { + dst->value_int = (int64_t)dst->value.x; + } else if (dst->value_int != 0 && dst->value.x == 0.0f) { + dst->value.x = (ufbx_real)dst->value_int; + } + + ufbxi_check_err(&ac->error, ufbxi_check_string(&ac->error, &dst->prop_name, &src->prop_name)); + ufbxi_check_err(&ac->error, ufbxi_check_string(&ac->error, &dst->value_str, &src->value_str)); + + dst->_internal_key = ufbxi_get_name_key(dst->prop_name.data, dst->prop_name.length); + } + + // Sort `anim->prop_overrides` first by `prop_name` only so we can deduplicate and + // convert them to global strings in `ufbxi_strings[]` if possible. + ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_prop_name_less, NULL); + + const ufbx_string *global_str = ufbxi_strings, *global_end = global_str + ufbxi_arraycount(ufbxi_strings); + ufbx_string prev_name = { ufbxi_empty_char }; + ufbxi_for_list(ufbx_prop_override, over, anim->prop_overrides) { + if (over->value_str.length > 0) { + ufbxi_check_err(&ac->error, ufbxi_push_anim_string(ac, &over->value_str)); + } + + if (ufbxi_str_equal(over->prop_name, prev_name)) { + over->prop_name = prev_name; + continue; + } + + while (global_str != global_end && ufbxi_str_less(*global_str, over->prop_name)) { + ++global_str; + } + + if (global_str != global_end && ufbxi_str_equal(*global_str, over->prop_name)) { + over->prop_name = *global_str; + } else { + ufbxi_check_err(&ac->error, ufbxi_push_anim_string(ac, &over->prop_name)); + } + + prev_name = over->prop_name; + } + + // Sort `anim->prop_overrides` to the actual order expected by evaluation. + ufbxi_unstable_sort(anim->prop_overrides.data, anim->prop_overrides.count, sizeof(ufbx_prop_override), &ufbxi_prop_override_less, NULL); + + for (size_t i = 1; i < prop_overrides.count; i++) { + const ufbx_prop_override *prev = &anim->prop_overrides.data[i - 1]; + const ufbx_prop_override *next = &anim->prop_overrides.data[i]; + if (prev->element_id == next->element_id && prev->prop_name.data == next->prop_name.data) { + ufbxi_fmt_err_info(&ac->error, "element %u prop \"%s\"", prev->element_id, prev->prop_name.data); + ufbxi_fail_err_msg(&ac->error, "Duplicate override", "Duplicate override"); + } + } + } + + if (ac->opts.transform_overrides.count > 0) { + anim->transform_overrides.count = ac->opts.transform_overrides.count; + anim->transform_overrides.data = ufbxi_push_copy(&ac->result, ufbx_transform_override, anim->transform_overrides.count, ac->opts.transform_overrides.data); + ufbxi_check_err(&ac->error, anim->transform_overrides.data); + ufbxi_unstable_sort(anim->transform_overrides.data, anim->transform_overrides.count, sizeof(ufbx_transform_override), &ufbxi_transform_override_less, NULL); + } + + ac->imp = ufbxi_push(&ac->result, ufbxi_anim_imp, 1); + ufbxi_check_err(&ac->error, ac->imp); + + ufbxi_init_ref(&ac->imp->refcount, UFBXI_ANIM_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, scene))->refcount); + + ac->imp->magic = UFBXI_ANIM_IMP_MAGIC; + ac->imp->anim = ac->anim; + ac->imp->refcount.ator = ac->ator_result; + ac->imp->refcount.buf = ac->result; + + return 1; +} + +// -- Animation baking + +typedef struct { + ufbxi_refcount refcount; + ufbx_baked_anim bake; + uint32_t magic; +} ufbxi_baked_anim_imp; + +#if UFBXI_FEATURE_ANIMATION_BAKING + +typedef struct { + double time; + uint32_t flags; +} ufbxi_bake_time; + +UFBX_LIST_TYPE(ufbxi_bake_time_list, ufbxi_bake_time); + +typedef struct { + ufbx_error error; + ufbxi_allocator ator_tmp; + ufbxi_allocator ator_result; + + ufbxi_buf result; + ufbxi_buf tmp; + ufbxi_buf tmp_prop; + ufbxi_buf tmp_times; + ufbxi_buf tmp_bake_props; + ufbxi_buf tmp_nodes; + ufbxi_buf tmp_elements; + ufbxi_buf tmp_props; + ufbxi_buf tmp_bake_stack; + + ufbxi_bake_time_list layer_weight_times; + + ufbx_baked_node **baked_nodes; + bool *nodes_to_bake; + + char *tmp_arr; + size_t tmp_arr_size; + + const ufbx_scene *scene; + const ufbx_anim *anim; + ufbx_bake_opts opts; + + double ktime_offset; + + double time_begin; + double time_end; + double time_min; + double time_max; + + ufbx_baked_anim bake; + ufbxi_baked_anim_imp *imp; +} ufbxi_bake_context; + +typedef struct { + uint32_t sort_id; + uint32_t element_id; + const char *prop_name; + ufbx_anim_value *anim_value; +} ufbxi_bake_prop; + +static bool ufbxi_bake_prop_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbxi_bake_prop *a = (const ufbxi_bake_prop*)va; + const ufbxi_bake_prop *b = (const ufbxi_bake_prop*)vb; + if (a->sort_id != b->sort_id) return a->sort_id < b->sort_id; + if (a->element_id != b->element_id) return a->element_id < b->element_id; + if (a->prop_name != b->prop_name) return strcmp(a->prop_name, b->prop_name) < 0; + return false; +} + +ufbx_static_assert(bake_step_left, UFBX_BAKED_KEY_STEP_LEFT == 0x1); +ufbx_static_assert(bake_step_right, UFBX_BAKED_KEY_STEP_RIGHT == 0x2); +ufbx_static_assert(bake_step_key, UFBX_BAKED_KEY_STEP_KEY == 0x4); +static ufbxi_forceinline int ufbxi_cmp_bake_time(ufbxi_bake_time a, ufbxi_bake_time b) +{ + if (a.time != b.time) return a.time < b.time ? -1 : 1; + // Bit twiddling for a fast sorting of `0x1 (LEFT) < 0x0 < 0x2 (RIGHT)` + // by `step ^ 1`: `0x0 (LEFT) < 0x1 < 0x3 (RIGHT)` + uint32_t a_step = a.flags & 0x3, b_step = b.flags & 0x3; + if (a_step != b_step) return (a_step ^ 0x1) < (b_step ^ 0x1) ? -1 : 1; + return 0; +} + +ufbxi_nodiscard static ufbxi_forceinline int ufbxi_bake_push_time(ufbxi_bake_context *bc, double time, uint32_t flags) +{ + ufbxi_bake_time *p_key = ufbxi_push_fast(&bc->tmp_times, ufbxi_bake_time, 1); + if (!p_key) return 0; + p_key->time = time; + p_key->flags = flags; + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_times(ufbxi_bake_context *bc, const ufbx_anim_value *anim_value, bool resample_linear, uint32_t key_flag) +{ + double sample_rate = bc->opts.resample_rate; + double min_duration = bc->opts.minimum_sample_rate > 0.0 ? 1.0 / bc->opts.minimum_sample_rate : 0.0; + + for (size_t curve_ix = 0; curve_ix < 3; curve_ix++) { + ufbx_anim_curve *curve = anim_value->curves[curve_ix]; + if (!curve) continue; + + const ufbx_keyframe *keys = curve->keyframes.data; + size_t num_keys = curve->keyframes.count; + for (size_t key_ix = 0; key_ix < num_keys; key_ix++) { + ufbx_keyframe a = keys[key_ix]; + double a_time = a.time; + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, key_flag)); + if (key_ix + 1 >= num_keys) break; + ufbx_keyframe b = keys[key_ix + 1]; + double b_time = b.time; + + // Skip fully flat sections + if (a.value == b.value && a.right.dy == 0.0f && b.left.dy == 0.0f) continue; + + if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_PREV) { + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, b_time, UFBX_BAKED_KEY_STEP_LEFT)); + } else if (a.interpolation == UFBX_INTERPOLATION_CONSTANT_NEXT) { + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, a_time, UFBX_BAKED_KEY_STEP_RIGHT)); + } else if ((resample_linear || a.interpolation == UFBX_INTERPOLATION_CUBIC) && sample_rate > 0.0) { + double duration = b_time - a_time; + if (duration <= min_duration) continue; + + double factor = 1.0; + while (duration * sample_rate / factor >= (double)bc->opts.max_keyframe_segments) { + factor *= 2.0; + } + + double padding = 0.5 / sample_rate; + double start = ufbx_ceil((a_time + padding) * sample_rate / factor) * factor; + double stop = b_time - padding; + for (size_t i = 0; i < bc->opts.max_keyframe_segments; i++) { + double time = (start + (double)i * factor) / sample_rate; + if (time >= stop) break; + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, time, 0)); + } + } + } + } + + return 1; +} + +static const char *const ufbxi_transform_props[] = { + ufbxi_Lcl_Translation, ufbxi_Lcl_Rotation, ufbxi_Lcl_Scaling, ufbxi_PreRotation, ufbxi_PostRotation, + ufbxi_RotationOffset, ufbxi_ScalingOffset, ufbxi_RotationPivot, ufbxi_ScalingPivot, ufbxi_RotationOrder, +}; + +static const char *const ufbxi_complex_translation_props[] = { + ufbxi_ScalingPivot, ufbxi_RotationPivot, ufbxi_RotationOffset, ufbxi_ScalingOffset, +}; + +static const char *const ufbxi_complex_rotation_props[] = { + ufbxi_PreRotation, ufbxi_PostRotation, ufbxi_RotationOrder, +}; + +static const char *const ufbxi_complex_rotation_sources[] = { + ufbxi_Lcl_Rotation, ufbxi_PreRotation, ufbxi_PostRotation, ufbxi_RotationOrder, +}; + +ufbxi_nodiscard static ufbxi_noinline bool ufbxi_in_list(const char *const *items, size_t count, const char *item) +{ + for (size_t i = 0; i < count; i++) { + if (items[i] == item) return true; + } + return false; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_sort_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time *times, size_t count) +{ + ufbxi_check_err(&bc->error, ufbxi_grow_array(&bc->ator_tmp, &bc->tmp_arr, &bc->tmp_arr_size, count * sizeof(ufbxi_bake_time))); + ufbxi_macro_stable_sort(ufbxi_bake_time, 32, times, bc->tmp_arr, count, ( ufbxi_cmp_bake_time(*a, *b) < 0 )); + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_finalize_bake_times(ufbxi_bake_context *bc, ufbxi_bake_time_list *p_dst) +{ + if (bc->layer_weight_times.count > 0) { + ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_times, ufbxi_bake_time, bc->layer_weight_times.count, bc->layer_weight_times.data)); + } + + if (bc->tmp_times.num_items == 0) { + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_begin, 0)); + ufbxi_check_err(&bc->error, ufbxi_bake_push_time(bc, bc->time_end, 0)); + } + + size_t num_times = bc->tmp_times.num_items; + ufbxi_bake_time *times = ufbxi_push_pop(&bc->tmp_prop, &bc->tmp_times, ufbxi_bake_time, num_times); + ufbxi_check_err(&bc->error, times); + + ufbxi_check_err(&bc->error, ufbxi_sort_bake_times(bc, times, num_times)); + + // Deduplicate times + if (num_times > 0) { + size_t dst = 0; + ufbxi_bake_time prev = times[0]; + for (size_t src = 1; src < num_times; src++) { + ufbxi_bake_time next = times[src]; + // Merge keys with the same time and step flags `(0x1, 0x2)` + if (next.time == prev.time) { + if (((next.flags ^ prev.flags) & 0x3) == 0) { + prev.flags |= next.flags; + continue; + } else if (prev.flags & UFBX_BAKED_KEY_STEP_LEFT) { + next.flags |= UFBX_BAKED_KEY_STEP_KEY; + } else if (next.flags & UFBX_BAKED_KEY_STEP_RIGHT) { + prev.flags |= UFBX_BAKED_KEY_STEP_KEY; + } + } + + times[dst++] = prev; + prev = next; + } + times[dst++] = prev; + num_times = dst; + } + + // Cull too close resampled keys, these may arise during merging multiple times + if (num_times > 0) { + double min_dist = 0.25 / bc->opts.resample_rate; + uint32_t keep_flags = UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT|UFBX_BAKED_KEY_STEP_KEY|UFBX_BAKED_KEY_KEYFRAME; + + size_t dst = 0; + for (size_t src = 0; src < num_times; src++) { + ufbxi_bake_time cur = times[src]; + double delta = UFBX_INFINITY; + + bool keep = true; + if ((cur.flags & keep_flags) == 0) { + if (dst > 0) delta = cur.time - times[dst - 1].time; + if (src + 1 < num_times) delta = ufbx_fmin(delta, times[src + 1].time - cur.time); + if (delta < min_dist) keep = false; + } + if (keep) { + times[dst++] = cur; + } + } + num_times = dst; + } + + // Enforce maximum sample rate + if (bc->opts.maximum_sample_rate > 0.0) { + const double epsilon = 0.0078125 / bc->opts.maximum_sample_rate; + double sample_rate = bc->opts.maximum_sample_rate; + double max_interval = 1.0 / bc->opts.maximum_sample_rate; + double min_interval = 1.0 / bc->opts.maximum_sample_rate - epsilon; + size_t dst = 0, src = 0; + + // Pre-expand constant keyframes + for (size_t i = 0; i < num_times; i++) { + if ((times[i].flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + double sign = (times[i].flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -1.0 : 1.0; + double time = times[i].time + sign * max_interval; + if (i > 0) time = ufbx_fmax(time, times[i - 1].time); + if (i + 1 < num_times) time = ufbx_fmin(time, times[i + 1].time); + times[i].time = time; + times[i].flags = UFBX_BAKED_KEY_REDUCED; + } + } + + ufbxi_bake_time prev_time = { -UFBX_INFINITY }; + while (src < num_times) { + ufbxi_bake_time src_time = times[src]; + src++; + + size_t start_src = src; + ufbxi_bake_time next_time; + next_time.time = ufbx_ceil(src_time.time * sample_rate - epsilon) / sample_rate; + next_time.flags = UFBX_BAKED_KEY_REDUCED; + while (src < num_times && times[src].time <= next_time.time + epsilon) { + src++; + } + + if (src != start_src || src_time.time - prev_time.time <= min_interval) { + prev_time = next_time; + } else { + prev_time = src_time; + } + + if (dst == 0 || prev_time.time > times[dst - 1].time) { + times[dst++] = prev_time; + } + } + + num_times = dst; + } + + if (num_times > 0) { + if (times[0].time < bc->time_min) bc->time_min = times[0].time; + if (times[num_times - 1].time > bc->time_max) bc->time_max = times[num_times - 1].time; + } + + p_dst->data = times; + p_dst->count = num_times; + + return 1; +} + +#define ufbxi_add_epsilon(a, epsilon) ((a)>0 ? (a)*(epsilon) : (a)/(epsilon)) +#define ufbxi_sub_epsilon(a, epsilon) ((a)>0 ? (a)/(epsilon) : (a)*(epsilon)) + +static ufbxi_noinline bool ufbxi_postprocess_step(ufbxi_bake_context *bc, double prev_time, double next_time, double *p_time, ufbx_baked_key_flags flags) +{ + ufbxi_dev_assert((flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0); + bool left = (flags & UFBX_BAKED_KEY_STEP_LEFT) != 0; + + double step = 0.001; + double epsilon = 1.0 + UFBX_FLT_EPSILON * 4.0f; + + double time = *p_time; + switch (bc->opts.step_handling) { + case UFBX_BAKE_STEP_HANDLING_DEFAULT: + break; + case UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION: + step = bc->opts.step_custom_duration; + epsilon = 1.0 + bc->opts.step_custom_epsilon; + break; + case UFBX_BAKE_STEP_HANDLING_IDENTICAL_TIME: + return true; + case UFBX_BAKE_STEP_HANDLING_ADJACENT_DOUBLE: + if (left) { + *p_time = time = ufbx_nextafter(time, -UFBX_INFINITY); + return time > prev_time; + } else { + *p_time = time = ufbx_nextafter(time, UFBX_INFINITY); + return time < next_time; + } + case UFBX_BAKE_STEP_HANDLING_IGNORE: + return false; + default: + ufbxi_unreachable("Unhandled bake step handling"); + return false; + } + + if (left) { + double min_time = ufbx_fmax(prev_time + step, ufbxi_add_epsilon(prev_time, epsilon)); + *p_time = time = ufbx_fmin(time - step, ufbxi_sub_epsilon(time, epsilon)); + return time > min_time; + } else { + double max_time = ufbx_fmin(next_time - step, ufbxi_sub_epsilon(next_time, epsilon)); + *p_time = time = ufbx_fmax(time + step, ufbxi_add_epsilon(time, epsilon)); + return time < max_time; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_vec3(ufbxi_bake_context *bc, ufbx_baked_vec3_list *p_dst, bool *p_constant, ufbx_baked_vec3_list src) +{ + if (src.count == 0) return 1; + + // Offset times + if (bc->ktime_offset != 0.0) { + double scale = (double)bc->scene->metadata.ktime_second; + double offset = bc->ktime_offset; + for (size_t i = 0; i < src.count; i++) { + src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale; + } + } + + // Postprocess stepped tangents + { + size_t dst = 0; + double prev_time = src.data[0].time; + for (size_t i = 0; i < src.count; i++) { + ufbx_baked_vec3 cur = src.data[i]; + double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY; + bool keep = true; + if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags); + } + if (keep) { + src.data[dst] = cur; + dst++; + prev_time = cur.time; + } + } + src.count = dst; + } + + if (bc->opts.key_reduction_enabled) { + double threshold = bc->opts.key_reduction_threshold * bc->opts.key_reduction_threshold; + for (size_t pass = 0; pass < bc->opts.key_reduction_passes; pass++) { + size_t dst = 1; + for (size_t i = 1; i < src.count; i++) { + ufbx_baked_vec3 prev = src.data[i - 1]; + ufbx_baked_vec3 cur = src.data[i]; + if (i + 1 < src.count) { + ufbx_baked_vec3 next = src.data[i + 1]; + double delta = (cur.time - prev.time) / (next.time - prev.time); + ufbx_vec3 tmp = ufbxi_lerp3(prev.value, next.value, (ufbx_real)delta); + double error = 0.0; + error += ((double)tmp.x - (double)cur.value.x) * ((double)tmp.x - (double)cur.value.x); + error += ((double)tmp.y - (double)cur.value.y) * ((double)tmp.y - (double)cur.value.y); + error += ((double)tmp.z - (double)cur.value.z) * ((double)tmp.z - (double)cur.value.z); + if (error <= threshold) { + src.data[dst] = src.data[i + 1]; + i += 1; + dst += 1; + continue; + } + } + + src.data[dst] = src.data[i]; + dst += 1; + } + if (dst == src.count) break; + src.count = dst; + } + } + + bool constant = true; + ufbx_vec3 ref = src.data[0].value; + for (size_t i = 1; i < src.count; i++) { + ufbx_vec3 v = src.data[i].value; + if (v.x != ref.x || v.y != ref.y || v.z != ref.z) { + constant = false; + break; + } + } + *p_constant = constant; + + p_dst->count = src.count; + p_dst->data = ufbxi_push_copy(&bc->result, ufbx_baked_vec3, src.count, src.data); + ufbxi_check_err(&bc->error, p_dst->data); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_postprocess_quat(ufbxi_bake_context *bc, ufbx_baked_quat_list *p_dst, bool *p_constant, ufbx_baked_quat_list src) +{ + if (src.count == 0) return 1; + + // Offset times + if (bc->ktime_offset != 0.0) { + double scale = (double)bc->scene->metadata.ktime_second; + double offset = bc->ktime_offset; + for (size_t i = 0; i < src.count; i++) { + src.data[i].time = ufbx_rint(src.data[i].time * scale + offset) / scale; + } + } + + // Postprocess stepped tangents + { + size_t dst = 0; + double prev_time = src.data[0].time; + for (size_t i = 0; i < src.count; i++) { + ufbx_baked_quat cur = src.data[i]; + double next_time = i + 1 < src.count ? src.data[i + 1].time : UFBX_INFINITY; + bool keep = true; + if ((cur.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + keep = ufbxi_postprocess_step(bc, prev_time, next_time, &cur.time, cur.flags); + } + if (keep) { + prev_time = cur.time; + src.data[dst] = cur; + dst++; + } + } + src.count = dst; + } + + // Fix quaternion antipodality + for (size_t i = 1; i < src.count; i++) { + src.data[i].value = ufbx_quat_fix_antipodal(src.data[i].value, src.data[i - 1].value); + } + + if (bc->opts.key_reduction_enabled) { + double threshold = bc->opts.key_reduction_threshold * bc->opts.key_reduction_threshold; + for (size_t pass = 0; pass < bc->opts.key_reduction_passes; pass++) { + size_t dst = 1; + for (size_t i = 1; i < src.count; i++) { + ufbx_baked_quat prev = src.data[i - 1]; + ufbx_baked_quat cur = src.data[i]; + if (i + 1 < src.count) { + ufbx_baked_quat next = src.data[i + 1]; + double delta = (cur.time - prev.time) / (next.time - prev.time); + double error = 0.0; + + if (bc->opts.key_reduction_rotation) { + ufbx_quat tmp = ufbx_quat_slerp(prev.value, next.value, (ufbx_real)delta); + error += ((double)tmp.x - (double)cur.value.x) * ((double)tmp.x - (double)cur.value.x); + error += ((double)tmp.y - (double)cur.value.y) * ((double)tmp.y - (double)cur.value.y); + error += ((double)tmp.z - (double)cur.value.z) * ((double)tmp.z - (double)cur.value.z); + error += ((double)tmp.w - (double)cur.value.w) * ((double)tmp.w - (double)cur.value.w); + } else { + error += ((double)prev.value.x - (double)cur.value.x) * ((double)prev.value.x - (double)cur.value.x); + error += ((double)prev.value.y - (double)cur.value.y) * ((double)prev.value.y - (double)cur.value.y); + error += ((double)prev.value.z - (double)cur.value.z) * ((double)prev.value.z - (double)cur.value.z); + error += ((double)prev.value.w - (double)cur.value.w) * ((double)prev.value.w - (double)cur.value.w); + error += ((double)next.value.x - (double)cur.value.x) * ((double)next.value.x - (double)cur.value.x); + error += ((double)next.value.y - (double)cur.value.y) * ((double)next.value.y - (double)cur.value.y); + error += ((double)next.value.z - (double)cur.value.z) * ((double)next.value.z - (double)cur.value.z); + error += ((double)next.value.w - (double)cur.value.w) * ((double)next.value.w - (double)cur.value.w); + error *= 0.5; + } + + if (error <= threshold) { + src.data[dst] = src.data[i + 1]; + i += 1; + dst += 1; + continue; + } + } + + src.data[dst] = src.data[i]; + dst += 1; + } + if (dst == src.count) break; + src.count = dst; + } + } + + bool constant = true; + ufbx_quat ref = src.data[0].value; + for (size_t i = 1; i < src.count; i++) { + ufbx_quat v = src.data[i].value; + if (v.x != ref.x || v.y != ref.y || v.z != ref.z || v.w != ref.w) { + constant = false; + break; + } + } + *p_constant = constant; + + p_dst->count = src.count; + p_dst->data = ufbxi_push_copy(&bc->result, ufbx_baked_quat, src.count, src.data); + ufbxi_check_err(&bc->error, p_dst->data); + + return 1; +} + +static ufbxi_forceinline double ufbxi_bake_time_sample_time(ufbxi_bake_time time) +{ + // Move an infinitesimal step for stepped tangents + if ((time.flags & (UFBX_BAKED_KEY_STEP_LEFT|UFBX_BAKED_KEY_STEP_RIGHT)) != 0) { + double dir = (time.flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 ? -UFBX_INFINITY : UFBX_INFINITY; + return ufbx_nextafter(time.time, dir); + } else { + return time.time; + } +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_push_resampled_times(ufbxi_bake_context *bc, const ufbx_baked_vec3_list *p_keys) +{ + ufbx_baked_vec3_list keys = *p_keys; + + ufbxi_bake_time *times = ufbxi_push(&bc->tmp_times, ufbxi_bake_time, keys.count); + ufbxi_check_err(&bc->error, times); + for (size_t i = 0; i < keys.count; i++) { + ufbx_baked_key_flags flags = keys.data[i].flags; + double time = keys.data[i].time; + if ((flags & UFBX_BAKED_KEY_STEP_LEFT) != 0 && i + 1 < keys.count && (keys.data[i + 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) { + time = keys.data[i + 1].time; + } else if ((flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && i > 0 && (keys.data[i - 1].flags & UFBX_BAKED_KEY_STEP_KEY) != 0) { + time = keys.data[i - 1].time; + } + times[i].time = time; + times[i].flags = flags & 0x7; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node_imp(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count) +{ + ufbx_assert(bc->baked_nodes && bc->nodes_to_bake); + + ufbx_node *node = (ufbx_node*)bc->scene->elements.data[element_id]; + ufbxi_dev_assert(node->element.type == UFBX_ELEMENT_NODE); + + bool complex_translation = false; + bool complex_rotation = false; + + for (size_t i = 0; i < ufbxi_arraycount(ufbxi_complex_translation_props); i++) { + const char *name = ufbxi_complex_translation_props[i]; + ufbx_prop *prop = ufbxi_find_prop(&node->props, name); + if (prop && !ufbxi_is_vec3_zero(prop->value_vec3)) { + complex_translation = true; + } + ufbxi_for(ufbxi_bake_prop, bprop, props, count) { + if (bprop->prop_name == name) { + complex_translation = true; + } + } + } + + for (size_t i = 0; i < ufbxi_arraycount(ufbxi_complex_rotation_props); i++) { + const char *name = ufbxi_complex_rotation_props[i]; + ufbxi_for(ufbxi_bake_prop, bprop, props, count) { + if (bprop->prop_name == name) { + complex_rotation = true; + } + } + } + + ufbxi_bake_time_list times_t, times_r, times_s; + + // Translation + bool resample_translation = false; + + // Account for the _resampled_ scale helper scale animation to keep the + // translation scale consistent with the parent scaling. + ufbx_baked_node *scale_helper_t = NULL; + ufbx_vec3 constant_scale_t = { 1.0f, 1.0f, 1.0f }; + if (!node->is_scale_helper && node->parent && node->parent->scale_helper) { + scale_helper_t = bc->baked_nodes[node->parent->scale_helper->typed_id]; + if (scale_helper_t) { + if (!scale_helper_t->constant_scale) { + resample_translation = true; + } + ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_t->scale_keys)); + } else { + constant_scale_t = node->parent->scale_helper->inherit_scale; + } + } + + if (complex_translation) { + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + // Literally any transform related property can affect complex translation + if (ufbxi_in_list(ufbxi_transform_props, ufbxi_arraycount(ufbxi_transform_props), prop->prop_name)) { + bool resample_linear = resample_translation || prop->prop_name != ufbxi_Lcl_Translation; + uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Translation ? UFBX_BAKED_KEY_KEYFRAME : 0; + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag)); + } + } + } else { + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + if (prop->prop_name == ufbxi_Lcl_Translation) { + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_translation, UFBX_BAKED_KEY_KEYFRAME)); + } + } + } + + ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×_t)); + + // Rotation + if (complex_rotation) { + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + if (ufbxi_in_list(ufbxi_complex_rotation_sources, ufbxi_arraycount(ufbxi_complex_rotation_sources), prop->prop_name)) { + bool resample_linear = !bc->opts.no_resample_rotation || prop->prop_name != ufbxi_Lcl_Rotation; + uint32_t key_flag = prop->prop_name == ufbxi_Lcl_Rotation ? UFBX_BAKED_KEY_KEYFRAME : 0; + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_linear, key_flag)); + } + } + } else { + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + if (prop->prop_name == ufbxi_Lcl_Rotation) { + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, !bc->opts.no_resample_rotation, UFBX_BAKED_KEY_KEYFRAME)); + } + } + } + ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×_r)); + + // Scaling + bool resample_scale = false; + + // Account for the resampled scale + ufbx_baked_node *scale_helper_s = NULL; + ufbx_vec3 constant_scale_s = { 1.0f, 1.0f, 1.0f }; + if (node->is_scale_helper && node->parent && node->parent->inherit_scale_node && node->parent->inherit_scale_node->scale_helper) { + ufbx_node *inherit_helper = node->parent->inherit_scale_node->scale_helper; + scale_helper_s = bc->baked_nodes[inherit_helper->typed_id]; + if (scale_helper_s) { + if (!scale_helper_s->constant_scale) { + resample_scale = true; + } + ufbxi_check_err(&bc->error, ufbxi_push_resampled_times(bc, &scale_helper_s->scale_keys)); + } else { + constant_scale_s = inherit_helper->local_transform.scale; + } + } + + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + if (prop->prop_name == ufbxi_Lcl_Scaling) { + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, resample_scale, UFBX_BAKED_KEY_KEYFRAME)); + } + } + ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×_s)); + + ufbx_baked_vec3_list keys_t; + ufbx_baked_quat_list keys_r; + ufbx_baked_vec3_list keys_s; + + keys_t.count = times_t.count; + keys_t.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys_t.count); + ufbxi_check_err(&bc->error, keys_t.data); + + keys_r.count = times_r.count; + keys_r.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_quat, keys_r.count); + ufbxi_check_err(&bc->error, keys_r.data); + + keys_s.count = times_s.count; + keys_s.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys_s.count); + ufbxi_check_err(&bc->error, keys_s.data); + + size_t ix_t = 0, ix_r = 0, ix_s = 0; + while (ix_t < times_t.count || ix_r < times_r.count || ix_s < times_s.count) { + ufbxi_bake_time bake_time = { UFBX_INFINITY }; + uint32_t flags_r = 0, flags_t = 0, flags_s = 0; + + uint32_t flags = 0; + if (ix_r < times_r.count) { + bake_time = times_r.data[ix_r]; + flags_r = bake_time.flags; + bake_time.flags &= 0x7; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION; + } + if (ix_t < times_t.count) { + ufbxi_bake_time t = times_t.data[ix_t]; + int cmp = ufbxi_cmp_bake_time(t, bake_time); + if (cmp <= 0) { + if (cmp < 0) { + bake_time = t; + flags = 0; + } + bake_time.flags |= t.flags & 0x7; + flags_t = t.flags; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION; + } + } + if (ix_s < times_s.count) { + ufbxi_bake_time t = times_s.data[ix_s]; + int cmp = ufbxi_cmp_bake_time(t, bake_time); + if (cmp <= 0) { + if (cmp < 0) { + bake_time = t; + flags = 0; + } + bake_time.flags |= t.flags & 0x7; + flags_s = t.flags; + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_SCALE; + } + } + + flags |= UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER|UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE|UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES; + if (bc->opts.evaluate_flags & UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION) { + flags |= UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION; + } + + double eval_time = ufbxi_bake_time_sample_time(bake_time); + ufbx_transform transform = ufbx_evaluate_transform_flags(bc->anim, node, eval_time, flags); + + if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION) { + if (scale_helper_t) { + ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_t->scale_keys, eval_time); + transform.translation.x *= scale.x; + transform.translation.y *= scale.y; + transform.translation.z *= scale.z; + } + + transform.translation.x *= constant_scale_t.x; + transform.translation.y *= constant_scale_t.y; + transform.translation.z *= constant_scale_t.z; + + keys_t.data[ix_t].time = bake_time.time; + keys_t.data[ix_t].value = transform.translation; + keys_t.data[ix_t].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_t); + ix_t++; + } + if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) { + keys_r.data[ix_r].time = bake_time.time; + keys_r.data[ix_r].value = transform.rotation; + keys_r.data[ix_r].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_r); + ix_r++; + } + if (flags & UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) { + if (scale_helper_s) { + ufbx_vec3 scale = ufbx_evaluate_baked_vec3(scale_helper_s->scale_keys, eval_time); + transform.scale.x *= scale.x; + transform.scale.y *= scale.y; + transform.scale.z *= scale.z; + } + + transform.scale.x *= constant_scale_s.x; + transform.scale.y *= constant_scale_s.y; + transform.scale.z *= constant_scale_s.z; + + keys_s.data[ix_s].time = bake_time.time; + keys_s.data[ix_s].value = transform.scale; + keys_s.data[ix_s].flags = (ufbx_baked_key_flags)(bake_time.flags | flags_s); + ix_s++; + } + } + + ufbx_baked_node *baked_node = ufbxi_push_zero(&bc->tmp_nodes, ufbx_baked_node, 1); + ufbxi_check_err(&bc->error, baked_node); + + baked_node->element_id = node->element_id; + baked_node->typed_id = node->typed_id; + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->translation_keys, &baked_node->constant_translation, keys_t)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_quat(bc, &baked_node->rotation_keys, &baked_node->constant_rotation, keys_r)); + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_node->scale_keys, &baked_node->constant_scale, keys_s)); + + bc->baked_nodes[node->typed_id] = baked_node; + + ufbxi_buf_clear(&bc->tmp_prop); + + // If this node is a scale helper, make sure to bake its siblings and + // potentially their scale helpers if they are not a part of the animation. + if (node->is_scale_helper) { + ufbx_assert(node->parent); + ufbxi_for_ptr_list(ufbx_node, p_child, node->parent->children) { + ufbx_node *child = *p_child; + if (child == node) continue; + if (!bc->nodes_to_bake[child->typed_id]) { + bc->nodes_to_bake[child->typed_id] = true; + ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_bake_stack, uint32_t, 1, &child->element_id)); + } + if (child->inherit_scale_node && child->inherit_scale_node->scale_helper && child->scale_helper + && bc->nodes_to_bake[child->inherit_scale_node->scale_helper->typed_id]) { + ufbx_assert(bc->baked_nodes[child->inherit_scale_node->scale_helper->typed_id]); + if (!bc->nodes_to_bake[child->scale_helper->typed_id]) { + bc->nodes_to_bake[child->scale_helper->typed_id] = true; + ufbxi_check_err(&bc->error, ufbxi_push_copy(&bc->tmp_bake_stack, uint32_t, 1, &child->scale_helper->element_id)); + } + } + } + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_node(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count) +{ + ufbxi_check_err(&bc->error, ufbxi_bake_node_imp(bc, element_id, props, count)); + + // Baking a node may cause further nodes to be baked, so keep going + // until all dependencies are baked. + while (bc->tmp_bake_stack.num_items > 0) { + uint32_t child_id = 0; + ufbxi_pop(&bc->tmp_bake_stack, uint32_t, 1, &child_id); + ufbxi_check_err(&bc->error, ufbxi_bake_node_imp(bc, child_id, NULL, 0)); + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_prop(ufbxi_bake_context *bc, ufbx_element *element, const char *prop_name, ufbxi_bake_prop *props, size_t count) +{ + ufbxi_for(ufbxi_bake_prop, prop, props, count) { + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, false, UFBX_BAKED_KEY_KEYFRAME)); + } + + ufbxi_bake_time_list times; + ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, ×)); + + ufbx_baked_vec3_list keys; + keys.count = times.count; + keys.data = ufbxi_push(&bc->tmp_prop, ufbx_baked_vec3, keys.count); + ufbxi_check_err(&bc->error, keys.data); + + ufbx_string name; + name.data = prop_name; + name.length = strlen(prop_name); + + for (size_t i = 0; i < times.count; i++) { + ufbxi_bake_time bake_time = times.data[i]; + double eval_time = ufbxi_bake_time_sample_time(bake_time); + ufbx_prop prop = ufbx_evaluate_prop_flags_len(bc->anim, element, name.data, name.length, eval_time, bc->opts.evaluate_flags); + keys.data[i].time = bake_time.time; + keys.data[i].value = prop.value_vec3; + keys.data[i].flags = (ufbx_baked_key_flags)bake_time.flags; + } + + ufbx_baked_prop *baked_prop = ufbxi_push_zero(&bc->tmp_props, ufbx_baked_prop, 1); + ufbxi_check_err(&bc->error, baked_prop); + + baked_prop->name.length = strlen(prop_name); + baked_prop->name.data = ufbxi_push_copy(&bc->result, char, baked_prop->name.length + 1, prop_name); + ufbxi_check_err(&bc->error, baked_prop->name.data); + + ufbxi_check_err(&bc->error, ufbxi_bake_postprocess_vec3(bc, &baked_prop->keys, &baked_prop->constant_value, keys)); + + ufbxi_buf_clear(&bc->tmp_prop); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_element(ufbxi_bake_context *bc, uint32_t element_id, ufbxi_bake_prop *props, size_t count) +{ + ufbx_element *element = bc->scene->elements.data[element_id]; + if (element->type == UFBX_ELEMENT_NODE && !bc->opts.skip_node_transforms) { + ufbxi_check_err(&bc->error, ufbxi_bake_node(bc, element_id, props, count)); + } + + size_t begin = 0; + while (begin < count) { + const char *prop_name = props[begin].prop_name; + size_t end = begin + 1; + while (end < count && props[end].prop_name == prop_name) { + end++; + } + + // Don't bake transform related props for nodes unless specifically requested + if (element->type == UFBX_ELEMENT_NODE && !bc->opts.bake_transform_props && ufbxi_in_list(ufbxi_transform_props, ufbxi_arraycount(ufbxi_transform_props), prop_name)) { + begin = end; + continue; + } + + ufbxi_check_err(&bc->error, ufbxi_bake_anim_prop(bc, element, prop_name, props + begin, end - begin)); + begin = end; + } + + size_t num_props = bc->tmp_props.num_items; + if (num_props > 0) { + ufbx_baked_element *baked_elem = ufbxi_push_zero(&bc->tmp_elements, ufbx_baked_element, 1); + ufbxi_check_err(&bc->error, baked_elem); + + baked_elem->element_id = element->element_id; + baked_elem->props.count = num_props; + baked_elem->props.data = ufbxi_push_pop(&bc->result, &bc->tmp_props, ufbx_baked_prop, num_props); + ufbxi_check_err(&bc->error, baked_elem->props.data); + } + + return 1; +} + +static ufbxi_noinline bool ufbxi_baked_node_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_baked_node *a = (const ufbx_baked_node*)va, *b = (const ufbx_baked_node*)vb; + return a->typed_id < b->typed_id; +} + +static ufbxi_noinline bool ufbxi_baked_element_less(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_baked_element *a = (const ufbx_baked_element*)va, *b = (const ufbx_baked_element*)vb; + return a->element_id < b->element_id; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim(ufbxi_bake_context *bc) +{ + const ufbx_anim *anim = bc->anim; + const ufbx_scene *scene = bc->scene; + + if (!bc->opts.skip_node_transforms) { + bc->baked_nodes = ufbxi_push_zero(&bc->result, ufbx_baked_node*, scene->nodes.count); + ufbxi_check_err(&bc->error, bc->baked_nodes); + bc->nodes_to_bake = ufbxi_push_zero(&bc->result, bool, scene->nodes.count); + ufbxi_check_err(&bc->error, bc->nodes_to_bake); + } + + ufbxi_for_ptr_list(ufbx_anim_layer, p_layer, anim->layers) { + ufbx_anim_layer *layer = *p_layer; + + ufbxi_for_list(ufbx_anim_prop, anim_prop, layer->anim_props) { + ufbxi_bake_prop *prop = ufbxi_push(&bc->tmp_bake_props, ufbxi_bake_prop, 1); + ufbxi_check_err(&bc->error, prop); + + ufbx_element *element = anim_prop->element; + + // Sort nodes by `typed_id` to make sure we process them in order. + if (element->type == UFBX_ELEMENT_NODE) { + if (bc->nodes_to_bake) { + bc->nodes_to_bake[element->typed_id] = true; + } + prop->sort_id = element->typed_id; + } else { + prop->sort_id = UINT32_MAX; + } + + prop->element_id = element->element_id; + prop->prop_name = anim_prop->prop_name.data; + prop->anim_value = anim_prop->anim_value; + } + } + + size_t num_props = bc->tmp_bake_props.num_items; + ufbxi_bake_prop *props = ufbxi_push_pop(&bc->tmp, &bc->tmp_bake_props, ufbxi_bake_prop, num_props); + ufbxi_check_err(&bc->error, props); + + ufbxi_unstable_sort(props, num_props, sizeof(ufbxi_bake_prop), &ufbxi_bake_prop_less, NULL); + + // Pre-bake layer weight times + if (!bc->opts.ignore_layer_weight_animation) { + bool has_weight_times = false; + ufbxi_for(ufbxi_bake_prop, prop, props, num_props) { + if (prop->prop_name != ufbxi_Weight) continue; + ufbx_element *element = scene->elements.data[prop->element_id]; + if (element->type == UFBX_ELEMENT_ANIM_LAYER) { + ufbxi_check_err(&bc->error, ufbxi_bake_times(bc, prop->anim_value, true, 0)); + has_weight_times = true; + } + } + + if (has_weight_times) { + ufbxi_bake_time_list weight_times = { 0 }; + ufbxi_check_err(&bc->error, ufbxi_finalize_bake_times(bc, &weight_times)); + + bc->layer_weight_times.count = weight_times.count; + bc->layer_weight_times.data = ufbxi_push_copy(&bc->tmp, ufbxi_bake_time, weight_times.count, weight_times.data); + ufbxi_check_err(&bc->error, bc->layer_weight_times.data); + + ufbxi_buf_clear(&bc->tmp_prop); + } + } + + size_t begin = 0; + while (begin < num_props) { + uint32_t element_id = props[begin].element_id; + size_t end = begin + 1; + while (end < num_props && props[end].element_id == element_id) { + end++; + } + ufbxi_check_err(&bc->error, ufbxi_bake_element(bc, element_id, props + begin, end - begin)); + begin = end; + } + + size_t num_nodes = bc->tmp_nodes.num_items; + size_t num_elements = bc->tmp_elements.num_items; + + bc->bake.nodes.count = num_nodes; + bc->bake.nodes.data = ufbxi_push_pop(&bc->result, &bc->tmp_nodes, ufbx_baked_node, num_nodes); + ufbxi_check_err(&bc->error, bc->bake.nodes.data); + + bc->bake.elements.count = num_elements; + bc->bake.elements.data = ufbxi_push_pop(&bc->result, &bc->tmp_elements, ufbx_baked_element, num_elements); + ufbxi_check_err(&bc->error, bc->bake.elements.data); + + ufbxi_unstable_sort(bc->bake.nodes.data, bc->bake.nodes.count, sizeof(ufbx_baked_node), &ufbxi_baked_node_less, NULL); + ufbxi_unstable_sort(bc->bake.elements.data, bc->bake.elements.count, sizeof(ufbx_baked_element), &ufbxi_baked_element_less, NULL); + + if (bc->time_min < bc->time_max) { + bc->bake.key_time_min = bc->time_min; + bc->bake.key_time_max = bc->time_max; + } + + if (bc->time_begin < bc->time_end) { + bc->bake.playback_time_begin = bc->time_begin; + bc->bake.playback_time_end = bc->time_end; + bc->bake.playback_duration = bc->time_end - bc->time_begin; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_bake_anim_imp(ufbxi_bake_context *bc, const ufbx_anim *anim) +{ + if (bc->opts.resample_rate <= 0.0) bc->opts.resample_rate = 30.0; + if (bc->opts.minimum_sample_rate <= 0.0) bc->opts.minimum_sample_rate = 19.5; + if (bc->opts.max_keyframe_segments == 0) bc->opts.max_keyframe_segments = 32; + if (bc->opts.key_reduction_threshold == 0) bc->opts.key_reduction_threshold = 0.000001; + if (bc->opts.key_reduction_passes == 0) bc->opts.key_reduction_passes = 4; + + if (bc->opts.trim_start_time && anim->time_begin > 0.0) { + bc->ktime_offset = -anim->time_begin * (double)bc->scene->metadata.ktime_second; + } + + ufbxi_init_ator(&bc->error, &bc->ator_tmp, &bc->opts.temp_allocator, "temp"); + ufbxi_init_ator(&bc->error, &bc->ator_result, &bc->opts.result_allocator, "result"); + + bc->result.unordered = true; + bc->result.ator = &bc->ator_result; + + bc->tmp.unordered = true; + bc->tmp.ator = &bc->ator_tmp; + + bc->tmp_prop.ator = &bc->ator_tmp; + bc->tmp_prop.unordered = true; + bc->tmp_prop.clearable = true; + + bc->tmp_times.ator = &bc->ator_tmp; + bc->tmp_bake_props.ator = &bc->ator_tmp; + bc->tmp_nodes.ator = &bc->ator_tmp; + bc->tmp_elements.ator = &bc->ator_tmp; + bc->tmp_props.ator = &bc->ator_tmp; + bc->tmp_bake_stack.ator = &bc->ator_tmp; + + bc->anim = anim; + if (anim->time_begin < anim->time_end) { + bc->time_begin = anim->time_begin; + bc->time_end = anim->time_end; + } + bc->time_min = UFBX_INFINITY; + bc->time_max = -UFBX_INFINITY; + + bc->imp = ufbxi_push(&bc->result, ufbxi_baked_anim_imp, 1); + ufbxi_check_err(&bc->error, bc->imp); + + ufbxi_check_err(&bc->error, ufbxi_bake_anim(bc)); + + ufbxi_init_ref(&bc->imp->refcount, UFBXI_BAKED_ANIM_IMP_MAGIC, NULL); + + bc->bake.metadata.result_memory_used = bc->ator_result.current_size; + bc->bake.metadata.temp_memory_used = bc->ator_tmp.current_size; + bc->bake.metadata.result_allocs = bc->ator_result.num_allocs; + bc->bake.metadata.temp_allocs = bc->ator_tmp.num_allocs; + + bc->imp->magic = UFBXI_BAKED_ANIM_IMP_MAGIC; + bc->imp->bake = bc->bake; + bc->imp->refcount.ator = bc->ator_result; + bc->imp->refcount.buf = bc->result; + + return 1; +} + +#endif + +// -- NURBS + +static ufbxi_forceinline ufbx_real ufbxi_nurbs_weight(const ufbx_real_list *knots, size_t knot, size_t degree, ufbx_real u) +{ + if (knot >= knots->count) return 0.0f; + if (knots->count - knot < degree) return 0.0f; + ufbx_real prev_u = knots->data[knot], next_u = knots->data[knot + degree]; + if (prev_u >= next_u) return 0.0f; + if (u <= prev_u) return 0.0f; + if (u >= next_u) return 1.0f; + return (u - prev_u) / (next_u - prev_u); +} + +static ufbxi_forceinline ufbx_real ufbxi_nurbs_deriv(const ufbx_real_list *knots, size_t knot, size_t degree) +{ + if (knot >= knots->count) return 0.0f; + if (knots->count - knot < degree) return 0.0f; + ufbx_real prev_u = knots->data[knot], next_u = knots->data[knot + degree]; + if (prev_u >= next_u) return 0.0f; + return (ufbx_real)degree / (next_u - prev_u); +} + +typedef struct { + ufbxi_refcount refcount; + ufbx_line_curve curve; + uint32_t magic; +} ufbxi_line_curve_imp; + +ufbx_static_assert(line_curve_imp_offset, offsetof(ufbxi_line_curve_imp, curve) == sizeof(ufbxi_refcount)); + +#if UFBXI_FEATURE_TESSELLATION + +typedef struct { + ufbx_error error; + + ufbx_tessellate_curve_opts opts; + + const ufbx_nurbs_curve *curve; + + ufbxi_allocator ator_tmp; + ufbxi_allocator ator_result; + + ufbxi_buf result; + + ufbx_line_curve line; + + ufbxi_line_curve_imp *imp; + +} ufbxi_tessellate_curve_context; + +typedef struct { + ufbx_error error; + + ufbx_tessellate_surface_opts opts; + + const ufbx_nurbs_surface *surface; + + ufbxi_allocator ator_tmp; + ufbxi_allocator ator_result; + + ufbxi_buf tmp; + ufbxi_buf result; + + ufbxi_map position_map; + + ufbx_mesh mesh; + + ufbxi_mesh_imp *imp; + +} ufbxi_tessellate_surface_context; + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_curve_imp(ufbxi_tessellate_curve_context *tc) +{ + if (tc->opts.span_subdivision <= 0) { + tc->opts.span_subdivision = 4; + } + size_t num_sub = tc->opts.span_subdivision; + + const ufbx_nurbs_curve *curve = tc->curve; + ufbx_line_curve *line = &tc->line; + ufbxi_check_err_msg(&tc->error, curve->basis.valid && curve->control_points.count > 0, "Bad NURBS geometry"); + + ufbxi_init_ator(&tc->error, &tc->ator_tmp, &tc->opts.temp_allocator, "temp"); + ufbxi_init_ator(&tc->error, &tc->ator_result, &tc->opts.result_allocator, "result"); + + tc->result.unordered = true; + tc->result.ator = &tc->ator_result; + + size_t num_spans = curve->basis.spans.count; + + // Check conservatively that we don't overflow anything + { + size_t over_spans = num_spans * 2 * sizeof(ufbx_real); + size_t over = over_spans * num_sub; + ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over, over_spans, num_sub)); + } + + bool is_open = curve->basis.topology == UFBX_NURBS_TOPOLOGY_OPEN; + + size_t num_indices = num_spans + (num_spans - 1) * (num_sub - 1); + size_t num_vertices = num_indices - (is_open ? 0u : 1u); + ufbxi_check_err(&tc->error, num_indices <= INT32_MAX); + + uint32_t *indices = ufbxi_push(&tc->result, uint32_t, num_indices); + ufbx_vec3 *vertices = ufbxi_push(&tc->result, ufbx_vec3, num_vertices); + ufbx_line_segment *segments = ufbxi_push(&tc->result, ufbx_line_segment, 1); + ufbxi_check_err(&tc->error, indices && vertices && segments); + + for (size_t span_ix = 0; span_ix < num_spans; span_ix++) { + size_t num_splits = span_ix + 1 == num_spans ? 1 : num_sub; + + for (size_t sub_ix = 0; sub_ix < num_splits; sub_ix++) { + size_t ix = span_ix * num_sub + sub_ix; + + if (ix < num_vertices) { + ufbx_real u = curve->basis.spans.data[span_ix]; + if (sub_ix > 0) { + ufbx_real t = (ufbx_real)sub_ix / (ufbx_real)num_sub; + u = u * (1.0f - t) + t * curve->basis.spans.data[span_ix + 1]; + } + + ufbx_curve_point point = ufbx_evaluate_nurbs_curve(curve, u); + vertices[ix] = point.position; + indices[ix] = (uint32_t)ix; + } else { + indices[ix] = 0; + } + } + } + + segments[0].index_begin = 0; + segments[0].num_indices = (uint32_t)num_indices; + + line->element.name.data = ufbxi_empty_char; + line->element.type = UFBX_ELEMENT_LINE_CURVE; + line->element.typed_id = UINT32_MAX; + line->element.element_id = UINT32_MAX; + + line->color.x = 1.0f; + line->color.y = 1.0f; + line->color.z = 1.0f; + + line->control_points.data = vertices; + line->control_points.count = num_vertices; + line->point_indices.data = indices; + line->point_indices.count = num_indices; + line->segments.data = segments; + line->segments.count = 1; + + line->from_tessellated_nurbs = true; + + tc->imp = ufbxi_push(&tc->result, ufbxi_line_curve_imp, 1); + ufbxi_check_err(&tc->error, tc->imp); + + ufbxi_init_ref(&tc->imp->refcount, UFBXI_LINE_CURVE_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, curve->element.scene))->refcount); + + tc->imp->magic = UFBXI_LINE_CURVE_IMP_MAGIC; + tc->imp->curve = tc->line; + tc->imp->refcount.ator = tc->ator_result; + tc->imp->refcount.buf = tc->result; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_tessellate_nurbs_surface_imp(ufbxi_tessellate_surface_context *tc) +{ + if (tc->opts.span_subdivision_u <= 0) { + tc->opts.span_subdivision_u = 4; + } + if (tc->opts.span_subdivision_v <= 0) { + tc->opts.span_subdivision_v = 4; + } + + size_t sub_u = tc->opts.span_subdivision_u; + size_t sub_v = tc->opts.span_subdivision_v; + + const ufbx_nurbs_surface *surface = tc->surface; + ufbx_mesh *mesh = &tc->mesh; + ufbxi_check_err_msg(&tc->error, surface->basis_u.valid && surface->basis_v.valid + && surface->num_control_points_u > 0 && surface->num_control_points_v > 0, "Bad NURBS geometry"); + + ufbxi_init_ator(&tc->error, &tc->ator_tmp, &tc->opts.temp_allocator, "temp"); + ufbxi_init_ator(&tc->error, &tc->ator_result, &tc->opts.result_allocator, "result"); + + tc->result.unordered = true; + tc->tmp.unordered = true; + + tc->result.ator = &tc->ator_result; + tc->tmp.ator = &tc->ator_tmp; + + bool open_u = surface->basis_u.topology == UFBX_NURBS_TOPOLOGY_OPEN; + bool open_v = surface->basis_v.topology == UFBX_NURBS_TOPOLOGY_OPEN; + + size_t spans_u = surface->basis_u.spans.count; + size_t spans_v = surface->basis_v.spans.count; + + // Check conservatively that we don't overflow anything + { + size_t over_spans_u = spans_u * 2 * sizeof(ufbx_real); + size_t over_spans_v = spans_v * 2 * sizeof(ufbx_real); + size_t over_u = over_spans_u * sub_u; + size_t over_v = over_spans_v * sub_v; + size_t over_uv = over_u * over_v; + ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_u, over_spans_u, sub_u)); + ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_v, over_spans_v, sub_v)); + ufbxi_check_err(&tc->error, !ufbxi_does_overflow(over_uv, over_u, over_v)); + } + + size_t faces_u = (spans_u - 1) * sub_u; + size_t faces_v = (spans_v - 1) * sub_v; + + size_t indices_u = spans_u + (spans_u - 1) * (sub_u - 1); + size_t indices_v = spans_v + (spans_v - 1) * (sub_v - 1); + + size_t num_faces = faces_u * faces_v; + size_t num_indices = indices_u * indices_v; + ufbxi_check_err(&tc->error, num_indices <= INT32_MAX); + + uint32_t *position_ix = ufbxi_push(&tc->tmp, uint32_t, num_indices); + ufbx_vec3 *positions = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1); + ufbx_vec3 *normals = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1); + ufbx_vec2 *uvs = ufbxi_push(&tc->result, ufbx_vec2, num_indices + 1); + ufbx_vec3 *tangents = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1); + ufbx_vec3 *bitangents = ufbxi_push(&tc->result, ufbx_vec3, num_indices + 1); + ufbxi_check_err(&tc->error, position_ix && uvs && tangents && bitangents); + + *positions++ = ufbx_zero_vec3; + *normals++ = ufbx_zero_vec3; + *uvs++ = ufbx_zero_vec2; + *tangents++ = ufbx_zero_vec3; + *bitangents++ = ufbx_zero_vec3; + + uint32_t num_positions = 0; + + for (size_t span_v = 0; span_v < spans_v; span_v++) { + size_t splits_v = span_v + 1 == spans_v ? 1 : sub_v; + + for (size_t split_v = 0; split_v < splits_v; split_v++) { + size_t ix_v = span_v * sub_v + split_v; + ufbx_assert(ix_v < indices_v); + + ufbx_real v = surface->basis_v.spans.data[span_v]; + if (split_v > 0) { + ufbx_real t = (ufbx_real)split_v / (ufbx_real)splits_v; + v = v * (1.0f - t) + t * surface->basis_v.spans.data[span_v + 1]; + } + ufbx_real original_v = v; + if (span_v + 1 == spans_v && !open_v) { + v = surface->basis_v.spans.data[0]; + } + + for (size_t span_u = 0; span_u < spans_u; span_u++) { + size_t splits_u = span_u + 1 == spans_u ? 1 : sub_u; + for (size_t split_u = 0; split_u < splits_u; split_u++) { + size_t ix_u = span_u * sub_u + split_u; + ufbx_assert(ix_u < indices_u); + + ufbx_real u = surface->basis_u.spans.data[span_u]; + if (split_u > 0) { + ufbx_real t = (ufbx_real)split_u / (ufbx_real)splits_u; + u = u * (1.0f - t) + t * surface->basis_u.spans.data[span_u + 1]; + } + ufbx_real original_u = u; + if (span_u + 1 == spans_u && !open_u) { + u = surface->basis_u.spans.data[0]; + } + + ufbx_surface_point point = ufbx_evaluate_nurbs_surface(surface, u, v); + ufbx_vec3 pos = point.position; + + ufbx_vec3 tangent_u = ufbxi_slow_normalize3(&point.derivative_u); + ufbx_vec3 tangent_v = ufbxi_slow_normalize3(&point.derivative_v); + + // Check if there's any wrapped positions that we could match + size_t neighbors[5]; // ufbxi_uninit + size_t num_neighbors = 0; + + if ((span_v == 0 && (span_u > 0 || split_u > 0)) || (span_u == 0 && (span_v > 0 || split_v > 0))) { + // Top/left + neighbors[num_neighbors++] = 0; + } + if (span_v + 1 == spans_v) { + // Bottom + neighbors[num_neighbors++] = ix_u; + if (span_u > 0 || split_u > 0) { + neighbors[num_neighbors++] = ix_v * indices_u; + } + } + if (span_u + 1 == spans_u) { + // Right + neighbors[num_neighbors++] = ix_v * indices_u; + if (span_v > 0 || split_v > 0) { + neighbors[num_neighbors++] = indices_u - 1; + } + } + + size_t ix = ix_v * indices_u + ix_u; + + uint32_t pos_ix = num_positions; + for (size_t i = 0; i < num_neighbors; i++) { + size_t nb_ix = neighbors[i]; + ufbx_assert(nb_ix < ix); + uint32_t nb_pos_ix = position_ix[nb_ix]; + ufbx_vec3 nb_pos = positions[nb_pos_ix]; + ufbx_real dx = nb_pos.x - pos.x; + ufbx_real dy = nb_pos.y - pos.y; + ufbx_real dz = nb_pos.z - pos.z; + ufbx_real delta = dx*dx + dy*dy + dz*dz; + if (delta < 0.0000001f) { // TODO: Configurable / something more rigorous + pos_ix = nb_pos_ix; + break; + } + } + + position_ix[ix] = pos_ix; + if (pos_ix == num_positions) { + positions[pos_ix] = pos; + num_positions = pos_ix + 1; + } + uvs[ix].x = original_u; + uvs[ix].y = original_v; + tangents[ix] = tangent_u; + bitangents[ix] = tangent_v; + } + } + } + } + + ufbx_face *faces = ufbxi_push(&tc->result, ufbx_face, num_faces); + uint32_t *vertex_ix = ufbxi_push(&tc->result, uint32_t, num_faces * 4); + uint32_t *attrib_ix = ufbxi_push(&tc->result, uint32_t, num_faces * 4); + ufbxi_check_err(&tc->error, faces && vertex_ix && attrib_ix); + + size_t face_ix = 0; + size_t dst_index = 0; + + size_t num_triangles = 0; + + for (size_t face_v = 0; face_v < faces_v; face_v++) { + for (size_t face_u = 0; face_u < faces_u; face_u++) { + + attrib_ix[dst_index + 0] = (uint32_t)((face_v + 0) * indices_u + (face_u + 0)); + attrib_ix[dst_index + 1] = (uint32_t)((face_v + 0) * indices_u + (face_u + 1)); + attrib_ix[dst_index + 2] = (uint32_t)((face_v + 1) * indices_u + (face_u + 1)); + attrib_ix[dst_index + 3] = (uint32_t)((face_v + 1) * indices_u + (face_u + 0)); + + vertex_ix[dst_index + 0] = position_ix[attrib_ix[dst_index + 0]]; + vertex_ix[dst_index + 1] = position_ix[attrib_ix[dst_index + 1]]; + vertex_ix[dst_index + 2] = position_ix[attrib_ix[dst_index + 2]]; + vertex_ix[dst_index + 3] = position_ix[attrib_ix[dst_index + 3]]; + + bool is_triangle = false; + for (size_t prev_ix = 0; prev_ix < 4; prev_ix++) { + size_t next_ix = (prev_ix + 1) % 4; + if (vertex_ix[dst_index + prev_ix] == vertex_ix[dst_index + next_ix]) { + for (size_t i = next_ix; i < 3; i++) { + attrib_ix[dst_index + i] = attrib_ix[dst_index + i + 1]; + vertex_ix[dst_index + i] = vertex_ix[dst_index + i + 1]; + } + is_triangle = true; + break; + } + } + + faces[face_ix].index_begin = (uint32_t)dst_index; + faces[face_ix].num_indices = is_triangle ? 3 : 4; + dst_index += is_triangle ? 3 : 4; + num_triangles += is_triangle ? 1 : 2; + face_ix++; + } + } + + ufbxi_check_err(&tc->error, positions && normals); + + mesh->element.name.data = ufbxi_empty_char; + mesh->element.type = UFBX_ELEMENT_MESH; + mesh->element.typed_id = UINT32_MAX; + mesh->element.element_id = UINT32_MAX; + + mesh->vertices.data = positions; + mesh->vertices.count = num_positions; + mesh->num_vertices = num_positions; + mesh->vertex_indices.data = vertex_ix; + mesh->vertex_indices.count = dst_index; + + mesh->faces.data = faces; + mesh->faces.count = num_faces; + + mesh->vertex_position.exists = true; + mesh->vertex_position.values.data = positions; + mesh->vertex_position.values.count = num_positions; + mesh->vertex_position.indices.data = vertex_ix; + mesh->vertex_position.indices.count = dst_index; + mesh->vertex_position.unique_per_vertex = true; + + mesh->vertex_uv.exists = true; + mesh->vertex_uv.values.data = uvs; + mesh->vertex_uv.values.count = dst_index; + mesh->vertex_uv.indices.data = attrib_ix; + mesh->vertex_uv.indices.count = dst_index; + + mesh->vertex_normal.exists = true; + mesh->vertex_normal.values.data = normals; + mesh->vertex_normal.values.count = num_positions; + mesh->vertex_normal.indices.data = vertex_ix; + mesh->vertex_normal.indices.count = dst_index; + + mesh->vertex_tangent.exists = true; + mesh->vertex_tangent.values.data = tangents; + mesh->vertex_tangent.values.count = dst_index; + mesh->vertex_tangent.indices.data = attrib_ix; + mesh->vertex_tangent.indices.count = dst_index; + + mesh->vertex_bitangent.exists = true; + mesh->vertex_bitangent.values.data = bitangents; + mesh->vertex_bitangent.values.count = dst_index; + mesh->vertex_bitangent.indices.data = attrib_ix; + mesh->vertex_bitangent.indices.count = dst_index; + + mesh->num_faces = num_faces; + mesh->num_triangles = num_triangles; + mesh->num_indices = dst_index; + mesh->max_face_triangles = 2; + + if (surface->material) { + mesh->face_material.data = ufbxi_push_zero(&tc->result, uint32_t, num_faces); + ufbxi_check_err(&tc->error, mesh->face_material.data); + + ufbx_material **mat = ufbxi_push_zero(&tc->result, ufbx_material*, 1); + ufbxi_check_err(&tc->error, mat); + + *mat = surface->material; + mesh->materials.data = mat; + mesh->materials.count = 1; + } + + if (!tc->opts.skip_mesh_parts) { + mesh->material_parts.count = 1; + mesh->material_parts.data = ufbxi_push_zero(&tc->result, ufbx_mesh_part, 1); + ufbxi_check_err(&tc->error, mesh->material_parts.data); + } + + ufbxi_check_err(&tc->error, ufbxi_finalize_mesh_material(&tc->result, &tc->error, mesh)); + ufbxi_check_err(&tc->error, ufbxi_finalize_mesh(&tc->result, &tc->error, mesh)); + + mesh->generated_normals = true; + ufbx_compute_normals(mesh, &mesh->vertex_position, + mesh->vertex_normal.indices.data, mesh->vertex_normal.indices.count, + mesh->vertex_normal.values.data, mesh->vertex_normal.values.count); + + if (surface->flip_normals) { + ufbxi_nounroll ufbxi_for_list(ufbx_vec3, normal, mesh->vertex_normal.values) { + normal->x *= -1.0f; + normal->y *= -1.0f; + normal->z *= -1.0f; + } + } + + tc->imp = ufbxi_push(&tc->result, ufbxi_mesh_imp, 1); + ufbxi_check_err(&tc->error, tc->imp); + + ufbxi_init_ref(&tc->imp->refcount, UFBXI_MESH_IMP_MAGIC, &(ufbxi_get_imp(ufbxi_scene_imp, surface->element.scene))->refcount); + + tc->imp->magic = UFBXI_MESH_IMP_MAGIC; + tc->imp->mesh = tc->mesh; + tc->imp->refcount.ator = tc->ator_result; + tc->imp->refcount.buf = tc->result; + tc->imp->mesh.subdivision_evaluated = true; + + return 1; +} + +#endif + +// -- Topology + +#if UFBXI_FEATURE_KD + +typedef struct { + ufbx_real split; + uint32_t index_plus_one; // 0 for empty + uint32_t slow_left; + uint32_t slow_right; + uint32_t slow_end; +} ufbxi_kd_node; + +typedef struct { + ufbx_face face; + ufbx_vertex_vec3 positions; + ufbx_vec3 axes[3]; + ufbxi_kd_node kd_nodes[1 << (UFBXI_KD_FAST_DEPTH + 1)]; + uint32_t *kd_indices; + + // Temporary + ufbx_vec3 cur_axis_dir; + ufbx_face cur_face; +} ufbxi_ngon_context; + +typedef struct { + ufbx_real min_t[2]; + ufbx_real max_t[2]; + ufbx_vec2 points[3]; + uint32_t indices[3]; +} ufbxi_kd_triangle; + +ufbxi_noinline static ufbx_vec2 ufbxi_ngon_project(ufbxi_ngon_context *nc, uint32_t index) +{ + ufbx_vec3 point = nc->positions.values.data[nc->positions.indices.data[nc->face.index_begin + index]]; + + ufbx_vec2 p; + p.x = ufbxi_dot3(nc->axes[0], point); + p.y = ufbxi_dot3(nc->axes[1], point); + return p; +} + +ufbxi_forceinline static ufbx_real ufbxi_orient2d(ufbx_vec2 a, ufbx_vec2 b, ufbx_vec2 c) +{ + return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x); +} + +ufbxi_noinline static bool ufbxi_kd_check_point(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t index) +{ + if (index == tri->indices[0] || index == tri->indices[1] || index == tri->indices[2]) return false; + ufbx_vec2 p = ufbxi_ngon_project(nc, index); + + ufbx_real u = ufbxi_orient2d(p, tri->points[0], tri->points[1]); + ufbx_real v = ufbxi_orient2d(p, tri->points[1], tri->points[2]); + ufbx_real w = ufbxi_orient2d(p, tri->points[2], tri->points[0]); + + if (u <= 0.0f && v <= 0.0f && w <= 0.0f) return true; + if (u >= 0.0f && v >= 0.0f && w >= 0.0f) return true; + return false; +} + +// Recursion limited by 32-bit indices in input, minus halvings from `ufbxi_kd_check_fast()` +ufbxi_noinline static bool ufbxi_kd_check_slow(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t begin, uint32_t count, uint32_t axis) + ufbxi_recursive_function(bool, ufbxi_kd_check_slow, (nc, tri, begin, count, axis), 32 - UFBXI_KD_FAST_DEPTH, + (ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t begin, uint32_t count, uint32_t axis)) +{ + ufbx_vertex_vec3 pos = nc->positions; + uint32_t *kd_indices = nc->kd_indices; + + while (count > 0) { + uint32_t num_left = count / 2; + uint32_t begin_right = begin + num_left + 1; + uint32_t num_right = count - (num_left + 1); + + uint32_t index = kd_indices[begin + num_left]; + ufbx_vec3 point = pos.values.data[pos.indices.data[nc->face.index_begin + index]]; + ufbx_real split = ufbxi_dot3(point, nc->axes[axis]); + bool hit_left = tri->min_t[axis] <= split; + bool hit_right = tri->max_t[axis] >= split; + + if (hit_left && hit_right) { + if (ufbxi_kd_check_point(nc, tri, index)) { + return true; + } + + if (ufbxi_kd_check_slow(nc, tri, begin_right, num_right, axis ^ 1)) { + return true; + } + } + + axis ^= 1; + if (hit_left) { + count = num_left; + } else { + begin = begin_right; + count = num_right; + } + } + + return false; +} + +// Recursion limited by `UFBXI_KD_FAST_DEPTH` +ufbxi_noinline static bool ufbxi_kd_check_fast(ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t kd_index, uint32_t axis, uint32_t depth) + ufbxi_recursive_function(bool, ufbxi_kd_check_fast, (nc, tri, kd_index, axis, depth), UFBXI_KD_FAST_DEPTH, + (ufbxi_ngon_context *nc, const ufbxi_kd_triangle *tri, uint32_t kd_index, uint32_t axis, uint32_t depth)) +{ + for (;;) { + ufbxi_kd_node node = nc->kd_nodes[kd_index]; + if (node.index_plus_one == 0) return false; + + bool hit_left = tri->min_t[axis] <= node.split; + bool hit_right = tri->max_t[axis] >= node.split; + + uint32_t side = hit_left ? 0 : 1; + uint32_t child_kd_index = kd_index * 2 + 1 + side; + if (hit_left && hit_right) { + + // Check for the point on the split plane + uint32_t index = node.index_plus_one - 1; + if (ufbxi_kd_check_point(nc, tri, index)) { + return true; + } + + // Recurse always to the right if we hit both sides + if (depth + 1 == UFBXI_KD_FAST_DEPTH) { + if (ufbxi_kd_check_slow(nc, tri, node.slow_right, node.slow_end - node.slow_right, axis ^ 1)) { + return true; + } + } else { + if (ufbxi_kd_check_fast(nc, tri, child_kd_index + 1, axis ^ 1, depth + 1)) { + return true; + } + } + } + + depth++; + axis ^= 1; + kd_index = child_kd_index; + + if (depth == UFBXI_KD_FAST_DEPTH) { + if (hit_left) { + return ufbxi_kd_check_slow(nc, tri, node.slow_left, node.slow_right - node.slow_left, axis); + } else { + return ufbxi_kd_check_slow(nc, tri, node.slow_right, node.slow_end - node.slow_right, axis); + } + } + } +} + +ufbxi_noinline static bool ufbxi_kd_check(ufbxi_ngon_context *nc, const ufbx_vec2 *points, const uint32_t *indices) +{ + ufbxi_kd_triangle tri; // ufbxi_uninit + tri.points[0] = points[0]; + tri.points[1] = points[1]; + tri.points[2] = points[2]; + tri.indices[0] = indices[0]; + tri.indices[1] = indices[1]; + tri.indices[2] = indices[2]; + tri.min_t[0] = ufbxi_min_real(ufbxi_min_real(points[0].x, points[1].x), points[2].x); + tri.min_t[1] = ufbxi_min_real(ufbxi_min_real(points[0].y, points[1].y), points[2].y); + tri.max_t[0] = ufbxi_max_real(ufbxi_max_real(points[0].x, points[1].x), points[2].x); + tri.max_t[1] = ufbxi_max_real(ufbxi_max_real(points[0].y, points[1].y), points[2].y); + return ufbxi_kd_check_fast(nc, &tri, 0, 0, 0); +} + +ufbxi_noinline static bool ufbxi_kd_index_less(void *user, const void *va, const void *vb) +{ + ufbxi_ngon_context *nc = (ufbxi_ngon_context*)user; + ufbx_vertex_vec3 *pos = &nc->positions; + const uint32_t a = *(const uint32_t*)va, b = *(const uint32_t*)vb; + ufbx_real da = ufbxi_dot3(nc->cur_axis_dir, pos->values.data[pos->indices.data[nc->cur_face.index_begin + a]]); + ufbx_real db = ufbxi_dot3(nc->cur_axis_dir, pos->values.data[pos->indices.data[nc->cur_face.index_begin + b]]); + return da < db; +} + +// Recursion limited by 32-bit indices in input +ufbxi_noinline static void ufbxi_kd_build(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t *tmp, uint32_t num, uint32_t axis, uint32_t fast_index, uint32_t depth) + ufbxi_recursive_function_void(ufbxi_kd_build, (nc, indices, tmp, num, axis, fast_index, depth), 32, + (ufbxi_ngon_context *nc, uint32_t *indices, uint32_t *tmp, uint32_t num, uint32_t axis, uint32_t fast_index, uint32_t depth)) +{ + if (num == 0) return; + + ufbx_vertex_vec3 pos = nc->positions; + ufbx_vec3 axis_dir = nc->axes[axis]; + ufbx_face face = nc->face; + + nc->cur_axis_dir = axis_dir; + nc->cur_face = face; + + // Sort the remaining indices based on the axis + ufbxi_stable_sort(sizeof(uint32_t), 16, indices, tmp, num, &ufbxi_kd_index_less, nc); + + uint32_t num_left = num / 2; + uint32_t begin_right = num_left + 1; + uint32_t num_right = num - begin_right; + uint32_t dst_right = num_left + 1; + if (depth < UFBXI_KD_FAST_DEPTH) { + uint32_t skip_left = 1u << (UFBXI_KD_FAST_DEPTH - depth - 1); + dst_right = dst_right > skip_left ? dst_right - skip_left : 0; + + uint32_t index = indices[num_left]; + ufbxi_kd_node *kd = &nc->kd_nodes[fast_index]; + + kd->split = ufbxi_dot3(axis_dir, pos.values.data[pos.indices.data[face.index_begin + index]]); + kd->index_plus_one = index + 1; + + if (depth + 1 == UFBXI_KD_FAST_DEPTH) { + kd->slow_left = (uint32_t)(indices - nc->kd_indices); + kd->slow_right = kd->slow_left + num_left; + kd->slow_end = kd->slow_right + num_right; + } else { + kd->slow_left = UINT32_MAX; + kd->slow_right = UINT32_MAX; + kd->slow_end = UINT32_MAX; + } + } + + uint32_t child_fast = fast_index * 2 + 1; + ufbxi_kd_build(nc, indices, tmp, num_left, axis ^ 1, child_fast + 0, depth + 1); + + if (dst_right != begin_right) { + memmove(indices + dst_right, indices + begin_right, num_right * sizeof(uint32_t)); + } + + ufbxi_kd_build(nc, indices + dst_right, tmp, num_right, axis ^ 1, child_fast + 1, depth + 1); +} + +#endif + +#if UFBXI_FEATURE_TRIANGULATION + +ufbxi_noinline static ufbx_real ufbxi_ngon_tri_weight(const ufbx_vec2 *points) +{ + ufbx_vec2 p0 = points[0], p1 = points[1], p2 = points[2]; + ufbx_real orient = ufbxi_orient2d(p0, p1, p2); + if (orient <= 0.0f) return -1.0f; + + ufbx_real a = ufbxi_distsq2(p0, p1); + ufbx_real b = ufbxi_distsq2(p1, p2); + ufbx_real c = ufbxi_distsq2(p2, p0); + ufbx_real ab = (a + b - c) / (ufbx_real)ufbx_sqrt(4.0f * a * b); + ufbx_real bc = (b + c - a) / (ufbx_real)ufbx_sqrt(4.0f * b * c); + ufbx_real ca = (c + a - b) / (ufbx_real)ufbx_sqrt(4.0f * c * a); + return (ufbx_real)ufbx_fmax(UFBX_EPSILON, 2.0f - ufbx_fmax(ufbx_fmax(ab, bc), ca)); +} + +ufbxi_noinline static uint32_t ufbxi_triangulate_ngon(ufbxi_ngon_context *nc, uint32_t *indices, uint32_t num_indices) +{ + ufbx_face face = nc->face; + ufbx_assert(face.num_indices > 4); + + // Form an orthonormal basis to project the polygon into a 2D plane + ufbx_vec3 normal = ufbx_get_weighted_face_normal(&nc->positions, face); + ufbx_real len = ufbxi_length3(normal); + if (len > UFBX_EPSILON) { + normal = ufbxi_mul3(normal, 1.0f / len); + } else { + normal.x = 1.0f; + normal.y = 0.0f; + normal.z = 0.0f; + } + + ufbx_vec3 axis; // ufbxi_uninit + if (normal.x*normal.x < 0.5f) { + axis.x = 1.0f; + axis.y = 0.0f; + axis.z = 0.0f; + } else { + axis.x = 0.0f; + axis.y = 1.0f; + axis.z = 0.0f; + } + nc->axes[0] = ufbxi_slow_normalized_cross3(&axis, &normal); + nc->axes[1] = ufbxi_slow_normalized_cross3(&normal, &nc->axes[0]); + nc->axes[2] = normal; + + uint32_t *kd_indices = indices; + nc->kd_indices = kd_indices; + + uint32_t *kd_tmp = indices + face.num_indices; + + // Collect all the reflex corners for intersection testing. + uint32_t num_kd_indices = 0; + { + ufbx_vec2 a = ufbxi_ngon_project(nc, face.num_indices - 1); + ufbx_vec2 b = ufbxi_ngon_project(nc, 0); + for (uint32_t i = 0; i < face.num_indices; i++) { + uint32_t next = i + 1 < face.num_indices ? i + 1 : 0; + ufbx_vec2 c = ufbxi_ngon_project(nc, next); + + if (ufbxi_orient2d(a, b, c) <= 0.0f) { + kd_indices[num_kd_indices++] = i; + } + + a = b; + b = c; + } + } + + // Build a KD-tree of the vertices. + uint32_t num_skip_indices = (1u << (UFBXI_KD_FAST_DEPTH + 1)) - 1; + uint32_t kd_slow_indices = num_kd_indices > num_skip_indices ? num_kd_indices - num_skip_indices : 0; + ufbxi_ignore(kd_slow_indices); + ufbx_assert(kd_slow_indices + face.num_indices * 2 <= num_indices); + ufbxi_kd_build(nc, kd_indices, kd_tmp, num_kd_indices, 0, 0, 0); + + uint32_t *edges = indices + num_indices - face.num_indices * 2; + + // Initialize `edges` to be a connectivity structure where: + // `edges[2*i + 0]` is the previous vertex of `i` + // `edges[2*i + 1]` is the next vertex of `i` + // When clipped we mark indices with the high bit (0x80000000) + for (uint32_t i = 0; i < face.num_indices; i++) { + edges[i*2 + 0] = i > 0 ? i - 1 : face.num_indices - 1; + edges[i*2 + 1] = i + 1 < face.num_indices ? i + 1 : 0; + } + + // Core of the ear clipping algorithm. + // Iterate through the polygon corners looking for potential ears satisfying: + // - Angle must be less than 180deg + // - The triangle formed by the two edges must be contained within the polygon + // As these properties change only locally between modifications we only need + // to iterate the polygon once if we move backwards one step every time we clip an ear. + uint32_t indices_left = face.num_indices; + { + uint32_t point_indices[4] = { 0, 1, 2, 3 }; + ufbx_real weights[2]; // ufbxi_uninit + ufbx_vec2 points[4]; // ufbxi_uninit + + uint32_t num_steps = 0; + while (indices_left > 3) { + points[0] = ufbxi_ngon_project(nc, point_indices[0]); + points[1] = ufbxi_ngon_project(nc, point_indices[1]); + points[2] = ufbxi_ngon_project(nc, point_indices[2]); + points[3] = ufbxi_ngon_project(nc, point_indices[3]); + + weights[0] = ufbxi_ngon_tri_weight(points + 0); + weights[1] = ufbxi_ngon_tri_weight(points + 1); + + uint32_t first_side = weights[1] > weights[0] ? 1 : 0; + bool clipped = false; + ufbxi_nounroll for (uint32_t side_ix = 0; side_ix < 2; side_ix++) { + uint32_t side = side_ix ^ first_side; + if (!(weights[side] >= 0.0f)) break; + + // If there is no reflex angle contained within the triangle formed + // by `{ a, b, c }` connect the vertices `a - c` (prev, next) directly. + if (!ufbxi_kd_check(nc, points + side, point_indices + side)) { + uint32_t ia = point_indices[side + 0]; + uint32_t ib = point_indices[side + 1]; + uint32_t ic = point_indices[side + 2]; + + // Mark as clipped + edges[ib*2 + 0] |= 0x80000000; + edges[ib*2 + 1] |= 0x80000000; + + edges[ic*2 + 0] = ia; + edges[ia*2 + 1] = ic; + + indices_left -= 1; + + // TODO: This may cause O(n^2) behavior! + num_steps = 0; + + if (side == 1) { + point_indices[2] = point_indices[3]; + point_indices[3] = edges[point_indices[3]*2 + 1]; + } else { + point_indices[1] = point_indices[0]; + point_indices[0] = edges[point_indices[0]*2 + 0]; + } + + clipped = true; + break; + } + } + if (clipped) continue; + + // Continue forward + point_indices[0] = point_indices[1]; + point_indices[1] = point_indices[2]; + point_indices[2] = point_indices[3]; + point_indices[3] = edges[point_indices[3]*2 + 1]; + num_steps++; + + // If we have walked around the entire polygon it is irregular and + // ear cutting won't find any more triangles. + // TODO: This could be stricter? + if (num_steps >= face.num_indices*2) break; + } + + // Fallback: Cut non-ears until the polygon is completed. + // TODO: Could do something better here.. + uint32_t ix = point_indices[1]; + while (indices_left > 3) { + uint32_t prev = edges[ix*2 + 0]; + uint32_t next = edges[ix*2 + 1]; + + // Mark as clipped + edges[ix*2 + 0] |= 0x80000000; + edges[ix*2 + 1] |= 0x80000000; + + edges[prev*2 + 1] = next; + edges[next*2 + 0] = prev; + + indices_left -= 1; + ix = next; + } + + // Now we have a single triangle left at `ix`. + edges[ix*2 + 0] |= 0x80000000; + edges[ix*2 + 1] |= 0x80000000; + } + + // Expand the adjacency information `edges` into proper triangles. + // Care needs to be taken here as both refer to the same memory area: + // The last 4 triangles may overlap in source and destination so we write + // them to a stack buffer and copy them over in the end. + uint32_t max_triangles = face.num_indices - 2; + uint32_t num_triangles = 0, num_last_triangles = 0; + uint32_t last_triangles[4*3]; // ufbxi_uninit + + uint32_t index_begin = face.index_begin; + for (uint32_t ix = 0; ix < face.num_indices; ix++) { + uint32_t prev = edges[ix*2 + 0]; + uint32_t next = edges[ix*2 + 1]; + if (!(prev & 0x80000000)) continue; + + uint32_t *dst = indices + num_triangles * 3; + if (num_triangles + 4 >= max_triangles) { + dst = last_triangles + num_last_triangles * 3; + num_last_triangles++; + } + + dst[0] = index_begin + (prev & 0x7fffffff); + dst[1] = index_begin + ix; + dst[2] = index_begin + (next & 0x7fffffff); + num_triangles++; + } + + // Copy over the last triangles + ufbx_assert(num_triangles == max_triangles); + memcpy(indices + (max_triangles - num_last_triangles) * 3, last_triangles, num_last_triangles * 3 * sizeof(uint32_t)); + + return num_triangles; +} + +#endif + +static bool ufbxi_topo_less_index_prev_next(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_topo_edge *a = (const ufbx_topo_edge*)va, *b = (const ufbx_topo_edge*)vb; + if ((int32_t)a->prev != (int32_t)b->prev) return (int32_t)a->prev < (int32_t)b->prev; + return (int32_t)a->next < (int32_t)b->next; +} + +static bool ufbxi_topo_less_index_index(void *user, const void *va, const void *vb) +{ + (void)user; + const ufbx_topo_edge *a = (const ufbx_topo_edge*)va, *b = (const ufbx_topo_edge*)vb; + return (int32_t)a->index < (int32_t)b->index; +} + +ufbxi_noinline static void ufbxi_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo) +{ + size_t num_indices = mesh->num_indices; + + // Temporarily use `prev` and `next` for vertices + for (uint32_t fi = 0; fi < mesh->num_faces; fi++) { + ufbx_face face = mesh->faces.data[fi]; + for (uint32_t pi = 0; pi < face.num_indices; pi++) { + ufbx_topo_edge *te = &topo[face.index_begin + pi]; + uint32_t ni = (pi + 1) % face.num_indices; + uint32_t va = mesh->vertex_indices.data[face.index_begin + pi]; + uint32_t vb = mesh->vertex_indices.data[face.index_begin + ni]; + + if (vb < va) { + uint32_t vt = va; va = vb; vb = vt; + } + te->index = face.index_begin + pi; + te->twin = UFBX_NO_INDEX; + te->edge = UFBX_NO_INDEX; + te->prev = va; + te->next = vb; + te->face = fi; + te->flags = (ufbx_topo_flags)0; + } + } + + ufbxi_unstable_sort(topo, num_indices, sizeof(ufbx_topo_edge), &ufbxi_topo_less_index_prev_next, NULL); + + if (mesh->edges.data) { + for (uint32_t ei = 0; ei < mesh->num_edges; ei++) { + ufbx_edge edge = mesh->edges.data[ei]; + uint32_t va = mesh->vertex_indices.data[edge.a]; + uint32_t vb = mesh->vertex_indices.data[edge.b]; + if (vb < va) { + uint32_t vt = va; va = vb; vb = vt; + } + + size_t ix = num_indices; + ufbxi_macro_lower_bound_eq(ufbx_topo_edge, 32, &ix, topo, 0, num_indices, + (a->prev == va ? a->next < vb : a->prev < va), (a->prev == va && a->next == vb)); + + for (; ix < num_indices && topo[ix].prev == va && topo[ix].next == vb; ix++) { + topo[ix].edge = ei; + } + } + } + + // Connect paired edges + for (size_t i0 = 0; i0 < num_indices; ) { + size_t i1 = i0; + + uint32_t a = topo[i0].prev, b = topo[i0].next; + while (i1 + 1 < num_indices && topo[i1 + 1].prev == a && topo[i1 + 1].next == b) i1++; + + if (i1 == i0 + 1) { + topo[i0].twin = topo[i1].index; + topo[i1].twin = topo[i0].index; + } else if (i1 > i0 + 1) { + for (size_t i = i0; i <= i1; i++) { + topo[i].flags = (ufbx_topo_flags)(topo[i].flags | UFBX_TOPO_NON_MANIFOLD); + } + } + + i0 = i1 + 1; + } + + ufbxi_unstable_sort(topo, num_indices, sizeof(ufbx_topo_edge), &ufbxi_topo_less_index_index, NULL); + + // Fix `prev` and `next` to the actual index values + for (uint32_t fi = 0; fi < mesh->num_faces; fi++) { + ufbx_face face = mesh->faces.data[fi]; + for (uint32_t i = 0; i < face.num_indices; i++) { + ufbx_topo_edge *to = &topo[face.index_begin + i]; + to->prev = (uint32_t)(face.index_begin + (i + face.num_indices - 1) % face.num_indices); + to->next = (uint32_t)(face.index_begin + (i + 1) % face.num_indices); + } + } +} + +static bool ufbxi_is_edge_smooth(const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index, bool assume_smooth) +{ + ufbxi_ignore(num_topo); + ufbx_assert((size_t)index < num_topo); + if (mesh->edge_smoothing.data) { + uint32_t edge = topo[index].edge; + if (edge != UFBX_NO_INDEX && mesh->edge_smoothing.data[edge]) return true; + } + + if (mesh->face_smoothing.data) { + if (mesh->face_smoothing.data[topo[index].face]) return true; + uint32_t twin = topo[index].twin; + if (twin != UFBX_NO_INDEX) { + if (mesh->face_smoothing.data[topo[twin].face]) return true; + } + } + + if (!mesh->edge_smoothing.data && !mesh->face_smoothing.data && mesh->vertex_normal.exists) { + uint32_t twin = topo[index].twin; + if (twin != UFBX_NO_INDEX && mesh->vertex_normal.exists) { + ufbx_assert((size_t)twin < num_topo); + ufbx_vec3 a0 = ufbx_get_vertex_vec3(&mesh->vertex_normal, index); + ufbx_vec3 a1 = ufbx_get_vertex_vec3(&mesh->vertex_normal, topo[index].next); + ufbx_vec3 b0 = ufbx_get_vertex_vec3(&mesh->vertex_normal, topo[twin].next); + ufbx_vec3 b1 = ufbx_get_vertex_vec3(&mesh->vertex_normal, twin); + if (a0.x == b0.x && a0.y == b0.y && a0.z == b0.z) return true; + if (a1.x == b1.x && a1.y == b1.y && a1.z == b1.z) return true; + } + } else if (assume_smooth) { + return true; + } + + return false; +} + +// -- Subdivision + +#if UFBXI_FEATURE_SUBDIVISION + +typedef struct { + const void *data; + ufbx_real weight; +} ufbxi_subdivide_input; + +typedef int ufbxi_subdivide_sum_fn(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs); + +typedef struct { + ufbxi_subdivide_sum_fn *sum_fn; + void *sum_user; + + const void *values; + size_t stride; + + const uint32_t *indices; + + bool check_split_data; + bool ignore_indices; + + ufbx_subdivision_boundary boundary; + +} ufbxi_subdivide_layer_input; + +typedef struct { + void *values; + size_t num_values; + uint32_t *indices; + size_t num_indices; + bool unique_per_vertex; +} ufbxi_subdivide_layer_output; + +typedef struct { + ufbx_subdivision_weight *weights; + size_t num_weights; +} ufbxi_subdivision_vertex_weights; + +typedef struct { + ufbxi_mesh_imp *imp; + + ufbx_error error; + + ufbx_mesh *src_mesh_ptr; + ufbx_mesh src_mesh; + ufbx_mesh dst_mesh; + ufbx_topo_edge *topo; + size_t num_topo; + + ufbx_subdivide_opts opts; + + ufbxi_allocator ator_result; + ufbxi_allocator ator_tmp; + + ufbxi_buf result; + ufbxi_buf tmp; + ufbxi_buf source; + + ufbxi_subdivide_input *inputs; + size_t inputs_cap; + + ufbx_real *tmp_vertex_weights; + ufbx_subdivision_weight *tmp_weights; + size_t total_weights; + size_t max_vertex_weights; + +} ufbxi_subdivide_context; + +static int ufbxi_subdivide_sum_vec2(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) +{ + (void)user; + ufbx_vec2 dst = { 0 }; + ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) { + const ufbx_vec2 *src = (const ufbx_vec2*)inputs[i].data; + ufbx_real weight = inputs[i].weight; + dst.x += src->x * weight; + dst.y += src->y * weight; + } + *(ufbx_vec2*)output = dst; + + return 1; +} + +static int ufbxi_subdivide_sum_vec3(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) +{ + (void)user; + ufbx_vec3 dst = { 0 }; + ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) { + const ufbx_vec3 *src = (const ufbx_vec3*)inputs[i].data; + ufbx_real weight = inputs[i].weight; + dst.x += src->x * weight; + dst.y += src->y * weight; + dst.z += src->z * weight; + } + *(ufbx_vec3*)output = dst; + + return 1; +} + +static int ufbxi_subdivide_sum_vec4(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) +{ + (void)user; + ufbx_vec4 dst = { 0 }; + ufbxi_nounroll for (size_t i = 0; i != num_inputs; i++) { + const ufbx_vec4 *src = (const ufbx_vec4*)inputs[i].data; + ufbx_real weight = inputs[i].weight; + dst.x += src->x * weight; + dst.y += src->y * weight; + dst.z += src->z * weight; + dst.w += src->w * weight; + } + *(ufbx_vec4*)output = dst; + + return 1; +} + +static ufbxi_noinline bool ufbxi_subdivision_weight_less(void *user, const void *va, const void *vb) +{ + (void)user; + ufbx_subdivision_weight a = *(const ufbx_subdivision_weight*)va, b = *(const ufbx_subdivision_weight*)vb; + ufbxi_dev_assert(a.index != b.index); + if (a.weight != b.weight) return a.weight > b.weight; + return a.index < b.index; +} + +static int ufbxi_subdivide_sum_vertex_weights(void *user, void *output, const ufbxi_subdivide_input *inputs, size_t num_inputs) +{ + ufbxi_subdivide_context *sc = (ufbxi_subdivide_context*)user; + + ufbx_real *vertex_weights = sc->tmp_vertex_weights; + ufbx_subdivision_weight *tmp_weights = sc->tmp_weights; + size_t num_weights = 0; + + ufbxi_nounroll for (size_t input_ix = 0; input_ix != num_inputs; input_ix++) { + ufbxi_subdivision_vertex_weights src = *(const ufbxi_subdivision_vertex_weights*)inputs[input_ix].data; + ufbx_real input_weight = inputs[input_ix].weight; + + for (size_t weight_ix = 0; weight_ix < src.num_weights; weight_ix++) { + ufbx_real weight = input_weight * src.weights[weight_ix].weight; + if (weight < 1.175494351e-38f) continue; + + uint32_t vx = src.weights[weight_ix].index; + ufbxi_dev_assert(vx < sc->src_mesh.num_vertices); + + ufbx_real prev = vertex_weights[vx]; + vertex_weights[vx] = prev + weight; + if (prev == 0.0f) { + tmp_weights[num_weights++].index = vx; + } + } + } + + ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) { + uint32_t vx = tmp_weights[i].index; + tmp_weights[i].weight = vertex_weights[vx]; + vertex_weights[vx] = 0.0f; + } + + ufbxi_unstable_sort(tmp_weights, num_weights, sizeof(ufbx_subdivision_weight), ufbxi_subdivision_weight_less, NULL); + + if (sc->max_vertex_weights != SIZE_MAX) { + num_weights = ufbxi_min_sz(sc->max_vertex_weights, num_weights); + + // Normalize weights + ufbx_real prefix_weight = 0.0f; + ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) { + prefix_weight += tmp_weights[i].weight; + } + ufbxi_nounroll for (size_t i = 0; i != num_weights; i++) { + tmp_weights[i].weight /= prefix_weight; + } + } + + sc->total_weights += num_weights; + ufbx_subdivision_weight *weights = ufbxi_push_copy(&sc->tmp, ufbx_subdivision_weight, num_weights, tmp_weights); + ufbxi_check_err(&sc->error, weights); + + ufbxi_subdivision_vertex_weights *dst = (ufbxi_subdivision_vertex_weights*)output; + dst->weights = weights; + dst->num_weights = num_weights; + + return 1; +} + +static ufbxi_subdivide_sum_fn *const ufbxi_real_sum_fns[] = { + NULL, + &ufbxi_subdivide_sum_vec2, + &ufbxi_subdivide_sum_vec3, + &ufbxi_subdivide_sum_vec4, +}; + +ufbxi_noinline static bool ufbxi_is_edge_split(const ufbxi_subdivide_layer_input *input, const ufbx_topo_edge *topo, uint32_t index) +{ + uint32_t twin = topo[index].twin; + if (twin != UFBX_NO_INDEX) { + uint32_t a0 = input->indices[index]; + uint32_t a1 = input->indices[topo[index].next]; + uint32_t b0 = input->indices[topo[twin].next]; + uint32_t b1 = input->indices[twin]; + if (a0 == b0 && a1 == b1) return false; + if (!input->check_split_data) return true; + size_t stride = input->stride; + char *da0 = (char*)input->values + a0 * stride; + char *da1 = (char*)input->values + a1 * stride; + char *db0 = (char*)input->values + b0 * stride; + char *db1 = (char*)input->values + b1 * stride; + if (!memcmp(da0, db0, stride) && !memcmp(da1, db1, stride)) return false; + return true; + } + + return false; +} + +static ufbx_real ufbxi_edge_crease(const ufbx_mesh *mesh, bool split, const ufbx_topo_edge *topo, uint32_t index) +{ + if (topo[index].twin == UFBX_NO_INDEX) return 1.0f; + if (split) return 1.0f; + if (mesh->edge_crease.data && topo[index].edge != UFBX_NO_INDEX) return mesh->edge_crease.data[topo[index].edge] * (ufbx_real)10.0; + return 0.0f; +} + +static ufbxi_noinline int ufbxi_subdivide_layer(ufbxi_subdivide_context *sc, ufbxi_subdivide_layer_output *output, const ufbxi_subdivide_layer_input *input) +{ + ufbx_subdivision_boundary boundary = input->boundary; + + const ufbx_mesh *mesh = &sc->src_mesh; + const ufbx_topo_edge *topo = sc->topo; + size_t num_topo = sc->num_topo; + + uint32_t *edge_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices); + ufbxi_check_err(&sc->error, edge_indices); + + size_t num_edge_values = 0; + for (uint32_t ix = 0; ix < (uint32_t)mesh->num_indices; ix++) { + uint32_t twin = topo[ix].twin; + if (twin < ix && !ufbxi_is_edge_split(input, topo, ix)) { + edge_indices[ix] = edge_indices[twin]; + } else { + edge_indices[ix] = (uint32_t)num_edge_values++; + } + } + + size_t stride = input->stride; + size_t num_initial_values = (num_edge_values + mesh->num_faces + mesh->num_indices); + char *values = (char*)ufbxi_push_size(&sc->tmp, stride, num_initial_values); + ufbxi_check_err(&sc->error, values); + + char *face_values = values; + char *edge_values = face_values + mesh->num_faces * stride; + char *vertex_values = edge_values + num_edge_values * stride; + + size_t num_vertex_values = 0; + + uint32_t *vertex_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices); + ufbxi_check_err(&sc->error, vertex_indices); + + size_t min_inputs = ufbxi_max_sz(32, mesh->max_face_triangles + 2); + ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, min_inputs)); + ufbxi_subdivide_input *inputs = sc->inputs; + + // Assume initially unique per vertex, remove if not the case + output->unique_per_vertex = true; + + bool sharp_corners = false; + bool sharp_splits = false; + bool sharp_all = false; + + switch (boundary) { + case UFBX_SUBDIVISION_BOUNDARY_DEFAULT: + case UFBX_SUBDIVISION_BOUNDARY_SHARP_NONE: + case UFBX_SUBDIVISION_BOUNDARY_LEGACY: + // All smooth + break; + case UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS: + sharp_corners = true; + break; + case UFBX_SUBDIVISION_BOUNDARY_SHARP_BOUNDARY: + sharp_corners = true; + sharp_splits = true; + break; + case UFBX_SUBDIVISION_BOUNDARY_SHARP_INTERIOR: + sharp_all = true; + break; + default: + ufbxi_unreachable("Bad boundary mode"); + } + + ufbxi_subdivide_sum_fn *sum_fn = input->sum_fn; + void *sum_user = input->sum_user; + + // Mark unused indices as `UFBX_NO_INDEX` so we can patch non-manifold + ufbxi_nounroll for (size_t i = 0; i < mesh->num_indices; i++) { + vertex_indices[i] = UFBX_NO_INDEX; + } + + // Face points + for (size_t fi = 0; fi < mesh->num_faces; fi++) { + ufbx_face face = mesh->faces.data[fi]; + char *dst = face_values + fi * stride; + + ufbx_real weight = 1.0f / (ufbx_real)face.num_indices; + for (uint32_t ci = 0; ci < face.num_indices; ci++) { + uint32_t ix = face.index_begin + ci; + inputs[ci].data = (const char*)input->values + input->indices[ix] * stride; + inputs[ci].weight = weight; + } + + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, face.num_indices)); + } + + // Edge points + for (uint32_t ix = 0; ix < mesh->num_indices; ix++) { + char *dst = edge_values + edge_indices[ix] * stride; + + uint32_t twin = topo[ix].twin; + bool split = ufbxi_is_edge_split(input, topo, ix); + + if (split || (topo[ix].flags & UFBX_TOPO_NON_MANIFOLD) != 0) { + output->unique_per_vertex = false; + } + + ufbx_real crease = 0.0f; + if (split || twin == UFBX_NO_INDEX) { + crease = 1.0f; + } else if (topo[ix].edge != UFBX_NO_INDEX && mesh->edge_crease.data) { + crease = mesh->edge_crease.data[topo[ix].edge] * (ufbx_real)10.0; + } + if (sharp_all) crease = 1.0f; + + const char *v0 = (const char*)input->values + input->indices[ix] * stride; + const char *v1 = (const char*)input->values + input->indices[topo[ix].next] * stride; + + // TODO: Unify + if (twin < ix && !split) { + // Already calculated + } else if (crease <= 0.0f) { + const char *f0 = face_values + topo[ix].face * stride; + const char *f1 = face_values + topo[twin].face * stride; + inputs[0].data = v0; + inputs[0].weight = 0.25f; + inputs[1].data = v1; + inputs[1].weight = 0.25f; + inputs[2].data = f0; + inputs[2].weight = 0.25f; + inputs[3].data = f1; + inputs[3].weight = 0.25f; + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 4)); + } else if (crease >= 1.0f) { + inputs[0].data = v0; + inputs[0].weight = 0.5f; + inputs[1].data = v1; + inputs[1].weight = 0.5f; + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 2)); + } else if (crease < 1.0f) { + const char *f0 = face_values + topo[ix].face * stride; + const char *f1 = face_values + topo[twin].face * stride; + ufbx_real w0 = 0.25f + 0.25f * crease; + ufbx_real w1 = 0.25f - 0.25f * crease; + + inputs[0].data = v0; + inputs[0].weight = w0; + inputs[1].data = v1; + inputs[1].weight = w0; + inputs[2].data = f0; + inputs[2].weight = w1; + inputs[3].data = f1; + inputs[3].weight = w1; + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 4)); + } + } + + // Vertex points + for (size_t vi = 0; vi < mesh->num_vertices; vi++) { + uint32_t original_start = mesh->vertex_first_index.data[vi]; + if (original_start == UFBX_NO_INDEX) continue; + + // Find a topological boundary, or if not found a split edge + uint32_t start = original_start; + for (uint32_t cur = start;;) { + uint32_t prev = ufbx_topo_prev_vertex_edge(topo, num_topo, cur); + if (prev == UFBX_NO_INDEX) { start = cur; break; } // Topological boundary: Stop and use as start + if (ufbxi_is_edge_split(input, topo, prev)) start = cur; // Split edge: Consider as start + if (prev == original_start) break; // Loop: Stop, use original start or split if found + cur = prev; + } + + original_start = start; + while (start != UFBX_NO_INDEX) { + if (start != original_start) { + output->unique_per_vertex = false; + } + + uint32_t value_index = (uint32_t)num_vertex_values++; + char *dst = vertex_values + value_index * stride; + + // We need to compute the average crease value and keep track of + // two creased edges, if there's more we use the corner rule that + // does not need the information. + ufbx_real total_crease = 0.0f; + size_t num_crease = 0; + size_t num_split = 0; + bool on_boundary = false; + bool non_manifold = false; + size_t crease_input_indices[2]; // ufbxi_uninit + + // At start we always have two edges and a single face + uint32_t start_prev = topo[start].prev; + uint32_t end_edge = topo[start_prev].twin; + size_t valence = 2; + + non_manifold |= (topo[start].flags & UFBX_TOPO_NON_MANIFOLD) != 0; + non_manifold |= (topo[start_prev].flags & UFBX_TOPO_NON_MANIFOLD) != 0; + + const char *v0 = (const char*)input->values + input->indices[start] * stride; + + size_t num_inputs = 4; + + { + const char *e0 = (const char*)input->values + input->indices[topo[start].next] * stride; + const char *e1 = (const char*)input->values + input->indices[start_prev] * stride; + const char *f0 = face_values + topo[start].face * stride; + inputs[0].data = v0; + inputs[1].data = e0; + inputs[2].data = e1; + inputs[3].data = f0; + } + + bool start_split = ufbxi_is_edge_split(input, topo, start); + bool prev_split = end_edge != UFBX_NO_INDEX && ufbxi_is_edge_split(input, topo, end_edge); + + // Either of the first two edges may be creased + ufbx_real start_crease = ufbxi_edge_crease(mesh, start_split, topo, start); + if (start_crease > 0.0f) { + total_crease += start_crease; + crease_input_indices[num_crease++] = 1; + } + ufbx_real prev_crease = ufbxi_edge_crease(mesh, prev_split, topo, start_prev); + if (prev_crease > 0.0f) { + total_crease += prev_crease; + crease_input_indices[num_crease++] = 2; + } + + if (end_edge != UFBX_NO_INDEX) { + if (prev_split) { + num_split++; + } + } else { + on_boundary = true; + } + + ufbxi_check_err(&sc->error, vertex_indices[start] == UFBX_NO_INDEX); + vertex_indices[start] = value_index; + + if (start_split) { + // We need to special case if the first edge is split as we have + // handled it already in the code above.. + start = ufbx_topo_next_vertex_edge(topo, num_topo, start); + num_split++; + } else { + // Follow vertex edges until we either hit a topological/split boundary + // or loop back to the left edge we accounted for in `start_prev` + uint32_t cur = start; + for (;;) { + cur = ufbx_topo_next_vertex_edge(topo, num_topo, cur); + + // Topological boundary: Finished + if (cur == UFBX_NO_INDEX) { + on_boundary = true; + start = UFBX_NO_INDEX; + break; + } + + non_manifold |= (topo[cur].flags & UFBX_TOPO_NON_MANIFOLD) != 0; + ufbxi_check_err(&sc->error, vertex_indices[cur] == UFBX_NO_INDEX); + vertex_indices[cur] = value_index; + + bool split = ufbxi_is_edge_split(input, topo, cur); + + // Looped: Add the face from the other side still if not split + if (cur == end_edge && !split) { + ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, num_inputs + 1)); + const char *f0 = face_values + topo[cur].face * stride; + inputs[num_inputs].data = f0; + start = UFBX_NO_INDEX; + num_inputs += 1; + break; + } + + // Add the edge crease, this also handles boundaries as they + // have an implicit crease of 1.0 using `ufbxi_edge_crease()` + ufbx_real cur_crease = ufbxi_edge_crease(mesh, split, topo, cur); + if (cur_crease > 0.0f) { + total_crease += cur_crease; + if (num_crease < 2) crease_input_indices[num_crease] = num_inputs; + num_crease++; + } + + // Add the new edge and face to the sum + { + ufbxi_check_err(&sc->error, ufbxi_grow_array(&sc->ator_tmp, &sc->inputs, &sc->inputs_cap, num_inputs + 2)); + inputs = sc->inputs; + + const char *e0 = (char*)input->values + input->indices[topo[cur].next] * stride; + const char *f0 = face_values + topo[cur].face * stride; + inputs[num_inputs + 0].data = e0; + inputs[num_inputs + 1].data = f0; + num_inputs += 2; + } + valence++; + + // If we landed at a split edge advance to the next one + // and continue from there in the outer loop + if (split) { + start = ufbx_topo_next_vertex_edge(topo, num_topo, cur); + num_split++; + break; + } + } + } + + if (start == original_start) start = UFBX_NO_INDEX; + + // Weights for various subdivision masks + ufbx_real fe_weight = 1.0f / (ufbx_real)(valence*valence); + ufbx_real v_weight = (ufbx_real)(valence - 2) / (ufbx_real)valence; + + // Select the right subdivision mask depending on valence and crease + if (num_crease > 2 + || (sharp_corners && valence == 2 && (num_split > 0 || on_boundary)) + || (sharp_splits && (num_split > 0 || on_boundary)) + || sharp_all + || non_manifold) { + // Corner: Copy as-is + inputs[0].data = v0; + inputs[0].weight = 1.0f; + num_inputs = 1; + } else if (num_crease == 2) { + // Boundary: Interpolate edge + total_crease *= 0.5f; + if (total_crease < 0.0f) total_crease = 0.0f; + if (total_crease > 1.0f) total_crease = 1.0f; + + inputs[0].weight = v_weight * (1.0f - total_crease) + 0.75f * total_crease; + ufbx_real few = fe_weight * (1.0f - total_crease); + for (size_t i = 1; i < num_inputs; i++) { + inputs[i].weight = few; + } + + // Add weight to the creased edges + inputs[crease_input_indices[0]].weight += 0.125f * total_crease; + inputs[crease_input_indices[1]].weight += 0.125f * total_crease; + } else { + // Regular: Weighted sum with the accumulated edge/face points + inputs[0].weight = v_weight; + for (size_t i = 1; i < num_inputs; i++) { + inputs[i].weight = fe_weight; + } + + } + + if (mesh->vertex_crease.exists) { + ufbx_real v = ufbx_get_vertex_real(&mesh->vertex_crease, original_start); + v *= (ufbx_real)10.0; + if (v > 0.0f) { + if (v > 1.0) v = 1.0f; + + ufbx_real iv = 1.0f - v; + inputs[0].weight = 1.0f * v + (inputs[0].weight) * iv; + for (size_t i = 1; i < num_inputs; i++) { + inputs[i].weight *= iv; + } + } + } + +#if defined(UFBX_REGRESSION) + { + ufbx_real total_weight = 0.0f; + for (size_t i = 0; i < num_inputs; i++) { + total_weight += inputs[i].weight; + } + ufbx_assert(ufbx_fabs(total_weight - 1.0f) < 0.001f); + } +#endif + + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, num_inputs)); + } + } + + // Copy non-manifold vertex values as-is + for (size_t old_ix = 0; old_ix < mesh->num_indices; old_ix++) { + uint32_t ix = vertex_indices[old_ix]; + if (ix == UFBX_NO_INDEX) { + ix = (uint32_t)num_vertex_values++; + vertex_indices[old_ix] = ix; + const char *src = (const char*)input->values + input->indices[old_ix] * stride; + char *dst = vertex_values + ix * stride; + + inputs[0].data = src; + inputs[0].weight = 1.0f; + ufbxi_check_err(&sc->error, sum_fn(sum_user, dst, inputs, 1)); + } + } + + ufbx_assert(num_vertex_values <= mesh->num_indices); + size_t num_values = num_edge_values + mesh->num_faces + num_vertex_values; + char *new_values = (char*)ufbxi_push_size(&sc->result, stride, (num_values+1)); + ufbxi_check_err(&sc->error, new_values); + + memset(new_values, 0, stride); + new_values += stride; + + memcpy(new_values, values, num_values * stride); + + output->values = new_values; + output->num_values = num_values; + + if (!input->ignore_indices) { + uint32_t *new_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices * 4); + ufbxi_check_err(&sc->error, new_indices); + + uint32_t face_start = 0; + uint32_t edge_start = (uint32_t)(face_start + mesh->num_faces); + uint32_t vert_start = (uint32_t)(edge_start + num_edge_values); + uint32_t *p_ix = new_indices; + for (size_t ix = 0; ix < mesh->num_indices; ix++) { + p_ix[0] = vert_start + vertex_indices[ix]; + p_ix[1] = edge_start + edge_indices[ix]; + p_ix[2] = face_start + topo[ix].face; + p_ix[3] = edge_start + edge_indices[topo[ix].prev]; + p_ix += 4; + } + output->indices = new_indices; + output->num_indices = mesh->num_indices * 4; + } else { + output->indices = NULL; + output->num_indices = 0; + } + + return 1; +} + +static ufbxi_noinline int ufbxi_subdivide_attrib(ufbxi_subdivide_context *sc, ufbx_vertex_attrib *attrib, ufbx_subdivision_boundary boundary, bool check_split_data) +{ + if (!attrib->exists) return 1; + + ufbx_assert(attrib->value_reals >= 2 && attrib->value_reals <= 4); + + ufbxi_subdivide_layer_input input; // ufbxi_uninit + input.sum_fn = ufbxi_real_sum_fns[attrib->value_reals - 1]; + input.sum_user = NULL; + input.values = attrib->values.data; + input.indices = attrib->indices.data; + input.stride = attrib->value_reals * sizeof(ufbx_real); + input.boundary = boundary; + input.check_split_data = check_split_data; + input.ignore_indices = false; + + ufbxi_subdivide_layer_output output; // ufbxi_uninit + ufbxi_check_err(&sc->error, ufbxi_subdivide_layer(sc, &output, &input)); + + attrib->values.data = output.values; + attrib->indices.data = output.indices; + attrib->values.count = output.num_values; + attrib->indices.count = output.num_indices; + + return 1; +} + +static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_subdivision_copy_weights(ufbxi_subdivide_context *sc, ufbx_subdivision_weight_range_list ranges, ufbx_subdivision_weight_list weights) +{ + ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, ranges.count); + ufbxi_check_return_err(&sc->error, dst, NULL); + + ufbxi_nounroll for (size_t i = 0; i != ranges.count; i++) { + ufbx_subdivision_weight_range range = ranges.data[i]; + dst[i].weights = weights.data + range.weight_begin; + dst[i].num_weights = range.num_weights; + } + + return dst; +} + +static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_init_source_vertex_weights(ufbxi_subdivide_context *sc, size_t num_vertices) +{ + ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, num_vertices); + ufbx_subdivision_weight *weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, num_vertices); + ufbxi_check_return_err(&sc->error, dst && weights, NULL); + + ufbxi_nounroll for (size_t i = 0; i != num_vertices; i++) { + dst[i].weights = weights + i; + dst[i].num_weights = 1; + weights[i].index = (uint32_t)i; + weights[i].weight = 1.0f; + } + + return dst; +} + +static ufbxi_noinline ufbxi_subdivision_vertex_weights *ufbxi_init_skin_weights(ufbxi_subdivide_context *sc, size_t num_vertices, const ufbx_skin_deformer *skin) +{ + ufbxi_subdivision_vertex_weights *dst = ufbxi_push(&sc->tmp, ufbxi_subdivision_vertex_weights, num_vertices); + ufbxi_check_return_err(&sc->error, dst, NULL); + + for (size_t i = 0; i < num_vertices; i++) { + ufbxi_dev_assert(i < skin->vertices.count); + ufbx_skin_vertex vertex = skin->vertices.data[i]; + size_t num_weights = ufbxi_min_sz(sc->max_vertex_weights, vertex.num_weights); + + ufbx_subdivision_weight *weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, num_weights); + ufbxi_check_err(&sc->error, weights); + + const ufbx_skin_weight *skin_weights = skin->weights.data + vertex.weight_begin; + + dst[i].weights = weights; + dst[i].num_weights = num_weights; + ufbxi_nounroll for (size_t wi = 0; wi != num_weights; wi++) { + ufbxi_check_err(&sc->error, skin_weights[wi].cluster_index <= INT32_MAX); + weights[wi].index = skin_weights[wi].cluster_index; + weights[wi].weight = skin_weights[wi].weight; + } + } + + return dst; +} + +static ufbxi_noinline int ufbxi_subdivide_weights(ufbxi_subdivide_context *sc, ufbx_subdivision_weight_range_list *ranges, + ufbx_subdivision_weight_list *weights, const ufbxi_subdivision_vertex_weights *src) +{ + ufbxi_check_err(&sc->error, src); + + ufbxi_subdivide_layer_input input; // ufbxi_uninit + input.sum_fn = ufbxi_subdivide_sum_vertex_weights; + input.sum_user = sc; + input.values = src; + input.indices = sc->src_mesh.vertex_indices.data; + input.stride = sizeof(ufbxi_subdivision_vertex_weights); + input.boundary = sc->opts.boundary; + input.check_split_data = false; + input.ignore_indices = true; + + sc->total_weights = 0; + + ufbxi_subdivide_layer_output output; // ufbxi_uninit + ufbxi_check_err(&sc->error, ufbxi_subdivide_layer(sc, &output, &input)); + + size_t num_vertices = output.num_values; + ufbx_assert(num_vertices == sc->dst_mesh.vertex_position.values.count); + + ufbx_subdivision_weight_range *dst_ranges = ufbxi_push(&sc->result, ufbx_subdivision_weight_range, num_vertices); + ufbx_subdivision_weight *dst_weights = ufbxi_push(&sc->result, ufbx_subdivision_weight, sc->total_weights); + ufbxi_check_err(&sc->error, ranges && weights); + + ufbxi_subdivision_vertex_weights *src_weights = (ufbxi_subdivision_vertex_weights*)output.values; + + size_t weight_offset = 0; + for (size_t vi = 0; vi < num_vertices; vi++) { + ufbxi_subdivision_vertex_weights ws = src_weights[vi]; + ufbxi_check_err(&sc->error, (size_t)UINT32_MAX - weight_offset >= ws.num_weights); + + dst_ranges[vi].weight_begin = (uint32_t)weight_offset; + dst_ranges[vi].num_weights = (uint32_t)ws.num_weights; + memcpy(dst_weights + weight_offset, ws.weights, ws.num_weights * sizeof(ufbx_subdivision_weight)); + weight_offset += ws.num_weights; + } + + ranges->data = dst_ranges; + ranges->count = num_vertices; + weights->data = dst_weights; + weights->count = sc->total_weights; + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_vertex_crease(ufbxi_subdivide_context *sc, ufbx_vertex_real *ufbxi_restrict dst, const ufbx_vertex_real *ufbxi_restrict src) +{ + size_t src_indices = src->indices.count; + size_t src_values = src->values.count; + + dst->values.count = src_values + 1; + dst->values.data = ufbxi_push(&sc->result, ufbx_real, dst->values.count); + ufbxi_check_err(&sc->error, dst->values.data); + dst->values.data[src_values] = 0.0f; + + dst->indices.count = src_indices * 4; + dst->indices.data = ufbxi_push(&sc->result, uint32_t, dst->indices.count); + ufbxi_check_err(&sc->error, dst->indices.data); + + // Reduce the amount of vertex crease on each iteration + ufbxi_nounroll for (size_t i = 0; i < src_values; i++) { + ufbx_real crease = src->values.data[i]; + if (crease < 0.999f) crease -= 0.1f; + if (crease < 0.0f) crease = 0.0f; + dst->values.data[i] = crease; + } + + // Write the crease at the vertex corner and zero (at `src_values`) on other ones + uint32_t zero_index = (uint32_t)src_values; + ufbxi_nounroll for (size_t i = 0; i < src_indices; i++) { + uint32_t *quad = dst->indices.data + i * 4; + quad[0] = src->indices.data[i]; + quad[1] = zero_index; + quad[2] = zero_index; + quad[3] = zero_index; + } + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_level(ufbxi_subdivide_context *sc) +{ + const ufbx_mesh *mesh = &sc->src_mesh; + ufbx_mesh *result = &sc->dst_mesh; + + *result = *mesh; + + ufbx_topo_edge *topo = ufbxi_push(&sc->tmp, ufbx_topo_edge, mesh->num_indices); + ufbxi_check_err(&sc->error, topo); + ufbx_compute_topology(mesh, topo, mesh->num_indices); + sc->topo = topo; + sc->num_topo = mesh->num_indices; + + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->vertex_position, sc->opts.boundary, false)); + + memset(&result->vertex_uv, 0, sizeof(result->vertex_uv)); + memset(&result->vertex_tangent, 0, sizeof(result->vertex_tangent)); + memset(&result->vertex_bitangent, 0, sizeof(result->vertex_bitangent)); + memset(&result->vertex_color, 0, sizeof(result->vertex_color)); + + result->uv_sets.data = ufbxi_push_copy(&sc->result, ufbx_uv_set, result->uv_sets.count, result->uv_sets.data); + ufbxi_check_err(&sc->error, result->uv_sets.data); + + result->color_sets.data = ufbxi_push_copy(&sc->result, ufbx_color_set, result->color_sets.count, result->color_sets.data); + ufbxi_check_err(&sc->error, result->color_sets.data); + + ufbxi_for_list(ufbx_uv_set, set, result->uv_sets) { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_uv, sc->opts.uv_boundary, true)); + if (sc->opts.interpolate_tangents) { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_tangent, sc->opts.uv_boundary, true)); + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_bitangent, sc->opts.uv_boundary, true)); + } else { + memset(&set->vertex_tangent, 0, sizeof(set->vertex_tangent)); + memset(&set->vertex_bitangent, 0, sizeof(set->vertex_bitangent)); + } + } + + ufbxi_for_list(ufbx_color_set, set, result->color_sets) { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&set->vertex_color, sc->opts.uv_boundary, true)); + } + + if (result->uv_sets.count > 0) { + result->vertex_uv = result->uv_sets.data[0].vertex_uv; + result->vertex_bitangent = result->uv_sets.data[0].vertex_bitangent; + result->vertex_tangent = result->uv_sets.data[0].vertex_tangent; + } + if (result->color_sets.count > 0) { + result->vertex_color = result->color_sets.data[0].vertex_color; + } + + if (sc->opts.interpolate_normals && !sc->opts.ignore_normals) { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->vertex_normal, sc->opts.boundary, true)); + ufbxi_for_list(ufbx_vec3, normal, result->vertex_normal.values) { + *normal = ufbxi_slow_normalize3(normal); + } + if (mesh->skinned_normal.values.data == mesh->vertex_normal.values.data) { + result->skinned_normal = result->vertex_normal; + } else { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->skinned_normal, sc->opts.boundary, true)); + ufbxi_for_list(ufbx_vec3, normal, result->skinned_normal.values) { + *normal = ufbxi_slow_normalize3(normal); + } + } + } + + if (result->vertex_crease.exists) { + ufbxi_check_err(&sc->error, ufbxi_subdivide_vertex_crease(sc, &result->vertex_crease, &mesh->vertex_crease)); + } + + if (mesh->skinned_position.values.data == mesh->vertex_position.values.data) { + result->skinned_position = result->vertex_position; + } else { + ufbxi_check_err(&sc->error, ufbxi_subdivide_attrib(sc, (ufbx_vertex_attrib*)&result->skinned_position, sc->opts.boundary, false)); + } + + ufbx_subdivision_result *result_sub = ufbxi_push_zero(&sc->result, ufbx_subdivision_result, 1); + ufbxi_check_err(&sc->error, result_sub); + result->subdivision_result = result_sub; + + if (sc->opts.evaluate_source_vertices || sc->opts.evaluate_skin_weights) { + ufbx_subdivision_result *mesh_sub = mesh->subdivision_result; + + ufbx_skin_deformer *skin = NULL; + if (sc->opts.evaluate_skin_weights) { + if (mesh->skin_deformers.count > 0) { + ufbxi_check_err(&sc->error, sc->opts.skin_deformer_index < mesh->skin_deformers.count); + skin = mesh->skin_deformers.data[sc->opts.skin_deformer_index]; + } + } + + size_t max_weights = 0; + if (sc->opts.evaluate_source_vertices) { + max_weights = ufbxi_max_sz(max_weights, mesh->num_vertices); + } + if (skin) { + max_weights = ufbxi_max_sz(max_weights, skin->clusters.count); + } + + sc->tmp_vertex_weights = ufbxi_push_zero(&sc->tmp, ufbx_real, mesh->num_vertices); + sc->tmp_weights = ufbxi_push(&sc->tmp, ufbx_subdivision_weight, max_weights); + ufbxi_check_err(&sc->error, sc->tmp_vertex_weights && sc->tmp_weights); + + if (sc->opts.evaluate_source_vertices) { + sc->max_vertex_weights = sc->opts.max_source_vertices ? sc->opts.max_source_vertices : SIZE_MAX; + + ufbxi_subdivision_vertex_weights *weights; + if (mesh_sub && mesh_sub->source_vertex_ranges.count > 0) { + weights = ufbxi_subdivision_copy_weights(sc, mesh_sub->source_vertex_ranges, mesh_sub->source_vertex_weights); + } else { + weights = ufbxi_init_source_vertex_weights(sc, mesh->num_vertices); + } + + ufbxi_check_err(&sc->error, ufbxi_subdivide_weights(sc, &result_sub->source_vertex_ranges, &result_sub->source_vertex_weights, weights)); + } + + if (skin) { + sc->max_vertex_weights = sc->opts.max_skin_weights ? sc->opts.max_skin_weights : SIZE_MAX; + + ufbxi_subdivision_vertex_weights *weights; + if (mesh_sub && mesh_sub->source_vertex_ranges.count > 0) { + weights = ufbxi_subdivision_copy_weights(sc, mesh_sub->skin_cluster_ranges, mesh_sub->skin_cluster_weights); + } else { + weights = ufbxi_init_skin_weights(sc, mesh->num_vertices, skin); + } + + ufbxi_check_err(&sc->error, ufbxi_subdivide_weights(sc, &result_sub->skin_cluster_ranges, &result_sub->skin_cluster_weights, weights)); + } + + } + + result->num_vertices = result->vertex_position.values.count; + result->num_indices = mesh->num_indices * 4; + result->num_faces = mesh->num_indices; + result->num_triangles = mesh->num_indices * 2; + + result->vertex_indices.data = result->vertex_position.indices.data; + result->vertex_indices.count = result->num_indices; + result->vertices.data = result->vertex_position.values.data; + result->vertices.count = result->num_vertices; + + result->faces.count = result->num_faces; + result->faces.data = ufbxi_push(&sc->result, ufbx_face, result->num_faces); + ufbxi_check_err(&sc->error, result->faces.data); + + for (size_t i = 0; i < result->num_faces; i++) { + result->faces.data[i].index_begin = (uint32_t)(i * 4); + result->faces.data[i].num_indices = 4; + } + + if (mesh->edges.data) { + result->num_edges = mesh->num_edges*2 + result->num_faces; + result->edges.count = result->num_edges; + result->edges.data = ufbxi_push(&sc->result, ufbx_edge, result->num_edges); + ufbxi_check_err(&sc->error, result->edges.data); + + if (mesh->edge_crease.data) { + result->edge_crease.count = result->num_edges; + result->edge_crease.data = ufbxi_push(&sc->result, ufbx_real, result->num_edges); + ufbxi_check_err(&sc->error, result->edge_crease.data); + } + if (mesh->edge_smoothing.data) { + result->edge_smoothing.count = result->num_edges; + result->edge_smoothing.data = ufbxi_push(&sc->result, bool, result->num_edges); + ufbxi_check_err(&sc->error, result->edge_smoothing.data); + } + if (mesh->edge_visibility.data) { + result->edge_visibility.count = result->num_edges; + result->edge_visibility.data = ufbxi_push(&sc->result, bool, result->num_edges); + ufbxi_check_err(&sc->error, result->edge_visibility.data); + } + + size_t di = 0; + for (size_t i = 0; i < mesh->num_edges; i++) { + ufbx_edge edge = mesh->edges.data[i]; + uint32_t face_ix = topo[edge.a].face; + ufbx_face face = mesh->faces.data[face_ix]; + uint32_t offset = edge.a - face.index_begin; + uint32_t next = (offset + 1) % (uint32_t)face.num_indices; + + uint32_t a = (face.index_begin + offset) * 4; + uint32_t b = (face.index_begin + next) * 4; + + result->edges.data[di + 0].a = a; + result->edges.data[di + 0].b = a + 1; + result->edges.data[di + 1].a = b + 3; + result->edges.data[di + 1].b = b; + + if (mesh->edge_crease.data) { + ufbx_real crease = mesh->edge_crease.data[i]; + if (crease < 0.999f) crease -= (ufbx_real)0.1; + if (crease < 0.0f) crease = 0.0f; + result->edge_crease.data[di + 0] = crease; + result->edge_crease.data[di + 1] = crease; + } + + if (mesh->edge_smoothing.data) { + result->edge_smoothing.data[di + 0] = mesh->edge_smoothing.data[i]; + result->edge_smoothing.data[di + 1] = mesh->edge_smoothing.data[i]; + } + + if (mesh->edge_visibility.data) { + result->edge_visibility.data[di + 0] = mesh->edge_visibility.data[i]; + result->edge_visibility.data[di + 1] = mesh->edge_visibility.data[i]; + } + + di += 2; + } + + for (size_t fi = 0; fi < result->num_faces; fi++) { + result->edges.data[di].a = (uint32_t)(fi * 4 + 1); + result->edges.data[di].b = (uint32_t)(fi * 4 + 2); + + if (result->edge_crease.data) { + result->edge_crease.data[di] = 0.0f; + } + + if (result->edge_smoothing.data) { + result->edge_smoothing.data[di + 0] = true; + } + + if (result->edge_visibility.data) { + result->edge_visibility.data[di + 0] = false; + } + + di++; + } + } + + if (mesh->face_material.data) { + result->face_material.count = result->num_faces; + result->face_material.data = ufbxi_push(&sc->result, uint32_t, result->num_faces); + ufbxi_check_err(&sc->error, result->face_material.data); + } + if (mesh->face_smoothing.data) { + result->face_smoothing.count = result->num_faces; + result->face_smoothing.data = ufbxi_push(&sc->result, bool, result->num_faces); + ufbxi_check_err(&sc->error, result->face_smoothing.data); + } + if (mesh->face_group.data) { + result->face_group.count = result->num_faces; + result->face_group.data = ufbxi_push(&sc->result, uint32_t, result->num_faces); + ufbxi_check_err(&sc->error, result->face_group.data); + } + if (mesh->face_hole.data) { + result->face_hole.count = result->num_faces; + result->face_hole.data = ufbxi_push(&sc->result, bool, result->num_faces); + ufbxi_check_err(&sc->error, result->face_hole.data); + } + + if (result->material_parts.count > 0) { + result->material_parts.data = ufbxi_push_zero(&sc->result, ufbx_mesh_part, result->material_parts.count); + ufbxi_check_err(&sc->error, result->materials.data); + } + + size_t index_offset = 0; + for (size_t i = 0; i < mesh->num_faces; i++) { + ufbx_face face = mesh->faces.data[i]; + + uint32_t mat = 0; + if (mesh->face_material.data) { + mat = mesh->face_material.data[i]; + for (size_t ci = 0; ci < face.num_indices; ci++) { + result->face_material.data[index_offset + ci] = mat; + } + } + if (mesh->face_smoothing.data) { + bool flag = mesh->face_smoothing.data[i]; + for (size_t ci = 0; ci < face.num_indices; ci++) { + result->face_smoothing.data[index_offset + ci] = flag; + } + } + if (mesh->face_group.data) { + uint32_t group = mesh->face_group.data[i]; + for (size_t ci = 0; ci < face.num_indices; ci++) { + result->face_group.data[index_offset + ci] = group; + } + } + if (mesh->face_hole.data) { + bool flag = mesh->face_hole.data[i]; + for (size_t ci = 0; ci < face.num_indices; ci++) { + result->face_hole.data[index_offset + ci] = flag; + } + } + index_offset += face.num_indices; + } + + // Will be filled in by `ufbxi_finalize_mesh()`. + result->vertex_first_index.count = 0; + + ufbxi_check_err(&sc->error, ufbxi_finalize_mesh_material(&sc->result, &sc->error, result)); + ufbxi_check_err(&sc->error, ufbxi_finalize_mesh(&sc->result, &sc->error, result)); + ufbxi_check_err(&sc->error, ufbxi_update_face_groups(&sc->result, &sc->error, result, true)); + + return 1; +} + +ufbxi_nodiscard static ufbxi_noinline int ufbxi_subdivide_mesh_imp(ufbxi_subdivide_context *sc, size_t level) +{ + if (sc->opts.boundary == UFBX_SUBDIVISION_BOUNDARY_DEFAULT) { + sc->opts.boundary = sc->src_mesh.subdivision_boundary; + } + + if (sc->opts.uv_boundary == UFBX_SUBDIVISION_BOUNDARY_DEFAULT) { + sc->opts.uv_boundary = sc->src_mesh.subdivision_uv_boundary; + } + + ufbxi_init_ator(&sc->error, &sc->ator_tmp, &sc->opts.temp_allocator, "temp"); + ufbxi_init_ator(&sc->error, &sc->ator_result, &sc->opts.result_allocator, "result"); + + sc->result.unordered = true; + sc->source.unordered = true; + sc->tmp.unordered = true; + + sc->source.ator = &sc->ator_tmp; + sc->tmp.ator = &sc->ator_tmp; + + for (size_t i = 1; i < level; i++) { + sc->result.ator = &sc->ator_tmp; + + ufbxi_check_err(&sc->error, ufbxi_subdivide_mesh_level(sc)); + + sc->src_mesh = sc->dst_mesh; + + ufbxi_buf_free(&sc->source); + ufbxi_buf_free(&sc->tmp); + sc->source = sc->result; + memset(&sc->result, 0, sizeof(sc->result)); + } + + sc->result.ator = &sc->ator_result; + ufbxi_check_err(&sc->error, ufbxi_subdivide_mesh_level(sc)); + ufbxi_buf_free(&sc->tmp); + + ufbx_mesh *mesh = &sc->dst_mesh; + + // Subdivision always results in a mesh that consists only of quads + mesh->max_face_triangles = 2; + mesh->num_empty_faces = 0; + mesh->num_point_faces = 0; + mesh->num_line_faces = 0; + + if (!sc->opts.interpolate_normals) { + memset(&mesh->vertex_normal, 0, sizeof(mesh->vertex_normal)); + memset(&mesh->skinned_normal, 0, sizeof(mesh->skinned_normal)); + } + + if (!sc->opts.interpolate_normals && !sc->opts.ignore_normals) { + + ufbx_topo_edge *topo = ufbxi_push(&sc->tmp, ufbx_topo_edge, mesh->num_indices); + ufbxi_check_err(&sc->error, topo); + ufbx_compute_topology(mesh, topo, mesh->num_indices); + + uint32_t *normal_indices = ufbxi_push(&sc->result, uint32_t, mesh->num_indices); + ufbxi_check_err(&sc->error, normal_indices); + + size_t num_normals = ufbx_generate_normal_mapping(mesh, topo, mesh->num_indices, normal_indices, mesh->num_indices, true); + if (num_normals == mesh->num_vertices) { + mesh->skinned_normal.unique_per_vertex = true; + } + + ufbx_vec3 *normal_data = ufbxi_push(&sc->result, ufbx_vec3, num_normals + 1); + ufbxi_check_err(&sc->error, normal_data); + normal_data[0] = ufbx_zero_vec3; + normal_data++; + + ufbx_compute_normals(mesh, &mesh->skinned_position, normal_indices, mesh->num_indices, normal_data, num_normals); + + mesh->generated_normals = true; + mesh->vertex_normal.exists = true; + mesh->vertex_normal.values.data = normal_data; + mesh->vertex_normal.values.count = num_normals; + mesh->vertex_normal.indices.data = normal_indices; + mesh->vertex_normal.indices.count = mesh->num_indices; + + mesh->skinned_normal = mesh->vertex_normal; + } + + ufbxi_refcount *parent = NULL; + if (sc->src_mesh_ptr->subdivision_evaluated && sc->src_mesh_ptr->from_tessellated_nurbs) { + parent = &(ufbxi_get_imp(ufbxi_mesh_imp, sc->src_mesh_ptr))->refcount; + } else { + parent = &(ufbxi_get_imp(ufbxi_scene_imp, sc->src_mesh_ptr->element.scene))->refcount; + } + + ufbxi_patch_mesh_reals(mesh); + + sc->imp = ufbxi_push(&sc->result, ufbxi_mesh_imp, 1); + ufbxi_check_err(&sc->error, sc->imp); + + sc->dst_mesh.subdivision_result->result_memory_used = sc->ator_result.current_size; + sc->dst_mesh.subdivision_result->temp_memory_used = sc->ator_tmp.current_size; + sc->dst_mesh.subdivision_result->result_allocs = sc->ator_result.num_allocs; + sc->dst_mesh.subdivision_result->temp_allocs = sc->ator_tmp.num_allocs; + + ufbxi_init_ref(&sc->imp->refcount, UFBXI_MESH_IMP_MAGIC, parent); + + sc->imp->magic = UFBXI_MESH_IMP_MAGIC; + sc->imp->mesh = sc->dst_mesh; + sc->imp->refcount.ator = sc->ator_result; + sc->imp->refcount.buf = sc->result; + sc->imp->mesh.subdivision_evaluated = true; + + return 1; +} + +ufbxi_noinline static ufbx_mesh *ufbxi_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *user_opts, ufbx_error *p_error) +{ + ufbxi_subdivide_context sc = { 0 }; + if (user_opts) { + sc.opts = *user_opts; + } + + sc.src_mesh_ptr = (ufbx_mesh*)mesh; + sc.src_mesh = *mesh; + + int ok = ufbxi_subdivide_mesh_imp(&sc, level); + + ufbxi_free(&sc.ator_tmp, ufbxi_subdivide_input, sc.inputs, sc.inputs_cap); + ufbxi_buf_free(&sc.tmp); + ufbxi_buf_free(&sc.source); + + if (ok) { + ufbxi_free_ator(&sc.ator_tmp); + if (p_error) { + ufbxi_clear_error(p_error); + } + + ufbxi_mesh_imp *imp = sc.imp; + return &imp->mesh; + } else { + ufbxi_fix_error_type(&sc.error, "Failed to subdivide", p_error); + ufbxi_buf_free(&sc.result); + ufbxi_free_ator(&sc.ator_tmp); + ufbxi_free_ator(&sc.ator_result); + return NULL; + } +} + +#else + +ufbxi_noinline static ufbx_mesh *ufbxi_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *user_opts, ufbx_error *p_error) +{ + if (p_error) { + memset(p_error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(p_error, "UFBX_ENABLE_SUBDIVISION"); + ufbxi_report_err_msg(p_error, "UFBXI_FEATURE_SUBDIVISION", "Feature disabled"); + } + return NULL; +} + +#endif + +// -- Utility + +#if UFBXI_FEATURE_INDEX_GENERATION + +static int ufbxi_map_cmp_vertex(void *user, const void *va, const void *vb) +{ + size_t size = *(size_t*)user; +#if defined(UFBX_REGRESSION) + ufbx_assert(size % 8 == 0); +#endif + for (size_t i = 0; i < size; i += 8) { + uint64_t a = *(const uint64_t*)((const char*)va + i); + uint64_t b = *(const uint64_t*)((const char*)vb + i); + if (a != b) return a < b ? -1 : +1; + } + return 0; +} + +typedef struct { + char *begin, *ptr; + size_t vertex_size; + size_t packed_offset; +} ufbxi_vertex_stream; + +static ufbxi_noinline size_t ufbxi_generate_indices(const ufbx_vertex_stream *user_streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error) +{ + bool fail = false; + + ufbxi_allocator ator = { 0 }; + ufbxi_init_ator(error, &ator, allocator, "allocator"); + + ufbxi_vertex_stream local_streams[16]; // ufbxi_uninit + uint64_t local_packed_vertex[64]; // ufbxi_uninit + + ufbxi_vertex_stream *streams = NULL; + if (num_streams > ufbxi_arraycount(local_streams)) { + streams = ufbxi_alloc(&ator, ufbxi_vertex_stream, num_streams); + if (!streams) fail = true; + } else { + streams = local_streams; + } + + size_t packed_size = 0; + if (!fail) { + for (size_t i = 0; i < num_streams; i++) { + if (user_streams[i].vertex_count < num_indices) { + ufbxi_fmt_err_info(error, "%zu", i); + ufbxi_report_err_msg(error, "user_streams[i].vertex_count < num_indices", "Truncated vertex stream"); + fail = true; + break; + } + + size_t vertex_size = user_streams[i].vertex_size; + size_t align = ufbxi_size_align_mask(vertex_size); + packed_size = ufbxi_align_to_mask(packed_size, align); + streams[i].ptr = streams[i].begin = (char*)user_streams[i].data; + streams[i].vertex_size = vertex_size; + streams[i].packed_offset = packed_size; + packed_size += vertex_size; + } + packed_size = ufbxi_align_to_mask(packed_size, 7); + } + + if (!fail && packed_size == 0) { + ufbxi_report_err_msg(error, "packed_size != 0", "Zero vertex size"); + fail = true; + } + + char *packed_vertex = NULL; + if (!fail) { + if (packed_size > sizeof(local_packed_vertex)) { + ufbx_assert(packed_size % 8 == 0); + packed_vertex = (char*)ufbxi_alloc(&ator, uint64_t, packed_size / 8); + if (!packed_vertex) fail = true; + } else { + packed_vertex = (char*)local_packed_vertex; + } + } + + ufbxi_map map = { 0 }; + ufbxi_map_init(&map, &ator, &ufbxi_map_cmp_vertex, &packed_size); + + if (num_indices > 0 && !ufbxi_map_grow_size(&map, packed_size, num_indices)) { + fail = true; + } + + if (!fail) { + ufbx_assert(packed_vertex != NULL); + memset(packed_vertex, 0, packed_size); + + for (size_t i = 0; i < num_indices; i++) { + for (size_t si = 0; si < num_streams; si++) { + size_t size = streams[si].vertex_size, offset = streams[si].packed_offset; + char *ptr = streams[si].ptr; + memcpy(packed_vertex + offset, ptr, size); + streams[si].ptr = ptr + size; + } + + uint32_t hash = ufbxi_hash_string(packed_vertex, packed_size); + void *entry = ufbxi_map_find_size(&map, packed_size, hash, packed_vertex); + if (!entry) { + entry = ufbxi_map_insert_size(&map, packed_size, hash, packed_vertex); + if (!entry) { + fail = true; + break; + } + memcpy(entry, packed_vertex, packed_size); + } + uint32_t index = (uint32_t)(ufbxi_to_size((char*)entry - (char*)map.items) / packed_size); + indices[i] = index; + } + } + + size_t result_vertices = 0; + if (!fail) { + result_vertices = map.size; + + for (size_t si = 0; si < num_streams; si++) { + size_t vertex_size = streams[si].vertex_size; + char *dst = streams[si].begin; + char *src = ufbxi_add_ptr((char*)map.items, streams[si].packed_offset); + for (size_t i = 0; i < result_vertices; i++) { + memcpy(dst, src, vertex_size); + dst += vertex_size; + src += packed_size; + } + } + + ufbxi_clear_error(error); + } else { + ufbxi_fix_error_type(error, "Failed to generate indices", NULL); + } + + if (streams && streams != local_streams) { + ufbxi_free(&ator, ufbxi_vertex_stream, streams, num_streams); + } + if (packed_vertex && packed_vertex != (char*)local_packed_vertex) { + ufbxi_free(&ator, uint64_t, packed_vertex, packed_size / 8); + } + + ufbxi_map_free(&map); + ufbxi_free_ator(&ator); + + return result_vertices; +} + +#else + +static ufbxi_noinline size_t ufbxi_generate_indices(const ufbx_vertex_stream *user_streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error) +{ + if (error) { + memset(error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(error, "UFBX_ENABLE_INDEX_GENERATION"); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_INDEX_GENERATION", "Feature disabled"); + } + return 0; +} + +#endif + +static ufbxi_noinline void ufbxi_free_scene_imp(ufbxi_scene_imp *imp) +{ + ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC); + ufbxi_buf_free(&imp->string_buf); +} + +static ufbxi_noinline void ufbxi_init_ref(ufbxi_refcount *refcount, uint32_t magic, ufbxi_refcount *parent) +{ + if (parent) { + ufbxi_retain_ref(parent); + } + + ufbxi_atomic_counter_init(&refcount->refcount); + refcount->self_magic = UFBXI_REFCOUNT_IMP_MAGIC; + refcount->type_magic = magic; + refcount->parent = parent; +} + +static ufbxi_noinline void ufbxi_retain_ref(ufbxi_refcount *refcount) +{ + ufbx_assert(refcount->self_magic == UFBXI_REFCOUNT_IMP_MAGIC); + size_t count = ufbxi_atomic_counter_inc(&refcount->refcount); + ufbxi_ignore(count); + ufbx_assert(count < SIZE_MAX / 2); +} + +static ufbxi_noinline void ufbxi_release_ref(ufbxi_refcount *refcount) +{ + while (refcount) { + ufbx_assert(refcount->self_magic == UFBXI_REFCOUNT_IMP_MAGIC); + if (ufbxi_atomic_counter_dec(&refcount->refcount) > 0) return; + ufbxi_atomic_counter_free(&refcount->refcount); + + ufbxi_refcount *parent = refcount->parent; + uint32_t type_magic = refcount->type_magic; + + refcount->self_magic = 0; + refcount->type_magic = 0; + + // Type-specific cleanup + switch (type_magic) { + case UFBXI_SCENE_IMP_MAGIC: ufbxi_free_scene_imp((ufbxi_scene_imp*)refcount); break; + case UFBXI_CACHE_IMP_MAGIC: ufbxi_free_geometry_cache_imp((ufbxi_geometry_cache_imp*)refcount); break; + default: break; + } + + // We need to free `data_buf` last and be careful to copy it to + // the stack since the `ufbxi_refcount` that contains it is allocated + // from the same result buffer! + ufbxi_allocator ator = refcount->ator; + ufbxi_buf buf = refcount->buf; + buf.ator = &ator; + ufbxi_buf_free(&buf); + ufbxi_free_ator(&ator); + + refcount = parent; + } +} + +static ufbxi_noinline void *ufbxi_uninitialized_options(ufbx_error *p_error) +{ + if (p_error) { + memset(p_error, 0, sizeof(ufbx_error)); + p_error->type = UFBX_ERROR_UNINITIALIZED_OPTIONS; + p_error->description.data = "Uninitialized options"; + p_error->description.length = strlen("Uninitialized options"); + } + return NULL; +} + +#define ufbxi_check_opts_ptr(m_type, m_opts, m_error) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + ufbx_assert(opts_cleared_to_zero == 0); \ + if (opts_cleared_to_zero != 0) return (m_type*)ufbxi_uninitialized_options(m_error); \ + } } while (0) + +#define ufbxi_check_opts_return(m_value, m_opts, m_error) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + ufbx_assert(opts_cleared_to_zero == 0); \ + if (opts_cleared_to_zero != 0) { \ + ufbxi_uninitialized_options(m_error); \ + return m_value; \ + } \ + } } while (0) + +#define ufbxi_check_opts_return_no_error(m_value, m_opts) do { if (m_opts) { \ + uint32_t opts_cleared_to_zero = m_opts->_begin_zero | m_opts->_end_zero; \ + if (opts_cleared_to_zero != 0) return m_value; \ + } } while (0) + +// -- API + +#ifdef __cplusplus +extern "C" { +#endif + +ufbx_abi_data_def const ufbx_string ufbx_empty_string = { ufbxi_empty_char, 0 }; +ufbx_abi_data_def const ufbx_blob ufbx_empty_blob = { NULL, 0 }; +ufbx_abi_data_def const ufbx_matrix ufbx_identity_matrix = { 1,0,0, 0,1,0, 0,0,1, 0,0,0 }; +ufbx_abi_data_def const ufbx_transform ufbx_identity_transform = { {0,0,0}, {0,0,0,1}, {1,1,1} }; +ufbx_abi_data_def const ufbx_vec2 ufbx_zero_vec2 = { 0,0 }; +ufbx_abi_data_def const ufbx_vec3 ufbx_zero_vec3 = { 0,0,0 }; +ufbx_abi_data_def const ufbx_vec4 ufbx_zero_vec4 = { 0,0,0,0 }; +ufbx_abi_data_def const ufbx_quat ufbx_identity_quat = { 0,0,0,1 }; + +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_y_up = { + UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_POSITIVE_Z, +}; +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_right_handed_z_up = { + UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_NEGATIVE_Y, +}; +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_y_up = { + UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Y, UFBX_COORDINATE_AXIS_NEGATIVE_Z, +}; +ufbx_abi_data_def const ufbx_coordinate_axes ufbx_axes_left_handed_z_up = { + UFBX_COORDINATE_AXIS_POSITIVE_X, UFBX_COORDINATE_AXIS_POSITIVE_Z, UFBX_COORDINATE_AXIS_POSITIVE_Y, +}; + +ufbx_abi_data_def const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT] = { + sizeof(ufbx_unknown), + sizeof(ufbx_node), + sizeof(ufbx_mesh), + sizeof(ufbx_light), + sizeof(ufbx_camera), + sizeof(ufbx_bone), + sizeof(ufbx_empty), + sizeof(ufbx_line_curve), + sizeof(ufbx_nurbs_curve), + sizeof(ufbx_nurbs_surface), + sizeof(ufbx_nurbs_trim_surface), + sizeof(ufbx_nurbs_trim_boundary), + sizeof(ufbx_procedural_geometry), + sizeof(ufbx_stereo_camera), + sizeof(ufbx_camera_switcher), + sizeof(ufbx_marker), + sizeof(ufbx_lod_group), + sizeof(ufbx_skin_deformer), + sizeof(ufbx_skin_cluster), + sizeof(ufbx_blend_deformer), + sizeof(ufbx_blend_channel), + sizeof(ufbx_blend_shape), + sizeof(ufbx_cache_deformer), + sizeof(ufbx_cache_file), + sizeof(ufbx_material), + sizeof(ufbx_texture), + sizeof(ufbx_video), + sizeof(ufbx_shader), + sizeof(ufbx_shader_binding), + sizeof(ufbx_anim_stack), + sizeof(ufbx_anim_layer), + sizeof(ufbx_anim_value), + sizeof(ufbx_anim_curve), + sizeof(ufbx_display_layer), + sizeof(ufbx_selection_set), + sizeof(ufbx_selection_node), + sizeof(ufbx_character), + sizeof(ufbx_constraint), + sizeof(ufbx_audio_layer), + sizeof(ufbx_audio_clip), + sizeof(ufbx_pose), + sizeof(ufbx_metadata_object), +}; + +ufbx_abi bool ufbx_default_open_file(void *user, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_info *info) +{ + (void)user; + return ufbx_open_file_ctx(stream, info->context, path, path_len, NULL, NULL); +} + +ufbx_abi bool ufbx_open_file(ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error) +{ + return ufbx_open_file_ctx(stream, (ufbx_open_file_context)NULL, path, path_len, opts, error); +} + +ufbx_abi bool ufbx_open_file_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error) +{ + bool ok = false; + ufbxi_file_context fc; // ufbxi_uninit + ufbxi_begin_file_context(&fc, ctx, NULL); + if (path_len == SIZE_MAX) path_len = strlen(path); +#if !defined(UFBX_NO_STDIO) + ok = ufbxi_stdio_open(&fc, stream, path, path_len, opts ? opts->filename_null_terminated : false); +#else + (void)stream; + (void)path; + (void)path_len; + (void)opts; + ufbxi_fmt_err_info(&fc.error, "UFBX_NO_STDIO"); + ufbxi_report_err_msg(&fc.error, "UFBX_NO_STDIO", "Feature disabled"); +#endif + ufbxi_end_file_context(&fc, error, ok); + return ok; +} + +ufbx_abi bool ufbx_open_memory(ufbx_stream *stream, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error) +{ + return ufbx_open_memory_ctx(stream, (ufbx_open_file_context)NULL, data, data_size, opts, error); +} + +ufbx_abi bool ufbx_open_memory_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error) +{ + ufbx_open_memory_opts local_opts; // ufbxi_uninit + if (!opts) { + memset(&local_opts, 0, sizeof(local_opts)); + opts = &local_opts; + } + ufbx_assert(opts->_begin_zero == 0 && opts->_end_zero == 0); + + ufbxi_file_context fc; // ufbxi_uninit + ufbxi_begin_file_context(&fc, ctx, &opts->allocator); + + size_t copy_size = opts->no_copy ? 0 : data_size; + + // Align the allocation size to 8 bytes to make sure the header is aligned. + size_t self_size = ufbxi_align_to_mask(sizeof(ufbxi_memory_stream) + copy_size, 7); + + void *memory = ufbxi_alloc(&fc.ator, char, self_size); + if (!memory) { + ufbxi_end_file_context(&fc, error, false); + return false; + } + + ufbxi_memory_stream *mem = (ufbxi_memory_stream*)memory; + memset(mem, 0, sizeof(ufbxi_memory_stream)); + + mem->size = data_size; + mem->self_size = self_size; + mem->close_cb = opts->close_cb; + + if (opts->no_copy) { + mem->data = data; + } else { + memcpy(mem->data_copy, data, data_size); + mem->data = mem->data_copy; + } + + // Transplant the allocator in the result blob + if (fc.parent_ator) { + mem->parent_ator = fc.parent_ator; + } else { + fc.parent_ator = &mem->local_ator; + } + + stream->read_fn = ufbxi_memory_read; + stream->skip_fn = ufbxi_memory_skip; + stream->size_fn = ufbxi_memory_size; + stream->close_fn = ufbxi_memory_close; + stream->user = mem; + + ufbxi_end_file_context(&fc, error, true); + + return true; +} + +ufbx_abi bool ufbx_is_thread_safe(void) +{ + return UFBXI_THREAD_SAFE != 0; +} + +ufbx_abi ufbx_scene *ufbx_load_memory(const void *data, size_t size, const ufbx_load_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_scene, opts, error); + ufbxi_context uc; // ufbxi_uninit + memset(&uc, 0, sizeof(ufbxi_context)); + uc.data_begin = uc.data = (const char *)data; + uc.data_size = size; + uc.progress_bytes_total = size; + return ufbxi_load(&uc, opts, error); +} + +ufbx_abi ufbx_scene *ufbx_load_file(const char *filename, const ufbx_load_opts *opts, ufbx_error *error) +{ + return ufbx_load_file_len(filename, SIZE_MAX, opts, error); +} + +ufbx_abi ufbx_scene *ufbx_load_file_len(const char *filename, size_t filename_len, const ufbx_load_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_scene, opts, error); + ufbxi_context uc; // ufbxi_uninit + memset(&uc, 0, sizeof(ufbxi_context)); + uc.deferred_load = true; + uc.load_filename = filename; + uc.load_filename_len = filename_len; + return ufbxi_load(&uc, opts, error); +} + +ufbx_abi ufbx_scene *ufbx_load_stdio(void *file_void, const ufbx_load_opts *opts, ufbx_error *error) +{ + return ufbx_load_stdio_prefix(file_void, NULL, 0, opts, error); +} + +ufbx_abi ufbx_scene *ufbx_load_stdio_prefix(void *file_void, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error) +{ +#if !defined(UFBX_NO_STDIO) + if (!file_void) return NULL; + ufbx_stream stream = { 0 }; + ufbxi_stdio_init(&stream, file_void, false); + return ufbx_load_stream_prefix(&stream, prefix, prefix_size, opts, error); +#else + (void)file_void; + (void)prefix; + (void)prefix_size; + (void)opts; + + ufbxi_context uc; // ufbxi_uninit + memset(&uc, 0, sizeof(ufbxi_context)); + ufbxi_fmt_err_info(&uc.error, "UFBX_NO_STDIO"); + ufbxi_report_err_msg(&uc.error, "UFBX_NO_STDIO", "Feature disabled"); + uc.deferred_failure = true; + return ufbxi_load(&uc, NULL, error); +#endif +} + +ufbx_abi ufbx_scene *ufbx_load_stream(const ufbx_stream *stream, const ufbx_load_opts *opts, ufbx_error *error) +{ + return ufbx_load_stream_prefix(stream, NULL, 0, opts, error); +} + +ufbx_abi ufbx_scene *ufbx_load_stream_prefix(const ufbx_stream *stream, const void *prefix, size_t prefix_size, const ufbx_load_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_scene, opts, error); + ufbxi_context uc; // ufbxi_uninit + memset(&uc, 0, sizeof(ufbxi_context)); + uc.data_begin = uc.data = (const char *)prefix; + uc.data_size = prefix_size; + uc.read_fn = stream->read_fn; + uc.skip_fn = stream->skip_fn; + uc.size_fn = stream->size_fn; + uc.close_fn = stream->close_fn; + uc.read_user = stream->user; + + ufbx_scene *scene = ufbxi_load(&uc, opts, error); + return scene; +} + +ufbx_abi void ufbx_free_scene(ufbx_scene *scene) +{ + if (!scene) return; + + ufbxi_scene_imp *imp = ufbxi_get_imp(ufbxi_scene_imp, scene); + ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC); + if (imp->magic != UFBXI_SCENE_IMP_MAGIC) return; + ufbxi_release_ref(&imp->refcount); +} + +ufbx_abi void ufbx_retain_scene(ufbx_scene *scene) +{ + if (!scene) return; + + ufbxi_scene_imp *imp = ufbxi_get_imp(ufbxi_scene_imp, scene); + ufbx_assert(imp->magic == UFBXI_SCENE_IMP_MAGIC); + if (imp->magic != UFBXI_SCENE_IMP_MAGIC) return; + ufbxi_retain_ref(&imp->refcount); +} + +ufbx_abi ufbxi_noinline size_t ufbx_format_error(char *dst, size_t dst_size, const ufbx_error *error) +{ + if (!dst || !dst_size) return 0; + if (!error) { + *dst = '\0'; + return 0; + } + + size_t offset = 0; + + { + int num; + if (error->info_length > 0 && error->info_length < UFBX_ERROR_INFO_LENGTH) { + num = ufbxi_snprintf(dst + offset, dst_size - offset, "ufbx v%u.%u.%u error: %s (%.*s)\n", + UFBX_SOURCE_VERSION/1000000, UFBX_SOURCE_VERSION/1000%1000, UFBX_SOURCE_VERSION%1000, + error->description.data ? error->description.data : "Unknown error", + (int)error->info_length, error->info); + } else { + num = ufbxi_snprintf(dst + offset, dst_size - offset, "ufbx v%u.%u.%u error: %s\n", + UFBX_SOURCE_VERSION/1000000, UFBX_SOURCE_VERSION/1000%1000, UFBX_SOURCE_VERSION%1000, + error->description.data ? error->description.data : "Unknown error"); + } + + if (num > 0) offset = ufbxi_min_sz(offset + (size_t)num, dst_size - 1); + } + + size_t stack_size = ufbxi_min_sz(error->stack_size, UFBX_ERROR_STACK_MAX_DEPTH); + int line_width = 6; + for (size_t i = 0; i < stack_size; i++) { + const ufbx_error_frame *frame = &error->stack[i]; + int num = ufbxi_snprintf(dst + offset, dst_size - offset, "%*u:%s: %s\n", line_width, frame->source_line, frame->function.data, frame->description.data); + if (num > 0) offset = ufbxi_min_sz(offset + (size_t)num, dst_size - 1); + } + + return offset; +} + +ufbx_abi ufbx_prop *ufbx_find_prop_len(const ufbx_props *props, const char *name, size_t name_len) +{ + uint32_t key = ufbxi_get_name_key(name, name_len); + ufbx_string name_str = ufbxi_safe_string(name, name_len); + + while (props) { + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_prop, 4, &index, props->props.data, 0, props->props.count, + ( ufbxi_cmp_prop_less_ref(a, name_str, key) ), ( a->_internal_key == key && ufbxi_str_equal(a->name, name_str) )); + if (index != SIZE_MAX) return &props->props.data[index]; + + props = props->defaults; + } + + return NULL; +} + +ufbx_abi ufbx_real ufbx_find_real_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_real def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_real; + } else { + return def; + } +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_find_vec3_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_vec3 def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_vec3; + } else { + return def; + } +} + +ufbx_abi ufbxi_noinline int64_t ufbx_find_int_len(const ufbx_props *props, const char *name, size_t name_len, int64_t def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_int; + } else { + return def; + } +} + +ufbx_abi bool ufbx_find_bool_len(const ufbx_props *props, const char *name, size_t name_len, bool def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_int != 0; + } else { + return def; + } +} + +ufbx_abi ufbxi_noinline ufbx_string ufbx_find_string_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_string def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_str; + } else { + return def; + } +} + +ufbx_abi ufbx_blob ufbx_find_blob_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_blob def) +{ + ufbx_prop *prop = ufbx_find_prop_len(props, name, name_len); + if (prop) { + return prop->value_blob; + } else { + return def; + } +} + +ufbx_abi ufbx_prop *ufbx_find_prop_concat(const ufbx_props *props, const ufbx_string *parts, size_t num_parts) +{ + uint32_t key = ufbxi_get_concat_key(parts, num_parts); + + while (props) { + size_t index = SIZE_MAX; + + ufbxi_macro_lower_bound_eq(ufbx_prop, 2, &index, props->props.data, 0, props->props.count, + ( ufbxi_cmp_prop_less_concat(a, parts, num_parts, key) ), + ( a->_internal_key == key && ufbxi_concat_str_cmp(&a->name, parts, num_parts) == 0 )); + if (index != SIZE_MAX) return &props->props.data[index]; + + props = props->defaults; + } + + return NULL; +} + +ufbx_abi ufbx_element *ufbx_find_element_len(const ufbx_scene *scene, ufbx_element_type type, const char *name, size_t name_len) +{ + if (!scene) return NULL; + ufbx_string name_str = ufbxi_safe_string(name, name_len); + uint32_t key = ufbxi_get_name_key(name, name_len); + + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_name_element, 16, &index, scene->elements_by_name.data, 0, scene->elements_by_name.count, + ( ufbxi_cmp_name_element_less_ref(a, name_str, type, key) ), ( ufbxi_str_equal(a->name, name_str) && a->type == type )); + + return index < SIZE_MAX ? scene->elements_by_name.data[index].element : NULL; +} + +ufbx_abi ufbx_element *ufbx_get_prop_element(const ufbx_element *element, const ufbx_prop *prop, ufbx_element_type type) +{ + ufbx_assert(element && prop); + if (!element || !prop) return NULL; + return ufbxi_fetch_dst_element((ufbx_element*)element, false, prop->name.data, type); +} + +ufbx_abi ufbx_element *ufbx_find_prop_element_len(const ufbx_element *element, const char *name, size_t name_len, ufbx_element_type type) +{ + const ufbx_prop *prop = ufbx_find_prop_len(&element->props, name, name_len); + if (prop) { + return ufbx_get_prop_element(element, prop, type); + } else { + return NULL; + } +} + +ufbx_abi ufbx_node *ufbx_find_node_len(const ufbx_scene *scene, const char *name, size_t name_len) +{ + return (ufbx_node*)ufbx_find_element_len(scene, UFBX_ELEMENT_NODE, name, name_len); +} + +ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack_len(const ufbx_scene *scene, const char *name, size_t name_len) +{ + return (ufbx_anim_stack*)ufbx_find_element_len(scene, UFBX_ELEMENT_ANIM_STACK, name, name_len); +} + +ufbx_abi ufbx_material *ufbx_find_material_len(const ufbx_scene *scene, const char *name, size_t name_len) +{ + return (ufbx_material*)ufbx_find_element_len(scene, UFBX_ELEMENT_MATERIAL, name, name_len); +} + +ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop_len(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop, size_t prop_len) +{ + ufbx_assert(layer); + ufbx_assert(element); + if (!layer || !element) return NULL; + + ufbx_string prop_str = ufbxi_safe_string(prop, prop_len); + + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &index, layer->anim_props.data, 0, layer->anim_props.count, + ( a->element != element ? a->element < element : ufbxi_str_less(a->prop_name, prop_str) ), + ( a->element == element && ufbxi_str_equal(a->prop_name, prop_str) )); + + if (index == SIZE_MAX) return NULL; + return &layer->anim_props.data[index]; +} + +ufbx_abi ufbxi_noinline ufbx_anim_prop_list ufbx_find_anim_props(const ufbx_anim_layer *layer, const ufbx_element *element) +{ + ufbx_anim_prop_list result = { 0 }; + ufbx_assert(layer); + ufbx_assert(element); + if (!layer || !element) return result; + + size_t begin = layer->anim_props.count, end = begin; + ufbxi_macro_lower_bound_eq(ufbx_anim_prop, 16, &begin, layer->anim_props.data, 0, layer->anim_props.count, + ( a->element < element ), ( a->element == element )); + + ufbxi_macro_upper_bound_eq(ufbx_anim_prop, 16, &end, layer->anim_props.data, begin, layer->anim_props.count, + ( a->element == element )); + + if (begin != end) { + result.data = layer->anim_props.data + begin; + result.count = end - begin; + } + + return result; +} + +ufbx_abi ufbxi_noinline ufbx_matrix ufbx_get_compatible_matrix_for_normals(const ufbx_node *node) +{ + if (!node) return ufbx_identity_matrix; + + ufbx_transform geom_rot = ufbx_identity_transform; + geom_rot.rotation = node->geometry_transform.rotation; + ufbx_matrix geom_rot_mat = ufbx_transform_to_matrix(&geom_rot); + + ufbx_matrix norm_mat = ufbx_matrix_mul(&node->node_to_world, &geom_rot_mat); + norm_mat = ufbx_matrix_for_normals(&norm_mat); + return norm_mat; +} + +ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value) +{ + return ufbx_evaluate_curve_flags(curve, time, default_value, 0); +} + +ufbx_abi ufbx_real ufbx_evaluate_curve_flags(const ufbx_anim_curve *curve, double time, ufbx_real default_value, uint32_t flags) +{ + if (!curve) return default_value; + if (curve->keyframes.count <= 1) { + if (curve->keyframes.count == 1) { + return curve->keyframes.data[0].value; + } else { + return default_value; + } + } + + if ((flags & UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION) == 0) { + if (time < curve->min_time || time > curve->max_time) { + return ufbxi_extrapolate_curve(curve, time, flags); + } + } + + size_t begin = 0; + size_t end = curve->keyframes.count; + const ufbx_keyframe *keys = curve->keyframes.data; + while (end - begin >= 8) { + size_t mid = (begin + end) >> 1; + if (keys[mid].time <= time) { + begin = mid + 1; + } else { + end = mid; + } + } + + end = curve->keyframes.count; + for (; begin < end; begin++) { + const ufbx_keyframe *next = &keys[begin]; + if (next->time <= time) continue; + + // First keyframe + if (begin == 0) return next->value; + + const ufbx_keyframe *prev = next - 1; + + // Exact keyframe + if (prev->time == time) return prev->value; + + double rcp_delta = 1.0 / (next->time - prev->time); + double t = (time - prev->time) * rcp_delta; + + switch (prev->interpolation) { + + case UFBX_INTERPOLATION_CONSTANT_PREV: + return prev->value; + + case UFBX_INTERPOLATION_CONSTANT_NEXT: + return next->value; + + case UFBX_INTERPOLATION_LINEAR: + return (ufbx_real)(prev->value*(1.0 - t) + next->value*t); + + case UFBX_INTERPOLATION_CUBIC: + { + double x1 = prev->right.dx * rcp_delta; + double x2 = 1.0 - next->left.dx * rcp_delta; + t = ufbxi_find_cubic_bezier_t(x1, x2, t); + + double t2 = t*t, t3 = t2*t; + double u = 1.0 - t, u2 = u*u, u3 = u2*u; + + double y0 = prev->value; + double y3 = next->value; + double y1 = y0 + prev->right.dy; + double y2 = y3 - next->left.dy; + + return (ufbx_real)(u3*y0 + 3.0 * (u2*t*y1 + u*t2*y2) + t3*y3); + } + + default: + ufbxi_unreachable("Bad interpolation mode"); + return 0.0f; + + } + } + + // Last keyframe + return curve->keyframes.data[curve->keyframes.count - 1].value; +} + +ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time) +{ + return ufbx_evaluate_anim_value_real_flags(anim_value, time, 0); +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time) +{ + return ufbx_evaluate_anim_value_vec3_flags(anim_value, time, 0); +} + +ufbx_abi ufbxi_noinline ufbx_real ufbx_evaluate_anim_value_real_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags) +{ + if (!anim_value) { + return 0.0f; + } + + ufbx_real res = anim_value->default_value.x; + if (anim_value->curves[0]) res = ufbx_evaluate_curve_flags(anim_value->curves[0], time, res, flags); + return res; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_evaluate_anim_value_vec3_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags) +{ + if (!anim_value) { + ufbx_vec3 zero = { 0.0f }; + return zero; + } + + ufbx_vec3 res = anim_value->default_value; + if (anim_value->curves[0]) res.x = ufbx_evaluate_curve_flags(anim_value->curves[0], time, res.x, flags); + if (anim_value->curves[1]) res.y = ufbx_evaluate_curve_flags(anim_value->curves[1], time, res.y, flags); + if (anim_value->curves[2]) res.z = ufbx_evaluate_curve_flags(anim_value->curves[2], time, res.z, flags); + return res; +} + +ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time) +{ + return ufbx_evaluate_prop_flags_len(anim, element, name, name_len, time, 0); +} + +ufbx_abi ufbxi_noinline ufbx_prop ufbx_evaluate_prop_flags_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time, uint32_t flags) +{ + ufbx_prop result; + + ufbx_prop *prop = ufbx_find_prop_len(&element->props, name, name_len); + if (prop) { + result = *prop; + } else { + memset(&result, 0, sizeof(result)); + result.name.data = name; + result.name.length = name_len; + result._internal_key = ufbxi_get_name_key(name, name_len); + result.flags = UFBX_PROP_FLAG_NOT_FOUND; + result.value_str.data = ufbxi_empty_char; + result.value_str.length = 0; + result.value_blob.data = NULL; + result.value_blob.size = 0; + } + + if (anim->prop_overrides.count > 0) { + ufbxi_find_prop_override(&anim->prop_overrides, element->element_id, &result); + return result; + } + + if ((result.flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_CONNECTED)) == 0) return result; + + if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) { + ufbxi_evaluate_connected_prop(&result, anim, element, prop->name.data, time, flags); + } + + ufbxi_evaluate_props(anim, element, time, &result, 1, flags); + + return result; +} + +ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size) +{ + return ufbx_evaluate_props_flags(anim, element, time, buffer, buffer_size, 0); +} + +ufbx_abi ufbxi_noinline ufbx_props ufbx_evaluate_props_flags(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size, uint32_t flags) +{ + ufbx_props ret = { NULL }; + if (!element) return ret; + + size_t num_anim = 0; + ufbxi_prop_iter iter; // ufbxi_uninit + ufbxi_init_prop_iter(&iter, anim, element); + const ufbx_prop *prop = NULL; + while ((prop = ufbxi_next_prop(&iter)) != NULL) { + if (!(prop->flags & (UFBX_PROP_FLAG_ANIMATED|UFBX_PROP_FLAG_OVERRIDDEN|UFBX_PROP_FLAG_CONNECTED))) continue; + if (num_anim >= buffer_size) break; + + ufbx_prop *dst = &buffer[num_anim++]; + *dst = *prop; + + if ((prop->flags & UFBX_PROP_FLAG_CONNECTED) != 0 && !anim->ignore_connections) { + ufbxi_evaluate_connected_prop(dst, anim, element, prop->name.data, time, flags); + } + } + + ufbxi_evaluate_props(anim, element, time, buffer, num_anim, flags); + + ret.props.data = buffer; + ret.props.count = ret.num_animated = num_anim; + ret.defaults = (ufbx_props*)&element->props; + return ret; +} + +ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform(const ufbx_anim *anim, const ufbx_node *node, double time) +{ + return ufbx_evaluate_transform_flags(anim, node, time, 0); +} + +static const char *const ufbxi_transform_props_all[] = { + ufbxi_Lcl_Rotation, + ufbxi_Lcl_Scaling, + ufbxi_Lcl_Translation, + ufbxi_PostRotation, + ufbxi_PreRotation, + ufbxi_RotationOffset, + ufbxi_RotationOrder, + ufbxi_RotationPivot, + ufbxi_ScalingOffset, + ufbxi_ScalingPivot, +}; + +static const char *const ufbxi_transform_props_rotation[] = { + ufbxi_Lcl_Rotation, + ufbxi_PostRotation, + ufbxi_PreRotation, + ufbxi_RotationOrder, +}; + +static const char *const ufbxi_transform_props_scale[] = { + ufbxi_Lcl_Scaling, +}; + +static const char *const ufbxi_transform_props_rotation_scale[] = { + ufbxi_Lcl_Rotation, + ufbxi_Lcl_Scaling, + ufbxi_PostRotation, + ufbxi_PreRotation, + ufbxi_RotationOrder, +}; + +ufbx_abi ufbxi_noinline ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, const ufbx_node *node, double time, uint32_t flags) +{ + ufbx_assert(anim); + ufbx_assert(node); + if (!node) return ufbx_identity_transform; + if (!anim) return node->local_transform; + if (node->is_root) return node->local_transform; + + if ((flags & UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES) == 0) { + flags |= UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION; + } + + const char *const *prop_names = ufbxi_transform_props_all; + size_t num_prop_names = ufbxi_arraycount(ufbxi_transform_props_all); + uint32_t components = flags & (UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION); + if (components == (UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION|UFBX_TRANSFORM_FLAG_INCLUDE_SCALE)) { + prop_names = ufbxi_transform_props_rotation_scale; + num_prop_names = ufbxi_arraycount(ufbxi_transform_props_rotation_scale); + } else if (components == UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) { + prop_names = ufbxi_transform_props_rotation; + num_prop_names = ufbxi_arraycount(ufbxi_transform_props_rotation); + } else if (components == UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) { + prop_names = ufbxi_transform_props_scale; + num_prop_names = ufbxi_arraycount(ufbxi_transform_props_scale); + } else if (components == 0) { + return ufbx_identity_transform; + } + + const ufbx_vec3 *translation_scale = NULL; + ufbx_prop helper_scale; // ufbxi_uninit + ufbx_vec3 scale_factor = ufbxi_one_vec3; + bool use_scale_factor = false; + + if (node->parent && (flags & (UFBX_TRANSFORM_FLAG_INCLUDE_SCALE|UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION)) != 0) { + ufbx_node *parent = node->parent; + + if ((flags & UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE) == 0 && parent->inherit_scale_node) { + ufbx_node *p = parent->inherit_scale_node; + + if (node->is_scale_helper) { + use_scale_factor = true; + } + + while (p && p->scale_helper) { + ufbx_prop scale = ufbx_evaluate_prop(anim, &p->scale_helper->element, ufbxi_Lcl_Scaling, time); + scale_factor.x *= scale.value_vec3.x; + scale_factor.y *= scale.value_vec3.y; + scale_factor.z *= scale.value_vec3.z; + p = p->inherit_scale_node; + } + } + + if (parent->scale_helper && (flags & UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER) == 0) { + helper_scale = ufbx_evaluate_prop(anim, &parent->scale_helper->element, ufbxi_Lcl_Scaling, time); + if (helper_scale.flags & UFBX_PROP_FLAG_NOT_FOUND) { + helper_scale.value_vec3.x = 1.0f; + helper_scale.value_vec3.y = 1.0f; + helper_scale.value_vec3.z = 1.0f; + } + helper_scale.value_vec3.x *= scale_factor.x; + helper_scale.value_vec3.y *= scale_factor.y; + helper_scale.value_vec3.z *= scale_factor.z; + translation_scale = &helper_scale.value_vec3; + } + } + + uint32_t eval_flags = 0; + if (flags & UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION) { + eval_flags |= UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION; + } + + ufbx_prop buf[ufbxi_arraycount(ufbxi_transform_props_all)]; // ufbxi_uninit + ufbx_props props = ufbxi_evaluate_selected_props(anim, &node->element, time, buf, prop_names, num_prop_names, eval_flags); + ufbx_rotation_order order = (ufbx_rotation_order)ufbxi_find_enum(&props, ufbxi_RotationOrder, UFBX_ROTATION_ORDER_XYZ, UFBX_ROTATION_ORDER_SPHERIC); + + ufbx_transform transform; // ufbxi_uninit + if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION) != 0) { + transform = ufbxi_get_transform(&props, order, node, translation_scale); + } else { + transform.translation = ufbx_zero_vec3; + if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION) != 0) { + transform.rotation = ufbxi_get_rotation(&props, order, node); + } else { + transform.rotation = ufbx_identity_quat; + } + if ((components & UFBX_TRANSFORM_FLAG_INCLUDE_SCALE) != 0) { + transform.scale = ufbxi_get_scale(&props, node); + } else { + transform.scale = ufbxi_one_vec3; + } + } + + if (use_scale_factor) { + transform.scale.x *= scale_factor.x; + transform.scale.y *= scale_factor.y; + transform.scale.z *= scale_factor.z; + } + return transform; +} + +ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time) +{ + return ufbx_evaluate_blend_weight_flags(anim, channel, time, 0); +} + +ufbx_abi ufbx_real ufbx_evaluate_blend_weight_flags(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time, uint32_t flags) +{ + const char *prop_names[] = { + ufbxi_DeformPercent, + }; + + ufbx_prop buf[ufbxi_arraycount(prop_names)]; // ufbxi_uninit + ufbx_props props = ufbxi_evaluate_selected_props(anim, &channel->element, time, buf, prop_names, ufbxi_arraycount(prop_names), flags); + return ufbxi_find_real(&props, ufbxi_DeformPercent, channel->weight * (ufbx_real)100.0) * (ufbx_real)0.01; +} + +ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_scene, opts, error); +#if UFBXI_FEATURE_SCENE_EVALUATION + ufbxi_eval_context ec = { 0 }; + return ufbxi_evaluate_scene(&ec, (ufbx_scene*)scene, anim, time, opts, error); +#else + if (error) { + memset(error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(error, "UFBX_ENABLE_SCENE_EVALUATION"); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_SCENE_EVALUATION", "Feature disabled"); + } + return NULL; +#endif +} + +ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_anim, opts, error); + ufbx_assert(scene); + + ufbxi_create_anim_context ac = { UFBX_ERROR_NONE }; + if (opts) { + ac.opts = *opts; + } + + ac.scene = scene; + + int ok = ufbxi_create_anim_imp(&ac); + + if (ok) { + ufbxi_clear_error(error); + ufbxi_anim_imp *imp = ac.imp; + return &imp->anim; + } else { + ufbxi_fix_error_type(&ac.error, "Failed to create anim", error); + ufbxi_buf_free(&ac.result); + ufbxi_free_ator(&ac.ator_result); + return NULL; + } +} + +ufbx_abi void ufbx_free_anim(ufbx_anim *anim) +{ + if (!anim) return; + if (!anim->custom) return; + + ufbxi_anim_imp *imp = ufbxi_get_imp(ufbxi_anim_imp, anim); + ufbx_assert(imp->magic == UFBXI_ANIM_IMP_MAGIC); + if (imp->magic != UFBXI_ANIM_IMP_MAGIC) return; + ufbxi_release_ref(&imp->refcount); +} + +ufbx_abi void ufbx_retain_anim(ufbx_anim *anim) +{ + if (!anim) return; + if (!anim->custom) return; + + ufbxi_anim_imp *imp = ufbxi_get_imp(ufbxi_anim_imp, anim); + ufbx_assert(imp->magic == UFBXI_ANIM_IMP_MAGIC); + if (imp->magic != UFBXI_ANIM_IMP_MAGIC) return; + ufbxi_retain_ref(&imp->refcount); +} + +ufbx_abi ufbx_baked_anim *ufbx_bake_anim(const ufbx_scene *scene, const ufbx_anim *anim, const ufbx_bake_opts *opts, ufbx_error *error) +{ + ufbx_assert(scene); +#if UFBXI_FEATURE_ANIMATION_BAKING + ufbxi_check_opts_ptr(ufbx_baked_anim, opts, error); + if (!anim) { + anim = scene->anim; + } + + ufbxi_bake_context bc = { UFBX_ERROR_NONE }; + if (opts) { + bc.opts = *opts; + } + + bc.scene = scene; + + int ok = ufbxi_bake_anim_imp(&bc, anim); + + ufbxi_buf_free(&bc.tmp); + ufbxi_buf_free(&bc.tmp_prop); + ufbxi_buf_free(&bc.tmp_times); + ufbxi_buf_free(&bc.tmp_bake_props); + ufbxi_buf_free(&bc.tmp_nodes); + ufbxi_buf_free(&bc.tmp_elements); + ufbxi_buf_free(&bc.tmp_props); + ufbxi_buf_free(&bc.tmp_bake_stack); + ufbxi_free(&bc.ator_tmp, char, bc.tmp_arr, bc.tmp_arr_size); + ufbxi_free_ator(&bc.ator_tmp); + + if (ok) { + ufbxi_clear_error(error); + ufbxi_baked_anim_imp *imp = bc.imp; + return &imp->bake; + } else { + ufbxi_fix_error_type(&bc.error, "Failed to bake anim", error); + ufbxi_buf_free(&bc.result); + ufbxi_free_ator(&bc.ator_result); + return NULL; + } +#else + if (error) { + memset(error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(error, "UFBX_ENABLE_ANIMATION_BAKING"); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_ANIMATION_BAKING", "Feature disabled"); + } + return NULL; +#endif +} + +ufbx_abi void ufbx_retain_baked_anim(ufbx_baked_anim *bake) +{ + if (!bake) return; + + ufbxi_baked_anim_imp *imp = ufbxi_get_imp(ufbxi_baked_anim_imp, bake); + ufbx_assert(imp->magic == UFBXI_BAKED_ANIM_IMP_MAGIC); + if (imp->magic != UFBXI_BAKED_ANIM_IMP_MAGIC) return; + ufbxi_retain_ref(&imp->refcount); +} + +ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake) +{ + if (!bake) return; + + ufbxi_baked_anim_imp *imp = ufbxi_get_imp(ufbxi_baked_anim_imp, bake); + ufbx_assert(imp->magic == UFBXI_BAKED_ANIM_IMP_MAGIC); + if (imp->magic != UFBXI_BAKED_ANIM_IMP_MAGIC) return; + ufbxi_release_ref(&imp->refcount); +} + + +ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id) +{ + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_baked_node, 8, &index, bake->nodes.data, 0, bake->nodes.count, + ( a->typed_id < typed_id ), ( a->typed_id == typed_id) ); + return index < SIZE_MAX ? &bake->nodes.data[index] : NULL; +} + +ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node) +{ + if (!bake || !node) return NULL; + return ufbx_find_baked_node_by_typed_id(bake, node->typed_id); +} + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id) +{ + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_baked_element, 8, &index, bake->elements.data, 0, bake->elements.count, + ( a->element_id < element_id ), ( a->element_id == element_id) ); + return index < SIZE_MAX ? &bake->elements.data[index] : NULL; +} + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element) +{ + if (!bake || !element) return NULL; + return ufbx_find_baked_element_by_element_id(bake, element->element_id); +} + +ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time) +{ + size_t begin = 0; + size_t end = keyframes.count; + const ufbx_baked_vec3 *keys = keyframes.data; + while (end - begin >= 8) { + size_t mid = (begin + end) >> 1; + if (keys[mid].time <= time) { + begin = mid + 1; + } else { + end = mid; + } + } + + end = keyframes.count; + for (; begin < end; begin++) { + const ufbx_baked_vec3 *next = &keys[begin]; + if (next->time <= time) continue; + if (begin == 0) return next->value; + + const ufbx_baked_vec3 *prev = next - 1; + if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--; + if (time == prev->time) return prev->value; + double t = (time - prev->time) / (next->time - prev->time); + if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0; + if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0; + return ufbxi_lerp3(prev->value, next->value, (ufbx_real)t); + } + + return keyframes.data[keyframes.count - 1].value; +} + +ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, double time) +{ + size_t begin = 0; + size_t end = keyframes.count; + const ufbx_baked_quat *keys = keyframes.data; + while (end - begin >= 8) { + size_t mid = (begin + end) >> 1; + if (keys[mid].time <= time) { + begin = mid + 1; + } else { + end = mid; + } + } + + end = keyframes.count; + for (; begin < end; begin++) { + const ufbx_baked_quat *next = &keys[begin]; + if (next->time <= time) continue; + if (begin == 0) return next->value; + + const ufbx_baked_quat *prev = next - 1; + if (prev > keys && prev[-1].time == time) prev--; + if (time == prev->time) return prev->value; + double t = (time - prev->time) / (next->time - prev->time); + if (prev > keys && (prev->flags & UFBX_BAKED_KEY_STEP_RIGHT) != 0 && prev[-1].time == time) prev--; + if (prev->flags & UFBX_BAKED_KEY_STEP_LEFT) t = 0.0; + if (next->flags & UFBX_BAKED_KEY_STEP_RIGHT) t = 1.0; + return ufbx_quat_slerp(prev->value, next->value, (ufbx_real)t); + } + + return keyframes.data[keyframes.count - 1].value; +} + +ufbx_abi ufbx_bone_pose *ufbx_get_bone_pose(const ufbx_pose *pose, const ufbx_node *node) +{ + if (!pose || !node) return NULL; + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_bone_pose, 8, &index, pose->bone_poses.data, 0, pose->bone_poses.count, + ( a->bone_node->typed_id < node->typed_id ), ( a->bone_node == node )); + return index < SIZE_MAX ? &pose->bone_poses.data[index] : NULL; +} + +ufbx_abi ufbx_texture *ufbx_find_prop_texture_len(const ufbx_material *material, const char *name, size_t name_len) +{ + ufbx_string name_str = ufbxi_safe_string(name, name_len); + if (!material) return NULL; + + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_material_texture, 4, &index, material->textures.data, 0, material->textures.count, + ( ufbxi_str_less(a->material_prop, name_str) ), ( ufbxi_str_equal(a->material_prop, name_str) )); + return index < SIZE_MAX ? material->textures.data[index].texture : NULL; +} + +ufbx_abi ufbx_string ufbx_find_shader_prop_len(const ufbx_shader *shader, const char *name, size_t name_len) +{ + ufbx_shader_prop_binding_list bindings = ufbx_find_shader_prop_bindings_len(shader, name, name_len); + if (bindings.count > 0) { + return bindings.data[0].material_prop; + } + return ufbx_empty_string; +} + +ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings_len(const ufbx_shader *shader, const char *name, size_t name_len) +{ + ufbx_shader_prop_binding_list bindings = { NULL, 0 }; + + ufbx_string name_str = ufbxi_safe_string(name, name_len); + if (!shader) return bindings; + + ufbxi_for_ptr_list(ufbx_shader_binding, p_bind, shader->bindings) { + ufbx_shader_binding *bind = *p_bind; + + size_t begin = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_shader_prop_binding, 4, &begin, bind->prop_bindings.data, 0, bind->prop_bindings.count, + ( ufbxi_str_less(a->shader_prop, name_str) ), ( ufbxi_str_equal(a->shader_prop, name_str) )); + + if (begin != SIZE_MAX) { + + size_t end = begin; + ufbxi_macro_upper_bound_eq(ufbx_shader_prop_binding, 4, &end, bind->prop_bindings.data, begin, bind->prop_bindings.count, + ( ufbxi_str_equal(a->shader_prop, name_str) )); + + bindings.data = bind->prop_bindings.data + begin; + bindings.count = end - begin; + break; + } + } + + return bindings; +} + +ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input_len(const ufbx_shader_texture *shader, const char *name, size_t name_len) +{ + ufbx_string name_str = ufbxi_safe_string(name, name_len); + + size_t index = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_shader_texture_input, 4, &index, shader->inputs.data, 0, shader->inputs.count, + ( ufbxi_str_less(a->name, name_str) ), ( ufbxi_str_equal(a->name, name_str) )); + + if (index != SIZE_MAX) { + return &shader->inputs.data[index]; + } + + return NULL; +} + +ufbx_abi bool ufbx_coordinate_axes_valid(ufbx_coordinate_axes axes) +{ + if (axes.right < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.right > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false; + if (axes.up < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.up > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false; + if (axes.front < UFBX_COORDINATE_AXIS_POSITIVE_X || axes.front > UFBX_COORDINATE_AXIS_NEGATIVE_Z) return false; + + // Check that all the positive/negative axes are used + uint32_t mask = 0; + mask |= 1u << ((uint32_t)axes.right >> 1); + mask |= 1u << ((uint32_t)axes.up >> 1); + mask |= 1u << ((uint32_t)axes.front >> 1); + return (mask & 0x7u) == 0x7u; +} + +ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b) +{ + return ufbxi_mul_quat(a, b); +} + +ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v) +{ + return ufbxi_normalize3(v); +} + +ufbx_abi ufbxi_noinline ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b) +{ + return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; +} + +ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_normalize(ufbx_quat q) +{ + ufbx_real norm = ufbx_quat_dot(q, q); + if (norm == 0.0) return ufbx_identity_quat; + norm = (ufbx_real)ufbx_sqrt(norm); + q.x /= norm; + q.y /= norm; + q.z /= norm; + q.w /= norm; + return q; +} + +ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_fix_antipodal(ufbx_quat q, ufbx_quat reference) +{ + if (ufbx_quat_dot(q, reference) < 0.0f) { + q.x = -q.x; q.y = -q.y; q.z = -q.z; q.w = -q.w; + } + return q; +} + +ufbx_abi ufbxi_noinline ufbx_quat ufbx_quat_slerp(ufbx_quat a, ufbx_quat b, ufbx_real t) +{ + double dot = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; + if (dot < 0.0) { + dot = -dot; + b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w; + } + double omega = ufbx_acos(ufbx_fmin(ufbx_fmax(dot, 0.0), 1.0)); + if (omega <= 1.175494351e-38f) return a; + double rcp_so = 1.0 / ufbx_sin(omega); + double af = ufbx_sin((1.0 - t) * omega) * rcp_so; + double bf = ufbx_sin(t * omega) * rcp_so; + + double x = af*a.x + bf*b.x; + double y = af*a.y + bf*b.y; + double z = af*a.z + bf*b.z; + double w = af*a.w + bf*b.w; + double rcp_len = 1.0 / ufbx_sqrt(x*x + y*y + z*z + w*w); + + ufbx_quat ret; + ret.x = (ufbx_real)(x * rcp_len); + ret.y = (ufbx_real)(y * rcp_len); + ret.z = (ufbx_real)(z * rcp_len); + ret.w = (ufbx_real)(w * rcp_len); + return ret; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_rotate_vec3(ufbx_quat q, ufbx_vec3 v) +{ + ufbx_real xy = q.x*v.y - q.y*v.x; + ufbx_real xz = q.x*v.z - q.z*v.x; + ufbx_real yz = q.y*v.z - q.z*v.y; + ufbx_vec3 r; + r.x = 2.0f * (+ q.w*yz + q.y*xy + q.z*xz) + v.x; + r.y = 2.0f * (- q.x*xy - q.w*xz + q.z*yz) + v.y; + r.z = 2.0f * (- q.x*xz - q.y*yz + q.w*xy) + v.z; + return r; +} + +ufbx_abi ufbxi_noinline ufbx_quat ufbx_euler_to_quat(ufbx_vec3 v, ufbx_rotation_order order) +{ + double vx = v.x * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5); + double vy = v.y * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5); + double vz = v.z * (UFBXI_DEG_TO_RAD_DOUBLE * 0.5); + double cx = ufbx_cos(vx), sx = ufbx_sin(vx); + double cy = ufbx_cos(vy), sy = ufbx_sin(vy); + double cz = ufbx_cos(vz), sz = ufbx_sin(vz); + ufbx_quat q; + + // Generated by `misc/gen_rotation_order.py` + switch (order) { + case UFBX_ROTATION_ORDER_XYZ: + q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz); + break; + case UFBX_ROTATION_ORDER_XZY: + q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz); + break; + case UFBX_ROTATION_ORDER_YZX: + q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz); + break; + case UFBX_ROTATION_ORDER_YXZ: + q.x = (ufbx_real)(-cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy + cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz); + break; + case UFBX_ROTATION_ORDER_ZXY: + q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz - cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz + sx*sy*sz); + break; + case UFBX_ROTATION_ORDER_ZYX: + q.x = (ufbx_real)(cx*sy*sz + cy*cz*sx); + q.y = (ufbx_real)(cx*cz*sy - cy*sx*sz); + q.z = (ufbx_real)(cx*cy*sz + cz*sx*sy); + q.w = (ufbx_real)(cx*cy*cz - sx*sy*sz); + break; + default: + q.x = q.y = q.z = 0.0f; q.w = 1.0f; + break; + } + + return q; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_quat_to_euler(ufbx_quat q, ufbx_rotation_order order) +{ + // TODO: Derive these rigorously + #if defined(UFBX_REAL_IS_FLOAT) + const double eps = 0.9999999; + #else + const double eps = 0.999999999; + #endif + + double vx, vy, vz; + double t; + + double qx = q.x, qy = q.y, qz = q.z, qw = q.w; + + // Generated by `misc/gen_quat_to_euler.py` + switch (order) { + case UFBX_ROTATION_ORDER_XYZ: + t = 2.0f*(qw*qy - qx*qz); + if (ufbx_fabs(t) < eps) { + vy = ufbx_asin(t); + vz = ufbx_atan2(2.0f*(qw*qz + qx*qy), 2.0f*(qw*qw + qx*qx) - 1.0f); + vx = -ufbx_atan2(-2.0f*(qw*qx + qy*qz), 2.0f*(qw*qw + qz*qz) - 1.0f); + } else { + vy = ufbx_copysign(UFBXI_DPI*0.5, t); + vz = ufbx_atan2(-2.0f*t*(qw*qx - qy*qz), t*(2.0f*qw*qy + 2.0f*qx*qz)); + vx = 0.0f; + } + break; + case UFBX_ROTATION_ORDER_XZY: + t = 2.0f*(qw*qz + qx*qy); + if (ufbx_fabs(t) < eps) { + vz = ufbx_asin(t); + vy = ufbx_atan2(2.0f*(qw*qy - qx*qz), 2.0f*(qw*qw + qx*qx) - 1.0f); + vx = -ufbx_atan2(-2.0f*(qw*qx - qy*qz), 2.0f*(qw*qw + qy*qy) - 1.0f); + } else { + vz = ufbx_copysign(UFBXI_DPI*0.5, t); + vy = ufbx_atan2(2.0f*t*(qw*qx + qy*qz), -t*(2.0f*qx*qy - 2.0f*qw*qz)); + vx = 0.0f; + } + break; + case UFBX_ROTATION_ORDER_YZX: + t = 2.0f*(qw*qz - qx*qy); + if (ufbx_fabs(t) < eps) { + vz = ufbx_asin(t); + vx = ufbx_atan2(2.0f*(qw*qx + qy*qz), 2.0f*(qw*qw + qy*qy) - 1.0f); + vy = -ufbx_atan2(-2.0f*(qw*qy + qx*qz), 2.0f*(qw*qw + qx*qx) - 1.0f); + } else { + vz = ufbx_copysign(UFBXI_DPI*0.5, t); + vx = ufbx_atan2(-2.0f*t*(qw*qy - qx*qz), t*(2.0f*qw*qz + 2.0f*qx*qy)); + vy = 0.0f; + } + break; + case UFBX_ROTATION_ORDER_YXZ: + t = 2.0f*(qw*qx + qy*qz); + if (ufbx_fabs(t) < eps) { + vx = ufbx_asin(t); + vz = ufbx_atan2(2.0f*(qw*qz - qx*qy), 2.0f*(qw*qw + qy*qy) - 1.0f); + vy = -ufbx_atan2(-2.0f*(qw*qy - qx*qz), 2.0f*(qw*qw + qz*qz) - 1.0f); + } else { + vx = ufbx_copysign(UFBXI_DPI*0.5, t); + vz = ufbx_atan2(2.0f*t*(qw*qy + qx*qz), -t*(2.0f*qy*qz - 2.0f*qw*qx)); + vy = 0.0f; + } + break; + case UFBX_ROTATION_ORDER_ZXY: + t = 2.0f*(qw*qx - qy*qz); + if (ufbx_fabs(t) < eps) { + vx = ufbx_asin(t); + vy = ufbx_atan2(2.0f*(qw*qy + qx*qz), 2.0f*(qw*qw + qz*qz) - 1.0f); + vz = -ufbx_atan2(-2.0f*(qw*qz + qx*qy), 2.0f*(qw*qw + qy*qy) - 1.0f); + } else { + vx = ufbx_copysign(UFBXI_DPI*0.5, t); + vy = ufbx_atan2(-2.0f*t*(qw*qz - qx*qy), t*(2.0f*qw*qx + 2.0f*qy*qz)); + vz = 0.0f; + } + break; + case UFBX_ROTATION_ORDER_ZYX: + t = 2.0f*(qw*qy + qx*qz); + if (ufbx_fabs(t) < eps) { + vy = ufbx_asin(t); + vx = ufbx_atan2(2.0f*(qw*qx - qy*qz), 2.0f*(qw*qw + qz*qz) - 1.0f); + vz = -ufbx_atan2(-2.0f*(qw*qz - qx*qy), 2.0f*(qw*qw + qx*qx) - 1.0f); + } else { + vy = ufbx_copysign(UFBXI_DPI*0.5, t); + vx = ufbx_atan2(2.0f*t*(qw*qz + qx*qy), -t*(2.0f*qx*qz - 2.0f*qw*qy)); + vz = 0.0f; + } + break; + default: + vx = vy = vz = 0.0; + break; + } + + vx *= UFBXI_RAD_TO_DEG_DOUBLE; + vy *= UFBXI_RAD_TO_DEG_DOUBLE; + vz *= UFBXI_RAD_TO_DEG_DOUBLE; + + ufbx_vec3 v = { (ufbx_real)vx, (ufbx_real)vy, (ufbx_real)vz }; + return v; +} + +ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_mul(const ufbx_matrix *a, const ufbx_matrix *b) +{ + ufbx_assert(a && b); + if (!a || !b) return ufbx_identity_matrix; + + ufbx_matrix dst; + + dst.m03 = a->m00*b->m03 + a->m01*b->m13 + a->m02*b->m23 + a->m03; + dst.m13 = a->m10*b->m03 + a->m11*b->m13 + a->m12*b->m23 + a->m13; + dst.m23 = a->m20*b->m03 + a->m21*b->m13 + a->m22*b->m23 + a->m23; + + dst.m00 = a->m00*b->m00 + a->m01*b->m10 + a->m02*b->m20; + dst.m10 = a->m10*b->m00 + a->m11*b->m10 + a->m12*b->m20; + dst.m20 = a->m20*b->m00 + a->m21*b->m10 + a->m22*b->m20; + + dst.m01 = a->m00*b->m01 + a->m01*b->m11 + a->m02*b->m21; + dst.m11 = a->m10*b->m01 + a->m11*b->m11 + a->m12*b->m21; + dst.m21 = a->m20*b->m01 + a->m21*b->m11 + a->m22*b->m21; + + dst.m02 = a->m00*b->m02 + a->m01*b->m12 + a->m02*b->m22; + dst.m12 = a->m10*b->m02 + a->m11*b->m12 + a->m12*b->m22; + dst.m22 = a->m20*b->m02 + a->m21*b->m12 + a->m22*b->m22; + + return dst; +} + +ufbx_abi ufbx_real ufbx_matrix_determinant(const ufbx_matrix *m) +{ + return + - m->m02*m->m11*m->m20 + m->m01*m->m12*m->m20 + m->m02*m->m10*m->m21 + - m->m00*m->m12*m->m21 - m->m01*m->m10*m->m22 + m->m00*m->m11*m->m22; +} + +ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m) +{ + ufbx_real det = ufbx_matrix_determinant(m); + + ufbx_matrix r; + if (ufbx_fabs(det) <= UFBX_EPSILON) { + memset(&r, 0, sizeof(r)); + return r; + } + + ufbx_real rcp_det = 1.0f / det; + + r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * rcp_det; + r.m10 = ( + m->m12*m->m20 - m->m10*m->m22) * rcp_det; + r.m20 = ( - m->m11*m->m20 + m->m10*m->m21) * rcp_det; + r.m01 = ( + m->m02*m->m21 - m->m01*m->m22) * rcp_det; + r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * rcp_det; + r.m21 = ( + m->m01*m->m20 - m->m00*m->m21) * rcp_det; + r.m02 = ( - m->m02*m->m11 + m->m01*m->m12) * rcp_det; + r.m12 = ( + m->m02*m->m10 - m->m00*m->m12) * rcp_det; + r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * rcp_det; + r.m03 = (m->m03*m->m12*m->m21 - m->m02*m->m13*m->m21 - m->m03*m->m11*m->m22 + m->m01*m->m13*m->m22 + m->m02*m->m11*m->m23 - m->m01*m->m12*m->m23) * rcp_det; + r.m13 = (m->m02*m->m13*m->m20 - m->m03*m->m12*m->m20 + m->m03*m->m10*m->m22 - m->m00*m->m13*m->m22 - m->m02*m->m10*m->m23 + m->m00*m->m12*m->m23) * rcp_det; + r.m23 = (m->m03*m->m11*m->m20 - m->m01*m->m13*m->m20 - m->m03*m->m10*m->m21 + m->m00*m->m13*m->m21 + m->m01*m->m10*m->m23 - m->m00*m->m11*m->m23) * rcp_det; + + return r; +} + +ufbx_abi ufbxi_noinline ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m) +{ + ufbx_real det = ufbx_matrix_determinant(m); + ufbx_real det_sign = det >= 0.0f ? 1.0f : -1.0f; + + ufbx_matrix r; + r.m00 = ( - m->m12*m->m21 + m->m11*m->m22) * det_sign; + r.m01 = ( + m->m12*m->m20 - m->m10*m->m22) * det_sign; + r.m02 = ( - m->m11*m->m20 + m->m10*m->m21) * det_sign; + r.m10 = ( + m->m02*m->m21 - m->m01*m->m22) * det_sign; + r.m11 = ( - m->m02*m->m20 + m->m00*m->m22) * det_sign; + r.m12 = ( + m->m01*m->m20 - m->m00*m->m21) * det_sign; + r.m20 = ( - m->m02*m->m11 + m->m01*m->m12) * det_sign; + r.m21 = ( + m->m02*m->m10 - m->m00*m->m12) * det_sign; + r.m22 = ( - m->m01*m->m10 + m->m00*m->m11) * det_sign; + r.m03 = r.m13 = r.m23 = 0.0f; + + return r; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_position(const ufbx_matrix *m, ufbx_vec3 v) +{ + ufbx_assert(m); + if (!m) return ufbx_zero_vec3; + + ufbx_vec3 r; + r.x = m->m00*v.x + m->m01*v.y + m->m02*v.z + m->m03; + r.y = m->m10*v.x + m->m11*v.y + m->m12*v.z + m->m13; + r.z = m->m20*v.x + m->m21*v.y + m->m22*v.z + m->m23; + return r; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_transform_direction(const ufbx_matrix *m, ufbx_vec3 v) +{ + ufbx_assert(m); + if (!m) return ufbx_zero_vec3; + + ufbx_vec3 r; + r.x = m->m00*v.x + m->m01*v.y + m->m02*v.z; + r.y = m->m10*v.x + m->m11*v.y + m->m12*v.z; + r.z = m->m20*v.x + m->m21*v.y + m->m22*v.z; + return r; +} + +ufbx_abi ufbxi_noinline ufbx_matrix ufbx_transform_to_matrix(const ufbx_transform *t) +{ + ufbx_assert(t); + if (!t) return ufbx_identity_matrix; + + ufbx_quat q = t->rotation; + ufbx_real sx = 2.0f * t->scale.x, sy = 2.0f * t->scale.y, sz = 2.0f * t->scale.z; + ufbx_real xx = q.x*q.x, xy = q.x*q.y, xz = q.x*q.z, xw = q.x*q.w; + ufbx_real yy = q.y*q.y, yz = q.y*q.z, yw = q.y*q.w; + ufbx_real zz = q.z*q.z, zw = q.z*q.w; + ufbx_matrix m; + m.m00 = sx * (- yy - zz + 0.5f); + m.m10 = sx * (+ xy + zw); + m.m20 = sx * (- yw + xz); + m.m01 = sy * (- zw + xy); + m.m11 = sy * (- xx - zz + 0.5f); + m.m21 = sy * (+ xw + yz); + m.m02 = sz * (+ xz + yw); + m.m12 = sz * (- xw + yz); + m.m22 = sz * (- xx - yy + 0.5f); + m.m03 = t->translation.x; + m.m13 = t->translation.y; + m.m23 = t->translation.z; + return m; +} + +ufbx_abi ufbxi_noinline ufbx_transform ufbx_matrix_to_transform(const ufbx_matrix *m) +{ + ufbx_assert(m); + if (!m) return ufbx_identity_transform; + + ufbx_real det = ufbx_matrix_determinant(m); + + ufbx_transform t; + t.translation = m->cols[3]; + t.scale.x = ufbxi_length3(m->cols[0]); + t.scale.y = ufbxi_length3(m->cols[1]); + t.scale.z = ufbxi_length3(m->cols[2]); + + // Flip a single non-zero axis if negative determinant + ufbx_real sign_x = 1.0f; + ufbx_real sign_y = 1.0f; + ufbx_real sign_z = 1.0f; + if (det < 0.0f) { + if (t.scale.x > 0.0f) sign_x = -1.0f; + else if (t.scale.y > 0.0f) sign_y = -1.0f; + else if (t.scale.z > 0.0f) sign_z = -1.0f; + } + + ufbx_vec3 x = ufbxi_mul3(m->cols[0], t.scale.x > 0.0f ? sign_x / t.scale.x : 0.0f); + ufbx_vec3 y = ufbxi_mul3(m->cols[1], t.scale.y > 0.0f ? sign_y / t.scale.y : 0.0f); + ufbx_vec3 z = ufbxi_mul3(m->cols[2], t.scale.z > 0.0f ? sign_z / t.scale.z : 0.0f); + ufbx_real trace = x.x + y.y + z.z; + if (trace > 0.0f) { + ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, trace + 1.0)), b = (a != 0.0f) ? 0.5f / a : 0.0f; + t.rotation.x = (y.z - z.y) * b; + t.rotation.y = (z.x - x.z) * b; + t.rotation.z = (x.y - y.x) * b; + t.rotation.w = 0.5f * a; + } else if (x.x > y.y && x.x > z.z) { + ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 + x.x - y.y - z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f; + t.rotation.x = 0.5f * a; + t.rotation.y = (y.x + x.y) * b; + t.rotation.z = (z.x + x.z) * b; + t.rotation.w = (y.z - z.y) * b; + } + else if (y.y > z.z) { + ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 - x.x + y.y - z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f; + t.rotation.x = (y.x + x.y) * b; + t.rotation.y = 0.5f * a; + t.rotation.z = (z.y + y.z) * b; + t.rotation.w = (z.x - x.z) * b; + } + else { + ufbx_real a = (ufbx_real)ufbx_sqrt(ufbx_fmax(0.0, 1.0 - x.x - y.y + z.z)), b = (a != 0.0f) ? 0.5f / a : 0.0f; + t.rotation.x = (z.x + x.z) * b; + t.rotation.y = (z.y + y.z) * b; + t.rotation.z = 0.5f * a; + t.rotation.w = (x.y - y.x) * b; + } + + ufbx_real len = t.rotation.x*t.rotation.x + t.rotation.y*t.rotation.y + t.rotation.z*t.rotation.z + t.rotation.w*t.rotation.w; + if (ufbx_fabs(len - 1.0f) > UFBX_EPSILON) { + if (ufbx_fabs(len) <= UFBX_EPSILON) { + t.rotation = ufbx_identity_quat; + } else { + t.rotation.x /= len; + t.rotation.y /= len; + t.rotation.z /= len; + t.rotation.w /= len; + } + } + + t.scale.x *= sign_x; + t.scale.y *= sign_y; + t.scale.z *= sign_z; + + return t; +} + +ufbx_abi ufbxi_noinline ufbx_matrix ufbx_catch_get_skin_vertex_matrix(ufbx_panic *panic, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback) +{ + ufbx_assert(skin); + if (ufbxi_panicf(panic, vertex < skin->vertices.count, "vertex (%zu) out of bounds (%zu)", vertex, skin->vertices.count)) return ufbx_identity_matrix; + + if (!skin || vertex >= skin->vertices.count) return ufbx_identity_matrix; + ufbx_skin_vertex skin_vertex = skin->vertices.data[vertex]; + + ufbx_matrix mat = { 0.0f }; + ufbx_quat q0 = { 0.0f }, qe = { 0.0f }; + ufbx_quat first_q0 = { 0.0f }; + ufbx_vec3 qs = { 0.0f, 0.0f, 0.0f }; + ufbx_real total_weight = 0.0f; + + for (uint32_t i = 0; i < skin_vertex.num_weights; i++) { + ufbx_skin_weight weight = skin->weights.data[skin_vertex.weight_begin + i]; + ufbx_skin_cluster *cluster = skin->clusters.data[weight.cluster_index]; + const ufbx_node *node = cluster->bone_node; + if (!node) continue; + + total_weight += weight.weight; + if (skin_vertex.dq_weight > 0.0f) { + ufbx_transform t = cluster->geometry_to_world_transform; + ufbx_quat vq0 = t.rotation; + if (i == 0) first_q0 = vq0; + + if (ufbx_quat_dot(first_q0, vq0) < 0.0f) { + vq0.x = -vq0.x; + vq0.y = -vq0.y; + vq0.z = -vq0.z; + vq0.w = -vq0.w; + } + + ufbx_quat vqt = { 0.5f * t.translation.x, 0.5f * t.translation.y, 0.5f * t.translation.z }; + ufbx_quat vqe = ufbxi_mul_quat(vqt, vq0); + ufbxi_add_weighted_quat(&q0, vq0, weight.weight); + ufbxi_add_weighted_quat(&qe, vqe, weight.weight); + ufbxi_add_weighted_vec3(&qs, t.scale, weight.weight); + } + + if (skin_vertex.dq_weight < 1.0f) { + ufbxi_add_weighted_mat(&mat, &cluster->geometry_to_world, (1.0f-skin_vertex.dq_weight) * weight.weight); + } + } + + if (total_weight <= 0.0f) { + if (fallback) { + return *fallback; + } else { + return ufbx_identity_matrix; + } + } + + if (ufbx_fabs(total_weight - 1.0f) > UFBX_EPSILON) { + ufbx_real rcp_weight = ufbx_fabs(total_weight) > UFBX_EPSILON ? 1.0f / total_weight : 0.0f; + if (skin_vertex.dq_weight > 0.0f) { + q0.x *= rcp_weight; q0.y *= rcp_weight; q0.z *= rcp_weight; q0.w *= rcp_weight; + qe.x *= rcp_weight; qe.y *= rcp_weight; qe.z *= rcp_weight; qe.w *= rcp_weight; + qs.x *= rcp_weight; qs.y *= rcp_weight; qs.z *= rcp_weight; + } + if (skin_vertex.dq_weight < 1.0f) { + mat.m00 *= rcp_weight; mat.m01 *= rcp_weight; mat.m02 *= rcp_weight; mat.m03 *= rcp_weight; + mat.m10 *= rcp_weight; mat.m11 *= rcp_weight; mat.m12 *= rcp_weight; mat.m13 *= rcp_weight; + mat.m20 *= rcp_weight; mat.m21 *= rcp_weight; mat.m22 *= rcp_weight; mat.m23 *= rcp_weight; + } + } + + if (skin_vertex.dq_weight > 0.0f) { + ufbx_transform dqt; // ufbxi_uninit + ufbx_real rcp_len = (ufbx_real)(1.0 / ufbx_sqrt(q0.x*q0.x + q0.y*q0.y + q0.z*q0.z + q0.w*q0.w)); + ufbx_real rcp_len2x2 = 2.0f * rcp_len * rcp_len; + dqt.rotation.x = q0.x * rcp_len; + dqt.rotation.y = q0.y * rcp_len; + dqt.rotation.z = q0.z * rcp_len; + dqt.rotation.w = q0.w * rcp_len; + dqt.scale.x = qs.x; + dqt.scale.y = qs.y; + dqt.scale.z = qs.z; + dqt.translation.x = rcp_len2x2 * (- qe.w*q0.x + qe.x*q0.w - qe.y*q0.z + qe.z*q0.y); + dqt.translation.y = rcp_len2x2 * (- qe.w*q0.y + qe.x*q0.z + qe.y*q0.w - qe.z*q0.x); + dqt.translation.z = rcp_len2x2 * (- qe.w*q0.z - qe.x*q0.y + qe.y*q0.x + qe.z*q0.w); + ufbx_matrix dqm = ufbx_transform_to_matrix(&dqt); + if (skin_vertex.dq_weight < 1.0f) { + ufbxi_add_weighted_mat(&mat, &dqm, skin_vertex.dq_weight); + } else { + mat = dqm; + } + } + + return mat; +} + +ufbx_abi ufbxi_noinline uint32_t ufbx_get_blend_shape_offset_index(const ufbx_blend_shape *shape, size_t vertex) +{ + ufbx_assert(shape); + if (!shape) return UFBX_NO_INDEX; + + size_t index = SIZE_MAX; + uint32_t vertex_ix = (uint32_t)vertex; + + ufbxi_macro_lower_bound_eq(uint32_t, 16, &index, shape->offset_vertices.data, 0, shape->num_offsets, + ( *a < vertex_ix ), ( *a == vertex_ix )); + if (index >= UINT32_MAX) return UFBX_NO_INDEX; + + return (uint32_t)index; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_shape_vertex_offset(const ufbx_blend_shape *shape, size_t vertex) +{ + uint32_t index = ufbx_get_blend_shape_offset_index(shape, vertex); + if (index == UFBX_NO_INDEX) return ufbx_zero_vec3; + return shape->position_offsets.data[index]; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_get_blend_vertex_offset(const ufbx_blend_deformer *blend, size_t vertex) +{ + ufbx_assert(blend); + if (!blend) return ufbx_zero_vec3; + + ufbx_vec3 offset = ufbx_zero_vec3; + + ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, blend->channels) { + ufbx_blend_channel *chan = *p_chan; + ufbxi_for_list(ufbx_blend_keyframe, key, chan->keyframes) { + if (key->effective_weight == 0.0f) continue; + + ufbx_vec3 key_offset = ufbx_get_blend_shape_vertex_offset(key->shape, vertex); + ufbxi_add_weighted_vec3(&offset, key_offset, key->effective_weight); + } + } + + return offset; +} + +ufbx_abi void ufbx_add_blend_shape_vertex_offsets(const ufbx_blend_shape *shape, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight) +{ + if (weight == 0.0f) return; + if (!vertices) return; + + size_t num_offsets = shape->num_offsets; + uint32_t *vertex_indices = shape->offset_vertices.data; + ufbx_vec3 *offsets = shape->position_offsets.data; + ufbx_real_list weights = shape->offset_weights; + for (size_t i = 0; i < num_offsets; i++) { + uint32_t index = vertex_indices[i]; + if (index < num_vertices) { + ufbx_real vertex_weight = weight; + if (i < weights.count) { + vertex_weight *= weights.data[i]; + } + ufbxi_add_weighted_vec3(&vertices[index], offsets[i], vertex_weight); + } + } +} + +ufbx_abi void ufbx_add_blend_vertex_offsets(const ufbx_blend_deformer *blend, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight) +{ + ufbx_assert(blend); + if (!blend) return; + + ufbxi_for_ptr_list(ufbx_blend_channel, p_chan, blend->channels) { + ufbx_blend_channel *chan = *p_chan; + ufbxi_for_list(ufbx_blend_keyframe, key, chan->keyframes) { + if (key->effective_weight == 0.0f) continue; + ufbx_add_blend_shape_vertex_offsets(key->shape, vertices, num_vertices, weight * key->effective_weight); + } + } +} + +ufbx_abi size_t ufbx_evaluate_nurbs_basis(const ufbx_nurbs_basis *basis, ufbx_real u, ufbx_real *weights, size_t num_weights, ufbx_real *derivatives, size_t num_derivatives) +{ + ufbx_assert(basis); + if (!basis) return SIZE_MAX; + if (basis->order == 0) return SIZE_MAX; + if (!basis->valid) return SIZE_MAX; + + size_t degree = basis->order - 1; + ufbx_assert(degree >= 1); + + // Binary search for the knot span `[min_u, max_u]` where `min_u <= u < max_u` + ufbx_real_list knots = basis->knot_vector; + size_t knot = SIZE_MAX; + + if (u <= basis->t_min) { + knot = degree; + u = basis->t_min; + } else if (u >= basis->t_max) { + knot = basis->knot_vector.count - degree - 2; + u = basis->t_max; + } else { + ufbxi_macro_lower_bound_eq(ufbx_real, 8, &knot, knots.data, 0, knots.count - 1, + ( a[1] <= u ), ( a[0] <= u && u < a[1] )); + } + + // The found effective control points are found left from `knot`, locally + // we use `knot - ix` here as it's more convenient for the following algorithm + // but we return it as `knot - degree` so that users can find the control points + // at `points[knot], points[knot+1], ..., points[knot+degree]` + if (knot < degree) return SIZE_MAX; + + if (num_derivatives == 0) derivatives = NULL; + if (num_weights < basis->order) return knot - degree; + if (!weights) return knot - degree; + + weights[0] = 1.0f; + for (size_t p = 1; p <= degree; p++) { + + ufbx_real prev = 0.0f; + ufbx_real g = 1.0f - ufbxi_nurbs_weight(&knots, knot - p + 1, p, u); + ufbx_real dg = 0.0f; + if (derivatives && p == degree) { + dg = ufbxi_nurbs_deriv(&knots, knot - p + 1, p); + } + + for (size_t i = p; i > 0; i--) { + ufbx_real f = ufbxi_nurbs_weight(&knots, knot - p + i, p, u); + ufbx_real weight = weights[i - 1]; + weights[i] = f*weight + g*prev; + + if (derivatives && p == degree) { + ufbx_real df = ufbxi_nurbs_deriv(&knots, knot - p + i, p); + if (i < num_derivatives) { + derivatives[i] = df*weight - dg*prev; + } + dg = df; + } + + prev = weight; + g = 1.0f - f; + } + + weights[0] = g*prev; + if (derivatives && p == degree) { + derivatives[0] = -dg*prev; + } + } + + return knot - degree; +} + +ufbx_abi ufbxi_noinline ufbx_curve_point ufbx_evaluate_nurbs_curve(const ufbx_nurbs_curve *curve, ufbx_real u) +{ + ufbx_curve_point result = { false }; + + ufbx_assert(curve); + if (!curve) return result; + + ufbx_real weights[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit + ufbx_real derivs[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit + size_t base = ufbx_evaluate_nurbs_basis(&curve->basis, u, weights, UFBXI_MAX_NURBS_ORDER, derivs, UFBXI_MAX_NURBS_ORDER); + if (base == SIZE_MAX) return result; + + ufbx_vec4 p = { 0 }; + ufbx_vec4 d = { 0 }; + + size_t order = curve->basis.order; + if (order > UFBXI_MAX_NURBS_ORDER) return result; + if (curve->control_points.count == 0) return result; + + for (size_t i = 0; i < order; i++) { + size_t ix = (base + i) % curve->control_points.count; + ufbx_vec4 cp = curve->control_points.data[ix]; + ufbx_real weight = weights[i] * cp.w, deriv = derivs[i] * cp.w; + + p.x += cp.x * weight; + p.y += cp.y * weight; + p.z += cp.z * weight; + p.w += weight; + + d.x += cp.x * deriv; + d.y += cp.y * deriv; + d.z += cp.z * deriv; + d.w += deriv; + } + + ufbx_real rcp_w = 1.0f / p.w; + result.valid = true; + result.position.x = p.x * rcp_w; + result.position.y = p.y * rcp_w; + result.position.z = p.z * rcp_w; + result.derivative.x = (d.x - d.w*result.position.x) * rcp_w; + result.derivative.y = (d.y - d.w*result.position.y) * rcp_w; + result.derivative.z = (d.z - d.w*result.position.z) * rcp_w; + return result; +} + +ufbx_abi ufbxi_noinline ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v) +{ + ufbx_surface_point result = { false }; + + ufbx_assert(surface); + if (!surface) return result; + + ufbx_real weights_u[UFBXI_MAX_NURBS_ORDER], weights_v[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit + ufbx_real derivs_u[UFBXI_MAX_NURBS_ORDER], derivs_v[UFBXI_MAX_NURBS_ORDER]; // ufbxi_uninit + size_t base_u = ufbx_evaluate_nurbs_basis(&surface->basis_u, u, weights_u, UFBXI_MAX_NURBS_ORDER, derivs_u, UFBXI_MAX_NURBS_ORDER); + size_t base_v = ufbx_evaluate_nurbs_basis(&surface->basis_v, v, weights_v, UFBXI_MAX_NURBS_ORDER, derivs_v, UFBXI_MAX_NURBS_ORDER); + if (base_u == SIZE_MAX || base_v == SIZE_MAX) return result; + + ufbx_vec4 p = { 0 }; + ufbx_vec4 du = { 0 }; + ufbx_vec4 dv = { 0 }; + + size_t num_u = surface->num_control_points_u; + size_t num_v = surface->num_control_points_v; + size_t order_u = surface->basis_u.order; + size_t order_v = surface->basis_v.order; + if (order_u > UFBXI_MAX_NURBS_ORDER || order_v > UFBXI_MAX_NURBS_ORDER) return result; + if (num_u == 0 || num_v == 0) return result; + + for (size_t vi = 0; vi < order_v; vi++) { + size_t vix = (base_v + vi) % num_v; + ufbx_real weight_v = weights_v[vi], deriv_v = derivs_v[vi]; + + for (size_t ui = 0; ui < order_u; ui++) { + size_t uix = (base_u + ui) % num_u; + ufbx_real weight_u = weights_u[ui], deriv_u = derivs_u[ui]; + ufbx_vec4 cp = surface->control_points.data[vix * num_u + uix]; + + ufbx_real weight = weight_u * weight_v * cp.w; + ufbx_real wderiv_u = deriv_u * weight_v * cp.w; + ufbx_real wderiv_v = deriv_v * weight_u * cp.w; + + p.x += cp.x * weight; + p.y += cp.y * weight; + p.z += cp.z * weight; + p.w += weight; + + du.x += cp.x * wderiv_u; + du.y += cp.y * wderiv_u; + du.z += cp.z * wderiv_u; + du.w += wderiv_u; + + dv.x += cp.x * wderiv_v; + dv.y += cp.y * wderiv_v; + dv.z += cp.z * wderiv_v; + dv.w += wderiv_v; + } + } + + ufbx_real rcp_w = 1.0f / p.w; + result.valid = true; + result.position.x = p.x * rcp_w; + result.position.y = p.y * rcp_w; + result.position.z = p.z * rcp_w; + result.derivative_u.x = (du.x - du.w*result.position.x) * rcp_w; + result.derivative_u.y = (du.y - du.w*result.position.y) * rcp_w; + result.derivative_u.z = (du.z - du.w*result.position.z) * rcp_w; + result.derivative_v.x = (dv.x - dv.w*result.position.x) * rcp_w; + result.derivative_v.y = (dv.y - dv.w*result.position.y) * rcp_w; + result.derivative_v.z = (dv.z - dv.w*result.position.z) * rcp_w; + return result; +} + +ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error) +{ +#if UFBXI_FEATURE_TESSELLATION + ufbxi_check_opts_ptr(ufbx_line_curve, opts, error); + ufbx_assert(curve); + if (!curve) return NULL; + + ufbxi_tessellate_curve_context tc = { UFBX_ERROR_NONE }; + if (opts) { + tc.opts = *opts; + } + + tc.curve = curve; + + int ok = ufbxi_tessellate_nurbs_curve_imp(&tc); + + ufbxi_free_ator(&tc.ator_tmp); + + if (ok) { + ufbxi_clear_error(error); + ufbxi_line_curve_imp *imp = tc.imp; + return &imp->curve; + } else { + ufbxi_fix_error_type(&tc.error, "Failed to tessellate", error); + ufbxi_buf_free(&tc.result); + ufbxi_free_ator(&tc.ator_result); + return NULL; + } +#else + if (error) { + memset(error, 0, sizeof(ufbx_error)); + ufbxi_fmt_err_info(error, "UFBX_ENABLE_TESSELLATION"); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_TESSELLATION", "Feature disabled"); + } + return NULL; +#endif +} + +ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surface, const ufbx_tessellate_surface_opts *opts, ufbx_error *error) +{ +#if UFBXI_FEATURE_TESSELLATION + ufbx_assert(surface); + ufbxi_check_opts_ptr(ufbx_mesh, opts, error); + if (!surface) return NULL; + + ufbxi_tessellate_surface_context tc = { UFBX_ERROR_NONE }; + if (opts) { + tc.opts = *opts; + } + + tc.surface = surface; + + int ok = ufbxi_tessellate_nurbs_surface_imp(&tc); + + ufbxi_buf_free(&tc.tmp); + ufbxi_map_free(&tc.position_map); + ufbxi_free_ator(&tc.ator_tmp); + + if (ok) { + ufbxi_clear_error(error); + ufbxi_mesh_imp *imp = tc.imp; + return &imp->mesh; + } else { + ufbxi_fix_error_type(&tc.error, "Failed to tessellate", error); + ufbxi_buf_free(&tc.result); + ufbxi_free_ator(&tc.ator_result); + return NULL; + } +#else + if (error) { + memset(error, 0, sizeof(ufbx_error)); + ufbxi_report_err_msg(error, "UFBXI_FEATURE_TESSELLATION", "Feature disabled"); + } + return NULL; +#endif +} + +ufbx_abi void ufbx_free_line_curve(ufbx_line_curve *line_curve) +{ + if (!line_curve) return; + if (!line_curve->from_tessellated_nurbs) return; + + ufbxi_line_curve_imp *imp = ufbxi_get_imp(ufbxi_line_curve_imp, line_curve); + ufbx_assert(imp->magic == UFBXI_LINE_CURVE_IMP_MAGIC); + if (imp->magic != UFBXI_LINE_CURVE_IMP_MAGIC) return; + ufbxi_release_ref(&imp->refcount); +} + +ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *line_curve) +{ + if (!line_curve) return; + if (!line_curve->from_tessellated_nurbs) return; + + ufbxi_line_curve_imp *imp = ufbxi_get_imp(ufbxi_line_curve_imp, line_curve); + ufbx_assert(imp->magic == UFBXI_LINE_CURVE_IMP_MAGIC); + if (imp->magic != UFBXI_LINE_CURVE_IMP_MAGIC) return; + ufbxi_retain_ref(&imp->refcount); +} + +ufbx_abi uint32_t ufbx_find_face_index(ufbx_mesh *mesh, size_t index) +{ + if (!mesh || index > UINT32_MAX) return UFBX_NO_INDEX; + uint32_t ix = (uint32_t)index; + + size_t face_ix = SIZE_MAX; + ufbxi_macro_lower_bound_eq(ufbx_face, 4, &face_ix, mesh->faces.data, 0, mesh->faces.count, + ( a->index_begin + a->num_indices <= ix ), ( ix >= a->index_begin && ix < a->index_begin + a->num_indices )); + return (uint32_t)face_ix; +} + +ufbx_abi ufbxi_noinline uint32_t ufbx_catch_triangulate_face(ufbx_panic *panic, uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face) +{ +#if UFBXI_FEATURE_TRIANGULATION + if (face.num_indices < 3) return 0; + + size_t required_indices = ((size_t)face.num_indices - 2) * 3; + if (ufbxi_panicf(panic, num_indices >= required_indices, "Face needs at least %zu indices for triangles, got space for %zu", required_indices, num_indices)) return 0; + if (ufbxi_panicf(panic, face.index_begin < mesh->num_indices, "Face index begin (%u) out of bounds (%zu)", face.index_begin, mesh->num_indices)) return 0; + if (ufbxi_panicf(panic, mesh->num_indices - face.index_begin >= face.num_indices, "Face index end (%u + %u) out of bounds (%zu)", face.index_begin, face.num_indices, mesh->num_indices)) return 0; + + if (face.num_indices == 3) { + // Fast case: Already a triangle + indices[0] = face.index_begin + 0; + indices[1] = face.index_begin + 1; + indices[2] = face.index_begin + 2; + return 1; + } else if (face.num_indices == 4) { + // Quad: Split along the shortest axis unless a vertex crosses the axis + uint32_t i0 = face.index_begin + 0; + uint32_t i1 = face.index_begin + 1; + uint32_t i2 = face.index_begin + 2; + uint32_t i3 = face.index_begin + 3; + ufbx_vec3 v0 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i0]]; + ufbx_vec3 v1 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i1]]; + ufbx_vec3 v2 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i2]]; + ufbx_vec3 v3 = mesh->vertex_position.values.data[mesh->vertex_position.indices.data[i3]]; + + ufbx_vec3 a = ufbxi_sub3(v2, v0); + ufbx_vec3 b = ufbxi_sub3(v3, v1); + + ufbx_vec3 na1 = ufbxi_normalize3(ufbxi_cross3(a, ufbxi_sub3(v1, v0))); + ufbx_vec3 na3 = ufbxi_normalize3(ufbxi_cross3(a, ufbxi_sub3(v0, v3))); + ufbx_vec3 nb0 = ufbxi_normalize3(ufbxi_cross3(b, ufbxi_sub3(v1, v0))); + ufbx_vec3 nb2 = ufbxi_normalize3(ufbxi_cross3(b, ufbxi_sub3(v2, v1))); + + ufbx_real dot_aa = ufbxi_dot3(a, a); + ufbx_real dot_bb = ufbxi_dot3(b, b); + ufbx_real dot_na = ufbxi_dot3(na1, na3); + ufbx_real dot_nb = ufbxi_dot3(nb0, nb2); + + bool split_a = dot_aa <= dot_bb; + + if (dot_na < 0.0f || dot_nb < 0.0f) { + split_a = dot_na >= dot_nb; + } + + if (split_a) { + indices[0] = i0; + indices[1] = i1; + indices[2] = i2; + indices[3] = i2; + indices[4] = i3; + indices[5] = i0; + } else { + indices[0] = i1; + indices[1] = i2; + indices[2] = i3; + indices[3] = i3; + indices[4] = i0; + indices[5] = i1; + } + + return 2; + } else { + ufbxi_ngon_context nc = { 0 }; + nc.positions = mesh->vertex_position; + nc.face = face; + + uint32_t num_indices_u32 = num_indices < UINT32_MAX ? (uint32_t)num_indices : UINT32_MAX; + + uint32_t local_indices[12]; // ufbxi_uninit + if (num_indices_u32 < 12) { + uint32_t num_tris = ufbxi_triangulate_ngon(&nc, local_indices, 12); + memcpy(indices, local_indices, num_tris * 3 * sizeof(uint32_t)); + return num_tris; + } else { + return ufbxi_triangulate_ngon(&nc, indices, num_indices_u32); + } + } +#else + ufbxi_panicf_imp(panic, "Triangulation disabled"); + return 0; +#endif +} + +ufbx_abi void ufbx_catch_compute_topology(ufbx_panic *panic, const ufbx_mesh *mesh, ufbx_topo_edge *indices, size_t num_indices) +{ + if (ufbxi_panicf(panic, num_indices >= mesh->num_indices, "Required mesh.num_indices (%zu) indices, got %zu", mesh->num_indices, num_indices)) return; + + ufbxi_compute_topology(mesh, indices); +} + +ufbx_abi uint32_t ufbx_catch_topo_next_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) +{ + if (index == UFBX_NO_INDEX) return UFBX_NO_INDEX; + if (ufbxi_panicf(panic, (size_t)index < num_topo, "index (%u) out of bounds (%zu)", index, num_topo)) return UFBX_NO_INDEX; + uint32_t twin = topo[index].twin; + if (twin == UFBX_NO_INDEX) return UFBX_NO_INDEX; + if (ufbxi_panicf(panic, (size_t)twin < num_topo, "Corrupted topology structure")) return UFBX_NO_INDEX; + return topo[twin].next; +} + +ufbx_abi uint32_t ufbx_catch_topo_prev_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) +{ + if (index == UFBX_NO_INDEX) return UFBX_NO_INDEX; + if (ufbxi_panicf(panic, (size_t)index < num_topo, "index (%u) out of bounds (%zu)", index, num_topo)) return UFBX_NO_INDEX; + return topo[topo[index].prev].twin; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic *panic, const ufbx_vertex_vec3 *positions, ufbx_face face) +{ + if (ufbxi_panicf(panic, face.index_begin <= positions->indices.count, "Face index begin (%u) out of bounds (%zu)", face.index_begin, positions->indices.count)) return ufbx_zero_vec3; + if (ufbxi_panicf(panic, positions->indices.count - face.index_begin >= face.num_indices, "Face index end (%u + %u) out of bounds (%zu)", face.index_begin, face.num_indices, positions->indices.count)) return ufbx_zero_vec3; + + if (face.num_indices < 3) { + return ufbx_zero_vec3; + } else if (face.num_indices == 3) { + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); + ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); + return ufbxi_cross3(ufbxi_sub3(b, a), ufbxi_sub3(c, a)); + } else if (face.num_indices == 4) { + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + 0); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + 1); + ufbx_vec3 c = ufbx_get_vertex_vec3(positions, face.index_begin + 2); + ufbx_vec3 d = ufbx_get_vertex_vec3(positions, face.index_begin + 3); + return ufbxi_cross3(ufbxi_sub3(c, a), ufbxi_sub3(d, b)); + } else { + // Newell's Method + ufbx_vec3 result = ufbx_zero_vec3; + for (size_t i = 0; i < face.num_indices; i++) { + size_t next = i + 1 < face.num_indices ? i + 1 : 0; + ufbx_vec3 a = ufbx_get_vertex_vec3(positions, face.index_begin + i); + ufbx_vec3 b = ufbx_get_vertex_vec3(positions, face.index_begin + next); + result.x += (a.y - b.y) * (a.z + b.z); + result.y += (a.z - b.z) * (a.x + b.x); + result.z += (a.x - b.x) * (a.y + b.y); + } + return result; + } +} + +size_t ufbx_catch_generate_normal_mapping(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth) +{ + uint32_t next_index = 0; + if (ufbxi_panicf(panic, num_normal_indices >= mesh->num_indices, "Expected at least mesh.num_indices (%zu), got %zu", mesh->num_indices, num_normal_indices)) return 0; + + for (size_t i = 0; i < mesh->num_indices; i++) { + normal_indices[i] = UFBX_NO_INDEX; + } + + // Walk around vertices and merge around smooth edges + for (size_t vi = 0; vi < mesh->num_vertices; vi++) { + uint32_t original_start = mesh->vertex_first_index.data[vi]; + if (original_start == UFBX_NO_INDEX) continue; + uint32_t start = original_start, cur = start; + + for (;;) { + uint32_t prev = ufbx_topo_next_vertex_edge(topo, num_topo, cur); + if (!ufbxi_is_edge_smooth(mesh, topo, num_topo, cur, assume_smooth)) start = cur; + if (prev == UFBX_NO_INDEX) { start = cur; break; } + if (prev == original_start) break; + cur = prev; + } + + normal_indices[start] = next_index++; + uint32_t next = start; + for (;;) { + next = ufbx_topo_prev_vertex_edge(topo, num_topo, next); + if (next == UFBX_NO_INDEX || next == start) break; + + if (!ufbxi_is_edge_smooth(mesh, topo, num_topo, next, assume_smooth)) { + ++next_index; + } + normal_indices[next] = next_index - 1; + } + } + + // Assign non-manifold indices + for (size_t i = 0; i < mesh->num_indices; i++) { + if (normal_indices[i] == UFBX_NO_INDEX) { + normal_indices[i] = next_index++; + } + } + + return (size_t)next_index; +} + +ufbx_abi size_t ufbx_generate_normal_mapping(const ufbx_mesh *mesh, const ufbx_topo_edge *topo, size_t num_topo, uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth) +{ + return ufbx_catch_generate_normal_mapping(NULL, mesh, topo, num_topo, normal_indices, num_normal_indices, assume_smooth); +} + +ufbx_abi void ufbx_catch_compute_normals(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals) +{ + if (ufbxi_panicf(panic, num_normal_indices >= mesh->num_indices, "Expected at least mesh.num_indices (%zu), got %zu", mesh->num_indices, num_normal_indices)) return; + + memset(normals, 0, sizeof(ufbx_vec3)*num_normals); + + for (size_t fi = 0; fi < mesh->num_faces; fi++) { + ufbx_face face = mesh->faces.data[fi]; + ufbx_vec3 normal = ufbx_get_weighted_face_normal(positions, face); + for (size_t ix = 0; ix < face.num_indices; ix++) { + uint32_t index = normal_indices[face.index_begin + ix]; + + if (ufbxi_panicf(panic, index < num_normals, "Normal index (%u) out of bounds (%zu) at %zu", index, num_normals, ix)) return; + + ufbx_vec3 *n = &normals[index]; + *n = ufbxi_add3(*n, normal); + } + } + + for (size_t i = 0; i < num_normals; i++) { + ufbx_real len = ufbxi_length3(normals[i]); + if (len > 0.0f) { + normals[i].x /= len; + normals[i].y /= len; + normals[i].z /= len; + } + } +} + +ufbx_abi void ufbx_compute_normals(const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, const uint32_t *normal_indices, size_t num_normal_indices, ufbx_vec3 *normals, size_t num_normals) +{ + ufbx_catch_compute_normals(NULL, mesh, positions, normal_indices, num_normal_indices, normals, num_normals); +} + +ufbx_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_mesh, opts, error); + if (!mesh) return NULL; + if (level == 0) return (ufbx_mesh*)mesh; + return ufbxi_subdivide_mesh(mesh, level, opts, error); +} + +ufbx_abi void ufbx_free_mesh(ufbx_mesh *mesh) +{ + if (!mesh) return; + if (!mesh->subdivision_evaluated && !mesh->from_tessellated_nurbs) return; + + ufbxi_mesh_imp *imp = ufbxi_get_imp(ufbxi_mesh_imp, mesh); + ufbx_assert(imp->magic == UFBXI_MESH_IMP_MAGIC); + if (imp->magic != UFBXI_MESH_IMP_MAGIC) return; + ufbxi_release_ref(&imp->refcount); +} + +ufbx_abi void ufbx_retain_mesh(ufbx_mesh *mesh) +{ + if (!mesh) return; + if (!mesh->subdivision_evaluated && !mesh->from_tessellated_nurbs) return; + + ufbxi_mesh_imp *imp = ufbxi_get_imp(ufbxi_mesh_imp, mesh); + ufbx_assert(imp->magic == UFBXI_MESH_IMP_MAGIC); + if (imp->magic != UFBXI_MESH_IMP_MAGIC) return; + ufbxi_retain_ref(&imp->refcount); +} + +ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache( + const char *filename, + const ufbx_geometry_cache_opts *opts, ufbx_error *error) +{ + return ufbx_load_geometry_cache_len(filename, strlen(filename), + opts, error); +} + +ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache_len( + const char *filename, size_t filename_len, + const ufbx_geometry_cache_opts *opts, ufbx_error *error) +{ + ufbxi_check_opts_ptr(ufbx_geometry_cache, opts, error); + ufbx_string str = ufbxi_safe_string(filename, filename_len); + return ufbxi_load_geometry_cache(str, opts, error); +} + +ufbx_abi void ufbx_free_geometry_cache(ufbx_geometry_cache *cache) +{ + if (!cache) return; + + ufbxi_geometry_cache_imp *imp = ufbxi_get_imp(ufbxi_geometry_cache_imp, cache); + ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC); + if (imp->magic != UFBXI_CACHE_IMP_MAGIC) return; + if (imp->owned_by_scene) return; + ufbxi_release_ref(&imp->refcount); +} + +ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache) +{ + if (!cache) return; + + ufbxi_geometry_cache_imp *imp = ufbxi_get_imp(ufbxi_geometry_cache_imp, cache); + ufbx_assert(imp->magic == UFBXI_CACHE_IMP_MAGIC); + if (imp->magic != UFBXI_CACHE_IMP_MAGIC) return; + if (imp->owned_by_scene) return; + ufbxi_retain_ref(&imp->refcount); +} + +typedef struct { + union { + double f64[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; + float f32[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; + } src; + ufbx_real dst[UFBXI_GEOMETRY_CACHE_BUFFER_SIZE]; +} ufbxi_geometry_cache_buffer; + +ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_real(const ufbx_cache_frame *frame, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts) +{ +#if UFBXI_FEATURE_GEOMETRY_CACHE + ufbxi_check_opts_return_no_error(0, user_opts); + if (!frame || count == 0) return 0; + ufbx_assert(data); + if (!data) return 0; + + ufbx_geometry_cache_data_opts opts; // ufbxi_uninit + if (user_opts) { + opts = *user_opts; + } else { + memset(&opts, 0, sizeof(opts)); + } + + if (!opts.open_file_cb.fn) { + opts.open_file_cb.fn = ufbx_default_open_file; + } + + bool use_double = false; + + size_t src_count = 0; + + switch (frame->data_format) { + case UFBX_CACHE_DATA_FORMAT_UNKNOWN: src_count = 0; break; + case UFBX_CACHE_DATA_FORMAT_REAL_FLOAT: src_count = frame->data_count; break; + case UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT: src_count = frame->data_count * 3; break; + case UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE: src_count = frame->data_count; use_double = true; break; + case UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE: src_count = frame->data_count * 3; use_double = true; break; + default: ufbxi_unreachable("Bad data_format"); break; + } + + bool src_big_endian = false; + switch (frame->data_encoding) { + case UFBX_CACHE_DATA_ENCODING_UNKNOWN: return 0; + case UFBX_CACHE_DATA_ENCODING_LITTLE_ENDIAN: src_big_endian = false; break; + case UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN: src_big_endian = true; break; + default: ufbxi_unreachable("Bad data_encoding"); break; + } + + // Test endianness + bool dst_big_endian; + { + uint8_t buf[2]; + uint16_t val = 0xbbaa; + memcpy(buf, &val, 2); + dst_big_endian = buf[0] == 0xbb; + } + + if (src_count == 0) return 0; + src_count = ufbxi_min_sz(src_count, count); + + ufbx_stream stream = { 0 }; + if (!ufbxi_open_file(&opts.open_file_cb, &stream, frame->filename.data, frame->filename.length, NULL, NULL, UFBX_OPEN_FILE_GEOMETRY_CACHE)) { + return 0; + } + + // Skip to the correct point in the file + uint64_t offset = frame->data_offset; + if (stream.skip_fn) { + while (offset > 0) { + size_t to_skip = (size_t)ufbxi_min64(offset, UFBXI_MAX_SKIP_SIZE); + if (!stream.skip_fn(stream.user, to_skip)) break; + offset -= to_skip; + } + } else { + char buffer[4096]; // ufbxi_uninit + while (offset > 0) { + size_t to_skip = (size_t)ufbxi_min64(offset, sizeof(buffer)); + size_t num_read = stream.read_fn(stream.user, buffer, to_skip); + if (num_read != to_skip) break; + offset -= to_skip; + } + } + + // Failed to skip all the way + if (offset > 0) { + if (stream.close_fn) { + stream.close_fn(stream.user); + } + return 0; + } + + ufbx_real *dst = data; + size_t mirror_ix = (size_t)frame->mirror_axis - 1; + ufbxi_geometry_cache_buffer buffer; // ufbxi_uninit + while (src_count > 0) { + size_t to_read = ufbxi_min_sz(src_count, UFBXI_GEOMETRY_CACHE_BUFFER_SIZE); + src_count -= to_read; + size_t num_read = 0; + if (use_double) { + size_t bytes_read = stream.read_fn(stream.user, buffer.src.f64, to_read * sizeof(double)); + if (bytes_read == SIZE_MAX) bytes_read = 0; + num_read = bytes_read / sizeof(double); + if (src_big_endian != dst_big_endian) { + for (size_t i = 0; i < num_read; i++) { + char t, *v = (char*)&buffer.src.f64[i]; + t = v[0]; v[0] = v[7]; v[7] = t; + t = v[1]; v[1] = v[6]; v[6] = t; + t = v[2]; v[2] = v[5]; v[5] = t; + t = v[3]; v[3] = v[4]; v[4] = t; + } + } + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] = (ufbx_real)buffer.src.f64[i]; + } + } else { + size_t bytes_read = stream.read_fn(stream.user, buffer.src.f32, to_read * sizeof(float)); + if (bytes_read == SIZE_MAX) bytes_read = 0; + num_read = bytes_read / sizeof(float); + if (src_big_endian != dst_big_endian) { + for (size_t i = 0; i < num_read; i++) { + char t, *v = (char*)&buffer.src.f32[i]; + t = v[0]; v[0] = v[3]; v[3] = t; + t = v[1]; v[1] = v[2]; v[2] = t; + } + } + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] = (ufbx_real)buffer.src.f32[i]; + } + } + + if (!opts.ignore_transform) { + ufbx_real scale = frame->scale_factor; + if (scale != 1.0f) { + for (size_t i = 0; i < num_read; i++) { + buffer.dst[i] *= scale; + } + } + if (frame->mirror_axis) { + while (mirror_ix < num_read) { + buffer.dst[mirror_ix] = -buffer.dst[mirror_ix]; + mirror_ix += 3; + } + mirror_ix -= num_read; + } + } + + if (dst) { + ufbx_real weight = opts.use_weight ? opts.weight : 1.0f; + if (opts.additive) { + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + dst[i] += buffer.dst[i] * weight; + } + } else { + ufbxi_nounroll for (size_t i = 0; i < num_read; i++) { + dst[i] = buffer.dst[i] * weight; + } + } + dst += num_read; + } + + if (num_read != to_read) break; + } + + if (stream.close_fn) { + stream.close_fn(stream.user); + } + + return ufbxi_to_size(dst - data); +#else + return 0; +#endif +} + +ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t count, const ufbx_geometry_cache_data_opts *user_opts) +{ +#if UFBXI_FEATURE_GEOMETRY_CACHE + ufbxi_check_opts_return_no_error(0, user_opts); + if (!channel || count == 0) return 0; + ufbx_assert(data); + if (!data) return 0; + if (channel->frames.count == 0) return 0; + + ufbx_geometry_cache_data_opts opts; + if (user_opts) { + opts = *user_opts; + } else { + memset(&opts, 0, sizeof(opts)); + } + + size_t begin = 0; + size_t end = channel->frames.count; + const ufbx_cache_frame *frames = channel->frames.data; + while (end - begin >= 8) { + size_t mid = (begin + end) >> 1; + if (frames[mid].time < time) { + begin = mid + 1; + } else { + end = mid; + } + } + + const double eps = 0.00000001; + + end = channel->frames.count; + for (; begin < end; begin++) { + const ufbx_cache_frame *next = &frames[begin]; + if (next->time < time) continue; + + // First keyframe + if (begin == 0) { + return ufbx_read_geometry_cache_real(next, data, count, &opts); + } + + const ufbx_cache_frame *prev = next - 1; + + // Snap to exact frames if near + if (ufbx_fabs(next->time - time) < eps) { + return ufbx_read_geometry_cache_real(next, data, count, &opts); + } + if (ufbx_fabs(prev->time - time) < eps) { + return ufbx_read_geometry_cache_real(prev, data, count, &opts); + } + + double rcp_delta = 1.0 / (next->time - prev->time); + double t = (time - prev->time) * rcp_delta; + + ufbx_real original_weight = opts.use_weight ? opts.weight : 1.0f; + + opts.use_weight = true; + opts.weight = (ufbx_real)(original_weight * (1.0 - t)); + size_t num_prev = ufbx_read_geometry_cache_real(prev, data, count, &opts); + + opts.additive = true; + opts.weight = (ufbx_real)(original_weight * t); + return ufbx_read_geometry_cache_real(next, data, num_prev, &opts); + } + + // Last frame + const ufbx_cache_frame *last = &frames[end - 1]; + return ufbx_read_geometry_cache_real(last, data, count, &opts); +#else + return 0; +#endif +} + +ufbx_abi ufbxi_noinline size_t ufbx_read_geometry_cache_vec3(const ufbx_cache_frame *frame, ufbx_vec3 *data, size_t count, const ufbx_geometry_cache_data_opts *opts) +{ +#if UFBXI_FEATURE_GEOMETRY_CACHE + if (!frame || count == 0) return 0; + ufbx_assert(data); + if (!data) return 0; + return ufbx_read_geometry_cache_real(frame, (ufbx_real*)data, count * 3, opts) / 3; +#else + return 0; +#endif +} + +ufbx_abi ufbxi_noinline size_t ufbx_sample_geometry_cache_vec3(const ufbx_cache_channel *channel, double time, ufbx_vec3 *data, size_t count, const ufbx_geometry_cache_data_opts *opts) +{ +#if UFBXI_FEATURE_GEOMETRY_CACHE + if (!channel || count == 0) return 0; + ufbx_assert(data); + if (!data) return 0; + return ufbx_sample_geometry_cache_real(channel, time, (ufbx_real*)data, count * 3, opts) / 3; +#else + return 0; +#endif +} + +ufbx_abi ufbx_dom_node *ufbx_dom_find_len(const ufbx_dom_node *parent, const char *name, size_t name_len) +{ + ufbx_string ref = ufbxi_safe_string(name, name_len); + ufbxi_for_ptr_list(ufbx_dom_node, p_child, parent->children) { + if (ufbxi_str_equal((*p_child)->name, ref)) return (ufbx_dom_node*)*p_child; + } + return NULL; +} + +ufbx_abi size_t ufbx_generate_indices(const ufbx_vertex_stream *streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error) +{ + ufbx_error local_error; // ufbxi_uninit + if (!error) { + error = &local_error; + } + memset(error, 0, sizeof(ufbx_error)); + return ufbxi_generate_indices(streams, num_streams, indices, num_indices, allocator, error); +} + +ufbx_abi void ufbx_thread_pool_run_task(ufbx_thread_pool_context ctx, uint32_t index) +{ + ufbxi_thread_pool_execute((ufbxi_thread_pool*)ctx, index); +} + +ufbx_abi void ufbx_thread_pool_set_user_ptr(ufbx_thread_pool_context ctx, void *user) +{ + ufbxi_thread_pool *pool = (ufbxi_thread_pool*)ctx; + pool->user_ptr = user; +} + +ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx) +{ + ufbxi_thread_pool *pool = (ufbxi_thread_pool*)ctx; + return pool->user_ptr; +} + +ufbx_abi ufbxi_noinline ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index) +{ + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return 0.0f; + return v->values.data[(int32_t)ix]; +} + +ufbx_abi ufbxi_noinline ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index) +{ + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec2; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec2; + return v->values.data[(int32_t)ix]; +} + +ufbx_abi ufbxi_noinline ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index) +{ + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec3; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec3; + return v->values.data[(int32_t)ix]; +} + +ufbx_abi ufbxi_noinline ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index) +{ + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return ufbx_zero_vec4; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return ufbx_zero_vec4; + return v->values.data[(int32_t)ix]; +} + +ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index) +{ + if (ufbxi_panicf(panic, index < v->indices.count, "index (%zu) out of range (%zu)", index, v->indices.count)) return 0.0f; + if (v->values_w.count == 0) return 0.0f; + uint32_t ix = v->indices.data[index]; + if (ufbxi_panicf(panic, (size_t)ix < v->values.count || ix == UFBX_NO_INDEX, "Corrupted or missing vertex attribute (%u) at %zu", ix, index)) return 0.0f; + return v->values_w.data[(int32_t)ix]; +} + +ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_UNKNOWN ? (ufbx_unknown*)element : NULL; } +ufbx_abi ufbx_node *ufbx_as_node(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NODE ? (ufbx_node*)element : NULL; } +ufbx_abi ufbx_mesh *ufbx_as_mesh(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MESH ? (ufbx_mesh*)element : NULL; } +ufbx_abi ufbx_light *ufbx_as_light(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LIGHT ? (ufbx_light*)element : NULL; } +ufbx_abi ufbx_camera *ufbx_as_camera(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CAMERA ? (ufbx_camera*)element : NULL; } +ufbx_abi ufbx_bone *ufbx_as_bone(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BONE ? (ufbx_bone*)element : NULL; } +ufbx_abi ufbx_empty *ufbx_as_empty(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_EMPTY ? (ufbx_empty*)element : NULL; } +ufbx_abi ufbx_line_curve *ufbx_as_line_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LINE_CURVE ? (ufbx_line_curve*)element : NULL; } +ufbx_abi ufbx_nurbs_curve *ufbx_as_nurbs_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_CURVE ? (ufbx_nurbs_curve*)element : NULL; } +ufbx_abi ufbx_nurbs_surface *ufbx_as_nurbs_surface(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_SURFACE ? (ufbx_nurbs_surface*)element : NULL; } +ufbx_abi ufbx_nurbs_trim_surface *ufbx_as_nurbs_trim_surface(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_TRIM_SURFACE ? (ufbx_nurbs_trim_surface*)element : NULL; } +ufbx_abi ufbx_nurbs_trim_boundary *ufbx_as_nurbs_trim_boundary(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_NURBS_TRIM_BOUNDARY ? (ufbx_nurbs_trim_boundary*)element : NULL; } +ufbx_abi ufbx_procedural_geometry *ufbx_as_procedural_geometry(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_PROCEDURAL_GEOMETRY ? (ufbx_procedural_geometry*)element : NULL; } +ufbx_abi ufbx_stereo_camera *ufbx_as_stereo_camera(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_STEREO_CAMERA ? (ufbx_stereo_camera*)element : NULL; } +ufbx_abi ufbx_camera_switcher *ufbx_as_camera_switcher(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CAMERA_SWITCHER ? (ufbx_camera_switcher*)element : NULL; } +ufbx_abi ufbx_marker *ufbx_as_marker(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MARKER ? (ufbx_marker*)element : NULL; } +ufbx_abi ufbx_lod_group *ufbx_as_lod_group(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_LOD_GROUP ? (ufbx_lod_group*)element : NULL; } +ufbx_abi ufbx_skin_deformer *ufbx_as_skin_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SKIN_DEFORMER ? (ufbx_skin_deformer*)element : NULL; } +ufbx_abi ufbx_skin_cluster *ufbx_as_skin_cluster(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SKIN_CLUSTER ? (ufbx_skin_cluster*)element : NULL; } +ufbx_abi ufbx_blend_deformer *ufbx_as_blend_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_DEFORMER ? (ufbx_blend_deformer*)element : NULL; } +ufbx_abi ufbx_blend_channel *ufbx_as_blend_channel(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_CHANNEL ? (ufbx_blend_channel*)element : NULL; } +ufbx_abi ufbx_blend_shape *ufbx_as_blend_shape(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_BLEND_SHAPE ? (ufbx_blend_shape*)element : NULL; } +ufbx_abi ufbx_cache_deformer *ufbx_as_cache_deformer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CACHE_DEFORMER ? (ufbx_cache_deformer*)element : NULL; } +ufbx_abi ufbx_cache_file *ufbx_as_cache_file(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CACHE_FILE ? (ufbx_cache_file*)element : NULL; } +ufbx_abi ufbx_material *ufbx_as_material(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_MATERIAL ? (ufbx_material*)element : NULL; } +ufbx_abi ufbx_texture *ufbx_as_texture(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_TEXTURE ? (ufbx_texture*)element : NULL; } +ufbx_abi ufbx_video *ufbx_as_video(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_VIDEO ? (ufbx_video*)element : NULL; } +ufbx_abi ufbx_shader *ufbx_as_shader(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SHADER ? (ufbx_shader*)element : NULL; } +ufbx_abi ufbx_shader_binding *ufbx_as_shader_binding(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SHADER_BINDING ? (ufbx_shader_binding*)element : NULL; } +ufbx_abi ufbx_anim_stack *ufbx_as_anim_stack(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_STACK ? (ufbx_anim_stack*)element : NULL; } +ufbx_abi ufbx_anim_layer *ufbx_as_anim_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_LAYER ? (ufbx_anim_layer*)element : NULL; } +ufbx_abi ufbx_anim_value *ufbx_as_anim_value(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_VALUE ? (ufbx_anim_value*)element : NULL; } +ufbx_abi ufbx_anim_curve *ufbx_as_anim_curve(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_ANIM_CURVE ? (ufbx_anim_curve*)element : NULL; } +ufbx_abi ufbx_display_layer *ufbx_as_display_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_DISPLAY_LAYER ? (ufbx_display_layer*)element : NULL; } +ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SELECTION_SET ? (ufbx_selection_set*)element : NULL; } +ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_SELECTION_NODE ? (ufbx_selection_node*)element : NULL; } +ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CHARACTER ? (ufbx_character*)element : NULL; } +ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_CONSTRAINT ? (ufbx_constraint*)element : NULL; } +ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_LAYER ? (ufbx_audio_layer*)element : NULL; } +ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_AUDIO_CLIP ? (ufbx_audio_clip*)element : NULL; } +ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_POSE ? (ufbx_pose*)element : NULL; } +ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element) { return element && element->type == UFBX_ELEMENT_METADATA_OBJECT ? (ufbx_metadata_object*)element : NULL; } + +ufbx_abi bool ufbx_dom_is_array(const ufbx_dom_node *node) { + if (!node || node->values.count != 1) return false; + ufbx_dom_value v = node->values.data[0]; + return v.type >= UFBX_DOM_VALUE_ARRAY_I32 && v.type <= UFBX_DOM_VALUE_ARRAY_BLOB; +} +ufbx_abi size_t ufbx_dom_array_size(const ufbx_dom_node *node) { + return ufbx_dom_is_array(node) ? (size_t)node->values.data[0].value_int : (size_t)0; +} +ufbx_abi ufbx_int32_list ufbx_dom_as_int32_list(const ufbx_dom_node *node) { + ufbx_int32_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == UFBX_DOM_VALUE_ARRAY_I32) { + ufbx_dom_value value = node->values.data[0]; + list.data = (int32_t*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(int32_t); + } + return list; +} +ufbx_abi ufbx_int64_list ufbx_dom_as_int64_list(const ufbx_dom_node *node) { + ufbx_int64_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == UFBX_DOM_VALUE_ARRAY_I64) { + ufbx_dom_value value = node->values.data[0]; + list.data = (int64_t*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(int64_t); + } + return list; +} +ufbx_abi ufbx_float_list ufbx_dom_as_float_list(const ufbx_dom_node *node) { + ufbx_float_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == UFBX_DOM_VALUE_ARRAY_F32) { + ufbx_dom_value value = node->values.data[0]; + list.data = (float*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(float); + } + return list; +} +ufbx_abi ufbx_double_list ufbx_dom_as_double_list(const ufbx_dom_node *node) { + ufbx_double_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == UFBX_DOM_VALUE_ARRAY_F64) { + ufbx_dom_value value = node->values.data[0]; + list.data = (double*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(double); + } + return list; +} +ufbx_abi ufbx_real_list ufbx_dom_as_real_list(const ufbx_dom_node *node) { + ufbx_real_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == (sizeof(ufbx_real) == sizeof(double) ? UFBX_DOM_VALUE_ARRAY_F64 : UFBX_DOM_VALUE_ARRAY_F32)) { + ufbx_dom_value value = node->values.data[0]; + list.data = (ufbx_real*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(ufbx_real); + } + return list; +} +ufbx_abi ufbx_blob_list ufbx_dom_as_blob_list(const ufbx_dom_node *node) { + ufbx_blob_list list = { NULL, 0 }; + if (node && node->values.count == 1 && node->values.data[0].type == UFBX_DOM_VALUE_ARRAY_BLOB) { + ufbx_dom_value value = node->values.data[0]; + list.data = (ufbx_blob*)value.value_blob.data; + list.count = value.value_blob.size / sizeof(ufbx_blob); + } + return list; +} + +// -- String API + +ufbx_abi ufbx_prop *ufbx_find_prop(const ufbx_props *props, const char *name) { return ufbx_find_prop_len(props, name, strlen(name)); } +ufbx_abi ufbx_real ufbx_find_real(const ufbx_props *props, const char *name, ufbx_real def) { return ufbx_find_real_len(props, name, strlen(name), def); } +ufbx_abi ufbx_vec3 ufbx_find_vec3(const ufbx_props *props, const char *name, ufbx_vec3 def) { return ufbx_find_vec3_len(props, name, strlen(name), def); } +ufbx_abi int64_t ufbx_find_int(const ufbx_props *props, const char *name, int64_t def) { return ufbx_find_int_len(props, name, strlen(name), def); } +ufbx_abi bool ufbx_find_bool(const ufbx_props *props, const char *name, bool def) { return ufbx_find_bool_len(props, name, strlen(name), def); } +ufbx_abi ufbx_string ufbx_find_string(const ufbx_props *props, const char *name, ufbx_string def) { return ufbx_find_string_len(props, name, strlen(name), def); } +ufbx_abi ufbx_blob ufbx_find_blob(const ufbx_props *props, const char *name, ufbx_blob def) { return ufbx_find_blob_len(props, name, strlen(name), def); } +ufbx_abi ufbx_element *ufbx_find_prop_element(const ufbx_element *element, const char *name, ufbx_element_type type) { return ufbx_find_prop_element_len(element, name, strlen(name), type); } +ufbx_abi ufbx_element *ufbx_find_element(const ufbx_scene *scene, ufbx_element_type type, const char *name) { return ufbx_find_element_len(scene, type, name, strlen(name)); } +ufbx_abi ufbx_node *ufbx_find_node(const ufbx_scene *scene, const char *name) { return ufbx_find_node_len(scene, name, strlen(name)); } +ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack(const ufbx_scene *scene, const char *name) { return ufbx_find_anim_stack_len(scene, name, strlen(name)); } +ufbx_abi ufbx_material *ufbx_find_material(const ufbx_scene *scene, const char *name) { return ufbx_find_material_len(scene, name, strlen(name)); } +ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop) { return ufbx_find_anim_prop_len(layer, element, prop, strlen(prop)); } +ufbx_abi ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time) { return ufbx_evaluate_prop_len(anim, element, name, strlen(name), time); } +ufbx_abi ufbx_prop ufbx_evaluate_prop_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags) { return ufbx_evaluate_prop_flags_len(anim, element, name, strlen(name), time, flags); } +ufbx_abi ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, const char *name) { return ufbx_find_prop_texture_len(material, name, strlen(name)); } +ufbx_abi ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_len(shader, name, strlen(name)); } +ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, const char *name) { return ufbx_find_shader_prop_bindings_len(shader, name, strlen(name)); } +ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx_shader_texture *shader, const char *name) { return ufbx_find_shader_texture_input_len(shader, name, strlen(name)); } +ufbx_abi ufbx_dom_node *ufbx_dom_find(const ufbx_dom_node *parent, const char *name) { return ufbx_dom_find_len(parent, name, strlen(name)); } + +// -- Catch API + +ufbx_abi uint32_t ufbx_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face) { + return ufbx_catch_triangulate_face(NULL, indices, num_indices, mesh, face); +} +ufbx_abi void ufbx_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo, size_t num_topo) { + ufbx_catch_compute_topology(NULL, mesh, topo, num_topo); +} +ufbx_abi uint32_t ufbx_topo_next_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) { + return ufbx_catch_topo_next_vertex_edge(NULL, topo, num_topo, index); +} +ufbx_abi uint32_t ufbx_topo_prev_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index) { + return ufbx_catch_topo_prev_vertex_edge(NULL, topo, num_topo, index); +} +ufbx_abi ufbx_vec3 ufbx_get_weighted_face_normal(const ufbx_vertex_vec3 *positions, ufbx_face face) { + return ufbx_catch_get_weighted_face_normal(NULL, positions, face); +} + +#ifdef __cplusplus +} +#endif + +#endif + +#if defined(UFBX_STRING_PREFIX) + #undef strlen + #undef memcpy + #undef memmove + #undef memset + #undef memchr + #undef memcmp + #undef strcmp + #undef strncmp +#endif + +#if defined(_MSC_VER) + #pragma warning(pop) +#elif defined(__clang__) + #pragma clang diagnostic pop +#elif defined(__GNUC__) + #pragma GCC diagnostic pop +#endif + diff --git a/Engine/cpp/ThirdParty/ufbx/ufbx.h b/Engine/cpp/ThirdParty/ufbx/ufbx.h new file mode 100644 index 00000000..bbb456e6 --- /dev/null +++ b/Engine/cpp/ThirdParty/ufbx/ufbx.h @@ -0,0 +1,6069 @@ +#ifndef UFBX_UFBX_H_INCLUDED +#define UFBX_UFBX_H_INCLUDED + +// -- User configuration + +#if defined(UFBX_CONFIG_HEADER) + #include UFBX_CONFIG_HEADER +#endif + +// -- Headers + +#if !defined(UFBX_NO_LIBC_TYPES) + #include + #include + #include +#endif + +// -- Platform + +#ifndef UFBX_STDC + #if defined(__STDC_VERSION__) + #define UFBX_STDC __STDC_VERSION__ + #else + #define UFBX_STDC 0 + #endif +#endif + +#ifndef UFBX_CPP + #if defined(__cplusplus) + #define UFBX_CPP __cplusplus + #else + #define UFBX_CPP 0 + #endif +#endif + +#ifndef UFBX_PLATFORM_MSC + #if !defined(UFBX_STANDARD_C) && defined(_MSC_VER) + #define UFBX_PLATFORM_MSC _MSC_VER + #else + #define UFBX_PLATFORM_MSC 0 + #endif +#endif + +#ifndef UFBX_PLATFORM_GNUC + #if !defined(UFBX_STANDARD_C) && defined(__GNUC__) + #define UFBX_PLATFORM_GNUC __GNUC__ + #else + #define UFBX_PLATFORM_GNUC 0 + #endif +#endif + +#ifndef UFBX_CPP11 + // MSVC does not advertise C++11 by default so we need special detection + #if UFBX_CPP >= 201103L || (UFBX_CPP > 0 && UFBX_PLATFORM_MSC >= 1900) + #define UFBX_CPP11 1 + #else + #define UFBX_CPP11 0 + #endif +#endif + +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable: 4061) // enumerator 'ENUM' in switch of enum 'enum' is not explicitly handled by a case label + #pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union + #pragma warning(disable: 4505) // unreferenced local function has been removed + #pragma warning(disable: 4820) // type': 'N' bytes padding added after data member 'member' +#elif defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wpedantic" + #pragma clang diagnostic ignored "-Wpadded" + #if defined(__cplusplus) + #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" + #pragma clang diagnostic ignored "-Wold-style-cast" + #endif +#elif defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wpedantic" + #pragma GCC diagnostic ignored "-Wpadded" + #if defined(__cplusplus) + #pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" + #pragma GCC diagnostic ignored "-Wold-style-cast" + #else + #if __GNUC__ >= 5 + #pragma GCC diagnostic ignored "-Wc90-c99-compat" + #pragma GCC diagnostic ignored "-Wc99-c11-compat" + #endif + #endif +#endif + +#if UFBX_PLATFORM_MSC + #define ufbx_inline static __forceinline +#elif UFBX_PLATFORM_GNUC + #define ufbx_inline static inline __attribute__((always_inline, unused)) +#else + #define ufbx_inline static +#endif + +// Assertion function used in ufbx, defaults to C standard `assert()`. +// You can define this to your custom preferred assert macro, but in that case +// make sure that it is also used within `ufbx.c`. +// Defining `UFBX_NO_ASSERT` to any value disables assertions. +#ifndef ufbx_assert + #if defined(UFBX_NO_ASSERT) || defined(UFBX_NO_LIBC) + #define ufbx_assert(cond) (void)0 + #else + #include + #define ufbx_assert(cond) assert(cond) + #endif +#endif + +// Pointer may be `NULL`. +#define ufbx_nullable + +// Changing this value from default or calling this function can lead into +// breaking API guarantees. +#define ufbx_unsafe + +// Linkage of the main ufbx API functions. +// Defaults to nothing, or `static` if `UFBX_STATIC` is defined. +// If you want to isolate ufbx to a single translation unit you can do the following: +// #define UFBX_STATIC +// #include "ufbx.h" +// #include "ufbx.c" +#ifndef ufbx_abi + #if defined(UFBX_STATIC) + #define ufbx_abi static + #else + #define ufbx_abi + #endif +#endif + +// Linkage of the main ufbx data fields in the header. +// Defaults to `extern`, or `static` if `UFBX_STATIC` is defined. +#ifndef ufbx_abi_data + #if defined(UFBX_STATIC) + #define ufbx_abi_data static + #else + #define ufbx_abi_data extern + #endif +#endif + +// Linkage of the main ufbx data fields in the source. +// Defaults to nothing, or `static` if `UFBX_STATIC` is defined. +#ifndef ufbx_abi_data_definition + #if defined(UFBX_STATIC) + #define ufbx_abi_data_def static + #else + #define ufbx_abi_data_def + #endif +#endif + +// -- Configuration + +#ifndef UFBX_REAL_TYPE + #if defined(UFBX_REAL_IS_FLOAT) + #define UFBX_REAL_TYPE float + #else + #define UFBX_REAL_TYPE double + #endif +#endif + +// Limits for embedded arrays within structures. +#define UFBX_ERROR_STACK_MAX_DEPTH 8 +#define UFBX_PANIC_MESSAGE_LENGTH 128 +#define UFBX_ERROR_INFO_LENGTH 256 + +// Number of thread groups to use if threading is enabled. +// A thread group processes a number of tasks and is then waited and potentially +// re-used later. In essence, this controls the granularity of threading. +#define UFBX_THREAD_GROUP_COUNT 4 + +// -- Language + +// bindgen-disable + +#if UFBX_CPP11 + +template +struct ufbxi_type_is { }; + +template +struct ufbxi_type_is { using type = int; }; + +template +struct ufbx_converter { }; + +#define UFBX_CONVERSION_IMPL(p_name) \ + template ::from(*(const p_name*)nullptr))>::type> \ + operator T() const { return ufbx_converter::from(*this); } + +#define UFBX_CONVERSION_TO_IMPL(p_name) \ + template ::to(*(const T*)nullptr))>::type> \ + p_name(const T &t) { *this = ufbx_converter::to(t); } + +#define UFBX_CONVERSION_LIST_IMPL(p_name) \ + template ::from_list((p_name*)nullptr, (size_t)0))>::type> \ + operator T() const { return ufbx_converter::from_list(data, count); } + +#else + +#define UFBX_CONVERSION_IMPL(p_name) +#define UFBX_CONVERSION_TO_IMPL(p_name) +#define UFBX_CONVERSION_LIST_IMPL(p_name) + +#endif + +#if defined(__cplusplus) + #define UFBX_LIST_TYPE(p_name, p_type) struct p_name { p_type *data; size_t count; \ + p_type &operator[](size_t index) const { ufbx_assert(index < count); return data[index]; } \ + p_type *begin() const { return data; } \ + p_type *end() const { return data + count; } \ + UFBX_CONVERSION_LIST_IMPL(p_type) \ + } +#else + #define UFBX_LIST_TYPE(p_name, p_type) typedef struct p_name { p_type *data; size_t count; } p_name +#endif + +// This cannot be enabled automatically if supported as the source file may be +// compiled with a different compiler using different settings than the header +// consumers, in practice it should work but it causes issues such as #70. +#if (UFBX_STDC >= 202311L || UFBX_CPP11) && defined(UFBX_USE_EXPLICIT_ENUM) + #define UFBX_ENUM_REPR : int + #define UFBX_ENUM_FORCE_WIDTH(p_prefix) + #define UFBX_FLAG_REPR : int + #define UFBX_FLAG_FORCE_WIDTH(p_prefix) + #define UFBX_HAS_FORCE_32BIT 0 +#else + #define UFBX_ENUM_REPR + #define UFBX_ENUM_FORCE_WIDTH(p_prefix) p_prefix##_FORCE_32BIT = 0x7fffffff + #define UFBX_FLAG_REPR + #define UFBX_FLAG_FORCE_WIDTH(p_prefix) p_prefix##_FORCE_32BIT = 0x7fffffff + #define UFBX_HAS_FORCE_32BIT 1 +#endif + +#define UFBX_ENUM_TYPE(p_name, p_prefix, p_last) \ + enum { p_prefix##_COUNT = p_last + 1 } + +#if UFBX_CPP + #define UFBX_VERTEX_ATTRIB_IMPL(p_type) \ + p_type &operator[](size_t index) const { ufbx_assert(index < indices.count); return values.data[indices.data[index]]; } +#else + #define UFBX_VERTEX_ATTRIB_IMPL(p_type) +#endif + +#if UFBX_CPP11 + #define UFBX_CALLBACK_IMPL(p_name, p_fn, p_return, p_params, p_args) \ + template static p_return _cpp_adapter p_params { F &f = *static_cast(user); return f p_args; } \ + p_name() = default; \ + p_name(p_fn *f) : fn(f), user(nullptr) { } \ + template p_name(F *f) : fn(&_cpp_adapter), user(static_cast(f)) { } +#else + #define UFBX_CALLBACK_IMPL(p_name, p_fn, p_return, p_params, p_args) +#endif + +// bindgen-enable + +// -- Version + +// Packing/unpacking for `UFBX_HEADER_VERSION` and `ufbx_source_version`. +#define ufbx_pack_version(major, minor, patch) ((major)*1000000u + (minor)*1000u + (patch)) +#define ufbx_version_major(version) ((uint32_t)(version)/1000000u%1000u) +#define ufbx_version_minor(version) ((uint32_t)(version)/1000u%1000u) +#define ufbx_version_patch(version) ((uint32_t)(version)%1000u) + +// Version of the ufbx header. +// `UFBX_VERSION` is simply an alias of `UFBX_HEADER_VERSION`. +// `ufbx_source_version` contains the version of the corresponding source file. +// HINT: The version can be compared numerically to the result of `ufbx_pack_version()`, +// for example `#if UFBX_VERSION >= ufbx_pack_version(0, 12, 0)`. +#define UFBX_HEADER_VERSION ufbx_pack_version(0, 22, 0) +#define UFBX_VERSION UFBX_HEADER_VERSION + +// -- Basic types + +// Main floating point type used everywhere in ufbx, defaults to `double`. +// If you define `UFBX_REAL_IS_FLOAT` to any value, `ufbx_real` will be defined +// as `float` instead. +// You can also manually define `UFBX_REAL_TYPE` to any floating point type. +typedef UFBX_REAL_TYPE ufbx_real; + +// Null-terminated UTF-8 encoded string within an FBX file +typedef struct ufbx_string { + const char *data; + size_t length; + + UFBX_CONVERSION_IMPL(ufbx_string) +} ufbx_string; + +// Opaque byte buffer blob +typedef struct ufbx_blob { + const void *data; + size_t size; + + UFBX_CONVERSION_IMPL(ufbx_blob) +} ufbx_blob; + +// 2D vector +typedef struct ufbx_vec2 { + union { + struct { ufbx_real x, y; }; + ufbx_real v[2]; + }; + + UFBX_CONVERSION_IMPL(ufbx_vec2) +} ufbx_vec2; + +// 3D vector +typedef struct ufbx_vec3 { + union { + struct { ufbx_real x, y, z; }; + ufbx_real v[3]; + }; + + UFBX_CONVERSION_IMPL(ufbx_vec3) +} ufbx_vec3; + +// 4D vector +typedef struct ufbx_vec4 { + union { + struct { ufbx_real x, y, z, w; }; + ufbx_real v[4]; + }; + + UFBX_CONVERSION_IMPL(ufbx_vec4) +} ufbx_vec4; + +// Quaternion +typedef struct ufbx_quat { + union { + struct { ufbx_real x, y, z, w; }; + ufbx_real v[4]; + }; + + UFBX_CONVERSION_IMPL(ufbx_quat) +} ufbx_quat; + +// Order in which Euler-angle rotation axes are applied for a transform +// NOTE: The order in the name refers to the order of axes *applied*, +// not the multiplication order: eg. `UFBX_ROTATION_ORDER_XYZ` is `Z*Y*X` +// [TODO: Figure out what the spheric rotation order is...] +typedef enum ufbx_rotation_order UFBX_ENUM_REPR { + UFBX_ROTATION_ORDER_XYZ, + UFBX_ROTATION_ORDER_XZY, + UFBX_ROTATION_ORDER_YZX, + UFBX_ROTATION_ORDER_YXZ, + UFBX_ROTATION_ORDER_ZXY, + UFBX_ROTATION_ORDER_ZYX, + UFBX_ROTATION_ORDER_SPHERIC, + + UFBX_ENUM_FORCE_WIDTH(UFBX_ROTATION_ORDER) +} ufbx_rotation_order; + +UFBX_ENUM_TYPE(ufbx_rotation_order, UFBX_ROTATION_ORDER, UFBX_ROTATION_ORDER_SPHERIC); + +// Explicit translation+rotation+scale transformation. +// NOTE: Rotation is a quaternion, not Euler angles! +typedef struct ufbx_transform { + ufbx_vec3 translation; + ufbx_quat rotation; + ufbx_vec3 scale; + + UFBX_CONVERSION_IMPL(ufbx_transform) +} ufbx_transform; + +// 4x3 matrix encoding an affine transformation. +// `cols[0..2]` are the X/Y/Z basis vectors, `cols[3]` is the translation +typedef struct ufbx_matrix { + union { + struct { + ufbx_real m00, m10, m20; + ufbx_real m01, m11, m21; + ufbx_real m02, m12, m22; + ufbx_real m03, m13, m23; + }; + ufbx_vec3 cols[4]; + ufbx_real v[12]; + }; + + UFBX_CONVERSION_IMPL(ufbx_matrix) +} ufbx_matrix; + +typedef struct ufbx_void_list { + void *data; + size_t count; +} ufbx_void_list; + +UFBX_LIST_TYPE(ufbx_bool_list, bool); +UFBX_LIST_TYPE(ufbx_uint32_list, uint32_t); +UFBX_LIST_TYPE(ufbx_real_list, ufbx_real); +UFBX_LIST_TYPE(ufbx_vec2_list, ufbx_vec2); +UFBX_LIST_TYPE(ufbx_vec3_list, ufbx_vec3); +UFBX_LIST_TYPE(ufbx_vec4_list, ufbx_vec4); +UFBX_LIST_TYPE(ufbx_string_list, ufbx_string); + +// Sentinel value used to represent a missing index. +#define UFBX_NO_INDEX ((uint32_t)~0u) + +// -- Document object model + +typedef enum ufbx_dom_value_type UFBX_ENUM_REPR { + UFBX_DOM_VALUE_NUMBER, + UFBX_DOM_VALUE_STRING, + UFBX_DOM_VALUE_BLOB, + UFBX_DOM_VALUE_ARRAY_I32, + UFBX_DOM_VALUE_ARRAY_I64, + UFBX_DOM_VALUE_ARRAY_F32, + UFBX_DOM_VALUE_ARRAY_F64, + UFBX_DOM_VALUE_ARRAY_BLOB, + UFBX_DOM_VALUE_ARRAY_IGNORED, + + UFBX_ENUM_FORCE_WIDTH(UFBX_DOM_VALUE_TYPE) +} ufbx_dom_value_type; + +UFBX_ENUM_TYPE(ufbx_dom_value_type, UFBX_DOM_VALUE_TYPE, UFBX_DOM_VALUE_ARRAY_IGNORED); + +typedef struct ufbx_dom_node ufbx_dom_node; + +UFBX_LIST_TYPE(ufbx_int32_list, int32_t); +UFBX_LIST_TYPE(ufbx_int64_list, int64_t); +UFBX_LIST_TYPE(ufbx_float_list, float); +UFBX_LIST_TYPE(ufbx_double_list, double); +UFBX_LIST_TYPE(ufbx_blob_list, ufbx_blob); + +typedef struct ufbx_dom_value { + ufbx_dom_value_type type; + ufbx_string value_str; + ufbx_blob value_blob; + int64_t value_int; + double value_float; +} ufbx_dom_value; + +UFBX_LIST_TYPE(ufbx_dom_node_list, ufbx_dom_node*); +UFBX_LIST_TYPE(ufbx_dom_value_list, ufbx_dom_value); + +struct ufbx_dom_node { + ufbx_string name; + ufbx_dom_node_list children; + ufbx_dom_value_list values; +}; + +// -- Properties + +// FBX elements have properties which are arbitrary key/value pairs that can +// have inherited default values or be animated. In most cases you don't need +// to access these unless you need a feature not implemented directly in ufbx. +// NOTE: Prefer using `ufbx_find_prop[_len](...)` to search for a property by +// name as it can find it from the defaults if necessary. + +typedef struct ufbx_prop ufbx_prop; +typedef struct ufbx_props ufbx_props; + +// Data type contained within the property. All the data fields are always +// populated regardless of type, so there's no need to switch by type usually +// eg. `prop->value_real` and `prop->value_int` have the same value (well, close) +// if `prop->type == UFBX_PROP_INTEGER`. String values are not converted from/to. +typedef enum ufbx_prop_type UFBX_ENUM_REPR { + UFBX_PROP_UNKNOWN, + UFBX_PROP_BOOLEAN, + UFBX_PROP_INTEGER, + UFBX_PROP_NUMBER, + UFBX_PROP_VECTOR, + UFBX_PROP_COLOR, + UFBX_PROP_COLOR_WITH_ALPHA, + UFBX_PROP_STRING, + UFBX_PROP_DATE_TIME, + UFBX_PROP_TRANSLATION, + UFBX_PROP_ROTATION, + UFBX_PROP_SCALING, + UFBX_PROP_DISTANCE, + UFBX_PROP_COMPOUND, + UFBX_PROP_BLOB, + UFBX_PROP_REFERENCE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_PROP_TYPE) +} ufbx_prop_type; + +UFBX_ENUM_TYPE(ufbx_prop_type, UFBX_PROP_TYPE, UFBX_PROP_REFERENCE); + +// Property flags: Advanced information about properties, not usually needed. +typedef enum ufbx_prop_flags UFBX_FLAG_REPR { + // Supports animation. + // NOTE: ufbx ignores this and allows animations on non-animatable properties. + UFBX_PROP_FLAG_ANIMATABLE = 0x1, + + // User defined (custom) property. + UFBX_PROP_FLAG_USER_DEFINED = 0x2, + + // Hidden in UI. + UFBX_PROP_FLAG_HIDDEN = 0x4, + + // Disallow modification from UI for components. + UFBX_PROP_FLAG_LOCK_X = 0x10, + UFBX_PROP_FLAG_LOCK_Y = 0x20, + UFBX_PROP_FLAG_LOCK_Z = 0x40, + UFBX_PROP_FLAG_LOCK_W = 0x80, + + // Disable animation from components. + UFBX_PROP_FLAG_MUTE_X = 0x100, + UFBX_PROP_FLAG_MUTE_Y = 0x200, + UFBX_PROP_FLAG_MUTE_Z = 0x400, + UFBX_PROP_FLAG_MUTE_W = 0x800, + + // Property created by ufbx when an element has a connected `ufbx_anim_prop` + // but doesn't contain the `ufbx_prop` it's referring to. + // NOTE: The property may have been found in the templated defaults. + UFBX_PROP_FLAG_SYNTHETIC = 0x1000, + + // The property has at least one `ufbx_anim_prop` in some layer. + UFBX_PROP_FLAG_ANIMATED = 0x2000, + + // Used by `ufbx_evaluate_prop()` to indicate the the property was not found. + UFBX_PROP_FLAG_NOT_FOUND = 0x4000, + + // The property is connected to another one. + // This use case is relatively rare so `ufbx_prop` does not track connections + // directly. You can find connections from `ufbx_element.connections_dst` where + // `ufbx_connection.dst_prop` is this property and `ufbx_connection.src_prop` is defined. + UFBX_PROP_FLAG_CONNECTED = 0x8000, + + // The value of this property is undefined (represented as zero). + UFBX_PROP_FLAG_NO_VALUE = 0x10000, + + // This property has been overridden by the user. + // See `ufbx_anim.prop_overrides` for more information. + UFBX_PROP_FLAG_OVERRIDDEN = 0x20000, + + // Value type. + // `REAL/VEC2/VEC3/VEC4` are mutually exclusive but may coexist with eg. `STRING` + // in some rare cases where the string defines the unit for the vector. + UFBX_PROP_FLAG_VALUE_REAL = 0x100000, + UFBX_PROP_FLAG_VALUE_VEC2 = 0x200000, + UFBX_PROP_FLAG_VALUE_VEC3 = 0x400000, + UFBX_PROP_FLAG_VALUE_VEC4 = 0x800000, + UFBX_PROP_FLAG_VALUE_INT = 0x1000000, + UFBX_PROP_FLAG_VALUE_STR = 0x2000000, + UFBX_PROP_FLAG_VALUE_BLOB = 0x4000000, + + UFBX_FLAG_FORCE_WIDTH(UFBX_PROP_FLAGS) +} ufbx_prop_flags; + +// Single property with name/type/value. +struct ufbx_prop { + ufbx_string name; + + uint32_t _internal_key; + + ufbx_prop_type type; + ufbx_prop_flags flags; + + ufbx_string value_str; + ufbx_blob value_blob; + int64_t value_int; + union { + ufbx_real value_real_arr[4]; + ufbx_real value_real; + ufbx_vec2 value_vec2; + ufbx_vec3 value_vec3; + ufbx_vec4 value_vec4; + }; +}; + +UFBX_LIST_TYPE(ufbx_prop_list, ufbx_prop); + +// List of alphabetically sorted properties with potential defaults. +// For animated objects in as scene from `ufbx_evaluate_scene()` this list +// only has the animated properties, the originals are stored under `defaults`. +struct ufbx_props { + ufbx_prop_list props; + size_t num_animated; + + ufbx_nullable ufbx_props *defaults; +}; + +typedef struct ufbx_scene ufbx_scene; + +// -- Elements + +// Element is the lowest level representation of the FBX file in ufbx. +// An element contains type, id, name, and properties (see `ufbx_props` above) +// Elements may be connected to each other arbitrarily via `ufbx_connection` + +typedef struct ufbx_element ufbx_element; + +// Unknown +typedef struct ufbx_unknown ufbx_unknown; + +// Nodes +typedef struct ufbx_node ufbx_node; + +// Node attributes (common) +typedef struct ufbx_mesh ufbx_mesh; +typedef struct ufbx_light ufbx_light; +typedef struct ufbx_camera ufbx_camera; +typedef struct ufbx_bone ufbx_bone; +typedef struct ufbx_empty ufbx_empty; + +// Node attributes (curves/surfaces) +typedef struct ufbx_line_curve ufbx_line_curve; +typedef struct ufbx_nurbs_curve ufbx_nurbs_curve; +typedef struct ufbx_nurbs_surface ufbx_nurbs_surface; +typedef struct ufbx_nurbs_trim_surface ufbx_nurbs_trim_surface; +typedef struct ufbx_nurbs_trim_boundary ufbx_nurbs_trim_boundary; + +// Node attributes (advanced) +typedef struct ufbx_procedural_geometry ufbx_procedural_geometry; +typedef struct ufbx_stereo_camera ufbx_stereo_camera; +typedef struct ufbx_camera_switcher ufbx_camera_switcher; +typedef struct ufbx_marker ufbx_marker; +typedef struct ufbx_lod_group ufbx_lod_group; + +// Deformers +typedef struct ufbx_skin_deformer ufbx_skin_deformer; +typedef struct ufbx_skin_cluster ufbx_skin_cluster; +typedef struct ufbx_blend_deformer ufbx_blend_deformer; +typedef struct ufbx_blend_channel ufbx_blend_channel; +typedef struct ufbx_blend_shape ufbx_blend_shape; +typedef struct ufbx_cache_deformer ufbx_cache_deformer; +typedef struct ufbx_cache_file ufbx_cache_file; + +// Materials +typedef struct ufbx_material ufbx_material; +typedef struct ufbx_texture ufbx_texture; +typedef struct ufbx_video ufbx_video; +typedef struct ufbx_shader ufbx_shader; +typedef struct ufbx_shader_binding ufbx_shader_binding; + +// Animation +typedef struct ufbx_anim_stack ufbx_anim_stack; +typedef struct ufbx_anim_layer ufbx_anim_layer; +typedef struct ufbx_anim_value ufbx_anim_value; +typedef struct ufbx_anim_curve ufbx_anim_curve; + +// Collections +typedef struct ufbx_display_layer ufbx_display_layer; +typedef struct ufbx_selection_set ufbx_selection_set; +typedef struct ufbx_selection_node ufbx_selection_node; + +// Constraints +typedef struct ufbx_character ufbx_character; +typedef struct ufbx_constraint ufbx_constraint; + +// Audio +typedef struct ufbx_audio_layer ufbx_audio_layer; +typedef struct ufbx_audio_clip ufbx_audio_clip; + +// Miscellaneous +typedef struct ufbx_pose ufbx_pose; +typedef struct ufbx_metadata_object ufbx_metadata_object; + +UFBX_LIST_TYPE(ufbx_element_list, ufbx_element*); +UFBX_LIST_TYPE(ufbx_unknown_list, ufbx_unknown*); +UFBX_LIST_TYPE(ufbx_node_list, ufbx_node*); +UFBX_LIST_TYPE(ufbx_mesh_list, ufbx_mesh*); +UFBX_LIST_TYPE(ufbx_light_list, ufbx_light*); +UFBX_LIST_TYPE(ufbx_camera_list, ufbx_camera*); +UFBX_LIST_TYPE(ufbx_bone_list, ufbx_bone*); +UFBX_LIST_TYPE(ufbx_empty_list, ufbx_empty*); +UFBX_LIST_TYPE(ufbx_line_curve_list, ufbx_line_curve*); +UFBX_LIST_TYPE(ufbx_nurbs_curve_list, ufbx_nurbs_curve*); +UFBX_LIST_TYPE(ufbx_nurbs_surface_list, ufbx_nurbs_surface*); +UFBX_LIST_TYPE(ufbx_nurbs_trim_surface_list, ufbx_nurbs_trim_surface*); +UFBX_LIST_TYPE(ufbx_nurbs_trim_boundary_list, ufbx_nurbs_trim_boundary*); +UFBX_LIST_TYPE(ufbx_procedural_geometry_list, ufbx_procedural_geometry*); +UFBX_LIST_TYPE(ufbx_stereo_camera_list, ufbx_stereo_camera*); +UFBX_LIST_TYPE(ufbx_camera_switcher_list, ufbx_camera_switcher*); +UFBX_LIST_TYPE(ufbx_marker_list, ufbx_marker*); +UFBX_LIST_TYPE(ufbx_lod_group_list, ufbx_lod_group*); +UFBX_LIST_TYPE(ufbx_skin_deformer_list, ufbx_skin_deformer*); +UFBX_LIST_TYPE(ufbx_skin_cluster_list, ufbx_skin_cluster*); +UFBX_LIST_TYPE(ufbx_blend_deformer_list, ufbx_blend_deformer*); +UFBX_LIST_TYPE(ufbx_blend_channel_list, ufbx_blend_channel*); +UFBX_LIST_TYPE(ufbx_blend_shape_list, ufbx_blend_shape*); +UFBX_LIST_TYPE(ufbx_cache_deformer_list, ufbx_cache_deformer*); +UFBX_LIST_TYPE(ufbx_cache_file_list, ufbx_cache_file*); +UFBX_LIST_TYPE(ufbx_material_list, ufbx_material*); +UFBX_LIST_TYPE(ufbx_texture_list, ufbx_texture*); +UFBX_LIST_TYPE(ufbx_video_list, ufbx_video*); +UFBX_LIST_TYPE(ufbx_shader_list, ufbx_shader*); +UFBX_LIST_TYPE(ufbx_shader_binding_list, ufbx_shader_binding*); +UFBX_LIST_TYPE(ufbx_anim_stack_list, ufbx_anim_stack*); +UFBX_LIST_TYPE(ufbx_anim_layer_list, ufbx_anim_layer*); +UFBX_LIST_TYPE(ufbx_anim_value_list, ufbx_anim_value*); +UFBX_LIST_TYPE(ufbx_anim_curve_list, ufbx_anim_curve*); +UFBX_LIST_TYPE(ufbx_display_layer_list, ufbx_display_layer*); +UFBX_LIST_TYPE(ufbx_selection_set_list, ufbx_selection_set*); +UFBX_LIST_TYPE(ufbx_selection_node_list, ufbx_selection_node*); +UFBX_LIST_TYPE(ufbx_character_list, ufbx_character*); +UFBX_LIST_TYPE(ufbx_constraint_list, ufbx_constraint*); +UFBX_LIST_TYPE(ufbx_audio_layer_list, ufbx_audio_layer*); +UFBX_LIST_TYPE(ufbx_audio_clip_list, ufbx_audio_clip*); +UFBX_LIST_TYPE(ufbx_pose_list, ufbx_pose*); +UFBX_LIST_TYPE(ufbx_metadata_object_list, ufbx_metadata_object*); + +typedef enum ufbx_element_type UFBX_ENUM_REPR { + UFBX_ELEMENT_UNKNOWN, // < `ufbx_unknown` + UFBX_ELEMENT_NODE, // < `ufbx_node` + UFBX_ELEMENT_MESH, // < `ufbx_mesh` + UFBX_ELEMENT_LIGHT, // < `ufbx_light` + UFBX_ELEMENT_CAMERA, // < `ufbx_camera` + UFBX_ELEMENT_BONE, // < `ufbx_bone` + UFBX_ELEMENT_EMPTY, // < `ufbx_empty` + UFBX_ELEMENT_LINE_CURVE, // < `ufbx_line_curve` + UFBX_ELEMENT_NURBS_CURVE, // < `ufbx_nurbs_curve` + UFBX_ELEMENT_NURBS_SURFACE, // < `ufbx_nurbs_surface` + UFBX_ELEMENT_NURBS_TRIM_SURFACE, // < `ufbx_nurbs_trim_surface` + UFBX_ELEMENT_NURBS_TRIM_BOUNDARY, // < `ufbx_nurbs_trim_boundary` + UFBX_ELEMENT_PROCEDURAL_GEOMETRY, // < `ufbx_procedural_geometry` + UFBX_ELEMENT_STEREO_CAMERA, // < `ufbx_stereo_camera` + UFBX_ELEMENT_CAMERA_SWITCHER, // < `ufbx_camera_switcher` + UFBX_ELEMENT_MARKER, // < `ufbx_marker` + UFBX_ELEMENT_LOD_GROUP, // < `ufbx_lod_group` + UFBX_ELEMENT_SKIN_DEFORMER, // < `ufbx_skin_deformer` + UFBX_ELEMENT_SKIN_CLUSTER, // < `ufbx_skin_cluster` + UFBX_ELEMENT_BLEND_DEFORMER, // < `ufbx_blend_deformer` + UFBX_ELEMENT_BLEND_CHANNEL, // < `ufbx_blend_channel` + UFBX_ELEMENT_BLEND_SHAPE, // < `ufbx_blend_shape` + UFBX_ELEMENT_CACHE_DEFORMER, // < `ufbx_cache_deformer` + UFBX_ELEMENT_CACHE_FILE, // < `ufbx_cache_file` + UFBX_ELEMENT_MATERIAL, // < `ufbx_material` + UFBX_ELEMENT_TEXTURE, // < `ufbx_texture` + UFBX_ELEMENT_VIDEO, // < `ufbx_video` + UFBX_ELEMENT_SHADER, // < `ufbx_shader` + UFBX_ELEMENT_SHADER_BINDING, // < `ufbx_shader_binding` + UFBX_ELEMENT_ANIM_STACK, // < `ufbx_anim_stack` + UFBX_ELEMENT_ANIM_LAYER, // < `ufbx_anim_layer` + UFBX_ELEMENT_ANIM_VALUE, // < `ufbx_anim_value` + UFBX_ELEMENT_ANIM_CURVE, // < `ufbx_anim_curve` + UFBX_ELEMENT_DISPLAY_LAYER, // < `ufbx_display_layer` + UFBX_ELEMENT_SELECTION_SET, // < `ufbx_selection_set` + UFBX_ELEMENT_SELECTION_NODE, // < `ufbx_selection_node` + UFBX_ELEMENT_CHARACTER, // < `ufbx_character` + UFBX_ELEMENT_CONSTRAINT, // < `ufbx_constraint` + UFBX_ELEMENT_AUDIO_LAYER, // < `ufbx_audio_layer` + UFBX_ELEMENT_AUDIO_CLIP, // < `ufbx_audio_clip` + UFBX_ELEMENT_POSE, // < `ufbx_pose` + UFBX_ELEMENT_METADATA_OBJECT, // < `ufbx_metadata_object` + + UFBX_ELEMENT_TYPE_FIRST_ATTRIB = UFBX_ELEMENT_MESH, + UFBX_ELEMENT_TYPE_LAST_ATTRIB = UFBX_ELEMENT_LOD_GROUP, + + UFBX_ENUM_FORCE_WIDTH(UFBX_ELEMENT_TYPE) +} ufbx_element_type; + +UFBX_ENUM_TYPE(ufbx_element_type, UFBX_ELEMENT_TYPE, UFBX_ELEMENT_METADATA_OBJECT); + +// Connection between two elements. +// Source and destination are somewhat arbitrary but the destination is +// often the "container" like a parent node or mesh containing a deformer. +typedef struct ufbx_connection { + ufbx_element *src; + ufbx_element *dst; + ufbx_string src_prop; + ufbx_string dst_prop; +} ufbx_connection; + +UFBX_LIST_TYPE(ufbx_connection_list, ufbx_connection); + +// Element "base-class" common to each element. +// Some fields (like `connections_src`) are advanced and not visible +// in the specialized element structs. +// NOTE: The `element_id` value is consistent when loading the +// _same_ file, but re-exporting the file will invalidate them. +struct ufbx_element { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + ufbx_element_type type; + ufbx_connection_list connections_src; + ufbx_connection_list connections_dst; + ufbx_nullable ufbx_dom_node *dom_node; + ufbx_scene *scene; +}; + +// -- Unknown + +struct ufbx_unknown { + // Shared "base-class" header, see `ufbx_element`. + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // FBX format specific type information. + // In ASCII FBX format: + // super_type: ID, "type::name", "sub_type" { ... } + ufbx_string type; + ufbx_string super_type; + ufbx_string sub_type; +}; + +// -- Nodes + +// Inherit type specifies how hierarchial node transforms are combined. +// This only affects the final scaling, as rotation and translation are always +// inherited correctly. +// NOTE: These don't map to `"InheritType"` property as there may be new ones for +// compatibility with various exporters. +typedef enum ufbx_inherit_mode UFBX_ENUM_REPR { + + // Normal matrix composition of hierarchy: `R*S*r*s`. + // child.node_to_world = parent.node_to_world * child.node_to_parent; + UFBX_INHERIT_MODE_NORMAL, + + // Ignore parent scale when computing the transform: `R*r*s`. + // ufbx_transform t = node.local_transform; + // t.translation *= parent.inherit_scale; + // t.scale *= node.inherit_scale_node.inherit_scale; + // child.node_to_world = parent.unscaled_node_to_world * t; + // Also known as "Segment scale compensate" in some software. + UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE, + + // Apply parent scale component-wise: `R*r*S*s`. + // ufbx_transform t = node.local_transform; + // t.translation *= parent.inherit_scale; + // t.scale *= node.inherit_scale_node.inherit_scale; + // child.node_to_world = parent.unscaled_node_to_world * t; + UFBX_INHERIT_MODE_COMPONENTWISE_SCALE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_INHERIT_MODE) +} ufbx_inherit_mode; + +UFBX_ENUM_TYPE(ufbx_inherit_mode, UFBX_INHERIT_MODE, UFBX_INHERIT_MODE_COMPONENTWISE_SCALE); + +// Axis used to mirror transformations for handedness conversion. +typedef enum ufbx_mirror_axis UFBX_ENUM_REPR { + + UFBX_MIRROR_AXIS_NONE, + UFBX_MIRROR_AXIS_X, + UFBX_MIRROR_AXIS_Y, + UFBX_MIRROR_AXIS_Z, + + UFBX_ENUM_FORCE_WIDTH(UFBX_MIRROR_AXIS) +} ufbx_mirror_axis; + +UFBX_ENUM_TYPE(ufbx_mirror_axis, UFBX_MIRROR_AXIS, UFBX_MIRROR_AXIS_Z); + +// Nodes form the scene transformation hierarchy and can contain attached +// elements such as meshes or lights. In normal cases a single `ufbx_node` +// contains only a single attached element, so using `type/mesh/...` is safe. +struct ufbx_node { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Node hierarchy + + // Parent node containing this one if not root. + // + // Always non-`NULL` for non-root nodes unless + // `ufbx_load_opts.allow_nodes_out_of_root` is enabled. + ufbx_nullable ufbx_node *parent; + + // List of child nodes parented to this node. + ufbx_node_list children; + + // Common attached element type and typed pointers. Set to `NULL` if not in + // use, so checking `attrib_type` is not required. + // + // HINT: If you need less common attributes access `ufbx_node.attrib`, you + // can use utility functions like `ufbx_as_nurbs_curve(attrib)` to convert + // and check the attribute in one step. + ufbx_nullable ufbx_mesh *mesh; + ufbx_nullable ufbx_light *light; + ufbx_nullable ufbx_camera *camera; + ufbx_nullable ufbx_bone *bone; + + // Less common attributes use these fields. + // + // Defined even if it is one of the above, eg. `ufbx_mesh`. In case there + // is multiple attributes this will be the first one. + ufbx_nullable ufbx_element *attrib; + + // Geometry transform helper if one exists. + // See `UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES`. + ufbx_nullable ufbx_node *geometry_transform_helper; + + // Scale helper if one exists. + // See `UFBX_INHERIT_MODE_HANDLING_HELPER_NODES`. + ufbx_nullable ufbx_node *scale_helper; + + // `attrib->type` if `attrib` is defined, otherwise `UFBX_ELEMENT_UNKNOWN`. + ufbx_element_type attrib_type; + + // List of _all_ attached attribute elements. + // + // In most cases there is only zero or one attributes per node, but if you + // have a very exotic FBX file nodes may have multiple attributes. + ufbx_element_list all_attribs; + + // Local transform in parent, geometry transform is a non-inherited + // transform applied only to attachments like meshes + ufbx_inherit_mode inherit_mode; + ufbx_inherit_mode original_inherit_mode; + ufbx_transform local_transform; + ufbx_transform geometry_transform; + + // Combined scale when using `UFBX_INHERIT_MODE_COMPONENTWISE_SCALE`. + // Contains `local_transform.scale` otherwise. + ufbx_vec3 inherit_scale; + + // Node where scale is inherited from for `UFBX_INHERIT_MODE_COMPONENTWISE_SCALE` + // and even for `UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE`. + // For componentwise-scale nodes, this will point to `parent`, for scale ignoring + // nodes this will point to the parent of the nearest componentwise-scaled node + // in the parent chain. + ufbx_nullable ufbx_node *inherit_scale_node; + + // Raw Euler angles in degrees for those who want them + + // Specifies the axis order `euler_rotation` is applied in. + ufbx_rotation_order rotation_order; + // Rotation around the local X/Y/Z axes in `rotation_order`. + // The angles are specified in degrees. + ufbx_vec3 euler_rotation; + + // Matrices derived from the transformations, for transforming geometry + // prefer using `geometry_to_world` as that supports geometric transforms. + + // Transform from this node to `parent` space. + // Equivalent to `ufbx_transform_to_matrix(&local_transform)`. + ufbx_matrix node_to_parent; + // Transform from this node to the world space, ie. multiplying all the + // `node_to_parent` matrices of the parent chain together. + ufbx_matrix node_to_world; + // Transform from the attribute to this node. Does not affect the transforms + // of `children`! + // Equivalent to `ufbx_transform_to_matrix(&geometry_transform)`. + ufbx_matrix geometry_to_node; + // Transform from attribute space to world space. + // Equivalent to `ufbx_matrix_mul(&node_to_world, &geometry_to_node)`. + ufbx_matrix geometry_to_world; + // Transform from this node to world space, ignoring self scaling. + ufbx_matrix unscaled_node_to_world; + + // ufbx-specific adjustment for switching between coodrinate/unit systems. + // HINT: In most cases you don't need to deal with these as these are baked + // into all the transforms above and into `ufbx_evaluate_transform()`. + ufbx_vec3 adjust_pre_translation; // < Translation applied between parent and self + ufbx_quat adjust_pre_rotation; // < Rotation applied between parent and self + ufbx_real adjust_pre_scale; // < Scaling applied between parent and self + ufbx_quat adjust_post_rotation; // < Rotation applied in local space at the end + ufbx_real adjust_post_scale; // < Scaling applied in local space at the end + ufbx_real adjust_translation_scale; // < Scaling applied to translation only + ufbx_mirror_axis adjust_mirror_axis; // < Mirror translation and rotation on this axis + + // Materials used by `mesh` or other `attrib`. + // There may be multiple copies of a single `ufbx_mesh` with different materials + // in the `ufbx_node` instances. + ufbx_material_list materials; + + // Bind pose + ufbx_nullable ufbx_pose *bind_pose; + + // Visibility state. + bool visible; + + // True if this node is the implicit root node of the scene. + bool is_root; + + // True if the node has a non-identity `geometry_transform`. + bool has_geometry_transform; + + // If `true` the transform is adjusted by ufbx, not enabled by default. + // See `adjust_pre_rotation`, `adjust_pre_scale`, `adjust_post_rotation`, + // and `adjust_post_scale`. + bool has_adjust_transform; + + // Scale is adjusted by root scale. + bool has_root_adjust_transform; + + // True if this node is a synthetic geometry transform helper. + // See `UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES`. + bool is_geometry_transform_helper; + + // True if the node is a synthetic scale compensation helper. + // See `UFBX_INHERIT_MODE_HANDLING_HELPER_NODES`. + bool is_scale_helper; + + // Parent node to children that can compensate for parent scale. + bool is_scale_compensate_parent; + + // How deep is this node in the parent hierarchy. Root node is at depth `0` + // and the immediate children of root at `1`. + uint32_t node_depth; +}; + +// Vertex attribute: All attributes are stored in a consistent indexed format +// regardless of how it's actually stored in the file. +// +// `values` is a contiguous array of attribute values. +// `indices` maps each mesh index into a value in the `values` array. +// +// If `unique_per_vertex` is set then the attribute is guaranteed to have a +// single defined value per vertex accessible via: +// attrib.values.data[attrib.indices.data[mesh->vertex_first_index[vertex_ix]] +typedef struct ufbx_vertex_attrib { + // Is this attribute defined by the mesh. + bool exists; + // List of values the attribute uses. + ufbx_void_list values; + // Indices into `values[]`, indexed up to `ufbx_mesh.num_indices`. + ufbx_uint32_list indices; + // Number of `ufbx_real` entries per value. + size_t value_reals; + // `true` if this attribute is defined per vertex, instead of per index. + bool unique_per_vertex; + // Optional 4th 'W' component for the attribute. + // May be defined for the following: + // ufbx_mesh.vertex_normal + // ufbx_mesh.vertex_tangent / ufbx_uv_set.vertex_tangent + // ufbx_mesh.vertex_bitangent / ufbx_uv_set.vertex_bitangent + // NOTE: This is not loaded by default, set `ufbx_load_opts.retain_vertex_attrib_w`. + ufbx_real_list values_w; +} ufbx_vertex_attrib; + +// 1D vertex attribute, see `ufbx_vertex_attrib` for information +typedef struct ufbx_vertex_real { + bool exists; + ufbx_real_list values; + ufbx_uint32_list indices; + size_t value_reals; + bool unique_per_vertex; + ufbx_real_list values_w; + + UFBX_VERTEX_ATTRIB_IMPL(ufbx_real) +} ufbx_vertex_real; + +// 2D vertex attribute, see `ufbx_vertex_attrib` for information +typedef struct ufbx_vertex_vec2 { + bool exists; + ufbx_vec2_list values; + ufbx_uint32_list indices; + size_t value_reals; + bool unique_per_vertex; + ufbx_real_list values_w; + + UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec2) +} ufbx_vertex_vec2; + +// 3D vertex attribute, see `ufbx_vertex_attrib` for information +typedef struct ufbx_vertex_vec3 { + bool exists; + ufbx_vec3_list values; + ufbx_uint32_list indices; + size_t value_reals; + bool unique_per_vertex; + ufbx_real_list values_w; + + UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec3) +} ufbx_vertex_vec3; + +// 4D vertex attribute, see `ufbx_vertex_attrib` for information +typedef struct ufbx_vertex_vec4 { + bool exists; + ufbx_vec4_list values; + ufbx_uint32_list indices; + size_t value_reals; + bool unique_per_vertex; + ufbx_real_list values_w; + + UFBX_VERTEX_ATTRIB_IMPL(ufbx_vec4) +} ufbx_vertex_vec4; + +// Vertex UV set/layer +typedef struct ufbx_uv_set { + ufbx_string name; + uint32_t index; + + // Vertex attributes, see `ufbx_mesh` attributes for more information + ufbx_vertex_vec2 vertex_uv; // < UV / texture coordinates + ufbx_vertex_vec3 vertex_tangent; // < (optional) Tangent vector in UV.x direction + ufbx_vertex_vec3 vertex_bitangent; // < (optional) Tangent vector in UV.y direction +} ufbx_uv_set; + +// Vertex color set/layer +typedef struct ufbx_color_set { + ufbx_string name; + uint32_t index; + + // Vertex attributes, see `ufbx_mesh` attributes for more information + ufbx_vertex_vec4 vertex_color; // < Per-vertex RGBA color +} ufbx_color_set; + +UFBX_LIST_TYPE(ufbx_uv_set_list, ufbx_uv_set); +UFBX_LIST_TYPE(ufbx_color_set_list, ufbx_color_set); + +// Edge between two _indices_ in a mesh +typedef struct ufbx_edge { + union { + struct { uint32_t a, b; }; + uint32_t indices[2]; + }; +} ufbx_edge; + +UFBX_LIST_TYPE(ufbx_edge_list, ufbx_edge); + +// Polygonal face with arbitrary number vertices, a single face contains a +// contiguous range of mesh indices, eg. `{5,3}` would have indices 5, 6, 7 +// +// NOTE: `num_indices` maybe less than 3 in which case the face is invalid! +// [TODO #23: should probably remove the bad faces at load time] +typedef struct ufbx_face { + uint32_t index_begin; + uint32_t num_indices; +} ufbx_face; + +UFBX_LIST_TYPE(ufbx_face_list, ufbx_face); + +// Subset of mesh faces used by a single material or group. +typedef struct ufbx_mesh_part { + + // Index of the mesh part. + uint32_t index; + + // Sub-set of the geometry + size_t num_faces; // < Number of faces (polygons) + size_t num_triangles; // < Number of triangles if triangulated + + size_t num_empty_faces; // < Number of faces with zero vertices + size_t num_point_faces; // < Number of faces with a single vertex + size_t num_line_faces; // < Number of faces with two vertices + + // Indices to `ufbx_mesh.faces[]`. + // Always contains `num_faces` elements. + ufbx_uint32_list face_indices; + +} ufbx_mesh_part; + +UFBX_LIST_TYPE(ufbx_mesh_part_list, ufbx_mesh_part); + +typedef struct ufbx_face_group { + int32_t id; // < Numerical ID for this group. + ufbx_string name; // < Name for the face group. +} ufbx_face_group; + +UFBX_LIST_TYPE(ufbx_face_group_list, ufbx_face_group); + +typedef struct ufbx_subdivision_weight_range { + uint32_t weight_begin; + uint32_t num_weights; +} ufbx_subdivision_weight_range; + +UFBX_LIST_TYPE(ufbx_subdivision_weight_range_list, ufbx_subdivision_weight_range); + +typedef struct ufbx_subdivision_weight { + ufbx_real weight; + uint32_t index; +} ufbx_subdivision_weight; + +UFBX_LIST_TYPE(ufbx_subdivision_weight_list, ufbx_subdivision_weight); + +typedef struct ufbx_subdivision_result { + size_t result_memory_used; + size_t temp_memory_used; + size_t result_allocs; + size_t temp_allocs; + + // Weights of vertices in the source model. + // Defined if `ufbx_subdivide_opts.evaluate_source_vertices` is set. + ufbx_subdivision_weight_range_list source_vertex_ranges; + ufbx_subdivision_weight_list source_vertex_weights; + + // Weights of skin clusters in the source model. + // Defined if `ufbx_subdivide_opts.evaluate_skin_weights` is set. + ufbx_subdivision_weight_range_list skin_cluster_ranges; + ufbx_subdivision_weight_list skin_cluster_weights; + +} ufbx_subdivision_result; + +typedef enum ufbx_subdivision_display_mode UFBX_ENUM_REPR { + UFBX_SUBDIVISION_DISPLAY_DISABLED, + UFBX_SUBDIVISION_DISPLAY_HULL, + UFBX_SUBDIVISION_DISPLAY_HULL_AND_SMOOTH, + UFBX_SUBDIVISION_DISPLAY_SMOOTH, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SUBDIVISION_DISPLAY_MODE) +} ufbx_subdivision_display_mode; + +UFBX_ENUM_TYPE(ufbx_subdivision_display_mode, UFBX_SUBDIVISION_DISPLAY_MODE, UFBX_SUBDIVISION_DISPLAY_SMOOTH); + +typedef enum ufbx_subdivision_boundary UFBX_ENUM_REPR { + UFBX_SUBDIVISION_BOUNDARY_DEFAULT, + UFBX_SUBDIVISION_BOUNDARY_LEGACY, + // OpenSubdiv: `VTX_BOUNDARY_EDGE_AND_CORNER` / `FVAR_LINEAR_CORNERS_ONLY` + UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS, + // OpenSubdiv: `VTX_BOUNDARY_EDGE_ONLY` / `FVAR_LINEAR_NONE` + UFBX_SUBDIVISION_BOUNDARY_SHARP_NONE, + // OpenSubdiv: `FVAR_LINEAR_BOUNDARIES` + UFBX_SUBDIVISION_BOUNDARY_SHARP_BOUNDARY, + // OpenSubdiv: `FVAR_LINEAR_ALL` + UFBX_SUBDIVISION_BOUNDARY_SHARP_INTERIOR, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SUBDIVISION_BOUNDARY) +} ufbx_subdivision_boundary; + +UFBX_ENUM_TYPE(ufbx_subdivision_boundary, UFBX_SUBDIVISION_BOUNDARY, UFBX_SUBDIVISION_BOUNDARY_SHARP_INTERIOR); + +// Polygonal mesh geometry. +// +// Example mesh with two triangles (x, z) and a quad (y). +// The faces have a constant UV coordinate x/y/z. +// The vertices have _per vertex_ normals that point up/down. +// +// ^ ^ ^ +// A---B-----C +// |x / /| +// | / y / | +// |/ / z| +// D-----E---F +// v v v +// +// Attributes may have multiple values within a single vertex, for example a +// UV seam vertex has two UV coordinates. Thus polygons are defined using +// an index that counts each corner of each face polygon. If an attribute is +// defined (even per-vertex) it will always have a valid `indices` array. +// +// {0,3} {3,4} {7,3} faces ({ index_begin, num_indices }) +// 0 1 2 3 4 5 6 7 8 9 index +// +// 0 1 3 1 2 4 3 2 4 5 vertex_indices[index] +// A B D B C E D C E F vertices[vertex_indices[index]] +// +// 0 0 1 0 0 1 1 0 1 1 vertex_normal.indices[index] +// ^ ^ v ^ ^ v v ^ v v vertex_normal.data[vertex_normal.indices[index]] +// +// 0 0 0 1 1 1 1 2 2 2 vertex_uv.indices[index] +// x x x y y y y z z z vertex_uv.data[vertex_uv.indices[index]] +// +// Vertex position can also be accessed uniformly through an accessor: +// 0 1 3 1 2 4 3 2 4 5 vertex_position.indices[index] +// A B D B C E D C E F vertex_position.data[vertex_position.indices[index]] +// +// Some geometry data is specified per logical vertex. Vertex positions are +// the only attribute that is guaranteed to be defined _uniquely_ per vertex. +// Vertex attributes _may_ be defined per vertex if `unique_per_vertex == true`. +// You can access the per-vertex values by first finding the first index that +// refers to the given vertex. +// +// 0 1 2 3 4 5 vertex +// A B C D E F vertices[vertex] +// +// 0 1 4 2 5 9 vertex_first_index[vertex] +// 0 0 0 1 1 1 vertex_normal.indices[vertex_first_index[vertex]] +// ^ ^ ^ v v v vertex_normal.data[vertex_normal.indices[vertex_first_index[vertex]]] +// +struct ufbx_mesh { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Number of "logical" vertices that would be treated as a single point, + // one vertex may be split to multiple indices for split attributes, eg. UVs + size_t num_vertices; // < Number of logical "vertex" points + size_t num_indices; // < Number of combiend vertex/attribute tuples + size_t num_faces; // < Number of faces (polygons) in the mesh + size_t num_triangles; // < Number of triangles if triangulated + + // Number of edges in the mesh. + // NOTE: May be zero in valid meshes if the file doesn't contain edge adjacency data! + size_t num_edges; + + size_t max_face_triangles; // < Maximum number of triangles in a face in this mesh + + size_t num_empty_faces; // < Number of faces with zero vertices + size_t num_point_faces; // < Number of faces with a single vertex + size_t num_line_faces; // < Number of faces with two vertices + + // Faces and optional per-face extra data + ufbx_face_list faces; // < Face index range + ufbx_bool_list face_smoothing; // < Should the face have soft normals + ufbx_uint32_list face_material; // < Indices to `ufbx_mesh.materials[]` and `ufbx_node.materials[]` + ufbx_uint32_list face_group; // < Face polygon group index, indices to `ufbx_mesh.face_groups[]` + ufbx_bool_list face_hole; // < Should the face be hidden as a "hole" + + // Edges and optional per-edge extra data + ufbx_edge_list edges; // < Edge index range + ufbx_bool_list edge_smoothing; // < Should the edge have soft normals + ufbx_real_list edge_crease; // < Crease value for subdivision surfaces + ufbx_bool_list edge_visibility; // < Should the edge be visible + + // Logical vertices and positions, alternatively you can use + // `vertex_position` for consistent interface with other attributes. + ufbx_uint32_list vertex_indices; + ufbx_vec3_list vertices; + + // First index referring to a given vertex, `UFBX_NO_INDEX` if the vertex is unused. + ufbx_uint32_list vertex_first_index; + + // Vertex attributes, see the comment over the struct. + // + // NOTE: Not all meshes have all attributes, in that case `indices/data == NULL`! + // + // NOTE: UV/tangent/bitangent and color are the from first sets, + // use `uv_sets/color_sets` to access the other layers. + ufbx_vertex_vec3 vertex_position; // < Vertex positions + ufbx_vertex_vec3 vertex_normal; // < (optional) Normal vectors, always defined if `ufbx_load_opts.generate_missing_normals` + ufbx_vertex_vec2 vertex_uv; // < (optional) UV / texture coordinates + ufbx_vertex_vec3 vertex_tangent; // < (optional) Tangent vector in UV.x direction + ufbx_vertex_vec3 vertex_bitangent; // < (optional) Tangent vector in UV.y direction + ufbx_vertex_vec4 vertex_color; // < (optional) Per-vertex RGBA color + ufbx_vertex_real vertex_crease; // < (optional) Crease value for subdivision surfaces + + // Multiple named UV/color sets + // NOTE: The first set contains the same data as `vertex_uv/color`! + ufbx_uv_set_list uv_sets; + ufbx_color_set_list color_sets; + + // Materials used by the mesh. + // NOTE: These can be wrong if you want to support per-instance materials! + // Use `ufbx_node.materials[]` to get the per-instance materials at the same indices. + ufbx_material_list materials; + + // Face groups for this mesh. + ufbx_face_group_list face_groups; + + // Segments that use a given material. + // Defined even if the mesh doesn't have any materials. + ufbx_mesh_part_list material_parts; + + // Segments for each face group. + ufbx_mesh_part_list face_group_parts; + + // Order of `material_parts` by first face that refers to it. + // Useful for compatibility with FBX SDK and various importers using it, + // as they use this material order by default. + ufbx_uint32_list material_part_usage_order; + + // Skinned vertex positions, for efficiency the skinned positions are the + // same as the static ones for non-skinned meshes and `skinned_is_local` + // is set to true meaning you need to transform them manually using + // `ufbx_transform_position(&node->geometry_to_world, skinned_pos)`! + bool skinned_is_local; + ufbx_vertex_vec3 skinned_position; + ufbx_vertex_vec3 skinned_normal; + + // Deformers + ufbx_skin_deformer_list skin_deformers; + ufbx_blend_deformer_list blend_deformers; + ufbx_cache_deformer_list cache_deformers; + ufbx_element_list all_deformers; + + // Subdivision + uint32_t subdivision_preview_levels; + uint32_t subdivision_render_levels; + ufbx_subdivision_display_mode subdivision_display_mode; + ufbx_subdivision_boundary subdivision_boundary; + ufbx_subdivision_boundary subdivision_uv_boundary; + + // The winding of the faces has been reversed. + bool reversed_winding; + + // Normals have been generated instead of evaluated. + // Either from missing normals (via `ufbx_load_opts.generate_missing_normals`), skinning, + // tessellation, or subdivision. + bool generated_normals; + + // Subdivision (result) + bool subdivision_evaluated; + ufbx_nullable ufbx_subdivision_result *subdivision_result; + + // Tessellation (result) + bool from_tessellated_nurbs; +}; + +// The kind of light source +typedef enum ufbx_light_type UFBX_ENUM_REPR { + // Single point at local origin, at `node->world_transform.position` + UFBX_LIGHT_POINT, + // Infinite directional light pointing locally towards `light->local_direction` + // For global: `ufbx_transform_direction(&node->node_to_world, light->local_direction)` + UFBX_LIGHT_DIRECTIONAL, + // Cone shaped light towards `light->local_direction`, between `light->inner/outer_angle`. + // For global: `ufbx_transform_direction(&node->node_to_world, light->local_direction)` + UFBX_LIGHT_SPOT, + // Area light, shape specified by `light->area_shape` + // TODO: Units? + UFBX_LIGHT_AREA, + // Volumetric light source + // TODO: How does this work + UFBX_LIGHT_VOLUME, + + UFBX_ENUM_FORCE_WIDTH(UFBX_LIGHT_TYPE) +} ufbx_light_type; + +UFBX_ENUM_TYPE(ufbx_light_type, UFBX_LIGHT_TYPE, UFBX_LIGHT_VOLUME); + +// How fast does the light intensity decay at a distance +typedef enum ufbx_light_decay UFBX_ENUM_REPR { + UFBX_LIGHT_DECAY_NONE, // < 1 (no decay) + UFBX_LIGHT_DECAY_LINEAR, // < 1 / d + UFBX_LIGHT_DECAY_QUADRATIC, // < 1 / d^2 (physically accurate) + UFBX_LIGHT_DECAY_CUBIC, // < 1 / d^3 + + UFBX_ENUM_FORCE_WIDTH(UFBX_LIGHT_DECAY) +} ufbx_light_decay; + +UFBX_ENUM_TYPE(ufbx_light_decay, UFBX_LIGHT_DECAY, UFBX_LIGHT_DECAY_CUBIC); + +typedef enum ufbx_light_area_shape UFBX_ENUM_REPR { + UFBX_LIGHT_AREA_SHAPE_RECTANGLE, + UFBX_LIGHT_AREA_SHAPE_SPHERE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_LIGHT_AREA_SHAPE) +} ufbx_light_area_shape; + +UFBX_ENUM_TYPE(ufbx_light_area_shape, UFBX_LIGHT_AREA_SHAPE, UFBX_LIGHT_AREA_SHAPE_SPHERE); + +// Light source attached to a `ufbx_node` +struct ufbx_light { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Color and intensity of the light, usually you want to use `color * intensity` + // NOTE: `intensity` is 0.01x of the property `"Intensity"` as that matches + // matches values in DCC programs before exporting. + ufbx_vec3 color; + ufbx_real intensity; + + // Direction the light is aimed at in node's local space, usually -Y + ufbx_vec3 local_direction; + + // Type of the light and shape parameters + ufbx_light_type type; + ufbx_light_decay decay; + ufbx_light_area_shape area_shape; + ufbx_real inner_angle; + ufbx_real outer_angle; + + bool cast_light; + bool cast_shadows; +}; + +typedef enum ufbx_projection_mode UFBX_ENUM_REPR { + // Perspective projection. + UFBX_PROJECTION_MODE_PERSPECTIVE, + + // Orthographic projection. + UFBX_PROJECTION_MODE_ORTHOGRAPHIC, + + UFBX_ENUM_FORCE_WIDTH(UFBX_PROJECTION_MODE) +} ufbx_projection_mode; + +UFBX_ENUM_TYPE(ufbx_projection_mode, UFBX_PROJECTION_MODE, UFBX_PROJECTION_MODE_ORTHOGRAPHIC); + +// Method of specifying the rendering resolution from properties +// NOTE: Handled internally by ufbx, ignore unless you interpret `ufbx_props` directly! +typedef enum ufbx_aspect_mode UFBX_ENUM_REPR { + // No defined resolution + UFBX_ASPECT_MODE_WINDOW_SIZE, + // `"AspectWidth"` and `"AspectHeight"` are relative to each other + UFBX_ASPECT_MODE_FIXED_RATIO, + // `"AspectWidth"` and `"AspectHeight"` are both pixels + UFBX_ASPECT_MODE_FIXED_RESOLUTION, + // `"AspectWidth"` is pixels, `"AspectHeight"` is relative to width + UFBX_ASPECT_MODE_FIXED_WIDTH, + // < `"AspectHeight"` is pixels, `"AspectWidth"` is relative to height + UFBX_ASPECT_MODE_FIXED_HEIGHT, + + UFBX_ENUM_FORCE_WIDTH(UFBX_ASPECT_MODE) +} ufbx_aspect_mode; + +UFBX_ENUM_TYPE(ufbx_aspect_mode, UFBX_ASPECT_MODE, UFBX_ASPECT_MODE_FIXED_HEIGHT); + +// Method of specifying the field of view from properties +// NOTE: Handled internally by ufbx, ignore unless you interpret `ufbx_props` directly! +typedef enum ufbx_aperture_mode UFBX_ENUM_REPR { + // Use separate `"FieldOfViewX"` and `"FieldOfViewY"` as horizontal/vertical FOV angles + UFBX_APERTURE_MODE_HORIZONTAL_AND_VERTICAL, + // Use `"FieldOfView"` as horizontal FOV angle, derive vertical angle via aspect ratio + UFBX_APERTURE_MODE_HORIZONTAL, + // Use `"FieldOfView"` as vertical FOV angle, derive horizontal angle via aspect ratio + UFBX_APERTURE_MODE_VERTICAL, + // Compute the field of view from the render gate size and focal length + UFBX_APERTURE_MODE_FOCAL_LENGTH, + + UFBX_ENUM_FORCE_WIDTH(UFBX_APERTURE_MODE) +} ufbx_aperture_mode; + +UFBX_ENUM_TYPE(ufbx_aperture_mode, UFBX_APERTURE_MODE, UFBX_APERTURE_MODE_FOCAL_LENGTH); + +// Method of specifying the render gate size from properties +// NOTE: Handled internally by ufbx, ignore unless you interpret `ufbx_props` directly! +typedef enum ufbx_gate_fit UFBX_ENUM_REPR { + // Use the film/aperture size directly as the render gate + UFBX_GATE_FIT_NONE, + // Fit the render gate to the height of the film, derive width from aspect ratio + UFBX_GATE_FIT_VERTICAL, + // Fit the render gate to the width of the film, derive height from aspect ratio + UFBX_GATE_FIT_HORIZONTAL, + // Fit the render gate so that it is fully contained within the film gate + UFBX_GATE_FIT_FILL, + // Fit the render gate so that it fully contains the film gate + UFBX_GATE_FIT_OVERSCAN, + // Stretch the render gate to match the film gate + // TODO: Does this differ from `UFBX_GATE_FIT_NONE`? + UFBX_GATE_FIT_STRETCH, + + UFBX_ENUM_FORCE_WIDTH(UFBX_GATE_FIT) +} ufbx_gate_fit; + +UFBX_ENUM_TYPE(ufbx_gate_fit, UFBX_GATE_FIT, UFBX_GATE_FIT_STRETCH); + +// Camera film/aperture size defaults +// NOTE: Handled internally by ufbx, ignore unless you interpret `ufbx_props` directly! +typedef enum ufbx_aperture_format UFBX_ENUM_REPR { + UFBX_APERTURE_FORMAT_CUSTOM, // < Use `"FilmWidth"` and `"FilmHeight"` + UFBX_APERTURE_FORMAT_16MM_THEATRICAL, // < 0.404 x 0.295 inches + UFBX_APERTURE_FORMAT_SUPER_16MM, // < 0.493 x 0.292 inches + UFBX_APERTURE_FORMAT_35MM_ACADEMY, // < 0.864 x 0.630 inches + UFBX_APERTURE_FORMAT_35MM_TV_PROJECTION, // < 0.816 x 0.612 inches + UFBX_APERTURE_FORMAT_35MM_FULL_APERTURE, // < 0.980 x 0.735 inches + UFBX_APERTURE_FORMAT_35MM_185_PROJECTION, // < 0.825 x 0.446 inches + UFBX_APERTURE_FORMAT_35MM_ANAMORPHIC, // < 0.864 x 0.732 inches (squeeze ratio: 2) + UFBX_APERTURE_FORMAT_70MM_PROJECTION, // < 2.066 x 0.906 inches + UFBX_APERTURE_FORMAT_VISTAVISION, // < 1.485 x 0.991 inches + UFBX_APERTURE_FORMAT_DYNAVISION, // < 2.080 x 1.480 inches + UFBX_APERTURE_FORMAT_IMAX, // < 2.772 x 2.072 inches + + UFBX_ENUM_FORCE_WIDTH(UFBX_APERTURE_FORMAT) +} ufbx_aperture_format; + +UFBX_ENUM_TYPE(ufbx_aperture_format, UFBX_APERTURE_FORMAT, UFBX_APERTURE_FORMAT_IMAX); + +typedef enum ufbx_coordinate_axis UFBX_ENUM_REPR { + UFBX_COORDINATE_AXIS_POSITIVE_X, + UFBX_COORDINATE_AXIS_NEGATIVE_X, + UFBX_COORDINATE_AXIS_POSITIVE_Y, + UFBX_COORDINATE_AXIS_NEGATIVE_Y, + UFBX_COORDINATE_AXIS_POSITIVE_Z, + UFBX_COORDINATE_AXIS_NEGATIVE_Z, + UFBX_COORDINATE_AXIS_UNKNOWN, + + UFBX_ENUM_FORCE_WIDTH(UFBX_COORDINATE_AXIS) +} ufbx_coordinate_axis; + +UFBX_ENUM_TYPE(ufbx_coordinate_axis, UFBX_COORDINATE_AXIS, UFBX_COORDINATE_AXIS_UNKNOWN); + +// Coordinate axes the scene is represented in. +// NOTE: `front` is the _opposite_ from forward! +typedef struct ufbx_coordinate_axes { + ufbx_coordinate_axis right; + ufbx_coordinate_axis up; + ufbx_coordinate_axis front; +} ufbx_coordinate_axes; + +// Camera attached to a `ufbx_node` +struct ufbx_camera { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Projection mode (perspective/orthographic). + ufbx_projection_mode projection_mode; + + // If set to `true`, `resolution` represents actual pixel values, otherwise + // it's only useful for its aspect ratio. + bool resolution_is_pixels; + + // Render resolution, either in pixels or arbitrary units, depending on above + ufbx_vec2 resolution; + + // Horizontal/vertical field of view in degrees + // Valid if `projection_mode == UFBX_PROJECTION_MODE_PERSPECTIVE`. + ufbx_vec2 field_of_view_deg; + + // Component-wise `tan(field_of_view_deg)`, also represents the size of the + // frustum slice at distance of 1. + // Valid if `projection_mode == UFBX_PROJECTION_MODE_PERSPECTIVE`. + ufbx_vec2 field_of_view_tan; + + // Orthographic camera extents. + // Valid if `projection_mode == UFBX_PROJECTION_MODE_ORTHOGRAPHIC`. + ufbx_real orthographic_extent; + + // Orthographic camera size. + // Valid if `projection_mode == UFBX_PROJECTION_MODE_ORTHOGRAPHIC`. + ufbx_vec2 orthographic_size; + + // Size of the projection plane at distance 1. + // Equal to `field_of_view_tan` if perspective, `orthographic_size` if orthographic. + ufbx_vec2 projection_plane; + + // Aspect ratio of the camera. + ufbx_real aspect_ratio; + + // Near plane of the frustum in units from the camera. + ufbx_real near_plane; + + // Far plane of the frustum in units from the camera. + ufbx_real far_plane; + + // Coordinate system that the projection uses. + // FBX saves cameras with +X forward and +Y up, but you can override this using + // `ufbx_load_opts.target_camera_axes` and it will be reflected here. + ufbx_coordinate_axes projection_axes; + + // Advanced properties used to compute the above + ufbx_aspect_mode aspect_mode; + ufbx_aperture_mode aperture_mode; + ufbx_gate_fit gate_fit; + ufbx_aperture_format aperture_format; + ufbx_real focal_length_mm; // < Focal length in millimeters + ufbx_vec2 film_size_inch; // < Film size in inches + ufbx_vec2 aperture_size_inch; // < Aperture/film gate size in inches + ufbx_real squeeze_ratio; // < Anamoprhic stretch ratio +}; + +// Bone attached to a `ufbx_node`, provides the logical length of the bone +// but most interesting information is directly in `ufbx_node`. +// NOTE: The FBX format calls these Skeleton node attributes. +struct ufbx_bone { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Visual radius of the bone + ufbx_real radius; + + // Length of the bone relative to the distance between two nodes + ufbx_real relative_length; + + // Is the bone a root bone + bool is_root; +}; + +// Empty/NULL/locator connected to a node, actual details in `ufbx_node` +// NOTE: The FBX format calls these Null node attributes. +struct ufbx_empty { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; +}; + +// -- Node attributes (curves/surfaces) + +// Segment of a `ufbx_line_curve`, indices refer to `ufbx_line_curve.point_indices[]` +typedef struct ufbx_line_segment { + uint32_t index_begin; + uint32_t num_indices; +} ufbx_line_segment; + +UFBX_LIST_TYPE(ufbx_line_segment_list, ufbx_line_segment); + +struct ufbx_line_curve { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + ufbx_vec3 color; + + ufbx_vec3_list control_points; // < List of possible values the line passes through + ufbx_uint32_list point_indices; // < Indices to `control_points[]` the line goes through + + ufbx_line_segment_list segments; + + // Tessellation (result) + bool from_tessellated_nurbs; +}; + +typedef enum ufbx_nurbs_topology UFBX_ENUM_REPR { + // The endpoints are not connected. + UFBX_NURBS_TOPOLOGY_OPEN, + // Repeats first `ufbx_nurbs_basis.order - 1` control points after the end. + UFBX_NURBS_TOPOLOGY_PERIODIC, + // Repeats the first control point after the end. + UFBX_NURBS_TOPOLOGY_CLOSED, + + UFBX_ENUM_FORCE_WIDTH(UFBX_NURBS_TOPOLOGY) +} ufbx_nurbs_topology; + +UFBX_ENUM_TYPE(ufbx_nurbs_topology, UFBX_NURBS_TOPOLOGY, UFBX_NURBS_TOPOLOGY_CLOSED); + +// NURBS basis functions for an axis +typedef struct ufbx_nurbs_basis { + + // Number of control points influencing a point on the curve/surface. + // Equal to the degree plus one. + uint32_t order; + + // Topology (periodicity) of the dimension. + ufbx_nurbs_topology topology; + + // Subdivision of the parameter range to control points. + ufbx_real_list knot_vector; + + // Range for the parameter value. + ufbx_real t_min; + ufbx_real t_max; + + // Parameter values of control points. + ufbx_real_list spans; + + // `true` if this axis is two-dimensional. + bool is_2d; + + // Number of control points that need to be copied to the end. + // This is just for convenience as it could be derived from `topology` and + // `order`. If for example `num_wrap_control_points == 3` you should repeat + // the first 3 control points after the end. + // HINT: You don't need to worry about this if you use ufbx functions + // like `ufbx_evaluate_nurbs_curve()` as they handle this internally. + size_t num_wrap_control_points; + + // `true` if the parametrization is well defined. + bool valid; + +} ufbx_nurbs_basis; + +struct ufbx_nurbs_curve { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Basis in the U axis + ufbx_nurbs_basis basis; + + // Linear array of control points + // NOTE: The control points are _not_ homogeneous, meaning you have to multiply + // them by `w` before evaluating the surface. + ufbx_vec4_list control_points; +}; + +struct ufbx_nurbs_surface { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Basis in the U/V axes + ufbx_nurbs_basis basis_u; + ufbx_nurbs_basis basis_v; + + // Number of control points for the U/V axes + size_t num_control_points_u; + size_t num_control_points_v; + + // 2D array of control points. + // Memory layout: `V * num_control_points_u + U` + // NOTE: The control points are _not_ homogeneous, meaning you have to multiply + // them by `w` before evaluating the surface. + ufbx_vec4_list control_points; + + // How many segments tessellate each span in `ufbx_nurbs_basis.spans`. + uint32_t span_subdivision_u; + uint32_t span_subdivision_v; + + // If `true` the resulting normals should be flipped when evaluated. + bool flip_normals; + + // Material for the whole surface. + // NOTE: May be `NULL`! + ufbx_nullable ufbx_material *material; +}; + +struct ufbx_nurbs_trim_surface { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; +}; + +struct ufbx_nurbs_trim_boundary { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; +}; + +// -- Node attributes (advanced) + +struct ufbx_procedural_geometry { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; +}; + +struct ufbx_stereo_camera { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + ufbx_nullable ufbx_camera *left; + ufbx_nullable ufbx_camera *right; +}; + +struct ufbx_camera_switcher { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; +}; + +typedef enum ufbx_marker_type UFBX_ENUM_REPR { + UFBX_MARKER_UNKNOWN, // < Unknown marker type + UFBX_MARKER_FK_EFFECTOR, // < FK (Forward Kinematics) effector + UFBX_MARKER_IK_EFFECTOR, // < IK (Inverse Kinematics) effector + + UFBX_ENUM_FORCE_WIDTH(UFBX_MARKER_TYPE) +} ufbx_marker_type; + +UFBX_ENUM_TYPE(ufbx_marker_type, UFBX_MARKER_TYPE, UFBX_MARKER_IK_EFFECTOR); + +// Tracking marker for effectors +struct ufbx_marker { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // Type of the marker + ufbx_marker_type type; +}; + +// LOD level display mode. +typedef enum ufbx_lod_display UFBX_ENUM_REPR { + UFBX_LOD_DISPLAY_USE_LOD, // < Display the LOD level if the distance is appropriate. + UFBX_LOD_DISPLAY_SHOW, // < Always display the LOD level. + UFBX_LOD_DISPLAY_HIDE, // < Never display the LOD level. + + UFBX_ENUM_FORCE_WIDTH(UFBX_LOD_DISPLAY) +} ufbx_lod_display; + +UFBX_ENUM_TYPE(ufbx_lod_display, UFBX_LOD_DISPLAY, UFBX_LOD_DISPLAY_HIDE); + +// Single LOD level within an LOD group. +// Specifies properties of the Nth child of the _node_ containing the LOD group. +typedef struct ufbx_lod_level { + + // Minimum distance to show this LOD level. + // NOTE: In world units by default, or in screen percentage if + // `ufbx_lod_group.relative_distances` is set. + ufbx_real distance; + + // LOD display mode. + // NOTE: Mostly for editing, you should probably ignore this + // unless making a modeling program. + ufbx_lod_display display; + +} ufbx_lod_level; + +UFBX_LIST_TYPE(ufbx_lod_level_list, ufbx_lod_level); + +// Group of LOD (Level of Detail) levels for an object. +// The actual LOD models are defined in the parent `ufbx_node.children`. +struct ufbx_lod_group { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + ufbx_node_list instances; + }; }; + + // If set to `true`, `ufbx_lod_level.distance` represents a screen size percentage. + bool relative_distances; + + // LOD levels matching in order to `ufbx_node.children`. + ufbx_lod_level_list lod_levels; + + // If set to `true` don't account for parent transform when computing the distance. + bool ignore_parent_transform; + + // If `use_distance_limit` is enabled hide the group if the distance is not between + // `distance_limit_min` and `distance_limit_max`. + bool use_distance_limit; + ufbx_real distance_limit_min; + ufbx_real distance_limit_max; +}; + +// -- Deformers + +// Method to evaluate the skinning on a per-vertex level +typedef enum ufbx_skinning_method UFBX_ENUM_REPR { + // Linear blend skinning: Blend transformation matrices by vertex weights + UFBX_SKINNING_METHOD_LINEAR, + // One vertex should have only one bone attached + UFBX_SKINNING_METHOD_RIGID, + // Convert the transformations to dual quaternions and blend in that space + UFBX_SKINNING_METHOD_DUAL_QUATERNION, + // Blend between `UFBX_SKINNING_METHOD_LINEAR` and `UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR` + // The blend weight can be found either per-vertex in `ufbx_skin_vertex.dq_weight` + // or in `ufbx_skin_deformer.dq_vertices/dq_weights` (indexed by vertex). + UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SKINNING_METHOD) +} ufbx_skinning_method; + +UFBX_ENUM_TYPE(ufbx_skinning_method, UFBX_SKINNING_METHOD, UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR); + +// Skin weight information for a single mesh vertex +typedef struct ufbx_skin_vertex { + + // Each vertex is influenced by weights from `ufbx_skin_deformer.weights[]` + // The weights are sorted by decreasing weight so you can take the first N + // weights to get a cheaper approximation of the vertex. + // NOTE: The weights are not guaranteed to be normalized! + uint32_t weight_begin; // < Index to start from in the `weights[]` array + uint32_t num_weights; // < Number of weights influencing the vertex + + // Blend weight between Linear Blend Skinning (0.0) and Dual Quaternion (1.0). + // Should be used if `skinning_method == UFBX_SKINNING_METHOD_BLENDED_DQ_LINEAR` + ufbx_real dq_weight; + +} ufbx_skin_vertex; + +UFBX_LIST_TYPE(ufbx_skin_vertex_list, ufbx_skin_vertex); + +// Single per-vertex per-cluster weight, see `ufbx_skin_vertex` +typedef struct ufbx_skin_weight { + uint32_t cluster_index; // < Index into `ufbx_skin_deformer.clusters[]` + ufbx_real weight; // < Amount this bone influence the vertex +} ufbx_skin_weight; + +UFBX_LIST_TYPE(ufbx_skin_weight_list, ufbx_skin_weight); + +// Skin deformer specifies a binding between a logical set of bones (a skeleton) +// and a mesh. Each bone is represented by a `ufbx_skin_cluster` that contains +// the binding matrix and a `ufbx_node *bone` that has the current transformation. +struct ufbx_skin_deformer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + ufbx_skinning_method skinning_method; + + // Clusters (bones) in the skin + ufbx_skin_cluster_list clusters; + + // Per-vertex weight information + ufbx_skin_vertex_list vertices; + ufbx_skin_weight_list weights; + + // Largest amount of weights a single vertex can have + size_t max_weights_per_vertex; + + // Blend weights between Linear Blend Skinning (0.0) and Dual Quaternion (1.0). + // HINT: You probably want to use `vertices` and `ufbx_skin_vertex.dq_weight` instead! + // NOTE: These may be out-of-bounds for a given mesh, `vertices` is always safe. + size_t num_dq_weights; + ufbx_uint32_list dq_vertices; + ufbx_real_list dq_weights; +}; + +// Cluster of vertices bound to a single bone. +struct ufbx_skin_cluster { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // The bone node the cluster is attached to + // NOTE: Always valid if found from `ufbx_skin_deformer.clusters[]` unless + // `ufbx_load_opts.connect_broken_elements` is `true`. + ufbx_nullable ufbx_node *bone_node; + + // Binding matrix from local mesh vertices to the bone + ufbx_matrix geometry_to_bone; + + // Binding matrix from local mesh _node_ to the bone. + // NOTE: Prefer `geometry_to_bone` in most use cases! + ufbx_matrix mesh_node_to_bone; + + // Matrix that specifies the rest/bind pose transform of the node, + // not generally needed for skinning, use `geometry_to_bone` instead. + ufbx_matrix bind_to_world; + + // Precomputed matrix/transform that accounts for the current bone transform + // ie. `ufbx_matrix_mul(&cluster->bone->node_to_world, &cluster->geometry_to_bone)` + ufbx_matrix geometry_to_world; + ufbx_transform geometry_to_world_transform; + + // Raw weights indexed by each _vertex_ of a mesh (not index!) + // HINT: It may be simpler to use `ufbx_skin_deformer.vertices[]/weights[]` instead! + // NOTE: These may be out-of-bounds for a given mesh, `ufbx_skin_deformer.vertices` is always safe. + size_t num_weights; // < Number of vertices in the cluster + ufbx_uint32_list vertices; // < Vertex indices in `ufbx_mesh.vertices[]` + ufbx_real_list weights; // < Per-vertex weight values +}; + +// Blend shape deformer can contain multiple channels (think of sliders between morphs) +// that may optionally have in-between keyframes. +struct ufbx_blend_deformer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Independent morph targets of the deformer. + ufbx_blend_channel_list channels; +}; + +// Blend shape associated with a target weight in a series of morphs +typedef struct ufbx_blend_keyframe { + // The target blend shape offsets. + ufbx_blend_shape *shape; + + // Weight value at which to apply the keyframe at full strength + ufbx_real target_weight; + + // The weight the shape should be currently applied with + ufbx_real effective_weight; +} ufbx_blend_keyframe; + +UFBX_LIST_TYPE(ufbx_blend_keyframe_list, ufbx_blend_keyframe); + +// Blend channel consists of multiple morph-key targets that are interpolated. +// In simple cases there will be only one keyframe that is the target shape. +struct ufbx_blend_channel { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Current weight of the channel + ufbx_real weight; + + // Key morph targets to blend between depending on `weight` + // In usual cases there's only one target per channel + ufbx_blend_keyframe_list keyframes; + + // Final blend shape ignoring any intermediate blend shapes. + ufbx_nullable ufbx_blend_shape *target_shape; +}; + +// Blend shape target containing the actual vertex offsets +struct ufbx_blend_shape { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Vertex offsets to apply over the base mesh + // NOTE: The `offset_vertices` may be out-of-bounds for a given mesh! + size_t num_offsets; // < Number of vertex offsets in the following arrays + ufbx_uint32_list offset_vertices; // < Indices to `ufbx_mesh.vertices[]` + ufbx_vec3_list position_offsets; // < Always specified per-vertex offsets + ufbx_vec3_list normal_offsets; // < Empty if not specified + + // Optional weights for the offsets. + // NOTE: These are technically not supported in FBX and are only written by Blender. + ufbx_real_list offset_weights; +}; + +typedef enum ufbx_cache_file_format UFBX_ENUM_REPR { + UFBX_CACHE_FILE_FORMAT_UNKNOWN, // < Unknown cache file format + UFBX_CACHE_FILE_FORMAT_PC2, // < .pc2 Point cache file + UFBX_CACHE_FILE_FORMAT_MC, // < .mc/.mcx Maya cache file + + UFBX_ENUM_FORCE_WIDTH(UFBX_CACHE_FILE_FORMAT) +} ufbx_cache_file_format; + +UFBX_ENUM_TYPE(ufbx_cache_file_format, UFBX_CACHE_FILE_FORMAT, UFBX_CACHE_FILE_FORMAT_MC); + +typedef enum ufbx_cache_data_format UFBX_ENUM_REPR { + UFBX_CACHE_DATA_FORMAT_UNKNOWN, // < Unknown data format + UFBX_CACHE_DATA_FORMAT_REAL_FLOAT, // < `float data[]` + UFBX_CACHE_DATA_FORMAT_VEC3_FLOAT, // < `struct { float x, y, z; } data[]` + UFBX_CACHE_DATA_FORMAT_REAL_DOUBLE, // < `double data[]` + UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE, // < `struct { double x, y, z; } data[]` + + UFBX_ENUM_FORCE_WIDTH(UFBX_CACHE_DATA_FORMAT) +} ufbx_cache_data_format; + +UFBX_ENUM_TYPE(ufbx_cache_data_format, UFBX_CACHE_DATA_FORMAT, UFBX_CACHE_DATA_FORMAT_VEC3_DOUBLE); + +typedef enum ufbx_cache_data_encoding UFBX_ENUM_REPR { + UFBX_CACHE_DATA_ENCODING_UNKNOWN, // < Unknown data encoding + UFBX_CACHE_DATA_ENCODING_LITTLE_ENDIAN, // < Contiguous little-endian array + UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN, // < Contiguous big-endian array + + UFBX_ENUM_FORCE_WIDTH(UFBX_CACHE_DATA_ENCODING) +} ufbx_cache_data_encoding; + +UFBX_ENUM_TYPE(ufbx_cache_data_encoding, UFBX_CACHE_DATA_ENCODING, UFBX_CACHE_DATA_ENCODING_BIG_ENDIAN); + +// Known interpretations of geometry cache data. +typedef enum ufbx_cache_interpretation UFBX_ENUM_REPR { + // Unknown interpretation, see `ufbx_cache_channel.interpretation_name` for more information. + UFBX_CACHE_INTERPRETATION_UNKNOWN, + + // Generic "points" interpretation, FBX SDK default. Usually fine to interpret + // as vertex positions if no other cache channels are specified. + UFBX_CACHE_INTERPRETATION_POINTS, + + // Vertex positions. + UFBX_CACHE_INTERPRETATION_VERTEX_POSITION, + + // Vertex normals. + UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL, + + UFBX_ENUM_FORCE_WIDTH(UFBX_CACHE_INTERPRETATION) +} ufbx_cache_interpretation; + +UFBX_ENUM_TYPE(ufbx_cache_interpretation, UFBX_CACHE_INTERPRETATION, UFBX_CACHE_INTERPRETATION_VERTEX_NORMAL); + +typedef struct ufbx_cache_frame { + + // Name of the channel this frame belongs to. + ufbx_string channel; + + // Time of this frame in seconds. + double time; + + // Name of the file containing the data. + // The specified file may contain multiple frames, use `data_offset` etc. to + // read at the right position. + ufbx_string filename; + + // Format of the wrapper file. + ufbx_cache_file_format file_format; + + // Axis to mirror the read data by. + ufbx_mirror_axis mirror_axis; + + // Factor to scale the geometry by. + ufbx_real scale_factor; + + ufbx_cache_data_format data_format; // < Format of the data in the file + ufbx_cache_data_encoding data_encoding; // < Binary encoding of the data + uint64_t data_offset; // < Byte offset into the file + uint32_t data_count; // < Number of data elements + uint32_t data_element_bytes; // < Size of a single data element in bytes + uint64_t data_total_bytes; // < Size of the whole data blob in bytes +} ufbx_cache_frame; + +UFBX_LIST_TYPE(ufbx_cache_frame_list, ufbx_cache_frame); + +typedef struct ufbx_cache_channel { + + // Name of the geometry cache channel. + ufbx_string name; + + // What does the data in this channel represent. + ufbx_cache_interpretation interpretation; + + // Source name for `interpretation`, especially useful if `interpretation` is + // `UFBX_CACHE_INTERPRETATION_UNKNOWN`. + ufbx_string interpretation_name; + + // List of frames belonging to this channel. + // Sorted by time (`ufbx_cache_frame.time`). + ufbx_cache_frame_list frames; + + // Axis to mirror the frames by. + ufbx_mirror_axis mirror_axis; + + // Factor to scale the geometry by. + ufbx_real scale_factor; + +} ufbx_cache_channel; + +UFBX_LIST_TYPE(ufbx_cache_channel_list, ufbx_cache_channel); + +typedef struct ufbx_geometry_cache { + ufbx_string root_filename; + ufbx_cache_channel_list channels; + ufbx_cache_frame_list frames; + ufbx_string_list extra_info; +} ufbx_geometry_cache; + +struct ufbx_cache_deformer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + ufbx_string channel; + ufbx_nullable ufbx_cache_file *file; + + // Only valid if `ufbx_load_opts.load_external_files` is set! + ufbx_nullable ufbx_geometry_cache *external_cache; + ufbx_nullable ufbx_cache_channel *external_channel; +}; + +struct ufbx_cache_file { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + ufbx_cache_file_format format; + + // Only valid if `ufbx_load_opts.load_external_files` is set! + ufbx_nullable ufbx_geometry_cache *external_cache; +}; + +// -- Materials + +// Material property, either specified with a constant value or a mapped texture +typedef struct ufbx_material_map { + + // Constant value or factor for the map. + // May be specified simultaneously with a texture, in this case most shading models + // use multiplicative tinting of the texture values. + union { + ufbx_real value_real; + ufbx_vec2 value_vec2; + ufbx_vec3 value_vec3; + ufbx_vec4 value_vec4; + }; + int64_t value_int; + + // Texture if connected, otherwise `NULL`. + // May be valid but "disabled" (application specific) if `texture_enabled == false`. + ufbx_nullable ufbx_texture *texture; + + // `true` if the file has specified any of the values above. + // NOTE: The value may be set to a non-zero default even if `has_value == false`, + // for example missing factors are set to `1.0` if a color is defined. + bool has_value; + + // Controls whether shading should use `texture`. + // NOTE: Some shading models allow this to be `true` even if `texture == NULL`. + bool texture_enabled; + + // Set to `true` if this feature should be disabled (specific to shader type). + bool feature_disabled; + + // Number of components in the value from 1 to 4 if defined, 0 if not. + uint8_t value_components; + +} ufbx_material_map; + +// Material feature +typedef struct ufbx_material_feature_info { + + // Whether the material model uses this feature or not. + // NOTE: The feature can be enabled but still not used if eg. the corresponding factor is at zero! + bool enabled; + + // Explicitly enabled/disabled by the material. + bool is_explicit; + +} ufbx_material_feature_info; + +// Texture attached to an FBX property +typedef struct ufbx_material_texture { + ufbx_string material_prop; // < Name of the property in `ufbx_material.props` + ufbx_string shader_prop; // < Shader-specific property mapping name + + // Texture attached to the property. + ufbx_texture *texture; + +} ufbx_material_texture; + +UFBX_LIST_TYPE(ufbx_material_texture_list, ufbx_material_texture); + +// Shading model type +typedef enum ufbx_shader_type UFBX_ENUM_REPR { + // Unknown shading model + UFBX_SHADER_UNKNOWN, + // FBX builtin diffuse material + UFBX_SHADER_FBX_LAMBERT, + // FBX builtin diffuse+specular material + UFBX_SHADER_FBX_PHONG, + // Open Shading Language standard surface + // https://github.com/Autodesk/standard-surface + UFBX_SHADER_OSL_STANDARD_SURFACE, + // Arnold standard surface + // https://docs.arnoldrenderer.com/display/A5AFMUG/Standard+Surface + UFBX_SHADER_ARNOLD_STANDARD_SURFACE, + // 3ds Max Physical Material + // https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2022/ENU/3DSMax-Lighting-Shading/files/GUID-C1328905-7783-4917-AB86-FC3CC19E8972-htm.html + UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL, + // 3ds Max PBR (Metal/Rough) material + // https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2021/ENU/3DSMax-Lighting-Shading/files/GUID-A16234A5-6500-4662-8B20-A5EC9FE1B255-htm.html + UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH, + // 3ds Max PBR (Spec/Gloss) material + // https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2021/ENU/3DSMax-Lighting-Shading/files/GUID-18087194-B2A6-43EF-9B80-8FD1736FAE52-htm.html + UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS, + // 3ds glTF Material + // https://help.autodesk.com/view/3DSMAX/2023/ENU/?guid=GUID-7ABFB805-1D9F-417E-9C22-704BFDF160FA + UFBX_SHADER_GLTF_MATERIAL, + // 3ds OpenPBR Material + // https://help.autodesk.com/view/3DSMAX/2025/ENU/?guid=GUID-CD90329C-1E2B-4BBA-9285-3BB46253B9C2 + UFBX_SHADER_OPENPBR_MATERIAL, + // Stingray ShaderFX shader graph. + // Contains a serialized `"ShaderGraph"` in `ufbx_props`. + UFBX_SHADER_SHADERFX_GRAPH, + // Variation of the FBX phong shader that can recover PBR properties like + // `metalness` or `roughness` from the FBX non-physical values. + // NOTE: Enable `ufbx_load_opts.use_blender_pbr_material`. + UFBX_SHADER_BLENDER_PHONG, + // Wavefront .mtl format shader (used by .obj files) + UFBX_SHADER_WAVEFRONT_MTL, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SHADER_TYPE) +} ufbx_shader_type; + +UFBX_ENUM_TYPE(ufbx_shader_type, UFBX_SHADER_TYPE, UFBX_SHADER_WAVEFRONT_MTL); + +// FBX builtin material properties, matches maps in `ufbx_material_fbx_maps` +typedef enum ufbx_material_fbx_map UFBX_ENUM_REPR { + UFBX_MATERIAL_FBX_DIFFUSE_FACTOR, + UFBX_MATERIAL_FBX_DIFFUSE_COLOR, + UFBX_MATERIAL_FBX_SPECULAR_FACTOR, + UFBX_MATERIAL_FBX_SPECULAR_COLOR, + UFBX_MATERIAL_FBX_SPECULAR_EXPONENT, + UFBX_MATERIAL_FBX_REFLECTION_FACTOR, + UFBX_MATERIAL_FBX_REFLECTION_COLOR, + UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, + UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, + UFBX_MATERIAL_FBX_EMISSION_FACTOR, + UFBX_MATERIAL_FBX_EMISSION_COLOR, + UFBX_MATERIAL_FBX_AMBIENT_FACTOR, + UFBX_MATERIAL_FBX_AMBIENT_COLOR, + UFBX_MATERIAL_FBX_NORMAL_MAP, + UFBX_MATERIAL_FBX_BUMP, + UFBX_MATERIAL_FBX_BUMP_FACTOR, + UFBX_MATERIAL_FBX_DISPLACEMENT_FACTOR, + UFBX_MATERIAL_FBX_DISPLACEMENT, + UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT_FACTOR, + UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT, + + UFBX_ENUM_FORCE_WIDTH(UFBX_MATERIAL_FBX_MAP) +} ufbx_material_fbx_map; + +UFBX_ENUM_TYPE(ufbx_material_fbx_map, UFBX_MATERIAL_FBX_MAP, UFBX_MATERIAL_FBX_VECTOR_DISPLACEMENT); + +// Known PBR material properties, matches maps in `ufbx_material_pbr_maps` +typedef enum ufbx_material_pbr_map UFBX_ENUM_REPR { + UFBX_MATERIAL_PBR_BASE_FACTOR, + UFBX_MATERIAL_PBR_BASE_COLOR, + UFBX_MATERIAL_PBR_ROUGHNESS, + UFBX_MATERIAL_PBR_METALNESS, + UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, + UFBX_MATERIAL_PBR_SPECULAR_FACTOR, + UFBX_MATERIAL_PBR_SPECULAR_COLOR, + UFBX_MATERIAL_PBR_SPECULAR_IOR, + UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, + UFBX_MATERIAL_PBR_SPECULAR_ROTATION, + UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, + UFBX_MATERIAL_PBR_TRANSMISSION_COLOR, + UFBX_MATERIAL_PBR_TRANSMISSION_DEPTH, + UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER, + UFBX_MATERIAL_PBR_TRANSMISSION_SCATTER_ANISOTROPY, + UFBX_MATERIAL_PBR_TRANSMISSION_DISPERSION, + UFBX_MATERIAL_PBR_TRANSMISSION_ROUGHNESS, + UFBX_MATERIAL_PBR_TRANSMISSION_EXTRA_ROUGHNESS, + UFBX_MATERIAL_PBR_TRANSMISSION_PRIORITY, + UFBX_MATERIAL_PBR_TRANSMISSION_ENABLE_IN_AOV, + UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, + UFBX_MATERIAL_PBR_SUBSURFACE_COLOR, + UFBX_MATERIAL_PBR_SUBSURFACE_RADIUS, + UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, + UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, + UFBX_MATERIAL_PBR_SUBSURFACE_TINT_COLOR, + UFBX_MATERIAL_PBR_SUBSURFACE_TYPE, + UFBX_MATERIAL_PBR_SHEEN_FACTOR, + UFBX_MATERIAL_PBR_SHEEN_COLOR, + UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, + UFBX_MATERIAL_PBR_COAT_FACTOR, + UFBX_MATERIAL_PBR_COAT_COLOR, + UFBX_MATERIAL_PBR_COAT_ROUGHNESS, + UFBX_MATERIAL_PBR_COAT_IOR, + UFBX_MATERIAL_PBR_COAT_ANISOTROPY, + UFBX_MATERIAL_PBR_COAT_ROTATION, + UFBX_MATERIAL_PBR_COAT_NORMAL, + UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_COLOR, + UFBX_MATERIAL_PBR_COAT_AFFECT_BASE_ROUGHNESS, + UFBX_MATERIAL_PBR_THIN_FILM_FACTOR, + UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, + UFBX_MATERIAL_PBR_THIN_FILM_IOR, + UFBX_MATERIAL_PBR_EMISSION_FACTOR, + UFBX_MATERIAL_PBR_EMISSION_COLOR, + UFBX_MATERIAL_PBR_OPACITY, + UFBX_MATERIAL_PBR_INDIRECT_DIFFUSE, + UFBX_MATERIAL_PBR_INDIRECT_SPECULAR, + UFBX_MATERIAL_PBR_NORMAL_MAP, + UFBX_MATERIAL_PBR_TANGENT_MAP, + UFBX_MATERIAL_PBR_DISPLACEMENT_MAP, + UFBX_MATERIAL_PBR_MATTE_FACTOR, + UFBX_MATERIAL_PBR_MATTE_COLOR, + UFBX_MATERIAL_PBR_AMBIENT_OCCLUSION, + UFBX_MATERIAL_PBR_GLOSSINESS, + UFBX_MATERIAL_PBR_COAT_GLOSSINESS, + UFBX_MATERIAL_PBR_TRANSMISSION_GLOSSINESS, + + UFBX_ENUM_FORCE_WIDTH(UFBX_MATERIAL_PBR_MAP) +} ufbx_material_pbr_map; + +UFBX_ENUM_TYPE(ufbx_material_pbr_map, UFBX_MATERIAL_PBR_MAP, UFBX_MATERIAL_PBR_TRANSMISSION_GLOSSINESS); + +// Known material features +typedef enum ufbx_material_feature UFBX_ENUM_REPR { + UFBX_MATERIAL_FEATURE_PBR, + UFBX_MATERIAL_FEATURE_METALNESS, + UFBX_MATERIAL_FEATURE_DIFFUSE, + UFBX_MATERIAL_FEATURE_SPECULAR, + UFBX_MATERIAL_FEATURE_EMISSION, + UFBX_MATERIAL_FEATURE_TRANSMISSION, + UFBX_MATERIAL_FEATURE_COAT, + UFBX_MATERIAL_FEATURE_SHEEN, + UFBX_MATERIAL_FEATURE_OPACITY, + UFBX_MATERIAL_FEATURE_AMBIENT_OCCLUSION, + UFBX_MATERIAL_FEATURE_MATTE, + UFBX_MATERIAL_FEATURE_UNLIT, + UFBX_MATERIAL_FEATURE_IOR, + UFBX_MATERIAL_FEATURE_DIFFUSE_ROUGHNESS, + UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS, + UFBX_MATERIAL_FEATURE_THIN_WALLED, + UFBX_MATERIAL_FEATURE_CAUSTICS, + UFBX_MATERIAL_FEATURE_EXIT_TO_BACKGROUND, + UFBX_MATERIAL_FEATURE_INTERNAL_REFLECTIONS, + UFBX_MATERIAL_FEATURE_DOUBLE_SIDED, + UFBX_MATERIAL_FEATURE_ROUGHNESS_AS_GLOSSINESS, + UFBX_MATERIAL_FEATURE_COAT_ROUGHNESS_AS_GLOSSINESS, + UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS, + + UFBX_ENUM_FORCE_WIDTH(UFBX_MATERIAL_FEATURE) +} ufbx_material_feature; + +UFBX_ENUM_TYPE(ufbx_material_feature, UFBX_MATERIAL_FEATURE, UFBX_MATERIAL_FEATURE_TRANSMISSION_ROUGHNESS_AS_GLOSSINESS); + +typedef struct ufbx_material_fbx_maps { + union { + ufbx_material_map maps[UFBX_MATERIAL_FBX_MAP_COUNT]; + struct { + ufbx_material_map diffuse_factor; + ufbx_material_map diffuse_color; + ufbx_material_map specular_factor; + ufbx_material_map specular_color; + ufbx_material_map specular_exponent; + ufbx_material_map reflection_factor; + ufbx_material_map reflection_color; + ufbx_material_map transparency_factor; + ufbx_material_map transparency_color; + ufbx_material_map emission_factor; + ufbx_material_map emission_color; + ufbx_material_map ambient_factor; + ufbx_material_map ambient_color; + ufbx_material_map normal_map; + ufbx_material_map bump; + ufbx_material_map bump_factor; + ufbx_material_map displacement_factor; + ufbx_material_map displacement; + ufbx_material_map vector_displacement_factor; + ufbx_material_map vector_displacement; + }; + }; +} ufbx_material_fbx_maps; + +typedef struct ufbx_material_pbr_maps { + union { + ufbx_material_map maps[UFBX_MATERIAL_PBR_MAP_COUNT]; + struct { + ufbx_material_map base_factor; + ufbx_material_map base_color; + ufbx_material_map roughness; + ufbx_material_map metalness; + ufbx_material_map diffuse_roughness; + ufbx_material_map specular_factor; + ufbx_material_map specular_color; + ufbx_material_map specular_ior; + ufbx_material_map specular_anisotropy; + ufbx_material_map specular_rotation; + ufbx_material_map transmission_factor; + ufbx_material_map transmission_color; + ufbx_material_map transmission_depth; + ufbx_material_map transmission_scatter; + ufbx_material_map transmission_scatter_anisotropy; + ufbx_material_map transmission_dispersion; + ufbx_material_map transmission_roughness; + ufbx_material_map transmission_extra_roughness; + ufbx_material_map transmission_priority; + ufbx_material_map transmission_enable_in_aov; + ufbx_material_map subsurface_factor; + ufbx_material_map subsurface_color; + ufbx_material_map subsurface_radius; + ufbx_material_map subsurface_scale; + ufbx_material_map subsurface_anisotropy; + ufbx_material_map subsurface_tint_color; + ufbx_material_map subsurface_type; + ufbx_material_map sheen_factor; + ufbx_material_map sheen_color; + ufbx_material_map sheen_roughness; + ufbx_material_map coat_factor; + ufbx_material_map coat_color; + ufbx_material_map coat_roughness; + ufbx_material_map coat_ior; + ufbx_material_map coat_anisotropy; + ufbx_material_map coat_rotation; + ufbx_material_map coat_normal; + ufbx_material_map coat_affect_base_color; + ufbx_material_map coat_affect_base_roughness; + ufbx_material_map thin_film_factor; + ufbx_material_map thin_film_thickness; + ufbx_material_map thin_film_ior; + ufbx_material_map emission_factor; + ufbx_material_map emission_color; + ufbx_material_map opacity; + ufbx_material_map indirect_diffuse; + ufbx_material_map indirect_specular; + ufbx_material_map normal_map; + ufbx_material_map tangent_map; + ufbx_material_map displacement_map; + ufbx_material_map matte_factor; + ufbx_material_map matte_color; + ufbx_material_map ambient_occlusion; + ufbx_material_map glossiness; + ufbx_material_map coat_glossiness; + ufbx_material_map transmission_glossiness; + }; + }; +} ufbx_material_pbr_maps; + +typedef struct ufbx_material_features { + union { + ufbx_material_feature_info features[UFBX_MATERIAL_FEATURE_COUNT]; + struct { + ufbx_material_feature_info pbr; + ufbx_material_feature_info metalness; + ufbx_material_feature_info diffuse; + ufbx_material_feature_info specular; + ufbx_material_feature_info emission; + ufbx_material_feature_info transmission; + ufbx_material_feature_info coat; + ufbx_material_feature_info sheen; + ufbx_material_feature_info opacity; + ufbx_material_feature_info ambient_occlusion; + ufbx_material_feature_info matte; + ufbx_material_feature_info unlit; + ufbx_material_feature_info ior; + ufbx_material_feature_info diffuse_roughness; + ufbx_material_feature_info transmission_roughness; + ufbx_material_feature_info thin_walled; + ufbx_material_feature_info caustics; + ufbx_material_feature_info exit_to_background; + ufbx_material_feature_info internal_reflections; + ufbx_material_feature_info double_sided; + ufbx_material_feature_info roughness_as_glossiness; + ufbx_material_feature_info coat_roughness_as_glossiness; + ufbx_material_feature_info transmission_roughness_as_glossiness; + }; + }; +} ufbx_material_features; + +// Surface material properties such as color, roughness, etc. Each property may +// be optionally bound to an `ufbx_texture`. +struct ufbx_material { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // FBX builtin properties + // NOTE: These may be empty if the material is using a custom shader + ufbx_material_fbx_maps fbx; + + // PBR material properties, defined for all shading models but may be + // somewhat approximate if `shader == NULL`. + ufbx_material_pbr_maps pbr; + + // Material features, primarily applies to `pbr`. + ufbx_material_features features; + + // Shading information + ufbx_shader_type shader_type; // < Always defined + ufbx_nullable ufbx_shader *shader; // < Optional extended shader information + ufbx_string shading_model_name; // < Often one of `{ "lambert", "phong", "unknown" }` + + // Prefix before shader property names with trailing `|`. + // For example `"3dsMax|Parameters|"` where properties would have names like + // `"3dsMax|Parameters|base_color"`. You can ignore this if you use the built-in + // `ufbx_material_fbx_maps fbx` and `ufbx_material_pbr_maps pbr` structures. + ufbx_string shader_prop_prefix; + + // All textures attached to the material, if you want specific maps if might be + // more convenient to use eg. `fbx.diffuse_color.texture` or `pbr.base_color.texture` + ufbx_material_texture_list textures; // < Sorted by `material_prop` +}; + +typedef enum ufbx_texture_type UFBX_ENUM_REPR { + + // Texture associated with an image file/sequence. `texture->filename` and + // and `texture->relative_filename` contain the texture's path. If the file + // has embedded content `texture->content` may hold `texture->content_size` + // bytes of raw image data. + UFBX_TEXTURE_FILE, + + // The texture consists of multiple texture layers blended together. + UFBX_TEXTURE_LAYERED, + + // Reserved as these _should_ exist in FBX files. + UFBX_TEXTURE_PROCEDURAL, + + // Node in a shader graph. + // Use `ufbx_texture.shader` for more information. + UFBX_TEXTURE_SHADER, + + UFBX_ENUM_FORCE_WIDTH(UFBX_TEXTURE_TYPE) +} ufbx_texture_type; + +UFBX_ENUM_TYPE(ufbx_texture_type, UFBX_TEXTURE_TYPE, UFBX_TEXTURE_SHADER); + +// Blend modes to combine layered textures with, compatible with common blend +// mode definitions in many art programs. Simpler blend modes have equations +// specified below where `src` is the layer to composite over `dst`. +// See eg. https://www.w3.org/TR/2013/WD-compositing-1-20131010/#blendingseparable +typedef enum ufbx_blend_mode UFBX_ENUM_REPR { + UFBX_BLEND_TRANSLUCENT, // < `src` effects result alpha + UFBX_BLEND_ADDITIVE, // < `src + dst` + UFBX_BLEND_MULTIPLY, // < `src * dst` + UFBX_BLEND_MULTIPLY_2X, // < `2 * src * dst` + UFBX_BLEND_OVER, // < `src * src_alpha + dst * (1-src_alpha)` + UFBX_BLEND_REPLACE, // < `src` Replace the contents + UFBX_BLEND_DISSOLVE, // < `random() + src_alpha >= 1.0 ? src : dst` + UFBX_BLEND_DARKEN, // < `min(src, dst)` + UFBX_BLEND_COLOR_BURN, // < `src > 0 ? 1 - min(1, (1-dst) / src) : 0` + UFBX_BLEND_LINEAR_BURN, // < `src + dst - 1` + UFBX_BLEND_DARKER_COLOR, // < `value(src) < value(dst) ? src : dst` + UFBX_BLEND_LIGHTEN, // < `max(src, dst)` + UFBX_BLEND_SCREEN, // < `1 - (1-src)*(1-dst)` + UFBX_BLEND_COLOR_DODGE, // < `src < 1 ? dst / (1 - src)` : (dst>0?1:0)` + UFBX_BLEND_LINEAR_DODGE, // < `src + dst` + UFBX_BLEND_LIGHTER_COLOR, // < `value(src) > value(dst) ? src : dst` + UFBX_BLEND_SOFT_LIGHT, // < https://www.w3.org/TR/2013/WD-compositing-1-20131010/#blendingsoftlight + UFBX_BLEND_HARD_LIGHT, // < https://www.w3.org/TR/2013/WD-compositing-1-20131010/#blendinghardlight + UFBX_BLEND_VIVID_LIGHT, // < Combination of `COLOR_DODGE` and `COLOR_BURN` + UFBX_BLEND_LINEAR_LIGHT, // < Combination of `LINEAR_DODGE` and `LINEAR_BURN` + UFBX_BLEND_PIN_LIGHT, // < Combination of `DARKEN` and `LIGHTEN` + UFBX_BLEND_HARD_MIX, // < Produces primary colors depending on similarity + UFBX_BLEND_DIFFERENCE, // < `abs(src - dst)` + UFBX_BLEND_EXCLUSION, // < `dst + src - 2 * src * dst` + UFBX_BLEND_SUBTRACT, // < `dst - src` + UFBX_BLEND_DIVIDE, // < `dst / src` + UFBX_BLEND_HUE, // < Replace hue + UFBX_BLEND_SATURATION, // < Replace saturation + UFBX_BLEND_COLOR, // < Replace hue and saturatio + UFBX_BLEND_LUMINOSITY, // < Replace value + UFBX_BLEND_OVERLAY, // < Same as `HARD_LIGHT` but with `src` and `dst` swapped + + UFBX_ENUM_FORCE_WIDTH(UFBX_BLEND_MODE) +} ufbx_blend_mode; + +UFBX_ENUM_TYPE(ufbx_blend_mode, UFBX_BLEND_MODE, UFBX_BLEND_OVERLAY); + +// Blend modes to combine layered textures with, compatible with common blend +typedef enum ufbx_wrap_mode UFBX_ENUM_REPR { + UFBX_WRAP_REPEAT, // < Repeat the texture past the [0,1] range + UFBX_WRAP_CLAMP, // < Clamp the normalized texture coordinates to [0,1] + + UFBX_ENUM_FORCE_WIDTH(UFBX_WRAP_MODE) +} ufbx_wrap_mode; + +UFBX_ENUM_TYPE(ufbx_wrap_mode, UFBX_WRAP_MODE, UFBX_WRAP_CLAMP); + +// Single layer in a layered texture +typedef struct ufbx_texture_layer { + ufbx_texture *texture; // < The inner texture to evaluate, never `NULL` + ufbx_blend_mode blend_mode; // < Equation to combine the layer to the background + ufbx_real alpha; // < Blend weight of this layer +} ufbx_texture_layer; + +UFBX_LIST_TYPE(ufbx_texture_layer_list, ufbx_texture_layer); + +typedef enum ufbx_shader_texture_type UFBX_ENUM_REPR { + UFBX_SHADER_TEXTURE_UNKNOWN, + + // Select an output of a multi-output shader. + // HINT: If this type is used the `ufbx_shader_texture.main_texture` and + // `ufbx_shader_texture.main_texture_output_index` fields are set. + UFBX_SHADER_TEXTURE_SELECT_OUTPUT, + + // Open Shading Language (OSL) shader. + // https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + UFBX_SHADER_TEXTURE_OSL, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SHADER_TEXTURE_TYPE) +} ufbx_shader_texture_type; + +UFBX_ENUM_TYPE(ufbx_shader_texture_type, UFBX_SHADER_TEXTURE_TYPE, UFBX_SHADER_TEXTURE_OSL); + +// Input to a shader texture, see `ufbx_shader_texture`. +typedef struct ufbx_shader_texture_input { + + // Name of the input. + ufbx_string name; + + // Constant value of the input. + union { + ufbx_real value_real; + ufbx_vec2 value_vec2; + ufbx_vec3 value_vec3; + ufbx_vec4 value_vec4; + }; + int64_t value_int; + ufbx_string value_str; + ufbx_blob value_blob; + + // Texture connected to this input. + ufbx_nullable ufbx_texture *texture; + + // Index of the output to use if `texture` is a multi-output shader node. + int64_t texture_output_index; + + // Controls whether shading should use `texture`. + // NOTE: Some shading models allow this to be `true` even if `texture == NULL`. + bool texture_enabled; + + // Property representing this input. + ufbx_prop *prop; + + // Property representing `texture`. + ufbx_nullable ufbx_prop *texture_prop; + + // Property representing `texture_enabled`. + ufbx_nullable ufbx_prop *texture_enabled_prop; + +} ufbx_shader_texture_input; + +UFBX_LIST_TYPE(ufbx_shader_texture_input_list, ufbx_shader_texture_input); + +// Texture that emulates a shader graph node. +// 3ds Max exports some materials as node graphs serialized to textures. +// ufbx can parse a small subset of these, as normal maps are often hidden behind +// some kind of bump node. +// NOTE: These encode a lot of details of 3ds Max internals, not recommended for direct use. +// HINT: `ufbx_texture.file_textures[]` contains a list of "real" textures that are connected +// to the `ufbx_texture` that is pretending to be a shader node. +typedef struct ufbx_shader_texture { + + // Type of this shader node. + ufbx_shader_texture_type type; + + // Name of the shader to use. + ufbx_string shader_name; + + // 64-bit opaque identifier for the shader type. + uint64_t shader_type_id; + + // Input values/textures (possibly further shader textures) to the shader. + // Sorted by `ufbx_shader_texture_input.name`. + ufbx_shader_texture_input_list inputs; + + // Shader source code if found. + ufbx_string shader_source; + ufbx_blob raw_shader_source; + + // Representative texture for this shader. + // Only specified if `main_texture.outputs[main_texture_output_index]` is semantically + // equivalent to this texture. + ufbx_texture *main_texture; + + // Output index of `main_texture` if it is a multi-output shader. + int64_t main_texture_output_index; + + // Prefix for properties related to this shader in `ufbx_texture`. + // NOTE: Contains the trailing '|' if not empty. + ufbx_string prop_prefix; + +} ufbx_shader_texture; + +// Unique texture within the file. +typedef struct ufbx_texture_file { + + // Index in `ufbx_scene.texture_files[]`. + uint32_t index; + + // Paths to the resource. + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + // Optional embedded content blob, eg. raw .png format data + ufbx_blob content; + +} ufbx_texture_file; + +UFBX_LIST_TYPE(ufbx_texture_file_list, ufbx_texture_file); + +// Texture that controls material appearance +struct ufbx_texture { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Texture type (file / layered / procedural / shader) + ufbx_texture_type type; + + // FILE: Paths to the resource + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + // FILE: Optional embedded content blob, eg. raw .png format data + ufbx_blob content; + + // FILE: Optional video texture + ufbx_nullable ufbx_video *video; + + // FILE: Index into `ufbx_scene.texture_files[]` or `UFBX_NO_INDEX`. + uint32_t file_index; + + // FILE: True if `file_index` has a valid value. + bool has_file; + + // LAYERED: Inner texture layers, ordered from _bottom_ to _top_ + ufbx_texture_layer_list layers; + + // SHADER: Shader information + // NOTE: May be specified even if `type == UFBX_TEXTURE_FILE` if `ufbx_load_opts.disable_quirks` + // is _not_ specified. Some known shaders that represent files are interpreted as `UFBX_TEXTURE_FILE`. + ufbx_nullable ufbx_shader_texture *shader; + + // List of file textures representing this texture. + // Defined even if `type == UFBX_TEXTURE_FILE` in which case the array contains only itself. + ufbx_texture_list file_textures; + + // Name of the UV set to use + ufbx_string uv_set; + + // Wrapping mode + ufbx_wrap_mode wrap_u; + ufbx_wrap_mode wrap_v; + + // UV transform + bool has_uv_transform; // < Has a non-identity `transform` and derived matrices. + ufbx_transform uv_transform; // < Texture transformation in UV space + ufbx_matrix texture_to_uv; // < Matrix representation of `transform` + ufbx_matrix uv_to_texture; // < UV coordinate to normalized texture coordinate matrix +}; + +// TODO: Video textures +struct ufbx_video { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Paths to the resource + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + // Optional embedded content blob + ufbx_blob content; +}; + +// Shader specifies a shading model and contains `ufbx_shader_binding` elements +// that define how to interpret FBX properties in the shader. +struct ufbx_shader { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Known shading model + ufbx_shader_type type; + + // TODO: Expose actual properties here + + // Bindings from FBX properties to the shader + // HINT: `ufbx_find_shader_prop()` translates shader properties to FBX properties + ufbx_shader_binding_list bindings; +}; + +// Binding from a material property to shader implementation +typedef struct ufbx_shader_prop_binding { + ufbx_string shader_prop; // < Property name used by the shader implementation + ufbx_string material_prop; // < Property name inside `ufbx_material.props` +} ufbx_shader_prop_binding; + +UFBX_LIST_TYPE(ufbx_shader_prop_binding_list, ufbx_shader_prop_binding); + +// Shader binding table +struct ufbx_shader_binding { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + ufbx_shader_prop_binding_list prop_bindings; // < Sorted by `shader_prop` +}; + +// -- Animation + +typedef struct ufbx_prop_override { + uint32_t element_id; + + uint32_t _internal_key; + + ufbx_string prop_name; + ufbx_vec4 value; + ufbx_string value_str; + int64_t value_int; +} ufbx_prop_override; + +UFBX_LIST_TYPE(ufbx_prop_override_list, ufbx_prop_override); + +typedef struct ufbx_transform_override { + uint32_t node_id; + ufbx_transform transform; +} ufbx_transform_override; + +UFBX_LIST_TYPE(ufbx_transform_override_list, ufbx_transform_override); + +// Animation descriptor used for evaluating animation. +// Usually obtained from `ufbx_scene` via either global animation `ufbx_scene.anim`, +// per-stack animation `ufbx_anim_stack.anim` or per-layer animation `ufbx_anim_layer.anim`. +// +// For advanced usage you can use `ufbx_create_anim()` to create animation descriptors +// with custom layers, property overrides, special flags, etc. +typedef struct ufbx_anim { + + // Time begin/end for the animation, both may be zero if absent. + double time_begin; + double time_end; + + // List of layers in the animation. + ufbx_anim_layer_list layers; + + // Optional overrides for weights for each layer in `layers[]`. + ufbx_real_list override_layer_weights; + + // Sorted by `element_id, prop_name` + ufbx_prop_override_list prop_overrides; + + // Sorted by `node_id` + ufbx_transform_override_list transform_overrides; + + // Evaluate connected properties as if they would not be connected. + bool ignore_connections; + + // Custom `ufbx_anim` created by `ufbx_create_anim()`. + bool custom; + +} ufbx_anim; + +struct ufbx_anim_stack { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + double time_begin; + double time_end; + + ufbx_anim_layer_list layers; + ufbx_anim *anim; +}; + +typedef struct ufbx_anim_prop { + ufbx_element *element; + + uint32_t _internal_key; + + ufbx_string prop_name; + ufbx_anim_value *anim_value; +} ufbx_anim_prop; + +UFBX_LIST_TYPE(ufbx_anim_prop_list, ufbx_anim_prop); + +struct ufbx_anim_layer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + ufbx_real weight; + bool weight_is_animated; + bool blended; + bool additive; + bool compose_rotation; + bool compose_scale; + + ufbx_anim_value_list anim_values; + ufbx_anim_prop_list anim_props; // < Sorted by `element,prop_name` + + ufbx_anim *anim; + + uint32_t _min_element_id; + uint32_t _max_element_id; + uint32_t _element_id_bitmask[4]; +}; + +struct ufbx_anim_value { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + ufbx_vec3 default_value; + ufbx_nullable ufbx_anim_curve *curves[3]; +}; + +// Animation curve segment interpolation mode between two keyframes +typedef enum ufbx_interpolation UFBX_ENUM_REPR { + UFBX_INTERPOLATION_CONSTANT_PREV, // < Hold previous key value + UFBX_INTERPOLATION_CONSTANT_NEXT, // < Hold next key value + UFBX_INTERPOLATION_LINEAR, // < Linear interpolation between two keys + UFBX_INTERPOLATION_CUBIC, // < Cubic interpolation, see `ufbx_tangent` + + UFBX_ENUM_FORCE_WIDTH(UFBX_INTERPOLATION) +} ufbx_interpolation; + +UFBX_ENUM_TYPE(ufbx_interpolation, UFBX_INTERPOLATION, UFBX_INTERPOLATION_CUBIC); + +typedef enum ufbx_extrapolation_mode UFBX_ENUM_REPR { + UFBX_EXTRAPOLATION_CONSTANT, // < Use the value of the first/last keyframe + UFBX_EXTRAPOLATION_REPEAT, // < Repeat the whole animation curve + UFBX_EXTRAPOLATION_MIRROR, // < Repeat with mirroring + UFBX_EXTRAPOLATION_SLOPE, // < Use the tangent of the last keyframe to linearly extrapolate + UFBX_EXTRAPOLATION_REPEAT_RELATIVE, // < Repeat the animation curve but connect the first and last keyframe values + + UFBX_ENUM_FORCE_WIDTH(UFBX_EXTRAPOLATION) +} ufbx_extrapolation_mode; + +UFBX_ENUM_TYPE(ufbx_extrapolation_mode, UFBX_EXTRAPOLATION_MODE, UFBX_EXTRAPOLATION_REPEAT_RELATIVE); + +typedef struct ufbx_extrapolation { + ufbx_extrapolation_mode mode; + + // Count used for repeating modes. + // Negative values mean infinite repetition. + int32_t repeat_count; +} ufbx_extrapolation; + +// Tangent vector at a keyframe, may be split into left/right +typedef struct ufbx_tangent { + float dx; // < Derivative in the time axis + float dy; // < Derivative in the (curve specific) value axis +} ufbx_tangent; + +// Single real `value` at a specified `time`, interpolation between two keyframes +// is determined by the `interpolation` field of the _previous_ key. +// If `interpolation == UFBX_INTERPOLATION_CUBIC` the span is evaluated as a +// cubic bezier curve through the following points: +// +// (prev->time, prev->value) +// (prev->time + prev->right.dx, prev->value + prev->right.dy) +// (next->time - next->left.dx, next->value - next->left.dy) +// (next->time, next->value) +// +// HINT: You can use `ufbx_evaluate_curve(ufbx_anim_curve *curve, double time)` +// rather than trying to manually handle all the interpolation modes. +typedef struct ufbx_keyframe { + double time; + ufbx_real value; + ufbx_interpolation interpolation; + ufbx_tangent left; + ufbx_tangent right; +} ufbx_keyframe; + +UFBX_LIST_TYPE(ufbx_keyframe_list, ufbx_keyframe); + +struct ufbx_anim_curve { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // List of keyframes that define the curve. + ufbx_keyframe_list keyframes; + + // Extrapolation before the curve. + ufbx_extrapolation pre_extrapolation; + // Extrapolation after the curve. + ufbx_extrapolation post_extrapolation; + + // Value range for all the keyframes. + ufbx_real min_value; + ufbx_real max_value; + + // Time range for all the keyframes. + double min_time; + double max_time; +}; + +// -- Collections + +// Collection of nodes to hide/freeze +struct ufbx_display_layer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Nodes included in the layer (exclusively at most one layer per node) + ufbx_node_list nodes; + + // Layer state + bool visible; // < Contained nodes are visible + bool frozen; // < Contained nodes cannot be edited + + ufbx_vec3 ui_color; // < Visual color for UI +}; + +// Named set of nodes/geometry features to select. +struct ufbx_selection_set { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Included nodes and geometry features + ufbx_selection_node_list nodes; +}; + +// Selection state of a node, potentially contains vertex/edge/face selection as well. +struct ufbx_selection_node { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Selection targets, possibly `NULL` + ufbx_nullable ufbx_node *target_node; + ufbx_nullable ufbx_mesh *target_mesh; + bool include_node; // < Is `target_node` included in the selection + + // Indices to selected components. + // Guaranteed to be valid as per `ufbx_load_opts.index_error_handling` + // if `target_mesh` is not `NULL`. + ufbx_uint32_list vertices; // < Indices to `ufbx_mesh.vertices` + ufbx_uint32_list edges; // < Indices to `ufbx_mesh.edges` + ufbx_uint32_list faces; // < Indices to `ufbx_mesh.faces` +}; + +// -- Constraints + +struct ufbx_character { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; +}; + +// Type of property constrain eg. position or look-at +typedef enum ufbx_constraint_type UFBX_ENUM_REPR { + UFBX_CONSTRAINT_UNKNOWN, + UFBX_CONSTRAINT_AIM, + UFBX_CONSTRAINT_PARENT, + UFBX_CONSTRAINT_POSITION, + UFBX_CONSTRAINT_ROTATION, + UFBX_CONSTRAINT_SCALE, + // Inverse kinematic chain to a single effector `ufbx_constraint.ik_effector` + // `targets` optionally contains a list of pole targets! + UFBX_CONSTRAINT_SINGLE_CHAIN_IK, + + UFBX_ENUM_FORCE_WIDTH(UFBX_CONSTRAINT_TYPE) +} ufbx_constraint_type; + +UFBX_ENUM_TYPE(ufbx_constraint_type, UFBX_CONSTRAINT_TYPE, UFBX_CONSTRAINT_SINGLE_CHAIN_IK); + +// Target to follow with a constraint +typedef struct ufbx_constraint_target { + ufbx_node *node; // < Target node reference + ufbx_real weight; // < Relative weight to other targets (does not always sum to 1) + ufbx_transform transform; // < Offset from the actual target +} ufbx_constraint_target; + +UFBX_LIST_TYPE(ufbx_constraint_target_list, ufbx_constraint_target); + +// Method to determine the up vector in aim constraints +typedef enum ufbx_constraint_aim_up_type UFBX_ENUM_REPR { + UFBX_CONSTRAINT_AIM_UP_SCENE, // < Align the up vector to the scene global up vector + UFBX_CONSTRAINT_AIM_UP_TO_NODE, // < Aim the up vector at `ufbx_constraint.aim_up_node` + UFBX_CONSTRAINT_AIM_UP_ALIGN_NODE, // < Copy the up vector from `ufbx_constraint.aim_up_node` + UFBX_CONSTRAINT_AIM_UP_VECTOR, // < Use `ufbx_constraint.aim_up_vector` as the up vector + UFBX_CONSTRAINT_AIM_UP_NONE, // < Don't align the up vector to anything + + UFBX_ENUM_FORCE_WIDTH(UFBX_CONSTRAINT_AIM_UP_TYPE) +} ufbx_constraint_aim_up_type; + +UFBX_ENUM_TYPE(ufbx_constraint_aim_up_type, UFBX_CONSTRAINT_AIM_UP_TYPE, UFBX_CONSTRAINT_AIM_UP_NONE); + +// Method to determine the up vector in aim constraints +typedef enum ufbx_constraint_ik_pole_type UFBX_ENUM_REPR { + UFBX_CONSTRAINT_IK_POLE_VECTOR, // < Use towards calculated from `ufbx_constraint.targets` + UFBX_CONSTRAINT_IK_POLE_NODE, // < Use `ufbx_constraint.ik_pole_vector` directly + + UFBX_ENUM_FORCE_WIDTH(UFBX_CONSTRAINT_IK_POLE_TYPE) +} ufbx_constraint_ik_pole_type; + +UFBX_ENUM_TYPE(ufbx_constraint_ik_pole_type, UFBX_CONSTRAINT_IK_POLE_TYPE, UFBX_CONSTRAINT_IK_POLE_NODE); + +struct ufbx_constraint { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Type of constraint to use + ufbx_constraint_type type; + ufbx_string type_name; + + // Node to be constrained + ufbx_nullable ufbx_node *node; + + // List of weighted targets for the constraint (pole vectors for IK) + ufbx_constraint_target_list targets; + + // State of the constraint + ufbx_real weight; + bool active; + + // Translation/rotation/scale axes the constraint is applied to + bool constrain_translation[3]; + bool constrain_rotation[3]; + bool constrain_scale[3]; + + // Offset from the constrained position + ufbx_transform transform_offset; + + // AIM: Target and up vectors + ufbx_vec3 aim_vector; + ufbx_constraint_aim_up_type aim_up_type; + ufbx_nullable ufbx_node *aim_up_node; + ufbx_vec3 aim_up_vector; + + // SINGLE_CHAIN_IK: Target for the IK, `targets` contains pole vectors! + ufbx_nullable ufbx_node *ik_effector; + ufbx_nullable ufbx_node *ik_end_node; + ufbx_vec3 ik_pole_vector; +}; + +// -- Audio + +struct ufbx_audio_layer { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Clips contained in this layer. + ufbx_audio_clip_list clips; +}; + +struct ufbx_audio_clip { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Filename relative to the currently loaded file. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_string filename; + // Absolute filename specified in the file. + ufbx_string absolute_filename; + // Relative filename specified in the file. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_string relative_filename; + + // Filename relative to the loaded file, non-UTF-8 encoded. + // HINT: If using functions other than `ufbx_load_file()`, you can provide + // `ufbx_load_opts.filename/raw_filename` to let ufbx resolve this. + ufbx_blob raw_filename; + // Absolute filename specified in the file, non-UTF-8 encoded. + ufbx_blob raw_absolute_filename; + // Relative filename specified in the file, non-UTF-8 encoded. + // NOTE: May be absolute if the file is saved in a different drive. + ufbx_blob raw_relative_filename; + + // Optional embedded content blob, eg. raw .png format data + ufbx_blob content; +}; + +// -- Miscellaneous + +typedef struct ufbx_bone_pose { + + // Node to apply the pose to. + ufbx_node *bone_node; + + // Matrix from node local space to world space. + ufbx_matrix bone_to_world; + + // Matrix from node local space to parent space. + // NOTE: FBX only stores world transformations so this is approximated from + // the parent world transform. + ufbx_matrix bone_to_parent; + +} ufbx_bone_pose; + +UFBX_LIST_TYPE(ufbx_bone_pose_list, ufbx_bone_pose); + +struct ufbx_pose { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; + + // Set if this pose is marked as a bind pose. + bool is_bind_pose; + + // List of bone poses. + // Sorted by `ufbx_node.typed_id`. + ufbx_bone_pose_list bone_poses; +}; + +struct ufbx_metadata_object { + union { ufbx_element element; struct { + ufbx_string name; + ufbx_props props; + uint32_t element_id; + uint32_t typed_id; + }; }; +}; + +// -- Named elements + +typedef struct ufbx_name_element { + ufbx_string name; + ufbx_element_type type; + + uint32_t _internal_key; + + ufbx_element *element; +} ufbx_name_element; + +UFBX_LIST_TYPE(ufbx_name_element_list, ufbx_name_element); + +// -- Scene + +// Scene is the root object loaded by ufbx that everything is accessed from. + +typedef enum ufbx_exporter UFBX_ENUM_REPR { + UFBX_EXPORTER_UNKNOWN, + UFBX_EXPORTER_FBX_SDK, + UFBX_EXPORTER_BLENDER_BINARY, + UFBX_EXPORTER_BLENDER_ASCII, + UFBX_EXPORTER_MOTION_BUILDER, + UFBX_EXPORTER_UFBX_WRITE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_EXPORTER) +} ufbx_exporter; + +UFBX_ENUM_TYPE(ufbx_exporter, UFBX_EXPORTER, UFBX_EXPORTER_UFBX_WRITE); + +typedef struct ufbx_application { + ufbx_string vendor; + ufbx_string name; + ufbx_string version; +} ufbx_application; + +typedef enum ufbx_file_format UFBX_ENUM_REPR { + UFBX_FILE_FORMAT_UNKNOWN, // < Unknown file format + UFBX_FILE_FORMAT_FBX, // < .fbx Kaydara/Autodesk FBX file + UFBX_FILE_FORMAT_OBJ, // < .obj Wavefront OBJ file + UFBX_FILE_FORMAT_MTL, // < .mtl Wavefront MTL (Material template library) file + + UFBX_ENUM_FORCE_WIDTH(UFBX_FILE_FORMAT) +} ufbx_file_format; + +UFBX_ENUM_TYPE(ufbx_file_format, UFBX_FILE_FORMAT, UFBX_FILE_FORMAT_MTL); + +typedef enum ufbx_warning_type UFBX_ENUM_REPR { + // Missing external file file (for example .mtl for Wavefront .obj file or a + // geometry cache) + UFBX_WARNING_MISSING_EXTERNAL_FILE, + + // Loaded a Wavefront .mtl file derived from the filename instead of a proper + // `mtllib` statement. + UFBX_WARNING_IMPLICIT_MTL, + + // Truncated array has been auto-expanded. + UFBX_WARNING_TRUNCATED_ARRAY, + + // Geometry data has been defined but has no data. + UFBX_WARNING_MISSING_GEOMETRY_DATA, + + // Duplicated connection between two elements that shouldn't have. + UFBX_WARNING_DUPLICATE_CONNECTION, + + // Vertex 'W' attribute length differs from main attribute. + UFBX_WARNING_BAD_VERTEX_W_ATTRIBUTE, + + // Missing polygon mapping type. + UFBX_WARNING_MISSING_POLYGON_MAPPING, + + // Unsupported version, loaded but may be incorrect. + // If the loading fails `UFBX_ERROR_UNSUPPORTED_VERSION` is issued instead. + UFBX_WARNING_UNSUPPORTED_VERSION, + + // Out-of-bounds index has been clamped to be in-bounds. + // HINT: You can use `ufbx_index_error_handling` to adjust behavior. + UFBX_WARNING_INDEX_CLAMPED, + + // Non-UTF8 encoded strings. + // HINT: You can use `ufbx_unicode_error_handling` to adjust behavior. + UFBX_WARNING_BAD_UNICODE, + + // Invalid base64-encoded embedded content ignored. + UFBX_WARNING_BAD_BASE64_CONTENT, + + // Non-node element connected to root. + UFBX_WARNING_BAD_ELEMENT_CONNECTED_TO_ROOT, + + // Duplicated object ID in the file, connections will be wrong. + UFBX_WARNING_DUPLICATE_OBJECT_ID, + + // Empty face has been removed. + // Use `ufbx_load_opts.allow_empty_faces` if you want to allow them. + UFBX_WARNING_EMPTY_FACE_REMOVED, + + // Unknown .obj file directive. + UFBX_WARNING_UNKNOWN_OBJ_DIRECTIVE, + + // Warnings after this one are deduplicated. + // See `ufbx_warning.count` for how many times they happened. + UFBX_WARNING_TYPE_FIRST_DEDUPLICATED = UFBX_WARNING_INDEX_CLAMPED, + + UFBX_ENUM_FORCE_WIDTH(UFBX_WARNING_TYPE) +} ufbx_warning_type; + +UFBX_ENUM_TYPE(ufbx_warning_type, UFBX_WARNING_TYPE, UFBX_WARNING_UNKNOWN_OBJ_DIRECTIVE); + +// Warning about a non-fatal issue in the file. +// Often contains information about issues that ufbx has corrected about the +// file but it might indicate something is not working properly. +typedef struct ufbx_warning { + // Type of the warning. + ufbx_warning_type type; + // Description of the warning. + ufbx_string description; + // The element related to this warning or `UFBX_NO_INDEX` if not related to a specific element. + uint32_t element_id; + // Number of times this warning was encountered. + size_t count; +} ufbx_warning; + +UFBX_LIST_TYPE(ufbx_warning_list, ufbx_warning); + +typedef enum ufbx_thumbnail_format UFBX_ENUM_REPR { + UFBX_THUMBNAIL_FORMAT_UNKNOWN, // < Unknown format + UFBX_THUMBNAIL_FORMAT_RGB_24, // < 8-bit RGB pixels, in memory R,G,B + UFBX_THUMBNAIL_FORMAT_RGBA_32, // < 8-bit RGBA pixels, in memory R,G,B,A + + UFBX_ENUM_FORCE_WIDTH(UFBX_THUMBNAIL_FORMAT) +} ufbx_thumbnail_format; + +UFBX_ENUM_TYPE(ufbx_thumbnail_format, UFBX_THUMBNAIL_FORMAT, UFBX_THUMBNAIL_FORMAT_RGBA_32); + +// Specify how unit / coordinate system conversion should be performed. +// Affects how `ufbx_load_opts.target_axes` and `ufbx_load_opts.target_unit_meters` work, +// has no effect if neither is specified. +typedef enum ufbx_space_conversion UFBX_ENUM_REPR { + + // Store the space conversion transform in the root node. + // Sets `ufbx_node.local_transform` of the root node. + UFBX_SPACE_CONVERSION_TRANSFORM_ROOT, + + // Perform the conversion by using "adjust" transforms. + // Compensates for the transforms using `ufbx_node.adjust_pre_rotation` and + // `ufbx_node.adjust_pre_scale`. You don't need to account for these unless + // you are manually building transforms from `ufbx_props`. + UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS, + + // Perform the conversion by scaling geometry in addition to adjusting transforms. + // Compensates transforms like `UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS` but + // applies scaling to geometry as well. + UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SPACE_CONVERSION) +} ufbx_space_conversion; + +UFBX_ENUM_TYPE(ufbx_space_conversion, UFBX_SPACE_CONVERSION, UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY); + +// How to handle FBX node geometry transforms. +// FBX nodes can have "geometry transforms" that affect only the attached meshes, +// but not the children. This is not allowed in many scene representations so +// ufbx provides some ways to simplify them. +// Geometry transforms can also be used to transform any other attributes such +// as lights or cameras. +typedef enum ufbx_geometry_transform_handling UFBX_ENUM_REPR { + + // Preserve the geometry transforms as-is. + // To be correct for all files you have to use `ufbx_node.geometry_transform`, + // `ufbx_node.geometry_to_node`, or `ufbx_node.geometry_to_world` to compensate + // for any potential geometry transforms. + UFBX_GEOMETRY_TRANSFORM_HANDLING_PRESERVE, + + // Add helper nodes between the nodes and geometry where needed. + // The created nodes have `ufbx_node.is_geometry_transform_helper` set and are + // named `ufbx_load_opts.geometry_transform_helper_name`. + UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES, + + // Modify the geometry of meshes attached to nodes with geometry transforms. + // Will add helper nodes like `UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES` if + // necessary, for example if there are multiple instances of the same mesh with + // geometry transforms. + UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY, + + // Modify the geometry of meshes attached to nodes with geometry transforms. + // NOTE: This will not work correctly for instanced geometry. + UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK, + + UFBX_ENUM_FORCE_WIDTH(UFBX_GEOMETRY_TRANSFORM_HANDLING) +} ufbx_geometry_transform_handling; + +UFBX_ENUM_TYPE(ufbx_geometry_transform_handling, UFBX_GEOMETRY_TRANSFORM_HANDLING, UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK); + +// How to handle FBX transform inherit modes. +typedef enum ufbx_inherit_mode_handling UFBX_ENUM_REPR { + + // Preserve inherit mode in `ufbx_node.inherit_mode`. + // NOTE: To correctly handle all scenes you would need to handle the + // non-standard inherit modes. + UFBX_INHERIT_MODE_HANDLING_PRESERVE, + + // Create scale helper nodes parented to nodes that need special inheritance. + // Scale helper nodes will have `ufbx_node.is_scale_helper` and parents of + // scale helpers will have `ufbx_node.scale_helper` pointing to it. + UFBX_INHERIT_MODE_HANDLING_HELPER_NODES, + + // Attempt to compensate for bone scale by inversely scaling children. + // NOTE: This only works for uniform non-animated scaling, if scale is + // non-uniform or animated, ufbx will add scale helpers in the same way + // as `UFBX_INHERIT_MODE_HANDLING_HELPER_NODES`. + UFBX_INHERIT_MODE_HANDLING_COMPENSATE, + + // Attempt to compensate for bone scale by inversely scaling children. + // Will never create helper nodes. + UFBX_INHERIT_MODE_HANDLING_COMPENSATE_NO_FALLBACK, + + // Ignore non-standard inheritance modes. + // Forces all nodes to have `UFBX_INHERIT_MODE_NORMAL` regardless of the + // inherit mode specified in the file. This can be useful for emulating + // results from importers/programs that don't support inherit modes. + UFBX_INHERIT_MODE_HANDLING_IGNORE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_INHERIT_MODE_HANDLING) +} ufbx_inherit_mode_handling; + +UFBX_ENUM_TYPE(ufbx_inherit_mode_handling, UFBX_INHERIT_MODE_HANDLING, UFBX_INHERIT_MODE_HANDLING_IGNORE); + +// How to handle FBX transform pivots. +typedef enum ufbx_pivot_handling UFBX_ENUM_REPR { + + // Take pivots into account when computing the transform. + UFBX_PIVOT_HANDLING_RETAIN, + + // Translate objects to be located at their pivot. + // NOTE: Only applied if rotation and scaling pivots are equal. + // NOTE: Results in geometric translation. Use `ufbx_geometry_transform_handling` + // to interpret these in a standard scene graph. + UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT, + + // Translate objects to be located at their rotation pivot. + // NOTE: Results in geometric translation. Use `ufbx_geometry_transform_handling` + // to interpret these in a standard scene graph. + // NOTE: By default the original transforms of empties are not retained when using this, + // use `ufbx_load_opts.pivot_handling_retain_empties` to prevent adjusting these pivots. + UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT, + + UFBX_ENUM_FORCE_WIDTH(UFBX_PIVOT_HANDLING) +} ufbx_pivot_handling; + +UFBX_ENUM_TYPE(ufbx_pivot_handling, UFBX_PIVOT_HANDLING, UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT); + +// Embedded thumbnail in the file, valid if the dimensions are non-zero. +typedef struct ufbx_thumbnail { + ufbx_props props; + + // Extents of the thumbnail + uint32_t width; + uint32_t height; + + // Format of `ufbx_thumbnail.data`. + ufbx_thumbnail_format format; + + // Thumbnail pixel data, layout as contiguous rows from bottom to top. + // See `ufbx_thumbnail.format` for the pixel format. + ufbx_blob data; +} ufbx_thumbnail; + +// Miscellaneous data related to the loaded file +typedef struct ufbx_metadata { + + // List of non-fatal warnings about the file. + // If you need to only check whether a specific warning was triggered you + // can use `ufbx_metadata.has_warning[]`. + ufbx_warning_list warnings; + + // FBX ASCII file format. + bool ascii; + + // FBX version in integer format, eg. 7400 for 7.4. + uint32_t version; + + // File format of the source file. + ufbx_file_format file_format; + + // Index arrays may contain `UFBX_NO_INDEX` instead of a valid index + // to indicate gaps. + bool may_contain_no_index; + + // May contain meshes with no defined vertex position. + // NOTE: `ufbx_mesh.vertex_position.exists` may be `false`! + bool may_contain_missing_vertex_position; + + // Arrays may contain items with `NULL` element references. + // See `ufbx_load_opts.connect_broken_elements`. + bool may_contain_broken_elements; + + // Some API guarantees do not apply (depending on unsafe options used). + // Loaded with `ufbx_load_opts.allow_unsafe` enabled. + bool is_unsafe; + + // Flag for each possible warning type. + // See `ufbx_metadata.warnings[]` for detailed warning information. + bool has_warning[UFBX_WARNING_TYPE_COUNT]; + + ufbx_string creator; + bool big_endian; + + ufbx_string filename; + ufbx_string relative_root; + + ufbx_blob raw_filename; + ufbx_blob raw_relative_root; + + ufbx_exporter exporter; + uint32_t exporter_version; + + ufbx_props scene_props; + + ufbx_application original_application; + ufbx_application latest_application; + + ufbx_thumbnail thumbnail; + + bool geometry_ignored; + bool animation_ignored; + bool embedded_ignored; + + size_t max_face_triangles; + + size_t result_memory_used; + size_t temp_memory_used; + size_t result_allocs; + size_t temp_allocs; + + size_t element_buffer_size; + size_t num_shader_textures; + + ufbx_real bone_prop_size_unit; + bool bone_prop_limb_length_relative; + + ufbx_real ortho_size_unit; + + int64_t ktime_second; // < One second in internal KTime units + + ufbx_string original_file_path; + ufbx_blob raw_original_file_path; + + // Conversion methods applied for the scene. + ufbx_space_conversion space_conversion; + ufbx_geometry_transform_handling geometry_transform_handling; + ufbx_inherit_mode_handling inherit_mode_handling; + ufbx_pivot_handling pivot_handling; + ufbx_mirror_axis handedness_conversion_axis; + + // Transform that has been applied to root for axis/unit conversion. + ufbx_quat root_rotation; + ufbx_real root_scale; + + // Axis that the scene has been mirrored by. + // All geometry has been mirrored in this axis. + ufbx_mirror_axis mirror_axis; + + // Amount geometry has been scaled. + // See `UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY`. + ufbx_real geometry_scale; + +} ufbx_metadata; + +typedef enum ufbx_time_mode UFBX_ENUM_REPR { + UFBX_TIME_MODE_DEFAULT, + UFBX_TIME_MODE_120_FPS, + UFBX_TIME_MODE_100_FPS, + UFBX_TIME_MODE_60_FPS, + UFBX_TIME_MODE_50_FPS, + UFBX_TIME_MODE_48_FPS, + UFBX_TIME_MODE_30_FPS, + UFBX_TIME_MODE_30_FPS_DROP, + UFBX_TIME_MODE_NTSC_DROP_FRAME, + UFBX_TIME_MODE_NTSC_FULL_FRAME, + UFBX_TIME_MODE_PAL, + UFBX_TIME_MODE_24_FPS, + UFBX_TIME_MODE_1000_FPS, + UFBX_TIME_MODE_FILM_FULL_FRAME, + UFBX_TIME_MODE_CUSTOM, + UFBX_TIME_MODE_96_FPS, + UFBX_TIME_MODE_72_FPS, + UFBX_TIME_MODE_59_94_FPS, + + UFBX_ENUM_FORCE_WIDTH(UFBX_TIME_MODE) +} ufbx_time_mode; + +UFBX_ENUM_TYPE(ufbx_time_mode, UFBX_TIME_MODE, UFBX_TIME_MODE_59_94_FPS); + +typedef enum ufbx_time_protocol UFBX_ENUM_REPR { + UFBX_TIME_PROTOCOL_SMPTE, + UFBX_TIME_PROTOCOL_FRAME_COUNT, + UFBX_TIME_PROTOCOL_DEFAULT, + + UFBX_ENUM_FORCE_WIDTH(UFBX_TIME_PROTOCOL) +} ufbx_time_protocol; + +UFBX_ENUM_TYPE(ufbx_time_protocol, UFBX_TIME_PROTOCOL, UFBX_TIME_PROTOCOL_DEFAULT); + +typedef enum ufbx_snap_mode UFBX_ENUM_REPR { + UFBX_SNAP_MODE_NONE, + UFBX_SNAP_MODE_SNAP, + UFBX_SNAP_MODE_PLAY, + UFBX_SNAP_MODE_SNAP_AND_PLAY, + + UFBX_ENUM_FORCE_WIDTH(UFBX_SNAP_MODE) +} ufbx_snap_mode; + +UFBX_ENUM_TYPE(ufbx_snap_mode, UFBX_SNAP_MODE, UFBX_SNAP_MODE_SNAP_AND_PLAY); + +// Global settings: Axes and time/unit scales +typedef struct ufbx_scene_settings { + ufbx_props props; + + // Mapping of X/Y/Z axes to world-space directions. + // HINT: Use `ufbx_load_opts.target_axes` to normalize this. + // NOTE: This contains the _original_ axes even if you supply `ufbx_load_opts.target_axes`. + ufbx_coordinate_axes axes; + + // How many meters does a single world-space unit represent. + // FBX files usually default to centimeters, reported as `0.01` here. + // HINT: Use `ufbx_load_opts.target_unit_meters` to normalize this. + ufbx_real unit_meters; + + // Frames per second the animation is defined at. + double frames_per_second; + + ufbx_vec3 ambient_color; + ufbx_string default_camera; + + // Animation user interface settings. + // HINT: Use `ufbx_scene_settings.frames_per_second` instead of interpreting these yourself. + ufbx_time_mode time_mode; + ufbx_time_protocol time_protocol; + ufbx_snap_mode snap_mode; + + // Original `axes.up` value for the scene. + // NOTE: This may be `UFBX_COORDINATE_AXIS_UNKNOWN` if not specified in the file. + ufbx_coordinate_axis original_axis_up; + + // Original `unit_meters` value for the scene. + ufbx_real original_unit_meters; + +} ufbx_scene_settings; + +struct ufbx_scene { + ufbx_metadata metadata; + + // Global settings + ufbx_scene_settings settings; + + // Node instances in the scene + ufbx_node *root_node; + + // Default animation descriptor + ufbx_anim *anim; + + union { + struct { + ufbx_unknown_list unknowns; + + // Nodes + ufbx_node_list nodes; + + // Node attributes (common) + ufbx_mesh_list meshes; + ufbx_light_list lights; + ufbx_camera_list cameras; + ufbx_bone_list bones; + ufbx_empty_list empties; + + // Node attributes (curves/surfaces) + ufbx_line_curve_list line_curves; + ufbx_nurbs_curve_list nurbs_curves; + ufbx_nurbs_surface_list nurbs_surfaces; + ufbx_nurbs_trim_surface_list nurbs_trim_surfaces; + ufbx_nurbs_trim_boundary_list nurbs_trim_boundaries; + + // Node attributes (advanced) + ufbx_procedural_geometry_list procedural_geometries; + ufbx_stereo_camera_list stereo_cameras; + ufbx_camera_switcher_list camera_switchers; + ufbx_marker_list markers; + ufbx_lod_group_list lod_groups; + + // Deformers + ufbx_skin_deformer_list skin_deformers; + ufbx_skin_cluster_list skin_clusters; + ufbx_blend_deformer_list blend_deformers; + ufbx_blend_channel_list blend_channels; + ufbx_blend_shape_list blend_shapes; + ufbx_cache_deformer_list cache_deformers; + ufbx_cache_file_list cache_files; + + // Materials + ufbx_material_list materials; + ufbx_texture_list textures; + ufbx_video_list videos; + ufbx_shader_list shaders; + ufbx_shader_binding_list shader_bindings; + + // Animation + ufbx_anim_stack_list anim_stacks; + ufbx_anim_layer_list anim_layers; + ufbx_anim_value_list anim_values; + ufbx_anim_curve_list anim_curves; + + // Collections + ufbx_display_layer_list display_layers; + ufbx_selection_set_list selection_sets; + ufbx_selection_node_list selection_nodes; + + // Constraints + ufbx_character_list characters; + ufbx_constraint_list constraints; + + // Audio + ufbx_audio_layer_list audio_layers; + ufbx_audio_clip_list audio_clips; + + // Miscellaneous + ufbx_pose_list poses; + ufbx_metadata_object_list metadata_objects; + }; + + ufbx_element_list elements_by_type[UFBX_ELEMENT_TYPE_COUNT]; + }; + + // Unique texture files referenced by the scene. + ufbx_texture_file_list texture_files; + + // All elements and connections in the whole file + ufbx_element_list elements; // < Sorted by `id` + ufbx_connection_list connections_src; // < Sorted by `src,src_prop` + ufbx_connection_list connections_dst; // < Sorted by `dst,dst_prop` + + // Elements sorted by name, type + ufbx_name_element_list elements_by_name; + + // Enabled if `ufbx_load_opts.retain_dom == true`. + ufbx_nullable ufbx_dom_node *dom_root; +}; + +// -- Curves + +typedef struct ufbx_curve_point { + bool valid; + ufbx_vec3 position; + ufbx_vec3 derivative; +} ufbx_curve_point; + +typedef struct ufbx_surface_point { + bool valid; + ufbx_vec3 position; + ufbx_vec3 derivative_u; + ufbx_vec3 derivative_v; +} ufbx_surface_point; + +// -- Mesh topology + +typedef enum ufbx_topo_flags UFBX_FLAG_REPR { + UFBX_TOPO_NON_MANIFOLD = 0x1, // < Edge with three or more faces + + UFBX_FLAG_FORCE_WIDTH(UFBX_TOPO_FLAGS) +} ufbx_topo_flags; + +typedef struct ufbx_topo_edge { + uint32_t index; // < Starting index of the edge, always defined + uint32_t next; // < Ending index of the edge / next per-face `ufbx_topo_edge`, always defined + uint32_t prev; // < Previous per-face `ufbx_topo_edge`, always defined + uint32_t twin; // < `ufbx_topo_edge` on the opposite side, `UFBX_NO_INDEX` if not found + uint32_t face; // < Index into `mesh->faces[]`, always defined + uint32_t edge; // < Index into `mesh->edges[]`, `UFBX_NO_INDEX` if not found + + ufbx_topo_flags flags; +} ufbx_topo_edge; + +// Vertex data array for `ufbx_generate_indices()`. +// NOTE: `ufbx_generate_indices()` compares the vertices using `memcmp()`, so +// any padding should be cleared to zero. +typedef struct ufbx_vertex_stream { + void *data; // < Data pointer of shape `char[vertex_count][vertex_size]`. + size_t vertex_count; // < Number of vertices in this stream, for sanity checking. + size_t vertex_size; // < Size of a vertex in bytes. +} ufbx_vertex_stream; + +// -- Memory callbacks + +// You can optionally provide an allocator to ufbx, the default is to use the +// CRT malloc/realloc/free + +// Allocate `size` bytes, must be at least 8 byte aligned +typedef void *ufbx_alloc_fn(void *user, size_t size); + +// Reallocate `old_ptr` from `old_size` to `new_size` +// NOTE: If omit `alloc_fn` and `free_fn` they will be translated to: +// `alloc(size)` -> `realloc_fn(user, NULL, 0, size)` +// `free_fn(ptr, size)` -> `realloc_fn(user, ptr, size, 0)` +typedef void *ufbx_realloc_fn(void *user, void *old_ptr, size_t old_size, size_t new_size); + +// Free pointer `ptr` (of `size` bytes) returned by `alloc_fn` or `realloc_fn` +typedef void ufbx_free_fn(void *user, void *ptr, size_t size); + +// Free the allocator itself +typedef void ufbx_free_allocator_fn(void *user); + +// Allocator callbacks and user context +// NOTE: The allocator will be stored to the loaded scene and will be called +// again from `ufbx_free_scene()` so make sure `user` outlives that! +// You can use `free_allocator_fn()` to free the allocator yourself. +typedef struct ufbx_allocator { + // Callback functions, see `typedef`s above for information + ufbx_alloc_fn *alloc_fn; + ufbx_realloc_fn *realloc_fn; + ufbx_free_fn *free_fn; + ufbx_free_allocator_fn *free_allocator_fn; + void *user; +} ufbx_allocator; + +typedef struct ufbx_allocator_opts { + // Allocator callbacks + ufbx_allocator allocator; + + // Maximum number of bytes to allocate before failing + size_t memory_limit; + + // Maximum number of allocations to attempt before failing + size_t allocation_limit; + + // Threshold to swap from batched allocations to individual ones + // Defaults to 1MB if set to zero + // NOTE: If set to `1` ufbx will allocate everything in the smallest + // possible chunks which may be useful for debugging (eg. ASAN) + size_t huge_threshold; + + // Maximum size of a single allocation containing sub-allocations. + // Defaults to 16MB if set to zero + // The maximum amount of wasted memory depends on `max_chunk_size` and + // `huge_threshold`: each chunk can waste up to `huge_threshold` bytes + // internally and the last chunk might be incomplete. So for example + // with the defaults we can waste around 1MB/16MB = 6.25% overall plus + // up to 32MB due to the two incomplete blocks. The actual amounts differ + // slightly as the chunks start out at 4kB and double in size each time, + // meaning that the maximum fixed overhead (up to 32MB with defaults) is + // at most ~30% of the total allocation size. + size_t max_chunk_size; + +} ufbx_allocator_opts; + +// -- IO callbacks + +// Try to read up to `size` bytes to `data`, return the amount of read bytes. +// Return `SIZE_MAX` to indicate an IO error. +typedef size_t ufbx_read_fn(void *user, void *data, size_t size); + +// Skip `size` bytes in the file. +typedef bool ufbx_skip_fn(void *user, size_t size); + +// Get the size of the file. +// Return `0` if unknown, `UINT64_MAX` if error. +typedef uint64_t ufbx_size_fn(void *user); + +// Close the file +typedef void ufbx_close_fn(void *user); + +typedef struct ufbx_stream { + ufbx_read_fn *read_fn; // < Required + ufbx_skip_fn *skip_fn; // < Optional: Will use `read_fn()` if missing + ufbx_size_fn *size_fn; // < Optional + ufbx_close_fn *close_fn; // < Optional + + // Context passed to other functions + void *user; +} ufbx_stream; + +typedef enum ufbx_open_file_type UFBX_ENUM_REPR { + UFBX_OPEN_FILE_MAIN_MODEL, // < Main model file + UFBX_OPEN_FILE_GEOMETRY_CACHE, // < Unknown geometry cache file + UFBX_OPEN_FILE_OBJ_MTL, // < .mtl material library file + + UFBX_ENUM_FORCE_WIDTH(UFBX_OPEN_FILE_TYPE) +} ufbx_open_file_type; + +UFBX_ENUM_TYPE(ufbx_open_file_type, UFBX_OPEN_FILE_TYPE, UFBX_OPEN_FILE_OBJ_MTL); + +typedef uintptr_t ufbx_open_file_context; + +typedef struct ufbx_open_file_info { + // Context that can be passed to the following functions to use a shared allocator: + // ufbx_open_file_ctx() + // ufbx_open_memory_ctx() + ufbx_open_file_context context; + + // Kind of file to load. + ufbx_open_file_type type; + + // Original filename in the file, not resolved or UTF-8 encoded. + // NOTE: Not necessarily NULL-terminated! + ufbx_blob original_filename; +} ufbx_open_file_info; + +// Callback for opening an external file from the filesystem +typedef bool ufbx_open_file_fn(void *user, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_info *info); + +typedef struct ufbx_open_file_cb { + ufbx_open_file_fn *fn; + void *user; + + UFBX_CALLBACK_IMPL(ufbx_open_file_cb, ufbx_open_file_fn, bool, + (void *user, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_info *info), + (stream, path, path_len, info)) +} ufbx_open_file_cb; + +// Options for `ufbx_open_file()`. +typedef struct ufbx_open_file_opts { + uint32_t _begin_zero; + + // Allocator to allocate the memory with. + ufbx_allocator_opts allocator; + + // The filename is guaranteed to be NULL-terminated. + ufbx_unsafe bool filename_null_terminated; + + uint32_t _end_zero; +} ufbx_open_file_opts; + +// Memory stream options +typedef void ufbx_close_memory_fn(void *user, void *data, size_t data_size); + +typedef struct ufbx_close_memory_cb { + ufbx_close_memory_fn *fn; + void *user; + + UFBX_CALLBACK_IMPL(ufbx_close_memory_cb, ufbx_close_memory_fn, void, + (void *user, void *data, size_t data_size), + (data, data_size)) +} ufbx_close_memory_cb; + +// Options for `ufbx_open_memory()`. +typedef struct ufbx_open_memory_opts { + uint32_t _begin_zero; + + // Allocator to allocate the memory with. + // NOTE: Used even if no copy is made to allocate a small metadata block. + ufbx_allocator_opts allocator; + + // Do not copy the memory. + // You can use `close_cb` to free the memory when the stream is closed. + // NOTE: This means the provided data pointer is referenced after creating + // the memory stream, make sure the data stays valid until the stream is closed! + ufbx_unsafe bool no_copy; + + // Callback to free the memory blob. + ufbx_close_memory_cb close_cb; + + uint32_t _end_zero; +} ufbx_open_memory_opts; + +// Detailed error stack frame. +// NOTE: You must compile `ufbx.c` with `UFBX_ENABLE_ERROR_STACK` to enable the error stack. +typedef struct ufbx_error_frame { + uint32_t source_line; + ufbx_string function; + ufbx_string description; +} ufbx_error_frame; + +// Error causes (and `UFBX_ERROR_NONE` for no error). +typedef enum ufbx_error_type UFBX_ENUM_REPR { + + // No error, operation has been performed successfully. + UFBX_ERROR_NONE, + + // Unspecified error, most likely caused by an invalid FBX file or a file + // that contains something ufbx can't handle. + UFBX_ERROR_UNKNOWN, + + // File not found. + UFBX_ERROR_FILE_NOT_FOUND, + + // Empty file. + UFBX_ERROR_EMPTY_FILE, + + // External file not found. + // See `ufbx_load_opts.load_external_files` for more information. + UFBX_ERROR_EXTERNAL_FILE_NOT_FOUND, + + // Out of memory (allocator returned `NULL`). + UFBX_ERROR_OUT_OF_MEMORY, + + // `ufbx_allocator_opts.memory_limit` exhausted. + UFBX_ERROR_MEMORY_LIMIT, + + // `ufbx_allocator_opts.allocation_limit` exhausted. + UFBX_ERROR_ALLOCATION_LIMIT, + + // File ended abruptly. + UFBX_ERROR_TRUNCATED_FILE, + + // IO read error. + // eg. returning `SIZE_MAX` from `ufbx_stream.read_fn` or stdio `ferror()` condition. + UFBX_ERROR_IO, + + // User cancelled the loading via `ufbx_load_opts.progress_cb` returning `UFBX_PROGRESS_CANCEL`. + UFBX_ERROR_CANCELLED, + + // Could not detect file format from file data or filename. + // HINT: You can supply it manually using `ufbx_load_opts.file_format` or use `ufbx_load_opts.filename` + // when using `ufbx_load_memory()` to let ufbx guess the format from the extension. + UFBX_ERROR_UNRECOGNIZED_FILE_FORMAT, + + // Options struct (eg. `ufbx_load_opts`) is not cleared to zero. + // Make sure you initialize the structure to zero via eg. + // ufbx_load_opts opts = { 0 }; // C + // ufbx_load_opts opts = { }; // C++ + UFBX_ERROR_UNINITIALIZED_OPTIONS, + + // The vertex streams in `ufbx_generate_indices()` are empty. + UFBX_ERROR_ZERO_VERTEX_SIZE, + + // Vertex stream passed to `ufbx_generate_indices()`. + UFBX_ERROR_TRUNCATED_VERTEX_STREAM, + + // Invalid UTF-8 encountered in a file when loading with `UFBX_UNICODE_ERROR_HANDLING_ABORT_LOADING`. + UFBX_ERROR_INVALID_UTF8, + + // Feature needed for the operation has been compiled out. + UFBX_ERROR_FEATURE_DISABLED, + + // Attempting to tessellate an invalid NURBS object. + // See `ufbx_nurbs_basis.valid`. + UFBX_ERROR_BAD_NURBS, + + // Out of bounds index in the file when loading with `UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING`. + UFBX_ERROR_BAD_INDEX, + + // Node is deeper than `ufbx_load_opts.node_depth_limit` in the hierarchy. + UFBX_ERROR_NODE_DEPTH_LIMIT, + + // Error parsing ASCII array in a thread. + // Threaded ASCII parsing is slightly more strict than non-threaded, for cursed files, + // set `ufbx_load_opts.force_single_thread_ascii_parsing` to `true`. + UFBX_ERROR_THREADED_ASCII_PARSE, + + // Unsafe options specified without enabling `ufbx_load_opts.allow_unsafe`. + UFBX_ERROR_UNSAFE_OPTIONS, + + // Duplicated override property in `ufbx_create_anim()` + UFBX_ERROR_DUPLICATE_OVERRIDE, + + // Unsupported file format version. + // ufbx still tries to load files with unsupported versions, see `UFBX_WARNING_UNSUPPORTED_VERSION`. + UFBX_ERROR_UNSUPPORTED_VERSION, + + UFBX_ENUM_FORCE_WIDTH(UFBX_ERROR_TYPE) +} ufbx_error_type; + +UFBX_ENUM_TYPE(ufbx_error_type, UFBX_ERROR_TYPE, UFBX_ERROR_UNSUPPORTED_VERSION); + +// Error description with detailed stack trace +// HINT: You can use `ufbx_format_error()` for formatting the error +typedef struct ufbx_error { + + // Type of the error, or `UFBX_ERROR_NONE` if successful. + ufbx_error_type type; + + // Description of the error type. + ufbx_string description; + + // Internal error stack. + // NOTE: You must compile `ufbx.c` with `UFBX_ENABLE_ERROR_STACK` to enable the error stack. + uint32_t stack_size; + ufbx_error_frame stack[UFBX_ERROR_STACK_MAX_DEPTH]; + + // Additional error information, such as missing file filename. + // `info` is a NULL-terminated UTF-8 string containing `info_length` bytes, excluding the trailing `'\0'`. + size_t info_length; + char info[UFBX_ERROR_INFO_LENGTH]; + +} ufbx_error; + +// -- Progress callbacks + +// Loading progress information. +typedef struct ufbx_progress { + uint64_t bytes_read; + uint64_t bytes_total; +} ufbx_progress; + +// Progress result returned from `ufbx_progress_fn()` callback. +// Determines whether ufbx should continue or abort the loading. +typedef enum ufbx_progress_result UFBX_ENUM_REPR { + + // Continue loading the file. + UFBX_PROGRESS_CONTINUE = 0x100, + + // Cancel loading and fail with `UFBX_ERROR_CANCELLED`. + UFBX_PROGRESS_CANCEL = 0x200, + + UFBX_ENUM_FORCE_WIDTH(UFBX_PROGRESS_RESULT) +} ufbx_progress_result; + +// Called periodically with the current progress. +// Return `UFBX_PROGRESS_CANCEL` to cancel further processing. +typedef ufbx_progress_result ufbx_progress_fn(void *user, const ufbx_progress *progress); + +typedef struct ufbx_progress_cb { + ufbx_progress_fn *fn; + void *user; + + UFBX_CALLBACK_IMPL(ufbx_progress_cb, ufbx_progress_fn, ufbx_progress_result, + (void *user, const ufbx_progress *progress), + (progress)) +} ufbx_progress_cb; + +// -- Inflate + +typedef struct ufbx_inflate_input ufbx_inflate_input; +typedef struct ufbx_inflate_retain ufbx_inflate_retain; + +// Source data/stream to decompress with `ufbx_inflate()` +struct ufbx_inflate_input { + // Total size of the data in bytes + size_t total_size; + + // (optional) Initial or complete data chunk + const void *data; + size_t data_size; + + // (optional) Temporary buffer, defaults to 256b stack buffer + void *buffer; + size_t buffer_size; + + // (optional) Streaming read function, concatenated after `data` + ufbx_read_fn *read_fn; + void *read_user; + + // (optional) Progress reporting + ufbx_progress_cb progress_cb; + uint64_t progress_interval_hint; // < Bytes between progress report calls + + // (optional) Change the progress scope + uint64_t progress_size_before; + uint64_t progress_size_after; + + // (optional) No the DEFLATE header + bool no_header; + + // (optional) No the Adler32 checksum + bool no_checksum; + + // (optional) Force internal fast lookup bit amount + size_t internal_fast_bits; +}; + +// Persistent data between `ufbx_inflate()` calls +// NOTE: You must set `initialized` to `false`, but `data` may be uninitialized +struct ufbx_inflate_retain { + bool initialized; + uint64_t data[1024]; +}; + +typedef enum ufbx_index_error_handling UFBX_ENUM_REPR { + // Clamp to a valid value. + UFBX_INDEX_ERROR_HANDLING_CLAMP, + // Set bad indices to `UFBX_NO_INDEX`. + // This is the recommended way if you need to deal with files with gaps in information. + // HINT: If you use this `ufbx_get_vertex_TYPE()` functions will return zero + // on invalid indices instead of failing. + UFBX_INDEX_ERROR_HANDLING_NO_INDEX, + // Fail loading entierely when encountering a bad index. + UFBX_INDEX_ERROR_HANDLING_ABORT_LOADING, + // Pass bad indices through as-is. + // Requires `ufbx_load_opts.allow_unsafe`. + // UNSAFE: Breaks any API guarantees regarding indexes being in bounds and makes + // `ufbx_get_vertex_TYPE()` memory-unsafe to use. + UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_INDEX_ERROR_HANDLING) +} ufbx_index_error_handling; + +UFBX_ENUM_TYPE(ufbx_index_error_handling, UFBX_INDEX_ERROR_HANDLING, UFBX_INDEX_ERROR_HANDLING_UNSAFE_IGNORE); + +typedef enum ufbx_unicode_error_handling UFBX_ENUM_REPR { + // Replace errors with U+FFFD "Replacement Character" + UFBX_UNICODE_ERROR_HANDLING_REPLACEMENT_CHARACTER, + // Replace errors with '_' U+5F "Low Line" + UFBX_UNICODE_ERROR_HANDLING_UNDERSCORE, + // Replace errors with '?' U+3F "Question Mark" + UFBX_UNICODE_ERROR_HANDLING_QUESTION_MARK, + // Remove errors from the output + UFBX_UNICODE_ERROR_HANDLING_REMOVE, + // Fail loading on encountering an Unicode error + UFBX_UNICODE_ERROR_HANDLING_ABORT_LOADING, + // Ignore and pass-through non-UTF-8 string data. + // Requires `ufbx_load_opts.allow_unsafe`. + // UNSAFE: Breaks API guarantee that `ufbx_string` is UTF-8 encoded. + UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE, + + UFBX_ENUM_FORCE_WIDTH(UFBX_UNICODE_ERROR_HANDLING) +} ufbx_unicode_error_handling; + +UFBX_ENUM_TYPE(ufbx_unicode_error_handling, UFBX_UNICODE_ERROR_HANDLING, UFBX_UNICODE_ERROR_HANDLING_UNSAFE_IGNORE); + +typedef enum ufbx_baked_key_flags UFBX_FLAG_REPR { + // This keyframe represents a constant step from the left side + UFBX_BAKED_KEY_STEP_LEFT = 0x1, + // This keyframe represents a constant step from the right side + UFBX_BAKED_KEY_STEP_RIGHT = 0x2, + // This keyframe is the main part of a step + // Bordering either `UFBX_BAKED_KEY_STEP_LEFT` or `UFBX_BAKED_KEY_STEP_RIGHT`. + UFBX_BAKED_KEY_STEP_KEY = 0x4, + // This keyframe is a real keyframe in the source animation + UFBX_BAKED_KEY_KEYFRAME = 0x8, + // This keyframe has been reduced by maximum sample rate. + // See `ufbx_bake_opts.maximum_sample_rate`. + UFBX_BAKED_KEY_REDUCED = 0x10, + + UFBX_FLAG_FORCE_WIDTH(UFBX_BAKED_KEY) +} ufbx_baked_key_flags; + +typedef struct ufbx_baked_vec3 { + double time; // < Time of the keyframe, in seconds + ufbx_vec3 value; // < Value at `time`, can be linearly interpolated + ufbx_baked_key_flags flags; // < Additional information about the keyframe +} ufbx_baked_vec3; + +UFBX_LIST_TYPE(ufbx_baked_vec3_list, ufbx_baked_vec3); + +typedef struct ufbx_baked_quat { + double time; // < Time of the keyframe, in seconds + ufbx_quat value; // < Value at `time`, can be (spherically) linearly interpolated + ufbx_baked_key_flags flags; // < Additional information about the keyframe +} ufbx_baked_quat; + +UFBX_LIST_TYPE(ufbx_baked_quat_list, ufbx_baked_quat); + +// Baked transform animation for a single node. +typedef struct ufbx_baked_node { + + // Typed ID of the node, maps to `ufbx_scene.nodes[]`. + uint32_t typed_id; + // Element ID of the element, maps to `ufbx_scene.elements[]`. + uint32_t element_id; + + // The translation channel has constant values for the whole animation. + bool constant_translation; + // The rotation channel has constant values for the whole animation. + bool constant_rotation; + // The scale channel has constant values for the whole animation. + bool constant_scale; + + // Translation keys for the animation, maps to `ufbx_node.local_transform.translation`. + ufbx_baked_vec3_list translation_keys; + // Rotation keyframes, maps to `ufbx_node.local_transform.rotation`. + ufbx_baked_quat_list rotation_keys; + // Scale keyframes, maps to `ufbx_node.local_transform.scale`. + ufbx_baked_vec3_list scale_keys; + +} ufbx_baked_node; + +UFBX_LIST_TYPE(ufbx_baked_node_list, ufbx_baked_node); + +// Baked property animation. +typedef struct ufbx_baked_prop { + // Name of the property, eg. `"Visibility"`. + ufbx_string name; + // The value of the property is constant for the whole animation. + bool constant_value; + // Property value keys. + ufbx_baked_vec3_list keys; +} ufbx_baked_prop; + +UFBX_LIST_TYPE(ufbx_baked_prop_list, ufbx_baked_prop); + +// Baked property animation for a single element. +typedef struct ufbx_baked_element { + // Element ID of the element, maps to `ufbx_scene.elements[]`. + uint32_t element_id; + // List of properties the animation modifies. + ufbx_baked_prop_list props; +} ufbx_baked_element; + +UFBX_LIST_TYPE(ufbx_baked_element_list, ufbx_baked_element); + +typedef struct ufbx_baked_anim_metadata { + // Memory statistics + size_t result_memory_used; + size_t temp_memory_used; + size_t result_allocs; + size_t temp_allocs; +} ufbx_baked_anim_metadata; + +// Animation baked into linearly interpolated keyframes. +// See `ufbx_bake_anim()`. +typedef struct ufbx_baked_anim { + + // Nodes that are modified by the animation. + // Some nodes may be missing if the specified animation does not transform them. + // Conversely, some non-obviously animated nodes may be included as exporters + // often may add dummy keyframes for objects. + ufbx_baked_node_list nodes; + + // Element properties modified by the animation. + ufbx_baked_element_list elements; + + // Playback time range for the animation. + double playback_time_begin; + double playback_time_end; + double playback_duration; + + // Keyframe time range. + double key_time_min; + double key_time_max; + + // Additional bake information. + ufbx_baked_anim_metadata metadata; + +} ufbx_baked_anim; + +// -- Thread API + +// Internal thread pool handle. +// Passed to `ufbx_thread_pool_run_task()` from an user thread to run ufbx tasks. +// HINT: This context can store a user pointer via `ufbx_thread_pool_set_user_ptr()`. +typedef uintptr_t ufbx_thread_pool_context; + +// Thread pool creation information from ufbx. +typedef struct ufbx_thread_pool_info { + uint32_t max_concurrent_tasks; +} ufbx_thread_pool_info; + +// Initialize the thread pool. +// Return `true` on success. +typedef bool ufbx_thread_pool_init_fn(void *user, ufbx_thread_pool_context ctx, const ufbx_thread_pool_info *info); + +// Run tasks `count` tasks in threads. +// You must call `ufbx_thread_pool_run_task()` with indices `[start_index, start_index + count)`. +// The threads are launched in batches indicated by `group`, see `UFBX_THREAD_GROUP_COUNT` for more information. +// Ideally, you should run all the task indices in parallel within each `ufbx_thread_pool_run_fn()` call. +typedef void ufbx_thread_pool_run_fn(void *user, ufbx_thread_pool_context ctx, uint32_t group, uint32_t start_index, uint32_t count); + +// Wait for previous tasks spawned in `ufbx_thread_pool_run_fn()` to finish. +// `group` specifies the batch to wait for, `max_index` contains `start_index + count` from that group instance. +typedef void ufbx_thread_pool_wait_fn(void *user, ufbx_thread_pool_context ctx, uint32_t group, uint32_t max_index); + +// Free the thread pool. +typedef void ufbx_thread_pool_free_fn(void *user, ufbx_thread_pool_context ctx); + +// Thread pool interface. +// See functions above for more information. +// +// Hypothetical example of calls, where `UFBX_THREAD_GROUP_COUNT=2` for simplicity: +// +// run_fn(group=0, start_index=0, count=4) -> t0 := threaded { ufbx_thread_pool_run_task(0..3) } +// run_fn(group=1, start_index=4, count=10) -> t1 := threaded { ufbx_thread_pool_run_task(4..10) } +// wait_fn(group=0, max_index=4) -> wait_threads(t0) +// run_fn(group=0, start_index=10, count=15) -> t0 := threaded { ufbx_thread_pool_run_task(10..14) } +// wait_fn(group=1, max_index=10) -> wait_threads(t1) +// wait_fn(group=0, max_index=15) -> wait_threads(t0) +// +typedef struct ufbx_thread_pool { + ufbx_thread_pool_init_fn *init_fn; // < Optional + ufbx_thread_pool_run_fn *run_fn; // < Required + ufbx_thread_pool_wait_fn *wait_fn; // < Required + ufbx_thread_pool_free_fn *free_fn; // < Optional + void *user; +} ufbx_thread_pool; + +// Thread pool options. +typedef struct ufbx_thread_opts { + + // Thread pool interface. + // HINT: You can use `extra/ufbx_os.h` to provide a thread pool. + ufbx_thread_pool pool; + + // Maximum of tasks to have in-flight. + // Default: 2048 + size_t num_tasks; + + // Maximum amount of memory to use for batched threaded processing. + // Default: 32MB + // NOTE: The actual used memory usage might be higher, if there are individual tasks + // that rqeuire a high amount of memory. + size_t memory_limit; + +} ufbx_thread_opts; + +// Flags to control nanimation evaluation functions. +typedef enum ufbx_evaluate_flags UFBX_FLAG_REPR { + + // Do not extrapolate past the keyframes. + UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION = 0x1, + + UFBX_FLAG_FORCE_WIDTH(ufbx_evaluate_flags) +} ufbx_evaluate_flags; + +// -- Main API + +// Options for `ufbx_load_file/memory/stream/stdio()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_load_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during loading + ufbx_allocator_opts result_allocator; // < Allocator used for the final scene + ufbx_thread_opts thread_opts; // < Threading options + + // Preferences + bool ignore_geometry; // < Do not load geometry datsa (vertices, indices, etc) + bool ignore_animation; // < Do not load animation curves + bool ignore_embedded; // < Do not load embedded content + bool ignore_all_content; // < Do not load any content (geometry, animation, embedded) + + bool evaluate_skinning; // < Evaluate skinning (see ufbx_mesh.skinned_vertices) + bool evaluate_caches; // < Evaluate vertex caches (see ufbx_mesh.skinned_vertices) + + // Try to open external files referenced by the main file automatically. + // Applies to geometry caches and .mtl files for OBJ. + // NOTE: This may be risky for untrusted data as the input files may contain + // references to arbitrary paths in the filesystem. + // NOTE: This only applies to files *implicitly* referenced by the scene, if + // you request additional files via eg. `ufbx_load_opts.obj_mtl_path` they + // are still loaded. + // NOTE: Will fail loading if any external files are not found by default, use + // `ufbx_load_opts.ignore_missing_external_files` to suppress this, in this case + // you can find the errors at `ufbx_metadata.warnings[]` as `UFBX_WARNING_MISSING_EXTERNAL_FILE`. + bool load_external_files; + + // Don't fail loading if external files are not found. + bool ignore_missing_external_files; + + // Don't compute `ufbx_skin_deformer` `vertices` and `weights` arrays saving + // a bit of memory and time if not needed + bool skip_skin_vertices; + + // Skip computing `ufbx_mesh.material_parts[]` and `ufbx_mesh.face_group_parts[]`. + bool skip_mesh_parts; + + // Clean-up skin weights by removing negative, zero and NAN weights. + bool clean_skin_weights; + + // Read Blender materials as PBR values. + // Blender converts PBR materials to legacy FBX Phong materials in a deterministic way. + // If this setting is enabled, such materials will be read as `UFBX_SHADER_BLENDER_PHONG`, + // which means ufbx will be able to parse roughness and metallic textures. + bool use_blender_pbr_material; + + // Don't adjust reading the FBX file depending on the detected exporter + bool disable_quirks; + + // Don't allow partially broken FBX files to load + bool strict; + + // Force ASCII parsing to use a single thread. + // The multi-threaded ASCII parsing is slightly more lenient as it ignores + // the self-reported size of ASCII arrays, that threaded parsing depends on. + bool force_single_thread_ascii_parsing; + + // UNSAFE: If enabled allows using unsafe options that may fundamentally + // break the API guarantees. + ufbx_unsafe bool allow_unsafe; + + // Specify how to handle broken indices. + ufbx_index_error_handling index_error_handling; + + // Connect related elements even if they are broken. If `false` (default) + // `ufbx_skin_cluster` with a missing `bone` field are _not_ included in + // the `ufbx_skin_deformer.clusters[]` array for example. + bool connect_broken_elements; + + // Allow nodes that are not connected in any way to the root. Conversely if + // disabled, all lone nodes will be parented under `ufbx_scene.root_node`. + bool allow_nodes_out_of_root; + + // Allow meshes with no vertex position attribute. + // NOTE: If this is set `ufbx_mesh.vertex_position.exists` may be `false`. + bool allow_missing_vertex_position; + + // Allow faces with zero indices. + bool allow_empty_faces; + + // Generate vertex normals for a meshes that are missing normals. + // You can see if the normals have been generated from `ufbx_mesh.generated_normals`. + bool generate_missing_normals; + + // Ignore `open_file_cb` when loading the main file. + bool open_main_file_with_default; + + // Path separator character, defaults to '\' on Windows and '/' otherwise. + char path_separator; + + // Maximum depth of the node hirerachy. + // Will fail with `UFBX_ERROR_NODE_DEPTH_LIMIT` if a node is deeper than this limit. + // NOTE: The default of 0 allows arbitrarily deep hierarchies. Be careful if using + // recursive algorithms without setting this limit. + uint32_t node_depth_limit; + + // Estimated file size for progress reporting + uint64_t file_size_estimate; + + // Buffer size in bytes to use for reading from files or IO callbacks + size_t read_buffer_size; + + // Filename to use as a base for relative file paths if not specified using + // `ufbx_load_file()`. Use `length = SIZE_MAX` for NULL-terminated strings. + // `raw_filename` will be derived from this if empty. + ufbx_string filename; + + // Raw non-UTF8 filename. Does not support NULL termination. + // `filename` will be derived from this if empty. + ufbx_blob raw_filename; + + // Progress reporting + ufbx_progress_cb progress_cb; + uint64_t progress_interval_hint; // < Bytes between progress report calls + + // External file callbacks (defaults to stdio.h) + ufbx_open_file_cb open_file_cb; + + // How to handle geometry transforms in the nodes. + // See `ufbx_geometry_transform_handling` for an explanation. + ufbx_geometry_transform_handling geometry_transform_handling; + + // How to handle unconventional transform inherit modes. + // See `ufbx_inherit_mode_handling` for an explanation. + ufbx_inherit_mode_handling inherit_mode_handling; + + // How to perform space conversion by `target_axes` and `target_unit_meters`. + // See `ufbx_space_conversion` for an explanation. + ufbx_space_conversion space_conversion; + + // How to handle pivots. + // See `ufbx_pivot_handling` for an explanation. + ufbx_pivot_handling pivot_handling; + + // Retain the original transforms of empties when converting pivots. + bool pivot_handling_retain_empties; + + // Axis used to mirror for conversion between left-handed and right-handed coordinates. + ufbx_mirror_axis handedness_conversion_axis; + + // Do not change winding of faces when converting handedness. + bool handedness_conversion_retain_winding; + + // Reverse winding of all faces. + // If `handedness_conversion_retain_winding` is not specified, mirrored meshes + // will retain their original winding. + bool reverse_winding; + + // Apply an implicit root transformation to match axes. + // Used if `ufbx_coordinate_axes_valid(target_axes)`. + ufbx_coordinate_axes target_axes; + + // Scale the scene so that one world-space unit is `target_unit_meters` meters. + // By default units are not scaled. + ufbx_real target_unit_meters; + + // Target space for camera. + // By default FBX cameras point towards the positive X axis. + // Used if `ufbx_coordinate_axes_valid(target_camera_axes)`. + ufbx_coordinate_axes target_camera_axes; + + // Target space for directed lights. + // By default FBX lights point towards the negative Y axis. + // Used if `ufbx_coordinate_axes_valid(target_light_axes)`. + ufbx_coordinate_axes target_light_axes; + + // Name for dummy geometry transform helper nodes. + // See `UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES`. + ufbx_string geometry_transform_helper_name; + + // Name for dummy scale helper nodes. + // See `UFBX_INHERIT_MODE_HANDLING_HELPER_NODES`. + ufbx_string scale_helper_name; + + // Normalize vertex normals. + bool normalize_normals; + + // Normalize tangents and bitangents. + bool normalize_tangents; + + // Override for the root transform + bool use_root_transform; + ufbx_transform root_transform; + + // Animation keyframe clamp threshold, only applies to specific interpolation modes. + double key_clamp_threshold; + + // Specify how to handle Unicode errors in strings. + ufbx_unicode_error_handling unicode_error_handling; + + // Retain the 'W' component of mesh normal/tangent/bitangent. + // See `ufbx_vertex_attrib.values_w`. + bool retain_vertex_attrib_w; + + // Retain the raw document structure using `ufbx_dom_node`. + bool retain_dom; + + // Force a specific file format instead of detecting it. + ufbx_file_format file_format; + + // How far to read into the file to determine the file format. + // Default: 16kB + size_t file_format_lookahead; + + // Do not attempt to detect file format from file content. + bool no_format_from_content; + + // Do not attempt to detect file format from filename extension. + // ufbx primarily detects file format from the file header, + // this is just used as a fallback. + bool no_format_from_extension; + + // (.obj) Try to find .mtl file with matching filename as the .obj file. + // Used if the file specified `mtllib` line is not found, eg. for a file called + // `model.obj` that contains the line `usemtl materials.mtl`, ufbx would first + // try to open `materials.mtl` and if that fails it tries to open `model.mtl`. + bool obj_search_mtl_by_filename; + + // (.obj) Don't split geometry into meshes by object. + bool obj_merge_objects; + + // (.obj) Don't split geometry into meshes by groups. + bool obj_merge_groups; + + // (.obj) Force splitting groups even on object boundaries. + bool obj_split_groups; + + // (.obj) Path to the .mtl file. + // Use `length = SIZE_MAX` for NULL-terminated strings. + // NOTE: This is used _instead_ of the one in the file even if not found + // and sidesteps `load_external_files` as it's _explicitly_ requested. + ufbx_string obj_mtl_path; + + // (.obj) Data for the .mtl file. + ufbx_blob obj_mtl_data; + + // The world unit in meters that .obj files are assumed to be in. + // .obj files do not define the working units. By default the unit scale + // is read as zero, and no unit conversion is performed. + ufbx_real obj_unit_meters; + + // Coordinate space .obj files are assumed to be in. + // .obj files do not define the coordinate space they use. By default no + // coordinate space is assumed and no conversion is performed. + ufbx_coordinate_axes obj_axes; + + uint32_t _end_zero; +} ufbx_load_opts; + +// Options for `ufbx_evaluate_scene()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_evaluate_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during evaluation + ufbx_allocator_opts result_allocator; // < Allocator used for the final scene + + bool evaluate_skinning; // < Evaluate skinning (see ufbx_mesh.skinned_vertices) + bool evaluate_caches; // < Evaluate vertex caches (see ufbx_mesh.skinned_vertices) + + // Evaluation flags. + // See `ufbx_evaluate_flags` for information. + uint32_t evaluate_flags; + + // WARNING: Potentially unsafe! Try to open external files such as geometry caches + bool load_external_files; + + // External file callbacks (defaults to stdio.h) + ufbx_open_file_cb open_file_cb; + + uint32_t _end_zero; +} ufbx_evaluate_opts; + +UFBX_LIST_TYPE(ufbx_const_uint32_list, const uint32_t); +UFBX_LIST_TYPE(ufbx_const_real_list, const ufbx_real); + +typedef struct ufbx_prop_override_desc { + // Element (`ufbx_element.element_id`) to override the property from + uint32_t element_id; + + // Property name to override. + ufbx_string prop_name; + + // Override value, use `value.x` for scalars. `value_int` is initialized + // from `value.x` if zero so keep `value` zeroed even if you don't need it! + ufbx_vec4 value; + ufbx_string value_str; + int64_t value_int; +} ufbx_prop_override_desc; + +UFBX_LIST_TYPE(ufbx_const_prop_override_desc_list, const ufbx_prop_override_desc); + +UFBX_LIST_TYPE(ufbx_const_transform_override_list, const ufbx_transform_override); + +typedef struct ufbx_anim_opts { + uint32_t _begin_zero; + + // Animation layers indices. + // Corresponding to `ufbx_scene.anim_layers[]`, aka `ufbx_anim_layer.typed_id`. + ufbx_const_uint32_list layer_ids; + + // Override layer weights, parallel to `ufbx_anim_opts.layer_ids[]`. + ufbx_const_real_list override_layer_weights; + + // Property overrides. + // These allow you to override FBX properties, such as 'UFBX_Lcl_Rotation`. + ufbx_const_prop_override_desc_list prop_overrides; + + // Transform overrides. + // These allow you to override individual nodes' `ufbx_node.local_transform`. + ufbx_const_transform_override_list transform_overrides; + + // Ignore connected properties + bool ignore_connections; + + ufbx_allocator_opts result_allocator; // < Allocator used to create the `ufbx_anim` + + uint32_t _end_zero; +} ufbx_anim_opts; + +// Specifies how to handle stepped tangents. +typedef enum ufbx_bake_step_handling UFBX_ENUM_REPR { + + // One millisecond default step duration, with potential extra slack for converting to `float`. + UFBX_BAKE_STEP_HANDLING_DEFAULT, + + // Use a custom interpolation duration for the constant step. + // See `ufbx_bake_opts.step_custom_duration` and optionally `ufbx_bake_opts.step_custom_epsilon`. + UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION, + + // Stepped keyframes are represented as keyframes at the exact same time. + // Use flags `UFBX_BAKED_KEY_STEP_LEFT` and `UFBX_BAKED_KEY_STEP_RIGHT` to differentiate + // between the primary key and edge limits. + UFBX_BAKE_STEP_HANDLING_IDENTICAL_TIME, + + // Represent stepped keyframe times as the previous/next representable `double` value. + // Using this and robust linear interpolation will handle stepped tangents correctly + // without having to look at the key flags. + // NOTE: Casting these values to `float` or otherwise modifying them can collapse + // the keyframes to have the identical time. + UFBX_BAKE_STEP_HANDLING_ADJACENT_DOUBLE, + + // Treat all stepped tangents as linearly interpolated. + UFBX_BAKE_STEP_HANDLING_IGNORE, + + UFBX_ENUM_FORCE_WIDTH(ufbx_bake_step_handling) +} ufbx_bake_step_handling; + +UFBX_ENUM_TYPE(ufbx_bake_step_handling, UFBX_BAKE_STEP_HANDLING, UFBX_BAKE_STEP_HANDLING_IGNORE); + +typedef struct ufbx_bake_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during loading + ufbx_allocator_opts result_allocator; // < Allocator used for the final baked animation + + // Move the keyframe times to start from zero regardless of the animation start time. + // For example, for an animation spanning between frames [30, 60] will be moved to + // [0, 30] in the baked animation. + // NOTE: This is in general not equivalent to subtracting `ufbx_anim.time_begin` + // from each keyframe, as this trimming is done exactly using internal FBX ticks. + bool trim_start_time; + + // Samples per second to use for resampling non-linear animation. + // Default: 30 + double resample_rate; + + // Minimum sample rate to not resample. + // Many exporters resample animation by default. To avoid double-resampling + // keyframe rates higher or equal to this will not be resampled. + // Default: 19.5 + double minimum_sample_rate; + + // Maximum sample rate to use, this will remove keys if they are too close together. + // Default: unlimited + double maximum_sample_rate; + + // Bake the raw versions of properties related to transforms. + bool bake_transform_props; + + // Do not bake node transforms. + bool skip_node_transforms; + + // Do not resample linear rotation keyframes. + // FBX interpolates rotation in Euler angles, so this might cause incorrect interpolation. + bool no_resample_rotation; + + // Ignore layer weight animation. + bool ignore_layer_weight_animation; + + // Maximum number of segments to generate from one keyframe. + // Default: 32 + size_t max_keyframe_segments; + + // How to handle stepped tangents. + ufbx_bake_step_handling step_handling; + + // Interpolation duration used by `UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION`. + double step_custom_duration; + + // Interpolation epsilon used by `UFBX_BAKE_STEP_HANDLING_CUSTOM_DURATION`. + // Defined as the minimum fractional decrease/increase in key time, ie. + // `time / (1.0 + step_custom_epsilon)` and `time * (1.0 + step_custom_epsilon)`. + double step_custom_epsilon; + + // Flags passed to animation evaluation functions. + // See `ufbx_evaluate_flags`. + uint32_t evaluate_flags; + + // Enable key reduction. + bool key_reduction_enabled; + + // Enable key reduction for non-constant rotations. + // Assumes rotations will be interpolated using a spherical linear interpolation at runtime. + bool key_reduction_rotation; + + // Threshold for reducing keys for linear segments. + // Default `0.000001`, use negative to disable. + double key_reduction_threshold; + + // Maximum passes over the keys to reduce. + // Every pass can potentially halve the the amount of keys. + // Default: `4` + size_t key_reduction_passes; + + uint32_t _end_zero; +} ufbx_bake_opts; + +// Options for `ufbx_tessellate_nurbs_curve()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_tessellate_curve_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during tessellation + ufbx_allocator_opts result_allocator; // < Allocator used for the final line curve + + // How many segments tessellate each span in `ufbx_nurbs_basis.spans`. + size_t span_subdivision; + + uint32_t _end_zero; +} ufbx_tessellate_curve_opts; + +// Options for `ufbx_tessellate_nurbs_surface()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_tessellate_surface_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during tessellation + ufbx_allocator_opts result_allocator; // < Allocator used for the final mesh + + // How many segments tessellate each span in `ufbx_nurbs_basis.spans`. + // NOTE: Default is `4`, _not_ `ufbx_nurbs_surface.span_subdivision_u/v` as that + // would make it easy to create an FBX file with an absurdly high subdivision + // rate (similar to mesh subdivision). Please enforce copy the value yourself + // enforcing whatever limits you deem reasonable. + size_t span_subdivision_u; + size_t span_subdivision_v; + + // Skip computing `ufbx_mesh.material_parts[]` + bool skip_mesh_parts; + + uint32_t _end_zero; +} ufbx_tessellate_surface_opts; + +// Options for `ufbx_subdivide_mesh()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_subdivide_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during subdivision + ufbx_allocator_opts result_allocator; // < Allocator used for the final mesh + + ufbx_subdivision_boundary boundary; + ufbx_subdivision_boundary uv_boundary; + + // Do not generate normals + bool ignore_normals; + + // Interpolate existing normals using the subdivision rules + // instead of generating new normals + bool interpolate_normals; + + // Subdivide also tangent attributes + bool interpolate_tangents; + + // Map subdivided vertices into weighted original vertices. + // NOTE: May be O(n^2) if `max_source_vertices` is not specified! + bool evaluate_source_vertices; + + // Limit source vertices per subdivided vertex. + size_t max_source_vertices; + + // Calculate bone influences over subdivided vertices (if applicable). + // NOTE: May be O(n^2) if `max_skin_weights` is not specified! + bool evaluate_skin_weights; + + // Limit bone influences per subdivided vertex. + size_t max_skin_weights; + + // Index of the skin deformer to use for `evaluate_skin_weights`. + size_t skin_deformer_index; + + uint32_t _end_zero; +} ufbx_subdivide_opts; + +// Options for `ufbx_load_geometry_cache()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_geometry_cache_opts { + uint32_t _begin_zero; + + ufbx_allocator_opts temp_allocator; // < Allocator used during loading + ufbx_allocator_opts result_allocator; // < Allocator used for the final scene + + // External file callbacks (defaults to stdio.h) + ufbx_open_file_cb open_file_cb; + + // FPS value for converting frame times to seconds + double frames_per_second; + + // Axis to mirror the geometry by. + ufbx_mirror_axis mirror_axis; + + // Enable scaling `scale_factor` all geometry by. + bool use_scale_factor; + + // Factor to scale the geometry by. + ufbx_real scale_factor; + + uint32_t _end_zero; +} ufbx_geometry_cache_opts; + +// Options for `ufbx_read_geometry_cache_TYPE()` +// NOTE: Initialize to zero with `{ 0 }` (C) or `{ }` (C++) +typedef struct ufbx_geometry_cache_data_opts { + uint32_t _begin_zero; + + // External file callbacks (defaults to stdio.h) + ufbx_open_file_cb open_file_cb; + + bool additive; + bool use_weight; + ufbx_real weight; + + // Ignore scene transform. + bool ignore_transform; + + uint32_t _end_zero; +} ufbx_geometry_cache_data_opts; + +typedef struct ufbx_panic { + bool did_panic; + size_t message_length; + char message[UFBX_PANIC_MESSAGE_LENGTH]; +} ufbx_panic; + +// -- API + +#ifdef __cplusplus +extern "C" { +#endif + +// Various zero/empty/identity values +ufbx_abi_data const ufbx_string ufbx_empty_string; +ufbx_abi_data const ufbx_blob ufbx_empty_blob; +ufbx_abi_data const ufbx_matrix ufbx_identity_matrix; +ufbx_abi_data const ufbx_transform ufbx_identity_transform; +ufbx_abi_data const ufbx_vec2 ufbx_zero_vec2; +ufbx_abi_data const ufbx_vec3 ufbx_zero_vec3; +ufbx_abi_data const ufbx_vec4 ufbx_zero_vec4; +ufbx_abi_data const ufbx_quat ufbx_identity_quat; + +// Commonly used coordinate axes. +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_right_handed_y_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_right_handed_z_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_left_handed_y_up; +ufbx_abi_data const ufbx_coordinate_axes ufbx_axes_left_handed_z_up; + +// Sizes of element types. eg `sizeof(ufbx_node)` +ufbx_abi_data const size_t ufbx_element_type_size[UFBX_ELEMENT_TYPE_COUNT]; + +// Version of the source file, comparable to `UFBX_HEADER_VERSION` +ufbx_abi_data const uint32_t ufbx_source_version; + + +// Practically always `true` (see below), if not you need to be careful with threads. +// +// Guaranteed to be `true` in _any_ of the following conditions: +// - ufbx.c has been compiled using: GCC / Clang / MSVC / ICC / EMCC / TCC +// - ufbx.c has been compiled as C++11 or later +// - ufbx.c has been compiled as C11 or later with `` support +// +// If `false` you can't call the following functions concurrently: +// ufbx_evaluate_scene() +// ufbx_free_scene() +// ufbx_subdivide_mesh() +// ufbx_tessellate_nurbs_surface() +// ufbx_free_mesh() +ufbx_abi bool ufbx_is_thread_safe(void); + +// Load a scene from a `size` byte memory buffer at `data` +ufbx_abi ufbx_scene *ufbx_load_memory( + const void *data, size_t data_size, + const ufbx_load_opts *opts, ufbx_error *error); + +// Load a scene by opening a file named `filename` +ufbx_abi ufbx_scene *ufbx_load_file( + const char *filename, + const ufbx_load_opts *opts, ufbx_error *error); +ufbx_abi ufbx_scene *ufbx_load_file_len( + const char *filename, size_t filename_len, + const ufbx_load_opts *opts, ufbx_error *error); + +// Load a scene by reading from an `FILE *file` stream +// NOTE: `file` is passed as a `void` pointer to avoid including +ufbx_abi ufbx_scene *ufbx_load_stdio( + void *file, + const ufbx_load_opts *opts, ufbx_error *error); + +// Load a scene by reading from an `FILE *file` stream with a prefix +// NOTE: `file` is passed as a `void` pointer to avoid including +ufbx_abi ufbx_scene *ufbx_load_stdio_prefix( + void *file, + const void *prefix, size_t prefix_size, + const ufbx_load_opts *opts, ufbx_error *error); + +// Load a scene from a user-specified stream +ufbx_abi ufbx_scene *ufbx_load_stream( + const ufbx_stream *stream, + const ufbx_load_opts *opts, ufbx_error *error); + +// Load a scene from a user-specified stream with a prefix +ufbx_abi ufbx_scene *ufbx_load_stream_prefix( + const ufbx_stream *stream, + const void *prefix, size_t prefix_size, + const ufbx_load_opts *opts, ufbx_error *error); + +// Free a previously loaded or evaluated scene +ufbx_abi void ufbx_free_scene(ufbx_scene *scene); + +// Increment `scene` refcount +ufbx_abi void ufbx_retain_scene(ufbx_scene *scene); + +// Format a textual description of `error`. +// Always produces a NULL-terminated string to `char dst[dst_size]`, truncating if +// necessary. Returns the number of characters written not including the NULL terminator. +ufbx_abi size_t ufbx_format_error(char *dst, size_t dst_size, const ufbx_error *error); + +// Query + +// Find a property `name` from `props`, returns `NULL` if not found. +// Searches through `ufbx_props.defaults` as well. +ufbx_abi ufbx_prop *ufbx_find_prop_len(const ufbx_props *props, const char *name, size_t name_len); +ufbx_abi ufbx_prop *ufbx_find_prop(const ufbx_props *props, const char *name); + +// Utility functions for finding the value of a property, returns `def` if not found. +// NOTE: For `ufbx_string` you need to ensure the lifetime of the default is +// sufficient as no copy is made. +ufbx_abi ufbx_real ufbx_find_real_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_real def); +ufbx_abi ufbx_real ufbx_find_real(const ufbx_props *props, const char *name, ufbx_real def); +ufbx_abi ufbx_vec3 ufbx_find_vec3_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_vec3 def); +ufbx_abi ufbx_vec3 ufbx_find_vec3(const ufbx_props *props, const char *name, ufbx_vec3 def); +ufbx_abi int64_t ufbx_find_int_len(const ufbx_props *props, const char *name, size_t name_len, int64_t def); +ufbx_abi int64_t ufbx_find_int(const ufbx_props *props, const char *name, int64_t def); +ufbx_abi bool ufbx_find_bool_len(const ufbx_props *props, const char *name, size_t name_len, bool def); +ufbx_abi bool ufbx_find_bool(const ufbx_props *props, const char *name, bool def); +ufbx_abi ufbx_string ufbx_find_string_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_string def); +ufbx_abi ufbx_string ufbx_find_string(const ufbx_props *props, const char *name, ufbx_string def); +ufbx_abi ufbx_blob ufbx_find_blob_len(const ufbx_props *props, const char *name, size_t name_len, ufbx_blob def); +ufbx_abi ufbx_blob ufbx_find_blob(const ufbx_props *props, const char *name, ufbx_blob def); + +// Find property in `props` with concatenated `parts[num_parts]`. +ufbx_abi ufbx_prop *ufbx_find_prop_concat(const ufbx_props *props, const ufbx_string *parts, size_t num_parts); + +// Get an element connected to a property. +ufbx_abi ufbx_element *ufbx_get_prop_element(const ufbx_element *element, const ufbx_prop *prop, ufbx_element_type type); + +// Find an element connected to a property by name. +ufbx_abi ufbx_element *ufbx_find_prop_element_len(const ufbx_element *element, const char *name, size_t name_len, ufbx_element_type type); +ufbx_abi ufbx_element *ufbx_find_prop_element(const ufbx_element *element, const char *name, ufbx_element_type type); + +// Find any element of type `type` in `scene` by `name`. +// For example if you want to find `ufbx_material` named `Mat`: +// (ufbx_material*)ufbx_find_element(scene, UFBX_ELEMENT_MATERIAL, "Mat"); +ufbx_abi ufbx_element *ufbx_find_element_len(const ufbx_scene *scene, ufbx_element_type type, const char *name, size_t name_len); +ufbx_abi ufbx_element *ufbx_find_element(const ufbx_scene *scene, ufbx_element_type type, const char *name); + +// Find node in `scene` by `name` (shorthand for `ufbx_find_element(UFBX_ELEMENT_NODE)`). +ufbx_abi ufbx_node *ufbx_find_node_len(const ufbx_scene *scene, const char *name, size_t name_len); +ufbx_abi ufbx_node *ufbx_find_node(const ufbx_scene *scene, const char *name); + +// Find an animation stack in `scene` by `name` (shorthand for `ufbx_find_element(UFBX_ELEMENT_ANIM_STACK)`) +ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack_len(const ufbx_scene *scene, const char *name, size_t name_len); +ufbx_abi ufbx_anim_stack *ufbx_find_anim_stack(const ufbx_scene *scene, const char *name); + +// Find a material in `scene` by `name` (shorthand for `ufbx_find_element(UFBX_ELEMENT_MATERIAL)`). +ufbx_abi ufbx_material *ufbx_find_material_len(const ufbx_scene *scene, const char *name, size_t name_len); +ufbx_abi ufbx_material *ufbx_find_material(const ufbx_scene *scene, const char *name); + +// Find a single animated property `prop` of `element` in `layer`. +// Returns `NULL` if not found. +ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop_len(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop, size_t prop_len); +ufbx_abi ufbx_anim_prop *ufbx_find_anim_prop(const ufbx_anim_layer *layer, const ufbx_element *element, const char *prop); + +// Find all animated properties of `element` in `layer`. +ufbx_abi ufbx_anim_prop_list ufbx_find_anim_props(const ufbx_anim_layer *layer, const ufbx_element *element); + +// Get a matrix that transforms normals in the same way as Autodesk software. +// NOTE: The resulting normals are slightly incorrect as this function deliberately +// inverts geometric transformation wrong. For better results use +// `ufbx_matrix_for_normals(&node->geometry_to_world)`. +ufbx_abi ufbx_matrix ufbx_get_compatible_matrix_for_normals(const ufbx_node *node); + +// Utility + +// Decompress a DEFLATE compressed buffer. +// Returns the decompressed size or a negative error code (see source for details). +// NOTE: You must supply a valid `retain` with `ufbx_inflate_retain.initialized == false` +// but the rest can be uninitialized. +ufbx_abi ptrdiff_t ufbx_inflate(void *dst, size_t dst_size, const ufbx_inflate_input *input, ufbx_inflate_retain *retain); + +// Same as `ufbx_open_file()` but compatible with the callback in `ufbx_open_file_fn`. +// The `user` parameter is actually not used here. +ufbx_abi bool ufbx_default_open_file(void *user, ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_info *info); + +// Open a `ufbx_stream` from a file. +// Use `path_len == SIZE_MAX` for NULL terminated string. +ufbx_abi bool ufbx_open_file(ufbx_stream *stream, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error); +ufbx_unsafe ufbx_abi bool ufbx_open_file_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const char *path, size_t path_len, const ufbx_open_file_opts *opts, ufbx_error *error); + +// NOTE: Uses the default ufbx allocator! +ufbx_abi bool ufbx_open_memory(ufbx_stream *stream, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error); +ufbx_unsafe ufbx_abi bool ufbx_open_memory_ctx(ufbx_stream *stream, ufbx_open_file_context ctx, const void *data, size_t data_size, const ufbx_open_memory_opts *opts, ufbx_error *error); + +// Animation evaluation + +// Evaluate a single animation `curve` at a `time`. +// Returns `default_value` only if `curve == NULL` or it has no keyframes. +ufbx_abi ufbx_real ufbx_evaluate_curve(const ufbx_anim_curve *curve, double time, ufbx_real default_value); +ufbx_abi ufbx_real ufbx_evaluate_curve_flags(const ufbx_anim_curve *curve, double time, ufbx_real default_value, uint32_t flags); + +// Evaluate a value from bundled animation curves. +ufbx_abi ufbx_real ufbx_evaluate_anim_value_real(const ufbx_anim_value *anim_value, double time); +ufbx_abi ufbx_vec3 ufbx_evaluate_anim_value_vec3(const ufbx_anim_value *anim_value, double time); +ufbx_abi ufbx_real ufbx_evaluate_anim_value_real_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags); +ufbx_abi ufbx_vec3 ufbx_evaluate_anim_value_vec3_flags(const ufbx_anim_value *anim_value, double time, uint32_t flags); + +// Evaluate an animated property `name` from `element` at `time`. +// NOTE: If the property is not found it will have the flag `UFBX_PROP_FLAG_NOT_FOUND`. +ufbx_abi ufbx_prop ufbx_evaluate_prop_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time); +ufbx_abi ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time); +ufbx_abi ufbx_prop ufbx_evaluate_prop_flags_len(const ufbx_anim *anim, const ufbx_element *element, const char *name, size_t name_len, double time, uint32_t flags); +ufbx_abi ufbx_prop ufbx_evaluate_prop_flags(const ufbx_anim *anim, const ufbx_element *element, const char *name, double time, uint32_t flags); + +// Evaluate all _animated_ properties of `element`. +// HINT: This function returns an `ufbx_props` structure with the original properties as +// `ufbx_props.defaults`. This lets you use `ufbx_find_prop/value()` for the results. +ufbx_abi ufbx_props ufbx_evaluate_props(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size); +ufbx_abi ufbx_props ufbx_evaluate_props_flags(const ufbx_anim *anim, const ufbx_element *element, double time, ufbx_prop *buffer, size_t buffer_size, uint32_t flags); + +// Flags to control `ufbx_evaluate_transform_flags()`. +typedef enum ufbx_transform_flags UFBX_FLAG_REPR { + + // Ignore parent scale helper. + UFBX_TRANSFORM_FLAG_IGNORE_SCALE_HELPER = 0x1, + + // Ignore componentwise scale. + // Note that if you don't specify this, ufbx will have to potentially + // evaluate the entire parent chain in the worst case. + UFBX_TRANSFORM_FLAG_IGNORE_COMPONENTWISE_SCALE = 0x2, + + // Require explicit components + UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES = 0x4, + + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.translation`. + UFBX_TRANSFORM_FLAG_INCLUDE_TRANSLATION = 0x10, + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.rotation`. + UFBX_TRANSFORM_FLAG_INCLUDE_ROTATION = 0x20, + // If `UFBX_TRANSFORM_FLAG_EXPLICIT_INCLUDES`: Evaluate `ufbx_transform.scale`. + UFBX_TRANSFORM_FLAG_INCLUDE_SCALE = 0x40, + + // Do not extrapolate keyframes. + // See `UFBX_EVALUATE_FLAG_NO_EXTRAPOLATION`. + UFBX_TRANSFORM_FLAG_NO_EXTRAPOLATION = 0x80, + + UFBX_FLAG_FORCE_WIDTH(UFBX_TRANSFORM_FLAGS) +} ufbx_transform_flags; + +// Evaluate the animated transform of a node given a time. +// The returned transform is the local transform of the node (ie. relative to the parent), +// comparable to `ufbx_node.local_transform`. +ufbx_abi ufbx_transform ufbx_evaluate_transform(const ufbx_anim *anim, const ufbx_node *node, double time); +ufbx_abi ufbx_transform ufbx_evaluate_transform_flags(const ufbx_anim *anim, const ufbx_node *node, double time, uint32_t flags); + +// Evaluate the blend shape weight of a blend channel. +// NOTE: Return value uses `1.0` for full weight, instead of `100.0` that the internal property `UFBX_Weight` uses. +ufbx_abi ufbx_real ufbx_evaluate_blend_weight(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time); +ufbx_abi ufbx_real ufbx_evaluate_blend_weight_flags(const ufbx_anim *anim, const ufbx_blend_channel *channel, double time, uint32_t flags); + +// Evaluate the whole `scene` at a specific `time` in the animation `anim`. +// The returned scene behaves as if it had been exported at a specific time +// in the specified animation, except that animated elements' properties contain +// only the animated values, the original ones are in `props->defaults`. +// +// NOTE: The returned scene refers to the original `scene` so the original +// scene cannot be freed until all evaluated scenes are freed. +ufbx_abi ufbx_scene *ufbx_evaluate_scene(const ufbx_scene *scene, const ufbx_anim *anim, double time, const ufbx_evaluate_opts *opts, ufbx_error *error); + +// Create a custom animation descriptor. +// `ufbx_anim_opts` is used to specify animation layers and weights. +// HINT: You can also leave `ufbx_anim_opts.layer_ids[]` empty and only specify +// overrides to evaluate the scene with different properties or local transforms. +ufbx_abi ufbx_anim *ufbx_create_anim(const ufbx_scene *scene, const ufbx_anim_opts *opts, ufbx_error *error); + +// Free an animation returned by `ufbx_create_anim()`. +ufbx_abi void ufbx_free_anim(ufbx_anim *anim); + +// Increase the animation reference count. +ufbx_abi void ufbx_retain_anim(ufbx_anim *anim); + +// Animation baking + +// "Bake" an animation to linearly interpolated keyframes. +// Composites the FBX transformation chain into quaternion rotations. +ufbx_abi ufbx_baked_anim *ufbx_bake_anim(const ufbx_scene *scene, const ufbx_anim *anim, const ufbx_bake_opts *opts, ufbx_error *error); + +ufbx_abi void ufbx_retain_baked_anim(ufbx_baked_anim *bake); +ufbx_abi void ufbx_free_baked_anim(ufbx_baked_anim *bake); + +ufbx_abi ufbx_baked_node *ufbx_find_baked_node_by_typed_id(ufbx_baked_anim *bake, uint32_t typed_id); +ufbx_abi ufbx_baked_node *ufbx_find_baked_node(ufbx_baked_anim *bake, ufbx_node *node); + +ufbx_abi ufbx_baked_element *ufbx_find_baked_element_by_element_id(ufbx_baked_anim *bake, uint32_t element_id); +ufbx_abi ufbx_baked_element *ufbx_find_baked_element(ufbx_baked_anim *bake, ufbx_element *element); + +// Evaluate baked animation `keyframes` at `time`. +// Internally linearly interpolates between two adjacent keyframes. +// Handles stepped tangents cleanly, which is not strictly necessary for custom interpolation. +ufbx_abi ufbx_vec3 ufbx_evaluate_baked_vec3(ufbx_baked_vec3_list keyframes, double time); + +// Evaluate baked animation `keyframes` at `time`. +// Internally spherically interpolates (`ufbx_quat_slerp()`) between two adjacent keyframes. +// Handles stepped tangents cleanly, which is not strictly necessary for custom interpolation. +ufbx_abi ufbx_quat ufbx_evaluate_baked_quat(ufbx_baked_quat_list keyframes, double time); + +// Poses + +// Retrieve the bone pose for `node`. +// Returns `NULL` if the pose does not contain `node`. +ufbx_abi ufbx_bone_pose *ufbx_get_bone_pose(const ufbx_pose *pose, const ufbx_node *node); + +// Materials + +// Find a texture for a given material FBX property. +ufbx_abi ufbx_texture *ufbx_find_prop_texture_len(const ufbx_material *material, const char *name, size_t name_len); +ufbx_abi ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, const char *name); + +// Find a texture for a given shader property. +ufbx_abi ufbx_string ufbx_find_shader_prop_len(const ufbx_shader *shader, const char *name, size_t name_len); +ufbx_abi ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, const char *name); + +// Map from a shader property to material property. +ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings_len(const ufbx_shader *shader, const char *name, size_t name_len); +ufbx_abi ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, const char *name); + +// Find an input in a shader texture. +ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input_len(const ufbx_shader_texture *shader, const char *name, size_t name_len); +ufbx_abi ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx_shader_texture *shader, const char *name); + +// Math + +// Returns `true` if `axes` forms a valid coordinate space. +ufbx_abi bool ufbx_coordinate_axes_valid(ufbx_coordinate_axes axes); + +// Vector math utility functions. +ufbx_abi ufbx_vec3 ufbx_vec3_normalize(ufbx_vec3 v); + +// Quaternion math utility functions. +ufbx_abi ufbx_real ufbx_quat_dot(ufbx_quat a, ufbx_quat b); +ufbx_abi ufbx_quat ufbx_quat_mul(ufbx_quat a, ufbx_quat b); +ufbx_abi ufbx_quat ufbx_quat_normalize(ufbx_quat q); +ufbx_abi ufbx_quat ufbx_quat_fix_antipodal(ufbx_quat q, ufbx_quat reference); +ufbx_abi ufbx_quat ufbx_quat_slerp(ufbx_quat a, ufbx_quat b, ufbx_real t); +ufbx_abi ufbx_vec3 ufbx_quat_rotate_vec3(ufbx_quat q, ufbx_vec3 v); +ufbx_abi ufbx_vec3 ufbx_quat_to_euler(ufbx_quat q, ufbx_rotation_order order); +ufbx_abi ufbx_quat ufbx_euler_to_quat(ufbx_vec3 v, ufbx_rotation_order order); + +// Matrix math utility functions. +ufbx_abi ufbx_matrix ufbx_matrix_mul(const ufbx_matrix *a, const ufbx_matrix *b); +ufbx_abi ufbx_real ufbx_matrix_determinant(const ufbx_matrix *m); +ufbx_abi ufbx_matrix ufbx_matrix_invert(const ufbx_matrix *m); + +// Get a matrix that can be used to transform geometry normals. +// NOTE: You must normalize the normals after transforming them with this matrix, +// eg. using `ufbx_vec3_normalize()`. +// NOTE: This function flips the normals if the determinant is negative. +ufbx_abi ufbx_matrix ufbx_matrix_for_normals(const ufbx_matrix *m); + +// Matrix transformation utilities. +ufbx_abi ufbx_vec3 ufbx_transform_position(const ufbx_matrix *m, ufbx_vec3 v); +ufbx_abi ufbx_vec3 ufbx_transform_direction(const ufbx_matrix *m, ufbx_vec3 v); + +// Conversions between `ufbx_matrix` and `ufbx_transform`. +ufbx_abi ufbx_matrix ufbx_transform_to_matrix(const ufbx_transform *t); +ufbx_abi ufbx_transform ufbx_matrix_to_transform(const ufbx_matrix *m); + +// Skinning + +// Get a matrix representing the deformation for a single vertex. +// Returns `fallback` if the vertex is not skinned. +ufbx_abi ufbx_matrix ufbx_catch_get_skin_vertex_matrix(ufbx_panic *panic, const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback); +ufbx_inline ufbx_matrix ufbx_get_skin_vertex_matrix(const ufbx_skin_deformer *skin, size_t vertex, const ufbx_matrix *fallback) { + return ufbx_catch_get_skin_vertex_matrix(NULL, skin, vertex, fallback); +} + +// Resolve the index into `ufbx_blend_shape.position_offsets[]` given a vertex. +// Returns `UFBX_NO_INDEX` if the vertex is not included in the blend shape. +ufbx_abi uint32_t ufbx_get_blend_shape_offset_index(const ufbx_blend_shape *shape, size_t vertex); + +// Get the offset for a given vertex in the blend shape. +// Returns `ufbx_zero_vec3` if the vertex is not a included in the blend shape. +ufbx_abi ufbx_vec3 ufbx_get_blend_shape_vertex_offset(const ufbx_blend_shape *shape, size_t vertex); + +// Get the _current_ blend offset given a blend deformer. +// NOTE: This depends on the current animated blend weight of the deformer. +ufbx_abi ufbx_vec3 ufbx_get_blend_vertex_offset(const ufbx_blend_deformer *blend, size_t vertex); + +// Apply the blend shape with `weight` to given vertices. +ufbx_abi void ufbx_add_blend_shape_vertex_offsets(const ufbx_blend_shape *shape, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight); + +// Apply the blend deformer with `weight` to given vertices. +// NOTE: This depends on the current animated blend weight of the deformer. +ufbx_abi void ufbx_add_blend_vertex_offsets(const ufbx_blend_deformer *blend, ufbx_vec3 *vertices, size_t num_vertices, ufbx_real weight); + +// Curves/surfaces + +// Low-level utility to evaluate NURBS the basis functions. +ufbx_abi size_t ufbx_evaluate_nurbs_basis(const ufbx_nurbs_basis *basis, ufbx_real u, ufbx_real *weights, size_t num_weights, ufbx_real *derivatives, size_t num_derivatives); + +// Evaluate a point on a NURBS curve given the parameter `u`. +ufbx_abi ufbx_curve_point ufbx_evaluate_nurbs_curve(const ufbx_nurbs_curve *curve, ufbx_real u); + +// Evaluate a point on a NURBS surface given the parameter `u` and `v`. +ufbx_abi ufbx_surface_point ufbx_evaluate_nurbs_surface(const ufbx_nurbs_surface *surface, ufbx_real u, ufbx_real v); + +// Tessellate a NURBS curve into a polyline. +ufbx_abi ufbx_line_curve *ufbx_tessellate_nurbs_curve(const ufbx_nurbs_curve *curve, const ufbx_tessellate_curve_opts *opts, ufbx_error *error); + +// Tessellate a NURBS surface into a mesh. +ufbx_abi ufbx_mesh *ufbx_tessellate_nurbs_surface(const ufbx_nurbs_surface *surface, const ufbx_tessellate_surface_opts *opts, ufbx_error *error); + +// Free a line returned by `ufbx_tessellate_nurbs_curve()`. +ufbx_abi void ufbx_free_line_curve(ufbx_line_curve *curve); + +// Increase the refcount of the line. +ufbx_abi void ufbx_retain_line_curve(ufbx_line_curve *curve); + +// Mesh Topology + +// Find the face that contains a given `index`. +// Returns `UFBX_NO_INDEX` if out of bounds. +ufbx_abi uint32_t ufbx_find_face_index(ufbx_mesh *mesh, size_t index); + +// Triangulate a mesh face, returning the number of triangles. +// NOTE: You need to space for `(face.num_indices - 2) * 3 - 1` indices! +// HINT: Using `ufbx_mesh.max_face_triangles * 3` is always safe. +ufbx_abi uint32_t ufbx_catch_triangulate_face(ufbx_panic *panic, uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face); +ufbx_abi uint32_t ufbx_triangulate_face(uint32_t *indices, size_t num_indices, const ufbx_mesh *mesh, ufbx_face face); + +// Generate the half-edge representation of `mesh` to `topo[mesh->num_indices]` +ufbx_abi void ufbx_catch_compute_topology(ufbx_panic *panic, const ufbx_mesh *mesh, ufbx_topo_edge *topo, size_t num_topo); +ufbx_abi void ufbx_compute_topology(const ufbx_mesh *mesh, ufbx_topo_edge *topo, size_t num_topo); + +// Get the next/previous edge around a vertex +// NOTE: Does not return the half-edge on the opposite side (ie. `topo[index].twin`) + +// Get the next half-edge in `topo`. +ufbx_abi uint32_t ufbx_catch_topo_next_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); +ufbx_abi uint32_t ufbx_topo_next_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); + +// Get the previous half-edge in `topo`. +ufbx_abi uint32_t ufbx_catch_topo_prev_vertex_edge(ufbx_panic *panic, const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); +ufbx_abi uint32_t ufbx_topo_prev_vertex_edge(const ufbx_topo_edge *topo, size_t num_topo, uint32_t index); + +// Calculate a normal for a given face. +// The returned normal is weighted by face area. +ufbx_abi ufbx_vec3 ufbx_catch_get_weighted_face_normal(ufbx_panic *panic, const ufbx_vertex_vec3 *positions, ufbx_face face); +ufbx_abi ufbx_vec3 ufbx_get_weighted_face_normal(const ufbx_vertex_vec3 *positions, ufbx_face face); + +// Generate indices for normals from the topology. +// Respects smoothing groups. +ufbx_abi size_t ufbx_catch_generate_normal_mapping(ufbx_panic *panic, const ufbx_mesh *mesh, + const ufbx_topo_edge *topo, size_t num_topo, + uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth); +ufbx_abi size_t ufbx_generate_normal_mapping(const ufbx_mesh *mesh, + const ufbx_topo_edge *topo, size_t num_topo, + uint32_t *normal_indices, size_t num_normal_indices, bool assume_smooth); + +// Compute normals given normal indices. +// You can use `ufbx_generate_normal_mapping()` to generate the normal indices. +ufbx_abi void ufbx_catch_compute_normals(ufbx_panic *panic, const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, + const uint32_t *normal_indices, size_t num_normal_indices, + ufbx_vec3 *normals, size_t num_normals); +ufbx_abi void ufbx_compute_normals(const ufbx_mesh *mesh, const ufbx_vertex_vec3 *positions, + const uint32_t *normal_indices, size_t num_normal_indices, + ufbx_vec3 *normals, size_t num_normals); + +// Subdivide a mesh using the Catmull-Clark subdivision `level` times. +ufbx_abi ufbx_mesh *ufbx_subdivide_mesh(const ufbx_mesh *mesh, size_t level, const ufbx_subdivide_opts *opts, ufbx_error *error); + +// Free a mesh returned from `ufbx_subdivide_mesh()` or `ufbx_tessellate_nurbs_surface()`. +ufbx_abi void ufbx_free_mesh(ufbx_mesh *mesh); + +// Increase the mesh reference count. +ufbx_abi void ufbx_retain_mesh(ufbx_mesh *mesh); + +// Geometry caches + +// Load geometry cache information from a file. +// As geometry caches can be massive, this does not actually read the data, but +// only seeks through the files to form the metadata. +ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache( + const char *filename, + const ufbx_geometry_cache_opts *opts, ufbx_error *error); +ufbx_abi ufbx_geometry_cache *ufbx_load_geometry_cache_len( + const char *filename, size_t filename_len, + const ufbx_geometry_cache_opts *opts, ufbx_error *error); + +// Free a geometry cache returned from `ufbx_load_geometry_cache()`. +ufbx_abi void ufbx_free_geometry_cache(ufbx_geometry_cache *cache); +// Increase the geometry cache reference count. +ufbx_abi void ufbx_retain_geometry_cache(ufbx_geometry_cache *cache); + +// Read a frame from a geometry cache. +ufbx_abi size_t ufbx_read_geometry_cache_real(const ufbx_cache_frame *frame, ufbx_real *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); +ufbx_abi size_t ufbx_read_geometry_cache_vec3(const ufbx_cache_frame *frame, ufbx_vec3 *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); +// Sample the a geometry cache channel, linearly blending between adjacent frames. +ufbx_abi size_t ufbx_sample_geometry_cache_real(const ufbx_cache_channel *channel, double time, ufbx_real *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); +ufbx_abi size_t ufbx_sample_geometry_cache_vec3(const ufbx_cache_channel *channel, double time, ufbx_vec3 *data, size_t num_data, const ufbx_geometry_cache_data_opts *opts); + +// DOM + +// Find a DOM node given a name. +ufbx_abi ufbx_dom_node *ufbx_dom_find_len(const ufbx_dom_node *parent, const char *name, size_t name_len); +ufbx_abi ufbx_dom_node *ufbx_dom_find(const ufbx_dom_node *parent, const char *name); + +// Utility + +// Generate an index buffer for a flat vertex buffer. +// `streams` specifies one or more vertex data arrays, each stream must contain `num_indices` vertices. +// This function compacts the data within `streams` in-place, writing the deduplicated indices to `indices`. +ufbx_abi size_t ufbx_generate_indices(const ufbx_vertex_stream *streams, size_t num_streams, uint32_t *indices, size_t num_indices, const ufbx_allocator_opts *allocator, ufbx_error *error); + +// Thread pool + +// Run a single thread pool task. +// See `ufbx_thread_pool_run_fn` for more information. +ufbx_unsafe ufbx_abi void ufbx_thread_pool_run_task(ufbx_thread_pool_context ctx, uint32_t index); + +// Get or set an arbitrary user pointer for the thread pool context. +// `ufbx_thread_pool_get_user_ptr()` returns `NULL` if unset. +ufbx_unsafe ufbx_abi void ufbx_thread_pool_set_user_ptr(ufbx_thread_pool_context ctx, void *user_ptr); +ufbx_unsafe ufbx_abi void *ufbx_thread_pool_get_user_ptr(ufbx_thread_pool_context ctx); + +// -- Inline API + +// Utility functions for reading geometry data for a single index. +ufbx_abi ufbx_real ufbx_catch_get_vertex_real(ufbx_panic *panic, const ufbx_vertex_real *v, size_t index); +ufbx_abi ufbx_vec2 ufbx_catch_get_vertex_vec2(ufbx_panic *panic, const ufbx_vertex_vec2 *v, size_t index); +ufbx_abi ufbx_vec3 ufbx_catch_get_vertex_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index); +ufbx_abi ufbx_vec4 ufbx_catch_get_vertex_vec4(ufbx_panic *panic, const ufbx_vertex_vec4 *v, size_t index); + +// Utility functions for reading geometry data for a single index. +ufbx_inline ufbx_real ufbx_get_vertex_real(const ufbx_vertex_real *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } +ufbx_inline ufbx_vec2 ufbx_get_vertex_vec2(const ufbx_vertex_vec2 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } +ufbx_inline ufbx_vec3 ufbx_get_vertex_vec3(const ufbx_vertex_vec3 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } +ufbx_inline ufbx_vec4 ufbx_get_vertex_vec4(const ufbx_vertex_vec4 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values.data[(int32_t)v->indices.data[index]]; } + +ufbx_abi ufbx_real ufbx_catch_get_vertex_w_vec3(ufbx_panic *panic, const ufbx_vertex_vec3 *v, size_t index); +ufbx_inline ufbx_real ufbx_get_vertex_w_vec3(const ufbx_vertex_vec3 *v, size_t index) { ufbx_assert(index < v->indices.count); return v->values_w.count > 0 ? v->values_w.data[(int32_t)v->indices.data[index]] : 0.0f; } + +// Functions for converting an untyped `ufbx_element` to a concrete type. +// Returns `NULL` if the element is not that type. +ufbx_abi ufbx_unknown *ufbx_as_unknown(const ufbx_element *element); +ufbx_abi ufbx_node *ufbx_as_node(const ufbx_element *element); +ufbx_abi ufbx_mesh *ufbx_as_mesh(const ufbx_element *element); +ufbx_abi ufbx_light *ufbx_as_light(const ufbx_element *element); +ufbx_abi ufbx_camera *ufbx_as_camera(const ufbx_element *element); +ufbx_abi ufbx_bone *ufbx_as_bone(const ufbx_element *element); +ufbx_abi ufbx_empty *ufbx_as_empty(const ufbx_element *element); +ufbx_abi ufbx_line_curve *ufbx_as_line_curve(const ufbx_element *element); +ufbx_abi ufbx_nurbs_curve *ufbx_as_nurbs_curve(const ufbx_element *element); +ufbx_abi ufbx_nurbs_surface *ufbx_as_nurbs_surface(const ufbx_element *element); +ufbx_abi ufbx_nurbs_trim_surface *ufbx_as_nurbs_trim_surface(const ufbx_element *element); +ufbx_abi ufbx_nurbs_trim_boundary *ufbx_as_nurbs_trim_boundary(const ufbx_element *element); +ufbx_abi ufbx_procedural_geometry *ufbx_as_procedural_geometry(const ufbx_element *element); +ufbx_abi ufbx_stereo_camera *ufbx_as_stereo_camera(const ufbx_element *element); +ufbx_abi ufbx_camera_switcher *ufbx_as_camera_switcher(const ufbx_element *element); +ufbx_abi ufbx_marker *ufbx_as_marker(const ufbx_element *element); +ufbx_abi ufbx_lod_group *ufbx_as_lod_group(const ufbx_element *element); +ufbx_abi ufbx_skin_deformer *ufbx_as_skin_deformer(const ufbx_element *element); +ufbx_abi ufbx_skin_cluster *ufbx_as_skin_cluster(const ufbx_element *element); +ufbx_abi ufbx_blend_deformer *ufbx_as_blend_deformer(const ufbx_element *element); +ufbx_abi ufbx_blend_channel *ufbx_as_blend_channel(const ufbx_element *element); +ufbx_abi ufbx_blend_shape *ufbx_as_blend_shape(const ufbx_element *element); +ufbx_abi ufbx_cache_deformer *ufbx_as_cache_deformer(const ufbx_element *element); +ufbx_abi ufbx_cache_file *ufbx_as_cache_file(const ufbx_element *element); +ufbx_abi ufbx_material *ufbx_as_material(const ufbx_element *element); +ufbx_abi ufbx_texture *ufbx_as_texture(const ufbx_element *element); +ufbx_abi ufbx_video *ufbx_as_video(const ufbx_element *element); +ufbx_abi ufbx_shader *ufbx_as_shader(const ufbx_element *element); +ufbx_abi ufbx_shader_binding *ufbx_as_shader_binding(const ufbx_element *element); +ufbx_abi ufbx_anim_stack *ufbx_as_anim_stack(const ufbx_element *element); +ufbx_abi ufbx_anim_layer *ufbx_as_anim_layer(const ufbx_element *element); +ufbx_abi ufbx_anim_value *ufbx_as_anim_value(const ufbx_element *element); +ufbx_abi ufbx_anim_curve *ufbx_as_anim_curve(const ufbx_element *element); +ufbx_abi ufbx_display_layer *ufbx_as_display_layer(const ufbx_element *element); +ufbx_abi ufbx_selection_set *ufbx_as_selection_set(const ufbx_element *element); +ufbx_abi ufbx_selection_node *ufbx_as_selection_node(const ufbx_element *element); +ufbx_abi ufbx_character *ufbx_as_character(const ufbx_element *element); +ufbx_abi ufbx_constraint *ufbx_as_constraint(const ufbx_element *element); +ufbx_abi ufbx_audio_layer *ufbx_as_audio_layer(const ufbx_element *element); +ufbx_abi ufbx_audio_clip *ufbx_as_audio_clip(const ufbx_element *element); +ufbx_abi ufbx_pose *ufbx_as_pose(const ufbx_element *element); +ufbx_abi ufbx_metadata_object *ufbx_as_metadata_object(const ufbx_element *element); + +// Functions for interfacing with DOM lists +ufbx_abi bool ufbx_dom_is_array(const ufbx_dom_node *node); +ufbx_abi size_t ufbx_dom_array_size(const ufbx_dom_node *node); +ufbx_abi ufbx_int32_list ufbx_dom_as_int32_list(const ufbx_dom_node *node); +ufbx_abi ufbx_int64_list ufbx_dom_as_int64_list(const ufbx_dom_node *node); +ufbx_abi ufbx_float_list ufbx_dom_as_float_list(const ufbx_dom_node *node); +ufbx_abi ufbx_double_list ufbx_dom_as_double_list(const ufbx_dom_node *node); +ufbx_abi ufbx_real_list ufbx_dom_as_real_list(const ufbx_dom_node *node); +ufbx_abi ufbx_blob_list ufbx_dom_as_blob_list(const ufbx_dom_node *node); + +#ifdef __cplusplus +} +#endif + +// bindgen-disable + +#if UFBX_CPP11 + +struct ufbx_string_view { + const char *data; + size_t length; + + ufbx_string_view() : data(nullptr), length(0) { } + ufbx_string_view(const char *data_, size_t length_) : data(data_), length(length_) { } + UFBX_CONVERSION_TO_IMPL(ufbx_string_view) +}; + +ufbx_inline ufbx_scene *ufbx_load_file(ufbx_string_view filename, const ufbx_load_opts *opts, ufbx_error *error) { return ufbx_load_file_len(filename.data, filename.length, opts, error); } +ufbx_inline ufbx_prop *ufbx_find_prop(const ufbx_props *props, ufbx_string_view name) { return ufbx_find_prop_len(props, name.data, name.length); } +ufbx_inline ufbx_real ufbx_find_real(const ufbx_props *props, ufbx_string_view name, ufbx_real def) { return ufbx_find_real_len(props, name.data, name.length, def); } +ufbx_inline ufbx_vec3 ufbx_find_vec3(const ufbx_props *props, ufbx_string_view name, ufbx_vec3 def) { return ufbx_find_vec3_len(props, name.data, name.length, def); } +ufbx_inline int64_t ufbx_find_int(const ufbx_props *props, ufbx_string_view name, int64_t def) { return ufbx_find_int_len(props, name.data, name.length, def); } +ufbx_inline bool ufbx_find_bool(const ufbx_props *props, ufbx_string_view name, bool def) { return ufbx_find_bool_len(props, name.data, name.length, def); } +ufbx_inline ufbx_string ufbx_find_string(const ufbx_props *props, ufbx_string_view name, ufbx_string def) { return ufbx_find_string_len(props, name.data, name.length, def); } +ufbx_inline ufbx_blob ufbx_find_blob(const ufbx_props *props, ufbx_string_view name, ufbx_blob def) { return ufbx_find_blob_len(props, name.data, name.length, def); } +ufbx_inline ufbx_element *ufbx_find_prop_element(const ufbx_element *element, ufbx_string_view name, ufbx_element_type type) { return ufbx_find_prop_element_len(element, name.data, name.length, type); } +ufbx_inline ufbx_element *ufbx_find_element(const ufbx_scene *scene, ufbx_element_type type, ufbx_string_view name) { return ufbx_find_element_len(scene, type, name.data, name.length); } +ufbx_inline ufbx_node *ufbx_find_node(const ufbx_scene *scene, ufbx_string_view name) { return ufbx_find_node_len(scene, name.data, name.length); } +ufbx_inline ufbx_anim_stack *ufbx_find_anim_stack(const ufbx_scene *scene, ufbx_string_view name) { return ufbx_find_anim_stack_len(scene, name.data, name.length); } +ufbx_inline ufbx_material *ufbx_find_material(const ufbx_scene *scene, ufbx_string_view name) { return ufbx_find_material_len(scene, name.data, name.length); } +ufbx_inline ufbx_anim_prop *ufbx_find_anim_prop(const ufbx_anim_layer *layer, const ufbx_element *element, ufbx_string_view prop) { return ufbx_find_anim_prop_len(layer, element, prop.data, prop.length); } +ufbx_inline ufbx_prop ufbx_evaluate_prop(const ufbx_anim *anim, const ufbx_element *element, ufbx_string_view name, double time) { return ufbx_evaluate_prop_len(anim, element, name.data, name.length, time); } +ufbx_inline ufbx_texture *ufbx_find_prop_texture(const ufbx_material *material, ufbx_string_view name) { return ufbx_find_prop_texture_len(material, name.data, name.length); } +ufbx_inline ufbx_string ufbx_find_shader_prop(const ufbx_shader *shader, ufbx_string_view name) { return ufbx_find_shader_prop_len(shader, name.data, name.length); } +ufbx_inline ufbx_shader_prop_binding_list ufbx_find_shader_prop_bindings(const ufbx_shader *shader, ufbx_string_view name) { return ufbx_find_shader_prop_bindings_len(shader, name.data, name.length); } +ufbx_inline ufbx_shader_texture_input *ufbx_find_shader_texture_input(const ufbx_shader_texture *shader, ufbx_string_view name) { return ufbx_find_shader_texture_input_len(shader, name.data, name.length); } +ufbx_inline ufbx_geometry_cache *ufbx_load_geometry_cache(ufbx_string_view filename, const ufbx_geometry_cache_opts *opts, ufbx_error *error) { return ufbx_load_geometry_cache_len(filename.data, filename.length, opts, error); } +ufbx_inline ufbx_dom_node *ufbx_dom_find(const ufbx_dom_node *parent, ufbx_string_view name) { return ufbx_dom_find_len(parent, name.data, name.length); } + +#endif + +#if UFBX_CPP11 + +template +struct ufbx_type_traits { enum { valid = 0 }; }; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_scene *ptr) { ufbx_retain_scene(ptr); } + static void free(ufbx_scene *ptr) { ufbx_free_scene(ptr); } +}; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_mesh *ptr) { ufbx_retain_mesh(ptr); } + static void free(ufbx_mesh *ptr) { ufbx_free_mesh(ptr); } +}; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_line_curve *ptr) { ufbx_retain_line_curve(ptr); } + static void free(ufbx_line_curve *ptr) { ufbx_free_line_curve(ptr); } +}; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_geometry_cache *ptr) { ufbx_retain_geometry_cache(ptr); } + static void free(ufbx_geometry_cache *ptr) { ufbx_free_geometry_cache(ptr); } +}; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_anim *ptr) { ufbx_retain_anim(ptr); } + static void free(ufbx_anim *ptr) { ufbx_free_anim(ptr); } +}; + +template<> struct ufbx_type_traits { + enum { valid = 1 }; + static void retain(ufbx_baked_anim *ptr) { ufbx_retain_baked_anim(ptr); } + static void free(ufbx_baked_anim *ptr) { ufbx_free_baked_anim(ptr); } +}; + +class ufbx_deleter { +public: + template + void operator()(T *ptr) const { + static_assert(ufbx_type_traits::valid, "ufbx_deleter() unsupported for type"); + ufbx_type_traits::free(ptr); + } +}; + +// RAII wrapper over refcounted ufbx types. + +// Behaves like `std::unique_ptr`. +template +class ufbx_unique_ptr { + T *ptr; + using traits = ufbx_type_traits; + static_assert(ufbx_type_traits::valid, "ufbx_unique_ptr unsupported for type"); +public: + ufbx_unique_ptr() noexcept : ptr(nullptr) { } + explicit ufbx_unique_ptr(T *ptr_) noexcept : ptr(ptr_) { } + ufbx_unique_ptr(ufbx_unique_ptr &&ref) noexcept : ptr(ref.ptr) { ref.ptr = nullptr; } + ~ufbx_unique_ptr() { traits::free(ptr); } + + ufbx_unique_ptr &operator=(ufbx_unique_ptr &&ref) noexcept { + if (&ref == this) return *this; + ptr = ref.ptr; + ref.ptr = nullptr; + return *this; + } + + void reset(T *new_ptr=nullptr) noexcept { + traits::free(ptr); + ptr = new_ptr; + } + + void swap(ufbx_unique_ptr &ref) noexcept { + T *tmp = ptr; + ptr = ref.ptr; + ref.ptr = tmp; + } + + T &operator*() const noexcept { return *ptr; } + T *operator->() const noexcept { return ptr; } + T *get() const noexcept { return ptr; } + explicit operator bool() const noexcept { return ptr != nullptr; } +}; + +// Behaves like `std::shared_ptr` except uses ufbx's internal reference counting, +// so it is half the size of a standard `shared_ptr` but might be marginally slower. +template +class ufbx_shared_ptr { + T *ptr; + using traits = ufbx_type_traits; + static_assert(ufbx_type_traits::valid, "ufbx_shared_ptr unsupported for type"); +public: + + ufbx_shared_ptr() noexcept : ptr(nullptr) { } + explicit ufbx_shared_ptr(T *ptr_) noexcept : ptr(ptr_) { } + ufbx_shared_ptr(const ufbx_shared_ptr &ref) noexcept : ptr(ref.ptr) { traits::retain(ref.ptr); } + ufbx_shared_ptr(ufbx_shared_ptr &&ref) noexcept : ptr(ref.ptr) { ref.ptr = nullptr; } + ~ufbx_shared_ptr() { traits::free(ptr); } + + ufbx_shared_ptr &operator=(const ufbx_shared_ptr &ref) noexcept { + if (&ref == this) return *this; + traits::free(ptr); + traits::retain(ref.ptr); + ptr = ref.ptr; + return *this; + } + + ufbx_shared_ptr &operator=(ufbx_shared_ptr &&ref) noexcept { + if (&ref == this) return *this; + ptr = ref.ptr; + ref.ptr = nullptr; + return *this; + } + + void reset(T *new_ptr=nullptr) noexcept { + traits::free(ptr); + ptr = new_ptr; + } + + void swap(ufbx_shared_ptr &ref) noexcept { + T *tmp = ptr; + ptr = ref.ptr; + ref.ptr = tmp; + } + + T &operator*() const noexcept { return *ptr; } + T *operator->() const noexcept { return ptr; } + T *get() const noexcept { return ptr; } + explicit operator bool() const noexcept { return ptr != nullptr; } +}; + +#endif +// bindgen-enable + +// -- Properties + +// Names of common properties in `ufbx_props`. +// Some of these differ from ufbx interpretations. + +// Local translation. +// Used by: `ufbx_node` +#define UFBX_Lcl_Translation "Lcl Translation" + +// Local rotation expressed in Euler degrees. +// Used by: `ufbx_node` +// The rotation order is defined by the `UFBX_RotationOrder` property. +#define UFBX_Lcl_Rotation "Lcl Rotation" + +// Local scaling factor, 3D vector. +// Used by: `ufbx_node` +#define UFBX_Lcl_Scaling "Lcl Scaling" + +// Euler rotation interpretation, used by `UFBX_Lcl_Rotation`. +// Used by: `ufbx_node`, enum value `ufbx_rotation_order`. +#define UFBX_RotationOrder "RotationOrder" + +// Scaling pivot: point around which scaling is performed. +// Used by: `ufbx_node`. +#define UFBX_ScalingPivot "ScalingPivot" + +// Scaling pivot: point around which rotation is performed. +// Used by: `ufbx_node`. +#define UFBX_RotationPivot "RotationPivot" + +// Scaling offset: translation added after scaling is performed. +// Used by: `ufbx_node`. +#define UFBX_ScalingOffset "ScalingOffset" + +// Rotation offset: translation added after rotation is performed. +// Used by: `ufbx_node`. +#define UFBX_RotationOffset "RotationOffset" + +// Pre-rotation: Rotation applied _after_ `UFBX_Lcl_Rotation`. +// Used by: `ufbx_node`. +// Affected by `UFBX_RotationPivot` but not `UFBX_RotationOrder`. +#define UFBX_PreRotation "PreRotation" + +// Post-rotation: Rotation applied _before_ `UFBX_Lcl_Rotation`. +// Used by: `ufbx_node`. +// Affected by `UFBX_RotationPivot` but not `UFBX_RotationOrder`. +#define UFBX_PostRotation "PostRotation" + +// Controls whether the node should be displayed or not. +// Used by: `ufbx_node`. +#define UFBX_Visibility "Visibility" + +// Weight of an animation layer in percentage (100.0 being full). +// Used by: `ufbx_anim_layer`. +#define UFBX_Weight "Weight" + +// Blend shape deformation weight (100.0 being full). +// Used by: `ufbx_blend_channel`. +#define UFBX_DeformPercent "DeformPercent" + +#if defined(_MSC_VER) + #pragma warning(pop) +#elif defined(__clang__) + #pragma clang diagnostic pop +#elif defined(__GNUC__) + #pragma GCC diagnostic pop +#endif + +#endif diff --git a/Samples/cpp/CMakeLists.txt b/Samples/cpp/CMakeLists.txt index 5dd136e2..1391f271 100644 --- a/Samples/cpp/CMakeLists.txt +++ b/Samples/cpp/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(Render) +add_subdirectory(RHI) diff --git a/Samples/cpp/RHI/CMakeLists.txt b/Samples/cpp/RHI/CMakeLists.txt new file mode 100644 index 00000000..8b1c7b0c --- /dev/null +++ b/Samples/cpp/RHI/CMakeLists.txt @@ -0,0 +1,68 @@ +# Draconic RHI samples + their shared framework (GPU/display dependent - built, +# not run in CI). + +# Shared sample framework: window bring-up (SDL3 shell), backend/device/swapchain, +# main loop, shader-compile helpers. Links every RHI backend + the shader compiler. +# DX12 is Windows-only (DRACO_HAS_DX12). +add_modules_library(Framework) +target_link_libraries(Framework PUBLIC + Core + Rendering_RHI + Rendering_RHI_Vulkan + Rendering_RHI_Validation + Rendering_Shaders + Shell) +if(WIN32) + target_link_libraries(Framework PUBLIC Rendering_RHI_DX12) + target_compile_definitions(Framework PUBLIC DRACO_HAS_DX12) +endif() + +# Each sample is one Main.cpp linking the framework. SDL3 is static, so its +# constructor-registered video/input drivers must be force-loaded (whole archive) +# at the executable link or they get stripped. +function(draco_add_rhi_sample name) + add_executable(${name} Main.cpp) + target_link_libraries(${name} PRIVATE Framework) + target_compile_options(${name} PRIVATE + "$<$:-Wno-error=unused-parameter;-Wno-error=unused-variable;-Wno-error=missing-field-initializers;-Wno-error=misleading-indentation>") + if(UNIX AND NOT APPLE) + target_link_libraries(${name} PRIVATE Threads::Threads dl) + target_link_options(${name} PRIVATE -Wl,--whole-archive $ -Wl,--no-whole-archive) + elseif(APPLE) + target_link_options(${name} PRIVATE -Wl,-force_load,$) + elseif(MSVC) + target_link_options(${name} PRIVATE /WHOLEARCHIVE:$) + endif() +endfunction() + +add_subdirectory(Sample001_Triangle) +add_subdirectory(Sample002_Textures) +add_subdirectory(Sample003_UniformBuffers) +add_subdirectory(Sample004_Compute) +add_subdirectory(Sample005_BindGroups) +add_subdirectory(Sample006_Blending) +add_subdirectory(Sample007_Instancing) +add_subdirectory(Sample008_DepthBuffer) +add_subdirectory(Sample009_Mipmaps) +add_subdirectory(Sample010_MSAA) +add_subdirectory(Sample011_MRT) +add_subdirectory(Sample012_Wireframe) +add_subdirectory(Sample013_BorderSampler) +add_subdirectory(Sample014_Blit) +add_subdirectory(Sample015_Queries) +add_subdirectory(Sample016_Readback) +add_subdirectory(Sample017_MultiQueue) +add_subdirectory(Sample018_Bindless) +add_subdirectory(Sample019_BatchUpload) +add_subdirectory(Sample020_MeshShaders) +add_subdirectory(Sample021_RayTracing) +add_subdirectory(Sample022_StencilOutline) +add_subdirectory(Sample023_CubeMap) +add_subdirectory(Sample024_OcclusionQuery) +add_subdirectory(Sample025_MultiDrawIndirect) +add_subdirectory(Sample026_DynamicOffsets) +add_subdirectory(Sample027_3DTexture) +add_subdirectory(Sample028_ProceduralRT) +add_subdirectory(Sample029_ResolveTexture) +add_subdirectory(Sample030_RenderBundles) +add_subdirectory(Smoketest) diff --git a/Samples/cpp/RHI/Framework/DepthBuffer.cppm b/Samples/cpp/RHI/Framework/DepthBuffer.cppm new file mode 100644 index 00000000..e0391a5f --- /dev/null +++ b/Samples/cpp/RHI/Framework/DepthBuffer.cppm @@ -0,0 +1,44 @@ +/// Reusable depth buffer helper for samples. + +export module samples.rhi.framework:depth_buffer; + +import core.stdtypes; +import core.status; +import rhi; + +using namespace draco; + +export namespace draco::samples::framework { + +struct DepthBuffer { + rhi::Texture* texture = nullptr; + rhi::TextureView* view = nullptr; + rhi::TextureFormat format = rhi::TextureFormat::Depth24PlusStencil8; + + Status recreate(rhi::Device* device, u32 width, u32 height, u32 sampleCount = 1) { + destroy(device); + auto desc = rhi::TextureDesc::depthBuffer(format, width, height, sampleCount, u8"Depth"); + if (device->createTexture(desc, texture) != ErrorCode::Ok) return ErrorCode::Unknown; + + rhi::TextureViewDesc vd{}; + vd.format = format; + vd.dimension = rhi::TextureViewDimension::Texture2D; + vd.baseMipLevel = 0; + vd.mipLevelCount = 1; + vd.baseArrayLayer = 0; + vd.arrayLayerCount = 1; + vd.label = u8"DepthView"; + if (device->createTextureView(texture, vd, view) != ErrorCode::Ok) { + device->destroyTexture(texture); + return ErrorCode::Unknown; + } + return ErrorCode::Ok; + } + + void destroy(rhi::Device* device) { + if (view) { device->destroyTextureView(view); view = nullptr; } + if (texture) { device->destroyTexture(texture); texture = nullptr; } + } +}; + +} // namespace draco::samples::framework diff --git a/Samples/cpp/RHI/Framework/Framework.cppm b/Samples/cpp/RHI/Framework/Framework.cppm new file mode 100644 index 00000000..b71bfd54 --- /dev/null +++ b/Samples/cpp/RHI/Framework/Framework.cppm @@ -0,0 +1,8 @@ +export module samples.rhi.framework; + +export import rhi; +export import shell; + +export import :sample_app; +export import :depth_buffer; +export import :shader_helpers; diff --git a/Samples/cpp/RHI/Framework/SampleApp.cppm b/Samples/cpp/RHI/Framework/SampleApp.cppm new file mode 100644 index 00000000..2dc025e5 --- /dev/null +++ b/Samples/cpp/RHI/Framework/SampleApp.cppm @@ -0,0 +1,233 @@ +// SampleApp - abstract base for RHI samples. Brings up a window (shell), a Vulkan +// backend (validation-wrapped), device, queue, and swap chain; pumps events, tracks +// timing, and calls onRender(). Resize is detected by polling the window size; the +// loop skips rendering while minimized. + +module; + +#include +#include +#include +#include + +export module samples.rhi.framework:sample_app; + +import core.stdtypes; +import core.status; +import rhi; +import rhi.vk; +#ifdef DRACO_HAS_DX12 +import rhi.dx12; +#endif +import rhi.validation; +import shell; +import shell.desktop; + +using namespace draco; + +export namespace draco::samples::framework { + +enum class BackendType { Vulkan, DX12 }; + +class SampleApp { +public: + explicit SampleApp(BackendType backend = BackendType::Vulkan, bool validation = true) + : m_backendType(backend), m_validationEnabled(validation) {} + virtual ~SampleApp() = default; + + SampleApp(const SampleApp&) = delete; + SampleApp& operator=(const SampleApp&) = delete; + + int run(int argc = 0, char** argv = nullptr); + +protected: + virtual std::u8string_view title() const { return u8"Draconic Sample"; } + virtual rhi::DeviceFeatures requiredFeatures() const { return {}; } + virtual rhi::TextureFormat swapChainFormat() const { return rhi::TextureFormat::RGBA8UnormSrgb; } + virtual rhi::PresentMode presentMode() const { return rhi::PresentMode::Fifo; } + virtual u32 bufferCount() const { return 2; } + + virtual Status onInit() = 0; + virtual void onRender() = 0; + virtual void onResize(u32, u32) {} + virtual void onShutdown() = 0; + + shell::IShell* m_shell = nullptr; // owns the shell (created in init(), freed in shutdown()) + shell::IWindow* m_window = nullptr; + rhi::Backend* m_backend = nullptr; + rhi::Device* m_device = nullptr; + rhi::Queue* m_graphicsQueue = nullptr; + rhi::Surface* m_surface = nullptr; + rhi::SwapChain* m_swapChain = nullptr; + + u32 m_width = 1280; + u32 m_height = 720; + bool m_running = false; + f32 m_deltaTime = 0.0f; + f32 m_totalTime = 0.0f; + + void exit() { m_running = false; } + +private: + BackendType m_backendType; + bool m_validationEnabled; + + Status init(); + void mainLoop(); + void shutdown(); + Status createBackend(); + Status createSwapChain(); + void checkAndResize(); +}; + +inline int SampleApp::run(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--dx12") == 0 || std::strcmp(argv[i], "--d3d12") == 0) { + m_backendType = BackendType::DX12; + } else if (std::strcmp(argv[i], "--vk") == 0 || std::strcmp(argv[i], "--vulkan") == 0) { + m_backendType = BackendType::Vulkan; + } else if (std::strcmp(argv[i], "--novalidation") == 0) { + m_validationEnabled = false; + } + } + + if (!init().isOk()) { shutdown(); return 1; } + mainLoop(); + shutdown(); + return 0; +} + +inline Status SampleApp::init() { + shell::WindowSettings ws{}; + ws.title = title(); + ws.width = m_width; + ws.height = m_height; + + m_shell = shell::createShell(ws); + if (m_shell == nullptr || m_shell->mainWindow() == nullptr) { + rhi::logError("SampleApp: shell/window init failed"); return ErrorCode::Unknown; + } + m_window = m_shell->mainWindow(); + m_width = m_window->width(); + m_height = m_window->height(); + + if (!createBackend().isOk()) return ErrorCode::Unknown; + + const shell::NativeWindow nw = m_window->native(); + if (!m_backend->createSurface(nw.window, nw.display, m_surface).isOk()) { + rhi::logError("SampleApp: createSurface failed"); return ErrorCode::Unknown; + } + + auto adapters = m_backend->enumerateAdapters(); + if (adapters.size() == 0) { rhi::logError("SampleApp: no adapters"); return ErrorCode::Unknown; } + rhi::Adapter* adapter = adapters[0]; // best GPU first + + { + rhi::AdapterInfo ai = adapter->info(); + const char* backendName = (m_backendType == BackendType::DX12) ? "DX12" : "Vulkan"; + const std::u8string name8 = std::u8string(ai.name); + std::printf("SampleApp: backend=%s adapter=%s\n", + backendName, reinterpret_cast(name8.c_str())); + } + + rhi::DeviceDesc dd{}; + dd.graphicsQueueCount = 1; + dd.requiredFeatures = requiredFeatures(); + if (!adapter->createDevice(dd, m_device).isOk()) { + rhi::logError("SampleApp: createDevice failed"); return ErrorCode::Unknown; + } + + m_graphicsQueue = m_device->getQueue(rhi::QueueType::Graphics); + if (m_graphicsQueue == nullptr) { rhi::logError("SampleApp: no graphics queue"); return ErrorCode::Unknown; } + + if (!createSwapChain().isOk()) return ErrorCode::Unknown; + return onInit(); +} + +inline Status SampleApp::createBackend() { + rhi::Backend* raw = nullptr; + switch (m_backendType) { + case BackendType::Vulkan: { + rhi::vk::VkBackendDesc desc{}; + desc.enableValidation = m_validationEnabled; + if (!rhi::vk::createBackend(desc, raw).isOk()) { + rhi::logError("SampleApp: vk::createBackend failed"); return ErrorCode::Unknown; + } + break; + } + case BackendType::DX12: { +#ifdef DRACO_HAS_DX12 + rhi::dx12::DxBackendDesc desc{}; + desc.enableValidation = m_validationEnabled; + if (!rhi::dx12::createDxBackend(desc, raw).isOk()) { + rhi::logError("SampleApp: dx12::createDxBackend failed"); return ErrorCode::Unknown; + } +#else + rhi::logError("SampleApp: DX12 backend not available on this platform"); + return ErrorCode::Unknown; +#endif + break; + } + } + m_backend = m_validationEnabled ? rhi::validation::createValidatedBackend(raw) : raw; + return m_backend != nullptr ? Status{} : Status{ ErrorCode::Unknown }; +} + +inline Status SampleApp::createSwapChain() { + rhi::SwapChainDesc desc{}; + desc.width = m_width; + desc.height = m_height; + desc.format = swapChainFormat(); + desc.presentMode = presentMode(); + desc.bufferCount = bufferCount(); + desc.label = u8"main"; + if (!m_device->createSwapChain(m_surface, desc, m_swapChain).isOk()) { + rhi::logError("SampleApp: createSwapChain failed"); return ErrorCode::Unknown; + } + return Status{}; +} + +inline void SampleApp::mainLoop() { + using clock = std::chrono::steady_clock; + using dur = std::chrono::duration; + + m_running = true; + auto lastTime = clock::now(); + + while (m_running && m_shell->isRunning()) { + m_shell->processEvents(); + + const auto now = clock::now(); + m_deltaTime = dur(now - lastTime).count(); + lastTime = now; + m_totalTime += m_deltaTime; + + if (!m_window->isMinimized()) { + checkAndResize(); + onRender(); + } + } +} + +inline void SampleApp::checkAndResize() { + const u32 nw = m_window->width(); + const u32 nh = m_window->height(); + if (nw == 0 || nh == 0) return; + if (nw == m_width && nh == m_height) return; + m_width = nw; m_height = nh; + m_device->waitIdle(); + m_swapChain->resize(m_width, m_height); + onResize(m_width, m_height); +} + +inline void SampleApp::shutdown() { + if (m_device) m_device->waitIdle(); + onShutdown(); + if (m_swapChain) { m_device->destroySwapChain(m_swapChain); m_swapChain = nullptr; } + if (m_surface) { m_device->destroySurface(m_surface); m_surface = nullptr; } + if (m_device) { m_device->destroy(); m_device = nullptr; } + if (m_backend) { m_backend->destroy(); m_backend = nullptr; } + if (m_shell != nullptr) { shell::destroyShell(m_shell); m_shell = nullptr; } // destroys the window + shell +} + +} // namespace draco::samples::framework diff --git a/Samples/cpp/RHI/Framework/ShaderHelpers.cppm b/Samples/cpp/RHI/Framework/ShaderHelpers.cppm new file mode 100644 index 00000000..f096d678 --- /dev/null +++ b/Samples/cpp/RHI/Framework/ShaderHelpers.cppm @@ -0,0 +1,78 @@ +/// Shader compilation helpers - wraps DXC compile + createShaderModule into one call. +/// Automatically selects SPIR-V (Vulkan) or DXIL (DX12) based on device type. +/// Applies Vulkan binding shifts when targeting SPIR-V. + +module; + +#include +#include +#include + +export module samples.rhi.framework:shader_helpers; + +import core.stdtypes; +import core.status; +import rhi; +import shaders; + +using namespace draco; + +export namespace draco::samples::framework { + +/// Compile HLSL source to a ShaderModule with a specific shader model. +inline Status compileToModule(shaders::Compiler* compiler, rhi::Device* device, + std::u8string_view hlslSource, shaders::ShaderStage stage, + std::u8string_view entryPoint, std::u8string_view label, + std::u8string_view shaderModel, + rhi::ShaderModule*& out) { + out = nullptr; + + bool isDX12 = (device->type == rhi::DeviceType::DX12); + auto target = isDX12 ? shaders::ShaderTarget::DXIL : shaders::ShaderTarget::SPIRV; + + shaders::CompileOptions opts{}; + opts.shaderModel = shaderModel; + opts.optimizationLevel = 3; + + // Vulkan needs binding shifts; DX12 uses register spaces natively. + if (!isDX12) { + opts.bindingShifts.constantBufferShift = 0; + opts.bindingShifts.textureShift = 1000; + opts.bindingShifts.uavShift = 2000; + opts.bindingShifts.samplerShift = 3000; + opts.bindingShiftSets = 4; + } + + shaders::CompileResult cr{}; + Status r = compiler->compile( + reinterpret_cast(hlslSource.data()), hlslSource.size(), + stage, entryPoint, target, opts, cr); + + if (r != ErrorCode::Ok) { + if (cr.messages) { + const std::u8string lbl = std::u8string(label); + rhi::logErrorf("Shader compile failed (%s): %s", + reinterpret_cast(lbl.c_str()), cr.messages); + } + compiler->freeResult(cr); + return ErrorCode::Unknown; + } + + rhi::ShaderModuleDesc desc{}; + desc.code = std::span(cr.bytecode, cr.bytecodeSize); + desc.label = label; + r = device->createShaderModule(desc, out); + + compiler->freeResult(cr); + return r; +} + +/// Compile HLSL source to a ShaderModule. Default shader model 6.0. +inline Status compileToModule(shaders::Compiler* compiler, rhi::Device* device, + std::u8string_view hlslSource, shaders::ShaderStage stage, + std::u8string_view entryPoint, std::u8string_view label, + rhi::ShaderModule*& out) { + return compileToModule(compiler, device, hlslSource, stage, entryPoint, label, u8"6_0", out); +} + +} // namespace draco::samples::framework diff --git a/Samples/cpp/RHI/Sample001_Triangle/CMakeLists.txt b/Samples/cpp/RHI/Sample001_Triangle/CMakeLists.txt new file mode 100644 index 00000000..36b506c4 --- /dev/null +++ b/Samples/cpp/RHI/Sample001_Triangle/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample001_Triangle) diff --git a/Samples/cpp/RHI/Sample001_Triangle/Main.cpp b/Samples/cpp/RHI/Sample001_Triangle/Main.cpp new file mode 100644 index 00000000..e3e6e933 --- /dev/null +++ b/Samples/cpp/RHI/Sample001_Triangle/Main.cpp @@ -0,0 +1,175 @@ +#include +/// Renders a colored triangle using vertex buffer + render pipeline. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class TriangleSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample001 - Triangle"; } + +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; + +private: + static constexpr const char8_t kShaderSource[] = u8R"( + struct VSInput { + float3 Position : TEXCOORD0; + float3 Color : TEXCOORD1; + }; + struct PSInput { + float4 Position : SV_POSITION; + float3 Color : TEXCOORD0; + }; + PSInput VSMain(VSInput input) { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET { + return float4(input.Color, 1.0); + } + )"; + + static constexpr float kVertexData[] = { + 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, + }; + + shaders::Compiler* m_compiler = nullptr; + rhi::Buffer* m_vertexBuf = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status TriangleSample::onInit() { + using draco::Status; + + // Shader compiler. + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Vertex, u8"VSMain", u8"TriangleVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Fragment, u8"PSMain", u8"TrianglePS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex buffer. + rhi::BufferDesc bd{}; bd.size = sizeof(kVertexData); bd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; bd.label = u8"TriangleVB"; + if (m_device->createBuffer(bd, m_vertexBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Upload. + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vertexBuf, 0, std::span(reinterpret_cast(kVertexData), sizeof(kVertexData))); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + + // Pipeline layout (empty). + rhi::BindGroupLayoutDesc bglDesc{}; bglDesc.label = u8"EmptyBGL"; + if (m_device->createBindGroupLayout(bglDesc, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::PipelineLayoutDesc pld{}; + rhi::BindGroupLayout* sets[1] = { m_bgl }; + pld.bindGroupLayouts = std::span(sets, 1); + pld.label = u8"TrianglePL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render pipeline. + rhi::VertexAttribute attrs[2] = { + { rhi::VertexFormat::Float32x3, 0, 0 }, + { rhi::VertexFormat::Float32x3, 12, 1 }, + }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 24; vbl.attributes = std::span(attrs, 2); + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); ct.writeMask = rhi::ColorWriteMask::All; + + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; + rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"TrianglePipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Command pool + fence. + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + return draco::ErrorCode::Ok; +} + +void TriangleSample::onRender() { + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; + ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.1f, 0.1f, 0.15f, 1.0f); + + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vertexBuf, 0); + rp->draw(3); + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void TriangleSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_vertexBuf) m_device->destroyBuffer(m_vertexBuf); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { TriangleSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample002_Textures/CMakeLists.txt b/Samples/cpp/RHI/Sample002_Textures/CMakeLists.txt new file mode 100644 index 00000000..2c98b126 --- /dev/null +++ b/Samples/cpp/RHI/Sample002_Textures/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample002_Textures) diff --git a/Samples/cpp/RHI/Sample002_Textures/Main.cpp b/Samples/cpp/RHI/Sample002_Textures/Main.cpp new file mode 100644 index 00000000..0d3f89f1 --- /dev/null +++ b/Samples/cpp/RHI/Sample002_Textures/Main.cpp @@ -0,0 +1,206 @@ +#include +/// Renders a checkerboard-textured quad using texture, sampler, bind group. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class TextureSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample002 - Textured Quad"; } + +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; + +private: + static constexpr const char8_t kShaderSource[] = u8R"( + Texture2D gTexture : register(t0, space0); + SamplerState gSampler : register(s0, space0); + struct VSInput { float3 Position : TEXCOORD0; float2 TexCoord : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float2 TexCoord : TEXCOORD0; }; + PSInput VSMain(VSInput input) { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.TexCoord = input.TexCoord; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET { + return gTexture.Sample(gSampler, input.TexCoord); + } + )"; + + static constexpr float kVertexData[] = { + -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, -0.5f, 0.0f, 1.0f, 1.0f, + -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, + }; + static constexpr draco::u16 kIndexData[] = { 0, 1, 2, 0, 2, 3 }; + + shaders::Compiler* m_compiler = nullptr; + rhi::Buffer* m_vb = nullptr; + rhi::Buffer* m_ib = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + rhi::Texture* m_tex = nullptr; + rhi::TextureView* m_texView = nullptr; + rhi::Sampler* m_sampler = nullptr; + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::BindGroup* m_bg = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status TextureSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Vertex, u8"VSMain", u8"QuadVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Fragment, u8"PSMain", u8"QuadPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex + index buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVertexData); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIndexData); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Checkerboard texture 64x64 RGBA8. + constexpr u32 tw = 64, th = 64; + u8 texPixels[tw * th * 4]; + for (u32 y = 0; y < th; ++y) + for (u32 x = 0; x < tw; ++x) { + bool checker = ((x / 8) + (y / 8)) % 2 == 0; + u32 i = (y * tw + x) * 4; + texPixels[i] = checker ? 255 : 50; texPixels[i+1] = checker ? 255 : 50; + texPixels[i+2] = checker ? 255 : 200; texPixels[i+3] = 255; + } + + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = tw; td.height = th; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + if (m_device->createTexture(td, m_tex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Upload. + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVertexData), sizeof(kVertexData))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIndexData), sizeof(kIndexData))); + rhi::TextureDataLayout layout{}; layout.bytesPerRow = tw * 4; layout.rowsPerImage = th; + batch->writeTexture(m_tex, std::span(texPixels, sizeof(texPixels)), layout, rhi::Extent3D{tw, th, 1}); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + + // Texture view + sampler. + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_tex, tvd, m_texView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Nearest; sd.magFilter = rhi::FilterMode::Nearest; + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bind group layout + bind group. + rhi::BindGroupLayoutEntry bglEntries[2] = { + rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment), + }; + rhi::BindGroupLayoutDesc bgld{}; bgld.entries = std::span(bglEntries, 2); + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupEntry bgEntries[2] = { + rhi::BindGroupEntry::textureEntry(m_texView), + rhi::BindGroupEntry::samplerEntry(m_sampler), + }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_bgl; bgd.entries = std::span(bgEntries, 2); + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout. + rhi::BindGroupLayout* sets[1] = { m_bgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 1); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render pipeline. + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x2, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 20; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); ct.writeMask = rhi::ColorWriteMask::All; + + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + return draco::ErrorCode::Ok; +} + +void TextureSample::onRender() { + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.2f, 0.2f, 0.25f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setBindGroup(0, m_bg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(6); + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void TextureSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_sampler) m_device->destroySampler(m_sampler); + if (m_texView) m_device->destroyTextureView(m_texView); + if (m_tex) m_device->destroyTexture(m_tex); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { TextureSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample003_UniformBuffers/CMakeLists.txt b/Samples/cpp/RHI/Sample003_UniformBuffers/CMakeLists.txt new file mode 100644 index 00000000..80c36c47 --- /dev/null +++ b/Samples/cpp/RHI/Sample003_UniformBuffers/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample003_UniformBuffers) diff --git a/Samples/cpp/RHI/Sample003_UniformBuffers/Main.cpp b/Samples/cpp/RHI/Sample003_UniformBuffers/Main.cpp new file mode 100644 index 00000000..5255eb95 --- /dev/null +++ b/Samples/cpp/RHI/Sample003_UniformBuffers/Main.cpp @@ -0,0 +1,195 @@ +#include +/// Sample003 - Rotating Cube with Uniform Buffers + Push Constants. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class UniformBufferSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample003 - Rotating Cube (Uniform Buffers)"; } + +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { m_depthBuf.recreate(m_device, w, h); } + void onShutdown() override; + +private: + static constexpr const char8_t kShaderSource[] = u8R"( + cbuffer UBO : register(b0, space0) { row_major float4x4 MVP; }; + struct PushData { float4 Tint; }; + [[vk::push_constant]] ConstantBuffer gPush : register(b0, space1); + struct VSInput { float3 Position : TEXCOORD0; float3 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Color : COLOR0; }; + PSInput VSMain(VSInput input) { + PSInput o; o.Position = mul(float4(input.Position, 1.0), MVP); o.Color = input.Color; return o; + } + float4 PSMain(PSInput input) : SV_TARGET { return float4(input.Color * gPush.Tint.rgb, 1.0); } + )"; + + static constexpr float kCubeVerts[] = { + -0.5f,-0.5f,-0.5f, 1,0,0, 0.5f,-0.5f,-0.5f, 0,1,0, 0.5f, 0.5f,-0.5f, 0,0,1, -0.5f, 0.5f,-0.5f, 1,1,0, + -0.5f,-0.5f, 0.5f, 1,0,1, 0.5f,-0.5f, 0.5f, 0,1,1, 0.5f, 0.5f, 0.5f, 1,1,1, -0.5f, 0.5f, 0.5f, .5f,.5f,.5f, + }; + static constexpr draco::u16 kCubeIdx[] = { + 0,2,1, 0,3,2, 4,5,6, 4,6,7, 4,7,3, 4,3,0, 1,2,6, 1,6,5, 3,7,6, 3,6,2, 4,0,1, 4,1,5, + }; + + shaders::Compiler* m_compiler = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_ub = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::BindGroup* m_bg = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; + void* m_ubMapped = nullptr; + sf::DepthBuffer m_depthBuf; +}; + +draco::Status UniformBufferSample::onInit() { + using draco::Status, std::span, draco::u8; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Vertex, u8"VSMain", u8"CubeVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Fragment, u8"PSMain", u8"CubePS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kCubeVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kCubeIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ubd{}; ubd.size = 64; ubd.usage = rhi::BufferUsage::Uniform; ubd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(ubd, m_ub) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_ubMapped = m_ub->map(); + + // Upload. + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kCubeVerts), sizeof(kCubeVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kCubeIdx), sizeof(kCubeIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Bind group layout + group. + rhi::BindGroupLayoutEntry bglE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment) }; + rhi::BindGroupLayoutDesc bgld{}; bgld.entries = std::span(bglE, 1); + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupEntry bgE[1] = { rhi::BindGroupEntry::bufferEntry(m_ub, 0, 64) }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_bgl; bgd.entries = std::span(bgE, 1); + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout with push constants. + rhi::BindGroupLayout* sets[1] = { m_bgl }; + rhi::PushConstantRange pc{ rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, 0, 16 }; + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(sets, 1); + pld.pushConstantRanges = std::span(&pc, 1); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Depth buffer. + m_depthBuf.recreate(m_device, m_width, m_height); + + // Render pipeline. + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x3, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 24; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive = { rhi::PrimitiveTopology::TriangleList, rhi::FrontFace::CW, rhi::CullMode::Back }; + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void UniformBufferSample::onRender() { + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Update MVP. + draco::f32 aspect = static_cast(m_width) / static_cast(m_height); + draco::f32 angle = m_totalTime * 1.2f; + Matrix4 model = (Matrix4::rotationY(angle) * Matrix4::rotationX( angle * 0.7f)); + Matrix4 view = Matrix4::lookAtRH(draco::math::Vector3{0, 1.5f, -3}, draco::math::Vector3{ 0, 0, 0}, draco::math::Vector3{ 0, 1, 0}); + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(45.0f), aspect, 0.1f, 100.0f); + Matrix4 mvp = model * view * proj; + std::memcpy(m_ubMapped, mvp.data(), 64); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.1f, 0.1f, 0.15f, 1.0f); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setBindGroup(0, m_bg); + draco::f32 pulse = (std::sin(m_totalTime * 2.0f) * 0.3f + 0.7f); + draco::f32 tint[4] = { pulse, pulse, pulse, 1.0f }; + rp->setPushConstants(rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, 0, 16, tint); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(36); + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void UniformBufferSample::onShutdown() { + m_depthBuf.destroy(m_device); + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_ub) m_device->destroyBuffer(m_ub); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { UniformBufferSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample004_Compute/CMakeLists.txt b/Samples/cpp/RHI/Sample004_Compute/CMakeLists.txt new file mode 100644 index 00000000..8fe81e81 --- /dev/null +++ b/Samples/cpp/RHI/Sample004_Compute/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample004_Compute) diff --git a/Samples/cpp/RHI/Sample004_Compute/Main.cpp b/Samples/cpp/RHI/Sample004_Compute/Main.cpp new file mode 100644 index 00000000..59b437b3 --- /dev/null +++ b/Samples/cpp/RHI/Sample004_Compute/Main.cpp @@ -0,0 +1,214 @@ +#include +/// Sample004 - Compute Shader (Animated Point Grid). + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class ComputeSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample004 - Compute (Animated Point Grid)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { m_depthBuf.recreate(m_device, w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kComputeSrc[] = u8R"( + cbuffer Params : register(b0, space0) { float Time; uint NumPoints; float Spacing; float Padding; }; + struct Vertex { float PosX, PosY, PosZ, ColR, ColG, ColB; }; + RWStructuredBuffer gVertices : register(u0, space0); + [numthreads(64, 1, 1)] + void CSMain(uint3 dtid : SV_DispatchThreadID) { + uint idx = dtid.x; if (idx >= NumPoints) return; + uint gridSize = (uint)sqrt((float)NumPoints); + uint row = idx / gridSize, col = idx % gridSize; + float fx = ((float)col / (float)(gridSize-1))*2.0 - 1.0; + float fz = ((float)row / (float)(gridSize-1))*2.0 - 1.0; + float dist = sqrt(fx*fx + fz*fz); + float fy = sin(dist*6.0 - Time*2.0) * 0.15; + gVertices[idx].PosX = fx; gVertices[idx].PosY = fy; gVertices[idx].PosZ = fz; + gVertices[idx].ColR = fx*0.5+0.5; gVertices[idx].ColG = fy*2.0+0.5; gVertices[idx].ColB = fz*0.5+0.5; + } + )"; + static constexpr const char8_t kRenderSrc[] = u8R"( + cbuffer ViewProj : register(b0, space0) { row_major float4x4 VP; }; + struct VSInput { float3 Position : TEXCOORD0; float3 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Color : COLOR0; + [[vk::builtin("PointSize")]] float PointSize : PSIZE; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = mul(float4(i.Position,1), VP); o.Color = i.Color; o.PointSize = 1.0; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return float4(i.Color, 1.0); } + )"; + + static constexpr draco::u32 kGrid = 64, kNumPts = kGrid*kGrid, kVertSz = 24, kBufSz = kNumPts*kVertSz; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_cs = nullptr, *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vtxBuf = nullptr, *m_paramsBuf = nullptr, *m_vpBuf = nullptr; + void *m_paramsMapped = nullptr, *m_vpMapped = nullptr; + rhi::BindGroupLayout *m_compBgl = nullptr, *m_renBgl = nullptr; + rhi::BindGroup *m_compBg = nullptr, *m_renBg = nullptr; + rhi::PipelineLayout *m_compPl = nullptr, *m_renPl = nullptr; + rhi::ComputePipeline *m_compPipe = nullptr; + rhi::RenderPipeline *m_renPipe = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; + sf::DepthBuffer m_depthBuf; +}; + +draco::Status ComputeSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kComputeSrc, shaders::ShaderStage::Compute, u8"CSMain", u8"CS", m_cs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kRenderSrc, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kRenderSrc, shaders::ShaderStage::Fragment,u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffers. + rhi::BufferDesc vbd{}; vbd.size = kBufSz; vbd.usage = rhi::BufferUsage::Storage | rhi::BufferUsage::Vertex; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vtxBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc pbd{}; pbd.size = 16; pbd.usage = rhi::BufferUsage::Uniform; pbd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(pbd, m_paramsBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_paramsMapped = m_paramsBuf->map(); + rhi::BufferDesc vpd{}; vpd.size = 64; vpd.usage = rhi::BufferUsage::Uniform; vpd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(vpd, m_vpBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_vpMapped = m_vpBuf->map(); + + // Compute BGL + BG + PL + pipeline. + rhi::BindGroupLayoutEntry cE[2] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Compute), + rhi::BindGroupLayoutEntry::storageBuffer(0, rhi::ShaderStage::Compute, false) }; + rhi::BindGroupLayoutDesc cBgld{}; cBgld.entries = std::span(cE, 2); + if (m_device->createBindGroupLayout(cBgld, m_compBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry cBgE[2] = { rhi::BindGroupEntry::bufferEntry(m_paramsBuf, 0, 16), + rhi::BindGroupEntry::bufferEntry(m_vtxBuf, 0, kBufSz) }; + rhi::BindGroupDesc cBgd{}; cBgd.layout = m_compBgl; cBgd.entries = std::span(cBgE, 2); + if (m_device->createBindGroup(cBgd, m_compBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* cSets[1] = { m_compBgl }; + rhi::PipelineLayoutDesc cPld{}; cPld.bindGroupLayouts = std::span(cSets, 1); + if (m_device->createPipelineLayout(cPld, m_compPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::ComputePipelineDesc cpd{}; cpd.layout = m_compPl; cpd.compute = { m_cs, u8"CSMain", rhi::ShaderStage::Compute }; + if (m_device->createComputePipeline(cpd, m_compPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render BGL + BG + PL + pipeline. + rhi::BindGroupLayoutEntry rE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex) }; + rhi::BindGroupLayoutDesc rBgld{}; rBgld.entries = std::span(rE, 1); + if (m_device->createBindGroupLayout(rBgld, m_renBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry rBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_vpBuf, 0, 64) }; + rhi::BindGroupDesc rBgd{}; rBgd.layout = m_renBgl; rBgd.entries = std::span(rBgE, 1); + if (m_device->createBindGroup(rBgd, m_renBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* rSets[1] = { m_renBgl }; + rhi::PipelineLayoutDesc rPld{}; rPld.bindGroupLayouts = std::span(rSets, 1); + if (m_device->createPipelineLayout(rPld, m_renPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + m_depthBuf.recreate(m_device, m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x3, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = kVertSz; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_renPl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::PointList; + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_renPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void ComputeSample::onRender() { + using draco::u32, draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Update params. + u32 numPts = kNumPts; + f32 params[4] = { m_totalTime, 0, 1.0f, 0 }; + std::memcpy(¶ms[1], &numPts, 4); + std::memcpy(m_paramsMapped, params, 16); + + // Update VP. + f32 aspect = static_cast(m_width) / static_cast(m_height); + f32 camAngle = m_totalTime * 0.3f, camDist = 2.5f; + Matrix4 view = Matrix4::lookAtRH(draco::math::Vector3{std::sin(camAngle)*camDist, 1.2f, std::cos(camAngle)*camDist}, draco::math::Vector3{ 0,0,0}, draco::math::Vector3{0,1,0}); + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(45.0f), aspect, 0.1f, 100.0f); + Matrix4 vp = view * proj; + std::memcpy(m_vpMapped, vp.data(), 64); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Compute pass. + rhi::BufferBarrier bb{}; bb.buffer = m_vtxBuf; bb.oldState = rhi::ResourceState::VertexBuffer; bb.newState = rhi::ResourceState::ShaderWrite; + rhi::BarrierGroup bg1{}; bg1.bufferBarriers = std::span(&bb, 1); + enc->barrier(bg1); + auto* cp = enc->beginComputePass(u8"GenerateVertices"); + cp->setPipeline(m_compPipe); cp->setBindGroup(0, m_compBg); + cp->dispatch((kNumPts + 63) / 64); cp->end(); + bb.oldState = rhi::ResourceState::ShaderWrite; bb.newState = rhi::ResourceState::VertexBuffer; + enc->barrier(bg1); + + // Render pass. + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1.0f); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_renPipe); rp->setBindGroup(0, m_renBg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vtxBuf, 0); + rp->draw(kNumPts); rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void ComputeSample::onShutdown() { + m_depthBuf.destroy(m_device); + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_renPipe) m_device->destroyRenderPipeline(m_renPipe); + if (m_renPl) m_device->destroyPipelineLayout(m_renPl); + if (m_renBg) m_device->destroyBindGroup(m_renBg); + if (m_renBgl) m_device->destroyBindGroupLayout(m_renBgl); + if (m_compPipe) m_device->destroyComputePipeline(m_compPipe); + if (m_compPl) m_device->destroyPipelineLayout(m_compPl); + if (m_compBg) m_device->destroyBindGroup(m_compBg); + if (m_compBgl) m_device->destroyBindGroupLayout(m_compBgl); + if (m_vpBuf) m_device->destroyBuffer(m_vpBuf); + if (m_paramsBuf) m_device->destroyBuffer(m_paramsBuf); + if (m_vtxBuf) m_device->destroyBuffer(m_vtxBuf); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_cs) m_device->destroyShaderModule(m_cs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { ComputeSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample005_BindGroups/CMakeLists.txt b/Samples/cpp/RHI/Sample005_BindGroups/CMakeLists.txt new file mode 100644 index 00000000..53510f82 --- /dev/null +++ b/Samples/cpp/RHI/Sample005_BindGroups/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample005_BindGroups) diff --git a/Samples/cpp/RHI/Sample005_BindGroups/Main.cpp b/Samples/cpp/RHI/Sample005_BindGroups/Main.cpp new file mode 100644 index 00000000..0bdcf943 --- /dev/null +++ b/Samples/cpp/RHI/Sample005_BindGroups/Main.cpp @@ -0,0 +1,220 @@ +#include +/// Sample005 - Multiple Bind Groups with Dynamic Offsets. +/// 4x4 grid of lit cubes, each with unique color via dynamic offset. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class BindGroupSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample005 - Multiple Bind Groups"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { m_depthBuf.recreate(m_device, w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + cbuffer GlobalUBO : register(b0, space0) { row_major float4x4 VP; }; + cbuffer ObjectUBO : register(b0, space1) { row_major float4x4 Model; float4 ObjColor; }; + struct VSInput { float3 Position : TEXCOORD0; float3 Normal : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Normal : NORMAL; float4 Color : COLOR; }; + PSInput VSMain(VSInput i) { + PSInput o; + float4 wp = mul(float4(i.Position, 1.0), Model); + o.Position = mul(wp, VP); + o.Normal = mul(i.Normal, (float3x3)Model); + o.Color = ObjColor; + return o; + } + float4 PSMain(PSInput i) : SV_TARGET { + float3 ld = normalize(float3(0.5, 1.0, -0.7)); + float ndotl = max(dot(normalize(i.Normal), ld), 0.0); + return float4(i.Color.rgb * (0.2 + 0.8 * ndotl), 1.0); + } + )"; + + static constexpr int kGrid = 4, kObjCount = kGrid * kGrid; + static constexpr draco::u32 kObjStride = 256; // DX12 CBV alignment + + // Cube with face normals (24 verts, 36 indices). + struct Vert { float px,py,pz, nx,ny,nz; }; + static constexpr Vert kCubeV[24] = { + {-.5f,-.5f,-.5f, 0,0,-1},{.5f,-.5f,-.5f, 0,0,-1},{.5f,.5f,-.5f, 0,0,-1},{-.5f,.5f,-.5f, 0,0,-1}, + {.5f,-.5f,.5f, 0,0,1},{-.5f,-.5f,.5f, 0,0,1},{-.5f,.5f,.5f, 0,0,1},{.5f,.5f,.5f, 0,0,1}, + {-.5f,-.5f,.5f,-1,0,0},{-.5f,-.5f,-.5f,-1,0,0},{-.5f,.5f,-.5f,-1,0,0},{-.5f,.5f,.5f,-1,0,0}, + {.5f,-.5f,-.5f,1,0,0},{.5f,-.5f,.5f,1,0,0},{.5f,.5f,.5f,1,0,0},{.5f,.5f,-.5f,1,0,0}, + {-.5f,.5f,-.5f,0,1,0},{.5f,.5f,-.5f,0,1,0},{.5f,.5f,.5f,0,1,0},{-.5f,.5f,.5f,0,1,0}, + {-.5f,-.5f,.5f,0,-1,0},{.5f,-.5f,.5f,0,-1,0},{.5f,-.5f,-.5f,0,-1,0},{-.5f,-.5f,-.5f,0,-1,0}, + }; + static constexpr draco::u16 kCubeI[36] = { + 0,1,2,0,2,3, 4,5,6,4,6,7, 8,9,10,8,10,11, 12,13,14,12,14,15, 16,17,18,16,18,19, 20,21,22,20,22,23 + }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_globalUbo = nullptr, *m_objUbo = nullptr; + void *m_globalMapped = nullptr, *m_objMapped = nullptr; + rhi::BindGroupLayout *m_globalBgl = nullptr, *m_objBgl = nullptr; + rhi::BindGroup *m_globalBg = nullptr, *m_objBg = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; + sf::DepthBuffer m_depthBuf; +}; + +draco::Status BindGroupSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kCubeV); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kCubeI); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kCubeV), sizeof(kCubeV))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kCubeI), sizeof(kCubeI))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::BufferDesc gbd{}; gbd.size = 256; gbd.usage = rhi::BufferUsage::Uniform; gbd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(gbd, m_globalUbo) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_globalMapped = m_globalUbo->map(); + rhi::BufferDesc obd{}; obd.size = kObjCount * kObjStride; obd.usage = rhi::BufferUsage::Uniform; obd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(obd, m_objUbo) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_objMapped = m_objUbo->map(); + + // Set 0: global VP. + rhi::BindGroupLayoutEntry gE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex) }; + rhi::BindGroupLayoutDesc gBgld{}; gBgld.entries = std::span(gE, 1); + if (m_device->createBindGroupLayout(gBgld, m_globalBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry gBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_globalUbo, 0, 64) }; + rhi::BindGroupDesc gBgd{}; gBgd.layout = m_globalBgl; gBgd.entries = std::span(gBgE, 1); + if (m_device->createBindGroup(gBgd, m_globalBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Set 1: per-object with dynamic offset. + rhi::BindGroupLayoutEntry oE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment) }; + oE[0].hasDynamicOffset = true; + rhi::BindGroupLayoutDesc oBgld{}; oBgld.entries = std::span(oE, 1); + if (m_device->createBindGroupLayout(oBgld, m_objBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry oBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_objUbo, 0, kObjStride) }; + rhi::BindGroupDesc oBgd{}; oBgd.layout = m_objBgl; oBgd.entries = std::span(oBgE, 1); + if (m_device->createBindGroup(oBgd, m_objBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout with 2 sets. + rhi::BindGroupLayout* sets[2] = { m_globalBgl, m_objBgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 2); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + m_depthBuf.recreate(m_device, m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x3, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 24; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive = { rhi::PrimitiveTopology::TriangleList, rhi::FrontFace::CW, rhi::CullMode::Back }; + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void BindGroupSample::onRender() { + using draco::f32, draco::u32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Update VP. + f32 aspect = static_cast(m_width) / static_cast(m_height); + f32 camAngle = m_totalTime * 0.3f, camDist = 8.0f; + Matrix4 view = Matrix4::lookAtRH(draco::math::Vector3{std::sin(camAngle)*camDist, 5.0f, -std::cos(camAngle)*camDist}, draco::math::Vector3{ 0,0,0}, draco::math::Vector3{0,1,0}); + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(45.0f), aspect, 0.1f, 100.0f); + Matrix4 vp = view * proj; + std::memcpy(m_globalMapped, vp.data(), 64); + + // Update per-object. + static constexpr f32 kColors[kObjCount * 4] = { + 1,.3f,.3f,1, .3f,1,.3f,1, .3f,.3f,1,1, 1,1,.3f,1, 1,.3f,1,1, .3f,1,1,1, 1,.6f,.2f,1, .6f,.2f,1,1, + .2f,.8f,.6f,1, .8f,.8f,.8f,1, .5f,.3f,.1f,1, .9f,.5f,.7f,1, .4f,.7f,.2f,1, .2f,.4f,.8f,1, .8f,.4f,.4f,1, .6f,.6f,.3f,1 + }; + f32 spacing = 2.0f, half = (kGrid - 1) * spacing * 0.5f; + for (int r = 0; r < kGrid; ++r) for (int c = 0; c < kGrid; ++c) { + int idx = r * kGrid + c; + f32 angle = m_totalTime * (0.5f + idx * 0.1f); + Matrix4 model = Matrix4::rotationY(angle); + model.m[3][0] = c * spacing - half; model.m[3][1] = 0; model.m[3][2] = r * spacing - half; + auto* dest = static_cast(m_objMapped) + idx * kObjStride; + std::memcpy(dest, model.data(), 64); + std::memcpy(dest + 64, &kColors[idx * 4], 16); + } + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->setBindGroup(0, m_globalBg); + for (int i = 0; i < kObjCount; ++i) { + u32 dynOff = static_cast(i * kObjStride); + rp->setBindGroup(1, m_objBg, std::span(&dynOff, 1)); + rp->drawIndexed(36); + } + rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void BindGroupSample::onShutdown() { + m_depthBuf.destroy(m_device); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_objBg) m_device->destroyBindGroup(m_objBg); if (m_objBgl) m_device->destroyBindGroupLayout(m_objBgl); + if (m_globalBg) m_device->destroyBindGroup(m_globalBg); if (m_globalBgl) m_device->destroyBindGroupLayout(m_globalBgl); + if (m_objUbo) m_device->destroyBuffer(m_objUbo); if (m_globalUbo) m_device->destroyBuffer(m_globalUbo); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BindGroupSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample006_Blending/CMakeLists.txt b/Samples/cpp/RHI/Sample006_Blending/CMakeLists.txt new file mode 100644 index 00000000..24532e83 --- /dev/null +++ b/Samples/cpp/RHI/Sample006_Blending/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample006_Blending) diff --git a/Samples/cpp/RHI/Sample006_Blending/Main.cpp b/Samples/cpp/RHI/Sample006_Blending/Main.cpp new file mode 100644 index 00000000..06cd5dde --- /dev/null +++ b/Samples/cpp/RHI/Sample006_Blending/Main.cpp @@ -0,0 +1,133 @@ +#include +/// Renders overlapping semi-transparent colored quads. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class BlendingSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample006 - Alpha Blending"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position, 1.0); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return i.Color; } + )"; + // 4 quads: background opaque + 3 overlapping translucent. + static constexpr float kVerts[] = { + -.9f,-.9f,.5f, .15f,.15f,.2f,1, .9f,-.9f,.5f, .15f,.15f,.2f,1, .9f,.9f,.5f, .15f,.15f,.2f,1, -.9f,.9f,.5f, .15f,.15f,.2f,1, + -.6f,-.4f,.3f, 1,.2f,.2f,.5f, .1f,-.4f,.3f, 1,.2f,.2f,.5f, .1f,.4f,.3f, 1,.2f,.2f,.5f, -.6f,.4f,.3f, 1,.2f,.2f,.5f, + -.3f,-.5f,.2f, .2f,1,.2f,.5f, .4f,-.5f,.2f, .2f,1,.2f,.5f, .4f,.3f,.2f, .2f,1,.2f,.5f, -.3f,.3f,.2f, .2f,1,.2f,.5f, + -.1f,-.3f,.1f, .2f,.3f,1,.5f, .6f,-.3f,.1f, .2f,.3f,1,.5f, .6f,.5f,.1f, .2f,.3f,1,.5f, -.1f,.5f,.1f, .2f,.3f,1,.5f, + }; + static constexpr draco::u16 kIdx[] = { 0,1,2,0,2,3, 4,5,6,4,6,7, 8,9,10,8,10,11, 12,13,14,12,14,15 }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_opaquePipe = nullptr, *m_blendPipe = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status BlendingSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Empty pipeline layout. + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ctOpaque{}; ctOpaque.format = m_swapChain->format(); + rhi::ColorTargetState ctBlend{}; ctBlend.format = m_swapChain->format(); + ctBlend.blend = rhi::BlendState::alphaBlend(); + + // Opaque pipeline (for background quad). + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ctOpaque, 1); + if (m_device->createRenderPipeline(rpd, m_opaquePipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Blend pipeline (for translucent quads). + rpd.fragment->targets = std::span(&ctBlend, 1); + if (m_device->createRenderPipeline(rpd, m_blendPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void BlendingSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + // Draw background opaque. + rp->setPipeline(m_opaquePipe); rp->drawIndexed(6, 1, 0, 0, 0); + // Draw 3 translucent quads. + rp->setPipeline(m_blendPipe); rp->drawIndexed(18, 1, 6, 0, 0); + rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void BlendingSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_blendPipe) m_device->destroyRenderPipeline(m_blendPipe); + if (m_opaquePipe) m_device->destroyRenderPipeline(m_opaquePipe); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BlendingSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample007_Instancing/CMakeLists.txt b/Samples/cpp/RHI/Sample007_Instancing/CMakeLists.txt new file mode 100644 index 00000000..d0123af6 --- /dev/null +++ b/Samples/cpp/RHI/Sample007_Instancing/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample007_Instancing) diff --git a/Samples/cpp/RHI/Sample007_Instancing/Main.cpp b/Samples/cpp/RHI/Sample007_Instancing/Main.cpp new file mode 100644 index 00000000..4cb355fd --- /dev/null +++ b/Samples/cpp/RHI/Sample007_Instancing/Main.cpp @@ -0,0 +1,208 @@ +#include +/// Renders 64 small quads in a grid using instanced draw with per-instance offset + color. +/// Instance buffer is CpuToGpu with per-frame wobble animation. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +struct InstanceData { + float offset[2]; + float color[4]; +}; + +class InstancingSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample007 - Instanced Rendering"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput + { + float3 Position : TEXCOORD0; + float2 Offset : TEXCOORD1; + float4 InstColor : TEXCOORD2; + }; + struct PSInput + { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position.xy + input.Offset, input.Position.z, 1.0); + output.Color = input.InstColor; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + )"; + static constexpr int kInstanceCount = 64; + + // Unit quad vertices (pos only). + static constexpr float kQuadVerts[] = { + -0.04f, -0.04f, 0.0f, + 0.04f, -0.04f, 0.0f, + 0.04f, 0.04f, 0.0f, + -0.04f, 0.04f, 0.0f, + }; + static constexpr draco::u16 kQuadIdx[] = { 0, 1, 2, 0, 2, 3 }; + + void updateInstances(); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_instBuf = nullptr; + void* m_instMapped = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status InstancingSample::onInit() { + using draco::Status, std::span, draco::u8, draco::f32, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex + index buffers (GpuOnly, static quad geometry). + rhi::BufferDesc vbd{}; vbd.size = sizeof(kQuadVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kQuadIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kQuadVerts), sizeof(kQuadVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kQuadIdx), sizeof(kQuadIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Instance buffer (CpuToGpu for per-frame updates). + rhi::BufferDesc instBd{}; instBd.size = kInstanceCount * sizeof(InstanceData); instBd.usage = rhi::BufferUsage::Vertex; instBd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(instBd, m_instBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_instMapped = m_instBuf->map(); + if (!m_instMapped) return draco::ErrorCode::Unknown; + + // Pipeline layout (empty - no bind groups needed). + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Two vertex buffer layouts: slot 0 = per-vertex, slot 1 = per-instance. + rhi::VertexAttribute vtxAttrs[1] = { {rhi::VertexFormat::Float32x3, 0, 0} }; + rhi::VertexBufferLayout vtxLayout{}; vtxLayout.stride = 12; vtxLayout.stepMode = rhi::VertexStepMode::Vertex; + vtxLayout.attributes = std::span(vtxAttrs, 1); + + rhi::VertexAttribute instAttrs[2] = { {rhi::VertexFormat::Float32x2, 0, 1}, {rhi::VertexFormat::Float32x4, 8, 2} }; + rhi::VertexBufferLayout instLayout{}; instLayout.stride = static_cast(sizeof(InstanceData)); instLayout.stepMode = rhi::VertexStepMode::Instance; + instLayout.attributes = std::span(instAttrs, 2); + + rhi::VertexBufferLayout layouts[2] = { vtxLayout, instLayout }; + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(layouts, 2); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void InstancingSample::updateInstances() { + auto* data = static_cast(m_instMapped); + int gridSize = static_cast(std::sqrt(static_cast(kInstanceCount))); + + for (int i = 0; i < kInstanceCount; ++i) { + int row = i / gridSize; + int col = i % gridSize; + + float spacing = 2.0f / static_cast(gridSize); + float baseX = -1.0f + spacing * 0.5f + col * spacing; + float baseY = -1.0f + spacing * 0.5f + row * spacing; + + // Animate: wobble in a circle. + float phase = m_totalTime * 2.0f + i * 0.3f; + float wobbleX = std::sin(phase) * 0.02f; + float wobbleY = std::cos(phase * 1.3f) * 0.02f; + + data[i].offset[0] = baseX + wobbleX; + data[i].offset[1] = baseY + wobbleY; + + // Color: hue based on index. + float t = static_cast(i) / static_cast(kInstanceCount); + constexpr float pi2 = 3.14159265f * 2.0f; + data[i].color[0] = std::abs(std::sin(t * pi2)); + data[i].color[1] = std::abs(std::sin(t * pi2 + 2.094f)); + data[i].color[2] = std::abs(std::sin(t * pi2 + 4.189f)); + data[i].color[3] = 1.0f; + } +} + +void InstancingSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + updateInstances(); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setVertexBuffer(1, m_instBuf, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(6, kInstanceCount); + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void InstancingSample::onShutdown() { + if (m_instBuf && m_instMapped) m_instBuf->unmap(); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_instBuf) m_device->destroyBuffer(m_instBuf); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { InstancingSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample008_DepthBuffer/CMakeLists.txt b/Samples/cpp/RHI/Sample008_DepthBuffer/CMakeLists.txt new file mode 100644 index 00000000..74f7a5f9 --- /dev/null +++ b/Samples/cpp/RHI/Sample008_DepthBuffer/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample008_DepthBuffer) diff --git a/Samples/cpp/RHI/Sample008_DepthBuffer/Main.cpp b/Samples/cpp/RHI/Sample008_DepthBuffer/Main.cpp new file mode 100644 index 00000000..f9510b18 --- /dev/null +++ b/Samples/cpp/RHI/Sample008_DepthBuffer/Main.cpp @@ -0,0 +1,190 @@ +#include +/// Three overlapping quads at different Z depths demonstrate depth testing. +/// Red (z=0.8, far), Green (z=0.5, mid), Blue (z=0.2, near) - drawn far-to-near, +/// depth buffer ensures correct visibility. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class DepthBufferSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample008 - Depth Buffer"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateDepth(w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput + { + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + }; + struct PSInput + { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + )"; + + // Three overlapping quads drawn in order: red (far), green (middle), blue (near). + // Stride: 7 floats per vertex (pos xyz + color rgba). + static constexpr float kVerts[] = { + // Quad 0: Red - large, behind (z=0.8), drawn first. + -0.6f, -0.6f, 0.8f, 1.0f, 0.2f, 0.2f, 1.0f, + 0.4f, -0.6f, 0.8f, 1.0f, 0.2f, 0.2f, 1.0f, + 0.4f, 0.6f, 0.8f, 1.0f, 0.2f, 0.2f, 1.0f, + -0.6f, 0.6f, 0.8f, 1.0f, 0.2f, 0.2f, 1.0f, + // Quad 1: Green - overlaps red, closer (z=0.5), drawn second. + -0.2f, -0.4f, 0.5f, 0.2f, 1.0f, 0.2f, 1.0f, + 0.6f, -0.4f, 0.5f, 0.2f, 1.0f, 0.2f, 1.0f, + 0.6f, 0.4f, 0.5f, 0.2f, 1.0f, 0.2f, 1.0f, + -0.2f, 0.4f, 0.5f, 0.2f, 1.0f, 0.2f, 1.0f, + // Quad 2: Blue - overlaps both, nearest (z=0.2), drawn third. + -0.4f, -0.7f, 0.2f, 0.2f, 0.3f, 1.0f, 1.0f, + 0.2f, -0.7f, 0.2f, 0.2f, 0.3f, 1.0f, 1.0f, + 0.2f, 0.7f, 0.2f, 0.2f, 0.3f, 1.0f, 1.0f, + -0.4f, 0.7f, 0.2f, 0.2f, 0.3f, 1.0f, 1.0f, + }; + static constexpr draco::u16 kIdx[] = { + 0, 1, 2, 0, 2, 3, + 4, 5, 6, 4, 6, 7, + 8, 9, 10, 8, 10, 11, + }; + + void recreateDepth(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::Texture *m_depthTex = nullptr; + rhi::TextureView *m_depthView = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +void DepthBufferSample::recreateDepth(draco::u32 w, draco::u32 h) { + if (m_depthView) { m_device->destroyTextureView(m_depthView); m_depthView = nullptr; } + if (m_depthTex) { m_device->destroyTexture(m_depthTex); m_depthTex = nullptr; } + + rhi::TextureDesc td = rhi::TextureDesc::depthBuffer(rhi::TextureFormat::Depth24PlusStencil8, w, h, 1, u8"DepthTex"); + m_device->createTexture(td, m_depthTex); + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::Depth24PlusStencil8; tvd.dimension = rhi::TextureViewDimension::Texture2D; + tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_depthTex, tvd, m_depthView); +} + +draco::Status DepthBufferSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Pipeline layout (empty - no bind groups needed). + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + recreateDepth(m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthWriteEnabled = true; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void DepthBufferSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthTex, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.1f, 0.1f, 0.15f, 1.0f); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthView; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + // Draw all 3 quads - depth buffer determines visibility. + rp->drawIndexed(18); + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void DepthBufferSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_depthView) m_device->destroyTextureView(m_depthView); + if (m_depthTex) m_device->destroyTexture(m_depthTex); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { DepthBufferSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample009_Mipmaps/CMakeLists.txt b/Samples/cpp/RHI/Sample009_Mipmaps/CMakeLists.txt new file mode 100644 index 00000000..e840075c --- /dev/null +++ b/Samples/cpp/RHI/Sample009_Mipmaps/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample009_Mipmaps) diff --git a/Samples/cpp/RHI/Sample009_Mipmaps/Main.cpp b/Samples/cpp/RHI/Sample009_Mipmaps/Main.cpp new file mode 100644 index 00000000..c1869181 --- /dev/null +++ b/Samples/cpp/RHI/Sample009_Mipmaps/Main.cpp @@ -0,0 +1,205 @@ +#include +/// Textured quad that recedes into the distance showing mip level selection. +/// Uses generateMipmaps() to auto-generate mip chain from a base texture. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class MipmapSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample009 - Mipmaps"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { m_depthBuf.recreate(m_device, w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + Texture2D gTexture : register(t0, space0); + SamplerState gSampler : register(s0, space0); + cbuffer UBO : register(b0, space1) { row_major float4x4 MVP; }; + struct VSInput { float3 Position : TEXCOORD0; float2 TexCoord : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float2 TexCoord : TEXCOORD0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = mul(float4(i.Position,1), MVP); o.TexCoord = i.TexCoord; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return gTexture.Sample(gSampler, i.TexCoord); } + )"; + // Receding floor plane. + static constexpr float kVerts[] = { + -4, 0, 0, 0, 0, 4, 0, 0, 8, 0, 4, 0, -20, 8, 10, -4, 0, -20, 0, 10 + }; + static constexpr draco::u16 kIdx[] = { 0,1,2, 0,2,3 }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_ub = nullptr; + void* m_ubMapped = nullptr; + rhi::Texture* m_tex = nullptr; rhi::TextureView* m_texView = nullptr; + rhi::Sampler* m_sampler = nullptr; + rhi::BindGroupLayout *m_texBgl = nullptr, *m_uboBgl = nullptr; + rhi::BindGroup *m_texBg = nullptr, *m_uboBg = nullptr; + rhi::PipelineLayout* m_pl = nullptr; rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; + sf::DepthBuffer m_depthBuf; +}; + +draco::Status MipmapSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ubd{}; ubd.size = 64; ubd.usage = rhi::BufferUsage::Uniform; ubd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(ubd, m_ub) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_ubMapped = m_ub->map(); + + // Checkerboard base texture 256x256 with 9 mip levels. + constexpr u32 tw = 256, th = 256, mipCount = 9; + u8 texPixels[tw * th * 4]; + for (u32 y = 0; y < th; ++y) for (u32 x = 0; x < tw; ++x) { + bool c = ((x/16) + (y/16)) % 2 == 0; + u32 i = (y*tw+x)*4; + texPixels[i]=c?255:30; texPixels[i+1]=c?255:30; texPixels[i+2]=c?255:200; texPixels[i+3]=255; + } + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = tw; td.height = th; + td.mipLevelCount = mipCount; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopySrc | rhi::TextureUsage::CopyDst | rhi::TextureUsage::RenderTarget; + if (m_device->createTexture(td, m_tex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Upload base mip + generate mips. + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + rhi::TextureDataLayout layout{}; layout.bytesPerRow = tw*4; layout.rowsPerImage = th; + batch->writeTexture(m_tex, std::span(texPixels, sizeof(texPixels)), layout, rhi::Extent3D{tw,th,1}); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Generate mipmaps then transition to shader-read. + rhi::CommandPool* tmpPool = nullptr; m_device->createCommandPool(rhi::QueueType::Graphics, tmpPool); + rhi::CommandEncoder* enc = nullptr; tmpPool->createEncoder(enc); + enc->generateMipmaps(m_tex); + // generateMipmaps leaves all mips in TRANSFER_SRC. + // Transition all mips to SHADER_READ for sampling. + rhi::TextureBarrier tb{}; tb.texture = m_tex; + tb.oldState = rhi::ResourceState::CopySrc; tb.newState = rhi::ResourceState::ShaderRead; + tb.baseMipLevel = 0; tb.mipLevelCount = mipCount; + tb.baseArrayLayer = 0; tb.arrayLayerCount = 1; + rhi::BarrierGroup bg{}; bg.textureBarriers = std::span(&tb, 1); + enc->barrier(bg); + rhi::CommandBuffer* cb = enc->finish(); + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1)); + m_graphicsQueue->waitIdle(); + tmpPool->destroyEncoder(enc); m_device->destroyCommandPool(tmpPool); + + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = mipCount; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_tex, tvd, m_texView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Linear; sd.magFilter = rhi::FilterMode::Linear; + sd.mipmapFilter = rhi::MipmapFilterMode::Linear; sd.maxLod = static_cast(mipCount); + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bind groups: set 0 = texture+sampler, set 1 = UBO. + rhi::BindGroupLayoutEntry tE[2] = { rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment) }; + rhi::BindGroupLayoutDesc tBgld{}; tBgld.entries = std::span(tE, 2); + if (m_device->createBindGroupLayout(tBgld, m_texBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry tBgE[2] = { rhi::BindGroupEntry::textureEntry(m_texView), rhi::BindGroupEntry::samplerEntry(m_sampler) }; + rhi::BindGroupDesc tBgd{}; tBgd.layout = m_texBgl; tBgd.entries = std::span(tBgE, 2); + if (m_device->createBindGroup(tBgd, m_texBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupLayoutEntry uE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex) }; + rhi::BindGroupLayoutDesc uBgld{}; uBgld.entries = std::span(uE, 1); + if (m_device->createBindGroupLayout(uBgld, m_uboBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry uBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_ub, 0, 64) }; + rhi::BindGroupDesc uBgd{}; uBgd.layout = m_uboBgl; uBgd.entries = std::span(uBgE, 1); + if (m_device->createBindGroup(uBgd, m_uboBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupLayout* sets[2] = { m_texBgl, m_uboBgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 2); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_depthBuf.recreate(m_device, m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x2, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 20; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void MipmapSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + f32 aspect = static_cast(m_width) / static_cast(m_height); + Matrix4 view = Matrix4::lookAtRH(draco::math::Vector3{0, 2, 2}, draco::math::Vector3{ 0, 0, -5}, draco::math::Vector3{0,1,0}); + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(60.0f), aspect, 0.1f, 100.0f); + Matrix4 mvp = view * proj; + std::memcpy(m_ubMapped, mvp.data(), 64); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; ca.clearValue = rhi::ClearColor(0.1f,0.1f,0.15f,1); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); rp->setBindGroup(0, m_texBg); rp->setBindGroup(1, m_uboBg); + rp->setViewport(0,0,static_cast(m_width),static_cast(m_height),0,1); + rp->setScissor(0,0,m_width,m_height); + rp->setVertexBuffer(0, m_vb, 0); rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(6); rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void MipmapSample::onShutdown() { + m_depthBuf.destroy(m_device); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_uboBg) m_device->destroyBindGroup(m_uboBg); if (m_uboBgl) m_device->destroyBindGroupLayout(m_uboBgl); + if (m_texBg) m_device->destroyBindGroup(m_texBg); if (m_texBgl) m_device->destroyBindGroupLayout(m_texBgl); + if (m_sampler) m_device->destroySampler(m_sampler); if (m_texView) m_device->destroyTextureView(m_texView); + if (m_tex) m_device->destroyTexture(m_tex); if (m_ub) m_device->destroyBuffer(m_ub); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MipmapSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample010_MSAA/CMakeLists.txt b/Samples/cpp/RHI/Sample010_MSAA/CMakeLists.txt new file mode 100644 index 00000000..4cc485c6 --- /dev/null +++ b/Samples/cpp/RHI/Sample010_MSAA/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample010_MSAA) diff --git a/Samples/cpp/RHI/Sample010_MSAA/Main.cpp b/Samples/cpp/RHI/Sample010_MSAA/Main.cpp new file mode 100644 index 00000000..f9bc6c8e --- /dev/null +++ b/Samples/cpp/RHI/Sample010_MSAA/Main.cpp @@ -0,0 +1,132 @@ +#include +/// Renders a triangle with 4x MSAA, resolving to the swap chain. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class MSAASample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample010 - MSAA (4x)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateMSAA(w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float3 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position,1); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return float4(i.Color, 1.0); } + )"; + static constexpr float kVerts[] = { 0,.7f,0, 1,0,0, .7f,-.5f,0, 0,1,0, -.7f,-.5f,0, 0,0,1 }; + static constexpr draco::u16 kIdx[] = { 0, 1, 2 }; + static constexpr draco::u32 kSamples = 4; + + void recreateMSAA(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; rhi::RenderPipeline *m_pipeline = nullptr; + rhi::Texture *m_msaaTex = nullptr; rhi::TextureView *m_msaaView = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +void MSAASample::recreateMSAA(draco::u32 w, draco::u32 h) { + if (m_msaaView) { m_device->destroyTextureView(m_msaaView); m_msaaView = nullptr; } + if (m_msaaTex) { m_device->destroyTexture(m_msaaTex); m_msaaTex = nullptr; } + rhi::TextureDesc td = rhi::TextureDesc::renderTarget(m_swapChain->format(), w, h, kSamples, u8"MSAATarget"); + m_device->createTexture(td, m_msaaTex); + rhi::TextureViewDesc tvd{}; tvd.format = m_swapChain->format(); tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_msaaTex, tvd, m_msaaView); +} + +draco::Status MSAASample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::PipelineLayoutDesc pld{}; if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x3, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 24; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.multisample.count = kSamples; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + recreateMSAA(m_width, m_height); + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void MSAASample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_msaaTex, rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + // Render into MSAA target, resolve to swap chain. + rhi::ColorAttachment ca{}; + ca.view = m_msaaView; + ca.resolveTarget = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(3); rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void MSAASample::onShutdown() { + if (m_msaaView) m_device->destroyTextureView(m_msaaView); if (m_msaaTex) m_device->destroyTexture(m_msaaTex); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MSAASample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample011_MRT/CMakeLists.txt b/Samples/cpp/RHI/Sample011_MRT/CMakeLists.txt new file mode 100644 index 00000000..3739de5c --- /dev/null +++ b/Samples/cpp/RHI/Sample011_MRT/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample011_MRT) diff --git a/Samples/cpp/RHI/Sample011_MRT/Main.cpp b/Samples/cpp/RHI/Sample011_MRT/Main.cpp new file mode 100644 index 00000000..2de2b3be --- /dev/null +++ b/Samples/cpp/RHI/Sample011_MRT/Main.cpp @@ -0,0 +1,209 @@ +#include +/// Pass 1: Renders triangles to 2 render targets (color + brightness). +/// Pass 2: Composites both side-by-side via fullscreen triangle. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class MRTSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample011 - MRT"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 /*w*/, draco::u32 /*h*/) override { createRenderTargets(); } + void onShutdown() override; +private: + static constexpr const char8_t kGBufShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + struct PSOutput { float4 Color : SV_TARGET0; float4 Brightness : SV_TARGET1; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position,1); o.Color = i.Color; return o; } + PSOutput PSMain(PSInput i) { PSOutput o; o.Color = i.Color; + float lum = dot(i.Color.rgb, float3(0.299,0.587,0.114)); + o.Brightness = float4(lum,lum,lum,1); return o; } + )"; + static constexpr const char8_t kCompShader[] = u8R"( + Texture2D gColorTex : register(t0, space0); + Texture2D gBrightTex : register(t1, space0); + SamplerState gSampler : register(s0, space0); + struct PSInput { float4 Position : SV_POSITION; float2 TexCoord : TEXCOORD0; }; + PSInput VSMain(uint vid : SV_VertexID) { PSInput o; + float2 uv = float2((vid << 1) & 2, vid & 2); + o.Position = float4(uv * 2.0 - 1.0, 0, 1); o.TexCoord = float2(uv.x, 1.0 - uv.y); return o; } + float4 PSMain(PSInput i) : SV_TARGET { + float2 uv = i.TexCoord; + if (uv.x < 0.5) return gColorTex.Sample(gSampler, float2(uv.x*2, uv.y)); + else return gBrightTex.Sample(gSampler, float2((uv.x-0.5)*2, uv.y)); } + )"; + static constexpr float kVerts[] = { + -.5f,-.5f,0, 1,.2f,.2f,1, .5f,-.5f,0, 1,.2f,.2f,1, 0,.6f,0, 1,.8f,.2f,1, + -.3f,-.3f,0, .2f,.3f,1,1, .7f,-.1f,0, .2f,.3f,1,1, .2f,.5f,0, .2f,.8f,1,1, + }; + static constexpr draco::u16 kIdx[] = { 0,1,2, 3,4,5 }; + + void createRenderTargets(); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_gbVs=nullptr, *m_gbPs=nullptr, *m_compVs=nullptr, *m_compPs=nullptr; + rhi::Buffer *m_vb=nullptr, *m_ib=nullptr; + rhi::Sampler* m_sampler = nullptr; + rhi::PipelineLayout *m_gbPl=nullptr, *m_compPl=nullptr; + rhi::RenderPipeline *m_gbPipe=nullptr, *m_compPipe=nullptr; + rhi::BindGroupLayout* m_compBgl = nullptr; + rhi::BindGroup* m_compBg = nullptr; + rhi::Texture *m_colorRT=nullptr, *m_brightRT=nullptr; + rhi::TextureView *m_colorRTView=nullptr, *m_brightRTView=nullptr; + rhi::CommandPool* m_pool=nullptr; rhi::Fence* m_fence=nullptr; + draco::u64 m_fenceVal = 0; +}; + +void MRTSample::createRenderTargets() { + if (m_compBg) { m_device->destroyBindGroup(m_compBg); m_compBg = nullptr; } + if (m_colorRTView) { m_device->destroyTextureView(m_colorRTView); m_colorRTView = nullptr; } + if (m_colorRT) { m_device->destroyTexture(m_colorRT); m_colorRT = nullptr; } + if (m_brightRTView) { m_device->destroyTextureView(m_brightRTView); m_brightRTView = nullptr; } + if (m_brightRT) { m_device->destroyTexture(m_brightRT); m_brightRT = nullptr; } + + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = m_width; td.height = m_height; + td.usage = rhi::TextureUsage::RenderTarget | rhi::TextureUsage::Sampled; + m_device->createTexture(td, m_colorRT); + m_device->createTexture(td, m_brightRT); + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_colorRT, tvd, m_colorRTView); + m_device->createTextureView(m_brightRT, tvd, m_brightRTView); + + rhi::BindGroupEntry bgE[3] = { rhi::BindGroupEntry::textureEntry(m_colorRTView), + rhi::BindGroupEntry::textureEntry(m_brightRTView), + rhi::BindGroupEntry::samplerEntry(m_sampler) }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_compBgl; bgd.entries = std::span(bgE, 3); + m_device->createBindGroup(bgd, m_compBg); +} + +draco::Status MRTSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kGBufShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"GBufVS", m_gbVs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kGBufShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"GBufPS", m_gbPs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kCompShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"CompVS", m_compVs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kCompShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"CompPS", m_compPs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Nearest; sd.magFilter = rhi::FilterMode::Nearest; + sd.addressU = rhi::AddressMode::ClampToEdge; sd.addressV = rhi::AddressMode::ClampToEdge; + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // GBuffer pipeline (empty layout, 2 color targets). + rhi::PipelineLayoutDesc gpld{}; if (m_device->createPipelineLayout(gpld, m_gbPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState gbCt[2] = { {.format = rhi::TextureFormat::RGBA8Unorm, .blend = {}, .writeMask = rhi::ColorWriteMask::All}, + {.format = rhi::TextureFormat::RGBA8Unorm, .blend = {}, .writeMask = rhi::ColorWriteMask::All} }; + rhi::RenderPipelineDesc grpd{}; grpd.layout = m_gbPl; + grpd.vertex.shader = { m_gbVs, u8"VSMain", rhi::ShaderStage::Vertex }; + grpd.vertex.buffers = std::span(&vbl, 1); + grpd.fragment = rhi::FragmentState{}; grpd.fragment->shader = { m_gbPs, u8"PSMain", rhi::ShaderStage::Fragment }; + grpd.fragment->targets = std::span(gbCt, 2); + if (m_device->createRenderPipeline(grpd, m_gbPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Composite BGL + pipeline (3 bindings: 2 textures + 1 sampler). + rhi::BindGroupLayoutEntry cE[3] = { + rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampledTexture(1, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment), + }; + rhi::BindGroupLayoutDesc cBgld{}; cBgld.entries = std::span(cE, 3); + if (m_device->createBindGroupLayout(cBgld, m_compBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* cSets[1] = { m_compBgl }; + rhi::PipelineLayoutDesc cpld{}; cpld.bindGroupLayouts = std::span(cSets, 1); + if (m_device->createPipelineLayout(cpld, m_compPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::ColorTargetState compCt{}; compCt.format = m_swapChain->format(); + rhi::RenderPipelineDesc crpd{}; crpd.layout = m_compPl; + crpd.vertex.shader = { m_compVs, u8"VSMain", rhi::ShaderStage::Vertex }; + crpd.fragment = rhi::FragmentState{}; crpd.fragment->shader = { m_compPs, u8"PSMain", rhi::ShaderStage::Fragment }; + crpd.fragment->targets = std::span(&compCt, 1); + if (m_device->createRenderPipeline(crpd, m_compPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + createRenderTargets(); + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void MRTSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Pass 1: render to 2 RTs. + enc->transitionTexture(m_colorRT, rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_brightRT, rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + rhi::ColorAttachment ca2[2]; + ca2[0].view = m_colorRTView; ca2[0].loadOp = rhi::LoadOp::Clear; ca2[0].storeOp = rhi::StoreOp::Store; ca2[0].clearValue = rhi::ClearColor(0.1f,0.1f,0.15f,1); + ca2[1].view = m_brightRTView; ca2[1].loadOp = rhi::LoadOp::Clear; ca2[1].storeOp = rhi::StoreOp::Store; ca2[1].clearValue = rhi::ClearColor::black(); + rhi::RenderPassDesc rpd1{}; rpd1.colorAttachments.push_back(ca2[0]); rpd1.colorAttachments.push_back(ca2[1]); + auto* rp1 = enc->beginRenderPass(rpd1); + rp1->setPipeline(m_gbPipe); + rp1->setViewport(0,0,static_cast(m_width),static_cast(m_height),0,1); + rp1->setScissor(0,0,m_width,m_height); + rp1->setVertexBuffer(0, m_vb, 0); rp1->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp1->drawIndexed(6); rp1->end(); + enc->transitionTexture(m_colorRT, rhi::ResourceState::RenderTarget, rhi::ResourceState::ShaderRead); + enc->transitionTexture(m_brightRT, rhi::ResourceState::RenderTarget, rhi::ResourceState::ShaderRead); + + // Pass 2: composite to swap chain. + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + rhi::ColorAttachment ca1{}; ca1.view = m_swapChain->currentTextureView(); + ca1.loadOp = rhi::LoadOp::Clear; ca1.storeOp = rhi::StoreOp::Store; ca1.clearValue = rhi::ClearColor::black(); + rhi::RenderPassDesc rpd2{}; rpd2.colorAttachments.push_back(ca1); + auto* rp2 = enc->beginRenderPass(rpd2); + rp2->setPipeline(m_compPipe); rp2->setBindGroup(0, m_compBg); + rp2->setViewport(0,0,static_cast(m_width),static_cast(m_height),0,1); + rp2->setScissor(0,0,m_width,m_height); + rp2->draw(3); rp2->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void MRTSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_compPipe) m_device->destroyRenderPipeline(m_compPipe); if (m_compPl) m_device->destroyPipelineLayout(m_compPl); + if (m_compBg) m_device->destroyBindGroup(m_compBg); if (m_compBgl) m_device->destroyBindGroupLayout(m_compBgl); + if (m_gbPipe) m_device->destroyRenderPipeline(m_gbPipe); if (m_gbPl) m_device->destroyPipelineLayout(m_gbPl); + if (m_brightRTView) m_device->destroyTextureView(m_brightRTView); if (m_brightRT) m_device->destroyTexture(m_brightRT); + if (m_colorRTView) m_device->destroyTextureView(m_colorRTView); if (m_colorRT) m_device->destroyTexture(m_colorRT); + if (m_sampler) m_device->destroySampler(m_sampler); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_compPs) m_device->destroyShaderModule(m_compPs); if (m_compVs) m_device->destroyShaderModule(m_compVs); + if (m_gbPs) m_device->destroyShaderModule(m_gbPs); if (m_gbVs) m_device->destroyShaderModule(m_gbVs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MRTSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample012_Wireframe/CMakeLists.txt b/Samples/cpp/RHI/Sample012_Wireframe/CMakeLists.txt new file mode 100644 index 00000000..035235d4 --- /dev/null +++ b/Samples/cpp/RHI/Sample012_Wireframe/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample012_Wireframe) diff --git a/Samples/cpp/RHI/Sample012_Wireframe/Main.cpp b/Samples/cpp/RHI/Sample012_Wireframe/Main.cpp new file mode 100644 index 00000000..c6b80368 --- /dev/null +++ b/Samples/cpp/RHI/Sample012_Wireframe/Main.cpp @@ -0,0 +1,167 @@ +#include +/// Renders a rotating icosahedron in wireframe mode. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class WireframeSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample012 - Wireframe"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { m_depthBuf.recreate(m_device, w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + cbuffer UBO : register(b0, space0) { row_major float4x4 MVP; }; + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = mul(float4(i.Position,1), MVP); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return i.Color; } + )"; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs=nullptr, *m_ps=nullptr; + rhi::Buffer *m_vb=nullptr, *m_ib=nullptr, *m_ub=nullptr; + void* m_ubMapped = nullptr; + rhi::BindGroupLayout* m_bgl=nullptr; rhi::BindGroup* m_bg=nullptr; + rhi::PipelineLayout* m_pl=nullptr; + rhi::RenderPipeline *m_wirePipe=nullptr; + rhi::CommandPool* m_pool=nullptr; rhi::Fence* m_fence=nullptr; + draco::u64 m_fenceVal = 0; + draco::u32 m_indexCount = 0; + sf::DepthBuffer m_depthBuf; +}; + +draco::Status WireframeSample::onInit() { + using draco::Status, std::span, draco::u8, draco::f32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Icosahedron. + f32 t = (1.0f + std::sqrt(5.0f)) / 2.0f; + f32 s = 1.0f / std::sqrt(1.0f + t*t); + f32 a = s, b = t*s; + f32 vertData[84] = { + -a,b,0, 1,.3f,.3f,1, a,b,0, .3f,1,.3f,1, -a,-b,0, .3f,.3f,1,1, a,-b,0, 1,1,.3f,1, + 0,-a,b, 1,.3f,1,1, 0,a,b, .3f,1,1,1, 0,-a,-b, 1,.6f,.3f,1, 0,a,-b, .6f,.3f,1,1, + b,0,-a, .3f,1,.6f,1, b,0,a, 1,.6f,.6f,1, -b,0,-a, .6f,1,.3f,1, -b,0,a, .6f,.3f,.6f,1, + }; + draco::u16 idxData[60] = { + 0,11,5, 0,5,1, 0,1,7, 0,7,10, 0,10,11, + 1,5,9, 5,11,4, 11,10,2, 10,7,6, 7,1,8, + 3,9,4, 3,4,2, 3,2,6, 3,6,8, 3,8,9, + 4,9,5, 2,4,11, 6,2,10, 8,6,7, 9,8,1, + }; + m_indexCount = 60; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(vertData); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(idxData); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(vertData), sizeof(vertData))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(idxData), sizeof(idxData))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::BufferDesc ubd{}; ubd.size = 256; ubd.usage = rhi::BufferUsage::Uniform; ubd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(ubd, m_ub) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_ubMapped = m_ub->map(); + + rhi::BindGroupLayoutEntry bglE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex) }; + rhi::BindGroupLayoutDesc bgld{}; bgld.entries = std::span(bglE, 1); + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry bgE[1] = { rhi::BindGroupEntry::bufferEntry(m_ub, 0, 64) }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_bgl; bgd.entries = std::span(bgE, 1); + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* sets[1] = { m_bgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 1); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + m_depthBuf.recreate(m_device, m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive = { rhi::PrimitiveTopology::TriangleList, rhi::FrontFace::CCW, rhi::CullMode::None, rhi::FillMode::Wireframe }; + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthCompare = rhi::CompareFunction::LessEqual; + rpd.depthStencil->depthWriteEnabled = false; + if (m_device->createRenderPipeline(rpd, m_wirePipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void WireframeSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + f32 aspect = static_cast(m_width) / static_cast(m_height); + Matrix4 model = Matrix4::rotationY(m_totalTime * 0.8f); + // Row-vector view: identity rotation, camera 3 units along +Z (RH: looking toward -Z). + // The view is the inverse of the camera transform, so the translation is the NEGATED + // eye position: m[3][2] = -3 (puts the object at view-space z=-3, in front of the camera). + // PerspectiveFovRH gives clip.w = -viewZ, so geometry must have negative view z. + f32 view[16] = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-3,1 }; + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(45.0f), aspect, 0.1f, 100.0f); + Matrix4 vMat; std::memcpy(vMat.data(), view, 64); + Matrix4 mvp = model * vMat * proj; + std::memcpy(m_ubMapped, mvp.data(), 64); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; ca.clearValue = rhi::ClearColor(0.06f,0.06f,0.1f,1); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_wirePipe); rp->setBindGroup(0, m_bg); + rp->setViewport(0,0,static_cast(m_width),static_cast(m_height),0,1); + rp->setScissor(0,0,m_width,m_height); + rp->setVertexBuffer(0, m_vb, 0); rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(m_indexCount); rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void WireframeSample::onShutdown() { + m_depthBuf.destroy(m_device); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_wirePipe) m_device->destroyRenderPipeline(m_wirePipe); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_ub) m_device->destroyBuffer(m_ub); if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { WireframeSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample013_BorderSampler/CMakeLists.txt b/Samples/cpp/RHI/Sample013_BorderSampler/CMakeLists.txt new file mode 100644 index 00000000..79abfe55 --- /dev/null +++ b/Samples/cpp/RHI/Sample013_BorderSampler/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample013_BorderSampler) diff --git a/Samples/cpp/RHI/Sample013_BorderSampler/Main.cpp b/Samples/cpp/RHI/Sample013_BorderSampler/Main.cpp new file mode 100644 index 00000000..4211fef0 --- /dev/null +++ b/Samples/cpp/RHI/Sample013_BorderSampler/Main.cpp @@ -0,0 +1,232 @@ +#include +/// Demonstrates sampler border colors: TransparentBlack, OpaqueBlack, OpaqueWhite. +/// Three quads with UVs extending beyond [0,1] to show the border region. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class BorderSamplerSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample013 - Border Sampler"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + Texture2D gTexture : register(t0, space0); + SamplerState gSampler : register(s0, space0); + cbuffer UBO : register(b0, space1) { float4 QuadOffset; }; + struct VSInput { float3 Position : TEXCOORD0; float2 TexCoord : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float2 TexCoord : TEXCOORD0; }; + PSInput VSMain(VSInput input) { + PSInput output; + output.Position = float4(input.Position.xy + QuadOffset.xy, input.Position.z, 1.0); + output.TexCoord = input.TexCoord; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET { + return gTexture.Sample(gSampler, input.TexCoord); + } + )"; + + // Quad with UVs from -0.5 to 1.5 to show border region. + static constexpr float kQuadVerts[] = { + -0.25f, -0.25f, 0.0f, -0.5f, -0.5f, + 0.25f, -0.25f, 0.0f, 1.5f, -0.5f, + 0.25f, 0.25f, 0.0f, 1.5f, 1.5f, + -0.25f, 0.25f, 0.0f, -0.5f, 1.5f, + }; + static constexpr draco::u16 kQuadIdx[] = { 0, 1, 2, 0, 2, 3 }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_ub = nullptr; + void* m_ubMapped = nullptr; + rhi::Texture* m_tex = nullptr; rhi::TextureView* m_texView = nullptr; + rhi::Sampler *m_sampTransparent = nullptr, *m_sampOpaqueBlack = nullptr, *m_sampOpaqueWhite = nullptr; + rhi::BindGroupLayout *m_texBgl = nullptr, *m_uboBgl = nullptr; + rhi::BindGroup *m_bgTransparent = nullptr, *m_bgOpaqueBlack = nullptr, *m_bgOpaqueWhite = nullptr; + rhi::BindGroup *m_uboBg = nullptr; + rhi::PipelineLayout *m_pl = nullptr; rhi::RenderPipeline *m_pipeline = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status BorderSamplerSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kQuadVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kQuadIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Uniform buffer: 3 slots * 256 bytes (DX12 CBV alignment). + rhi::BufferDesc ubd{}; ubd.size = 768; ubd.usage = rhi::BufferUsage::Uniform; ubd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(ubd, m_ub) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_ubMapped = m_ub->map(); + // Write all 3 offsets upfront. + float off0[4] = { -0.55f, 0.0f, 0.0f, 0.0f }; + float off1[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + float off2[4] = { 0.55f, 0.0f, 0.0f, 0.0f }; + std::memcpy(static_cast(m_ubMapped), off0, 16); + std::memcpy(static_cast(m_ubMapped) + 256, off1, 16); + std::memcpy(static_cast(m_ubMapped) + 512, off2, 16); + + // 8x8 checkerboard texture (red/white). + constexpr u32 tw = 8, th = 8; + u8 texData[tw * th * 4]; + for (u32 y = 0; y < th; ++y) for (u32 x = 0; x < tw; ++x) { + u32 i = (y * tw + x) * 4; + bool white = ((x + y) % 2) == 0; + texData[i+0] = white ? 255 : 220; texData[i+1] = white ? 255 : 60; + texData[i+2] = white ? 255 : 60; texData[i+3] = 255; + } + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = tw; td.height = th; + td.mipLevelCount = 1; td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + if (m_device->createTexture(td, m_tex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kQuadVerts), sizeof(kQuadVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kQuadIdx), sizeof(kQuadIdx))); + rhi::TextureDataLayout layout{}; layout.bytesPerRow = tw * 4; layout.rowsPerImage = th; + batch->writeTexture(m_tex, std::span(texData, sizeof(texData)), layout, rhi::Extent3D{tw, th, 1}); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_tex, tvd, m_texView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Three samplers with ClampToBorder and different border colors. + auto makeSampler = [&](rhi::SamplerBorderColor bc, rhi::Sampler*& out) -> draco::Status { + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Nearest; sd.magFilter = rhi::FilterMode::Nearest; + sd.addressU = rhi::AddressMode::ClampToBorder; sd.addressV = rhi::AddressMode::ClampToBorder; + sd.addressW = rhi::AddressMode::ClampToBorder; sd.borderColor = bc; + return m_device->createSampler(sd, out); + }; + if (makeSampler(rhi::SamplerBorderColor::TransparentBlack, m_sampTransparent) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (makeSampler(rhi::SamplerBorderColor::OpaqueBlack, m_sampOpaqueBlack) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (makeSampler(rhi::SamplerBorderColor::OpaqueWhite, m_sampOpaqueWhite) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bind group layout: set 0 = texture + sampler. + rhi::BindGroupLayoutEntry tE[2] = { rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment) }; + rhi::BindGroupLayoutDesc tBgld{}; tBgld.entries = std::span(tE, 2); + if (m_device->createBindGroupLayout(tBgld, m_texBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Three bind groups, one per sampler. + auto makeBG = [&](rhi::Sampler* s, rhi::BindGroup*& out) -> draco::Status { + rhi::BindGroupEntry e[2] = { rhi::BindGroupEntry::textureEntry(m_texView), rhi::BindGroupEntry::samplerEntry(s) }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_texBgl; bgd.entries = std::span(e, 2); + return m_device->createBindGroup(bgd, out); + }; + if (makeBG(m_sampTransparent, m_bgTransparent) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (makeBG(m_sampOpaqueBlack, m_bgOpaqueBlack) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (makeBG(m_sampOpaqueWhite, m_bgOpaqueWhite) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bind group layout: set 1 = uniform buffer with dynamic offset. + rhi::BindGroupLayoutEntry uEntry = rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex); + uEntry.hasDynamicOffset = true; + rhi::BindGroupLayoutEntry uE[1] = { uEntry }; + rhi::BindGroupLayoutDesc uBgld{}; uBgld.entries = std::span(uE, 1); + if (m_device->createBindGroupLayout(uBgld, m_uboBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry uBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_ub, 0, 16) }; + rhi::BindGroupDesc uBgd{}; uBgd.layout = m_uboBgl; uBgd.entries = std::span(uBgE, 1); + if (m_device->createBindGroup(uBgd, m_uboBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout. + rhi::BindGroupLayout* sets[2] = { m_texBgl, m_uboBgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 2); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x2, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 20; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + ct.blend = rhi::BlendState::alphaBlend(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void BorderSamplerSample::onRender() { + using draco::f32, draco::u32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.2f, 0.2f, 0.25f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + + // Draw 3 quads side by side with different samplers and dynamic UBO offsets. + rhi::BindGroup* texBGs[3] = { m_bgTransparent, m_bgOpaqueBlack, m_bgOpaqueWhite }; + u32 dynOffsets[3] = { 0, 256, 512 }; + for (int i = 0; i < 3; ++i) { + rp->setBindGroup(0, texBGs[i]); + u32 off[1] = { dynOffsets[i] }; + rp->setBindGroup(1, m_uboBg, std::span(off, 1)); + rp->drawIndexed(6); + } + + rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void BorderSamplerSample::onShutdown() { + if (m_ub && m_ubMapped) m_ub->unmap(); + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_uboBg) m_device->destroyBindGroup(m_uboBg); if (m_uboBgl) m_device->destroyBindGroupLayout(m_uboBgl); + if (m_bgOpaqueWhite) m_device->destroyBindGroup(m_bgOpaqueWhite); + if (m_bgOpaqueBlack) m_device->destroyBindGroup(m_bgOpaqueBlack); + if (m_bgTransparent) m_device->destroyBindGroup(m_bgTransparent); + if (m_texBgl) m_device->destroyBindGroupLayout(m_texBgl); + if (m_sampOpaqueWhite) m_device->destroySampler(m_sampOpaqueWhite); + if (m_sampOpaqueBlack) m_device->destroySampler(m_sampOpaqueBlack); + if (m_sampTransparent) m_device->destroySampler(m_sampTransparent); + if (m_texView) m_device->destroyTextureView(m_texView); if (m_tex) m_device->destroyTexture(m_tex); + if (m_ub) m_device->destroyBuffer(m_ub); if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BorderSamplerSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample014_Blit/CMakeLists.txt b/Samples/cpp/RHI/Sample014_Blit/CMakeLists.txt new file mode 100644 index 00000000..0b60bb51 --- /dev/null +++ b/Samples/cpp/RHI/Sample014_Blit/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample014_Blit) diff --git a/Samples/cpp/RHI/Sample014_Blit/Main.cpp b/Samples/cpp/RHI/Sample014_Blit/Main.cpp new file mode 100644 index 00000000..caf42850 --- /dev/null +++ b/Samples/cpp/RHI/Sample014_Blit/Main.cpp @@ -0,0 +1,150 @@ +#include +/// Renders a spinning triangle to a small 128x128 offscreen texture, then blits +/// it to the full swapchain (scaled up with linear filtering). + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class BlitSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample014 - Blit (Scaled Copy)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position,1); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return i.Color; } + )"; + static constexpr draco::u32 kOffscreenSize = 128; + + void updateTriangle(); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr; + rhi::PipelineLayout *m_pl = nullptr; rhi::RenderPipeline *m_pipeline = nullptr; + rhi::Texture *m_offscreenTex = nullptr; rhi::TextureView *m_offscreenView = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status BlitSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Triangle VB (CpuToGpu for per-frame rotation updates). + rhi::BufferDesc vbd{}; vbd.size = 84; vbd.usage = rhi::BufferUsage::Vertex; vbd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Offscreen render target. + rhi::TextureDesc td{}; td.format = m_swapChain->format(); td.width = kOffscreenSize; td.height = kOffscreenSize; + td.mipLevelCount = 1; td.usage = rhi::TextureUsage::RenderTarget | rhi::TextureUsage::CopySrc | rhi::TextureUsage::Sampled; + if (m_device->createTexture(td, m_offscreenTex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TextureViewDesc tvd{}; tvd.format = m_swapChain->format(); tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_offscreenTex, tvd, m_offscreenView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void BlitSample::updateTriangle() { + float angle = m_totalTime * 2.0f; + float c = std::cos(angle), s = std::sin(angle); + float basePos[6] = { 0.0f, 0.5f, 0.433f, -0.25f, -0.433f, -0.25f }; + float colors[12] = { 1,0.2f,0.2f,1, 0.2f,1,0.2f,1, 0.2f,0.4f,1,1 }; + float verts[21]; + for (int i = 0; i < 3; ++i) { + float x = basePos[i*2], y = basePos[i*2+1]; + verts[i*7+0] = x*c - y*s; verts[i*7+1] = x*s + y*c; verts[i*7+2] = 0.0f; + verts[i*7+3] = colors[i*4]; verts[i*7+4] = colors[i*4+1]; + verts[i*7+5] = colors[i*4+2]; verts[i*7+6] = colors[i*4+3]; + } + void* mapped = m_vb->map(); + if (mapped) { std::memcpy(mapped, verts, 84); m_vb->unmap(); } +} + +void BlitSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + updateTriangle(); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Pass 1: Render spinning triangle to offscreen texture. + enc->transitionTexture(m_offscreenTex, rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + { + rhi::ColorAttachment ca{}; ca.view = m_offscreenView; + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.15f, 0.1f, 0.2f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(kOffscreenSize), static_cast(kOffscreenSize), 0, 1); + rp->setScissor(0, 0, kOffscreenSize, kOffscreenSize); + rp->setVertexBuffer(0, m_vb, 0); + rp->draw(3); + rp->end(); + } + enc->transitionTexture(m_offscreenTex, rhi::ResourceState::RenderTarget, rhi::ResourceState::CopySrc); + + // Pass 2: Blit offscreen (128x128) to full swapchain (scaled up with linear filtering). + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::CopyDst); + enc->blit(m_offscreenTex, m_swapChain->currentTexture()); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::CopyDst, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void BlitSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_offscreenView) m_device->destroyTextureView(m_offscreenView); + if (m_offscreenTex) m_device->destroyTexture(m_offscreenTex); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BlitSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample015_Queries/CMakeLists.txt b/Samples/cpp/RHI/Sample015_Queries/CMakeLists.txt new file mode 100644 index 00000000..9c870272 --- /dev/null +++ b/Samples/cpp/RHI/Sample015_Queries/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample015_Queries) diff --git a/Samples/cpp/RHI/Sample015_Queries/Main.cpp b/Samples/cpp/RHI/Sample015_Queries/Main.cpp new file mode 100644 index 00000000..2deb67a0 --- /dev/null +++ b/Samples/cpp/RHI/Sample015_Queries/Main.cpp @@ -0,0 +1,169 @@ +#include +/// Demonstrates GPU timestamp queries to measure render pass duration. +/// Prints render pass GPU time to console every 2 seconds. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class QuerySample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample015 - GPU Queries"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position,1); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return i.Color; } + )"; + static constexpr float kVerts[] = { + 0.0f, 0.5f, 0.0f, 1.0f, 0.3f, 0.3f, 1.0f, + 0.5f, -0.5f, 0.0f, 0.3f, 1.0f, 0.3f, 1.0f, + -0.5f, -0.5f, 0.0f, 0.3f, 0.3f, 1.0f, 1.0f, + }; + static constexpr draco::u16 kIdx[] = { 0, 1, 2 }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; rhi::RenderPipeline *m_pipeline = nullptr; + rhi::QuerySet *m_tsQuerySet = nullptr; + rhi::Buffer *m_queryResultBuf = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; + int m_frameCount = 0; + float m_lastReportTime = 0.0f; +}; + +draco::Status QuerySample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Timestamp query set: 2 queries (before + after render pass). + rhi::QuerySetDesc qsd{}; qsd.type = rhi::QueryType::Timestamp; qsd.count = 2; + if (m_device->createQuerySet(qsd, m_tsQuerySet) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffer to receive resolved query results (2 * uint64 = 16 bytes). + rhi::BufferDesc qbd{}; qbd.size = 16; qbd.usage = rhi::BufferUsage::CopyDst; qbd.memory = rhi::MemoryLocation::GpuToCpu; + if (m_device->createBuffer(qbd, m_queryResultBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void QuerySample::onRender() { + using draco::f32, draco::u64, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + + // Read back previous frame's query results (after fence wait ensures GPU is done). + if (m_frameCount > 1) { + void* mapped = m_queryResultBuf->map(); + if (mapped) { + auto* timestamps = static_cast(mapped); + u64 begin = timestamps[0], end = timestamps[1], delta = end - begin; + f32 period = m_graphicsQueue->timestampPeriod(); + f32 gpuTimeUs = static_cast(delta) * period / 1000.0f; + if (m_totalTime - m_lastReportTime >= 2.0f) { + std::printf("GPU render pass time: %.2f us (%llu ticks, period=%.2f ns)\n", + gpuTimeUs, static_cast(delta), period); + m_lastReportTime = m_totalTime; + } + m_queryResultBuf->unmap(); + } + } + + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Reset queries for this frame. + enc->resetQuerySet(m_tsQuerySet, 0, 2); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + // Timestamp before render pass. + enc->writeTimestamp(m_tsQuerySet, 0); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(3); + rp->end(); + + // Timestamp after render pass. + enc->writeTimestamp(m_tsQuerySet, 1); + + // Resolve timestamps to buffer. + enc->resolveQuerySet(m_tsQuerySet, 0, 2, m_queryResultBuf, 0); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); + m_frameCount++; +} + +void QuerySample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_queryResultBuf) m_device->destroyBuffer(m_queryResultBuf); + if (m_tsQuerySet) m_device->destroyQuerySet(m_tsQuerySet); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { QuerySample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample016_Readback/CMakeLists.txt b/Samples/cpp/RHI/Sample016_Readback/CMakeLists.txt new file mode 100644 index 00000000..9f771a60 --- /dev/null +++ b/Samples/cpp/RHI/Sample016_Readback/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample016_Readback) diff --git a/Samples/cpp/RHI/Sample016_Readback/Main.cpp b/Samples/cpp/RHI/Sample016_Readback/Main.cpp new file mode 100644 index 00000000..501b0367 --- /dev/null +++ b/Samples/cpp/RHI/Sample016_Readback/Main.cpp @@ -0,0 +1,212 @@ +#include +/// Renders a colored triangle to a small offscreen texture, copies it to a +/// readback buffer, then reads pixel values on the CPU and prints them. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class ReadbackSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample016 - GPU Readback"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float4 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float4 Color : COLOR0; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = float4(i.Position,1); o.Color = i.Color; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return i.Color; } + )"; + static constexpr draco::u32 kTexSize = 16; + static constexpr float kVerts[] = { + 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, + }; + + void readbackPixels(); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_offPipeline = nullptr, *m_swapPipeline = nullptr; + rhi::Texture *m_offTex = nullptr; rhi::TextureView *m_offView = nullptr; + rhi::Buffer *m_readbackBuf = nullptr; + rhi::CommandPool *m_pool = nullptr; rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; + bool m_hasReadback = false; + float m_lastReportTime = 0.0f; +}; + +draco::Status ReadbackSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Small offscreen RGBA8 texture. + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = kTexSize; td.height = kTexSize; + td.mipLevelCount = 1; td.usage = rhi::TextureUsage::RenderTarget | rhi::TextureUsage::CopySrc; + if (m_device->createTexture(td, m_offTex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_offTex, tvd, m_offView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Readback buffer with row alignment (256 bytes for DX12 compat). + u32 bytesPerRow = ((kTexSize * 4 + 255) / 256) * 256; + rhi::BufferDesc rbd{}; rbd.size = bytesPerRow * kTexSize; rbd.usage = rhi::BufferUsage::CopyDst; rbd.memory = rhi::MemoryLocation::GpuToCpu; + if (m_device->createBuffer(rbd, m_readbackBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + + // Pipeline for offscreen (RGBA8Unorm). + rhi::ColorTargetState ct{}; ct.format = rhi::TextureFormat::RGBA8Unorm; + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_offPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline for swapchain (different format). + ct.format = m_swapChain->format(); + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_swapPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void ReadbackSample::readbackPixels() { + void* mapped = m_readbackBuf->map(); + if (!mapped) return; + draco::u32 bytesPerRow = ((kTexSize * 4 + 255) / 256) * 256; + auto* data = static_cast(mapped); + + std::printf("=== Readback: %ux%u RGBA8 texture ===\n", kTexSize, kTexSize); + auto printPixel = [&](draco::u32 x, draco::u32 y, const char* label) { + draco::u32 off = y * bytesPerRow + x * 4; + std::printf(" %s (%u,%u): R=%u G=%u B=%u A=%u\n", label, x, y, + data[off], data[off+1], data[off+2], data[off+3]); + }; + printPixel(0, 0, "Top-left"); + printPixel(kTexSize-1, 0, "Top-right"); + printPixel(kTexSize/2, kTexSize/2, "Center"); + printPixel(0, kTexSize-1, "Bottom-left"); + printPixel(kTexSize-1, kTexSize-1, "Bottom-right"); + + int nonBlack = 0; + for (draco::u32 y = 0; y < kTexSize; ++y) + for (draco::u32 x = 0; x < kTexSize; ++x) { + draco::u32 off = y * bytesPerRow + x * 4; + if (data[off] > 0 || data[off+1] > 0 || data[off+2] > 0) nonBlack++; + } + std::printf("Non-black pixels: %d / %u (%.0f%%)\n", nonBlack, kTexSize * kTexSize, + 100.0f * static_cast(nonBlack) / static_cast(kTexSize * kTexSize)); + m_readbackBuf->unmap(); +} + +void ReadbackSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + + if (m_hasReadback && (m_totalTime - m_lastReportTime >= 3.0f)) { + readbackPixels(); + m_lastReportTime = m_totalTime; + } + + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Render triangle to offscreen texture. + enc->transitionTexture(m_offTex, rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + { + rhi::ColorAttachment ca{}; ca.view = m_offView; + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.0f, 0.0f, 0.0f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_offPipeline); + rp->setViewport(0, 0, static_cast(kTexSize), static_cast(kTexSize), 0, 1); + rp->setScissor(0, 0, kTexSize, kTexSize); + rp->setVertexBuffer(0, m_vb, 0); + rp->draw(3); + rp->end(); + } + enc->transitionTexture(m_offTex, rhi::ResourceState::RenderTarget, rhi::ResourceState::CopySrc); + + // Copy texture to readback buffer. + draco::u32 bytesPerRow = ((kTexSize * 4 + 255) / 256) * 256; + rhi::BufferTextureCopyRegion region{}; + region.bufferOffset = 0; region.bytesPerRow = bytesPerRow; region.rowsPerImage = kTexSize; + region.textureExtent = rhi::Extent3D{ kTexSize, kTexSize, 1 }; + enc->copyTextureToBuffer(m_offTex, m_readbackBuf, region); + + // Also render to swapchain so we see something. + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + { + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_swapPipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->draw(3); + rp->end(); + } + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); + m_hasReadback = true; +} + +void ReadbackSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_swapPipeline) m_device->destroyRenderPipeline(m_swapPipeline); + if (m_offPipeline) m_device->destroyRenderPipeline(m_offPipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_readbackBuf) m_device->destroyBuffer(m_readbackBuf); + if (m_offView) m_device->destroyTextureView(m_offView); + if (m_offTex) m_device->destroyTexture(m_offTex); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { ReadbackSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample017_MultiQueue/CMakeLists.txt b/Samples/cpp/RHI/Sample017_MultiQueue/CMakeLists.txt new file mode 100644 index 00000000..9b45c2bb --- /dev/null +++ b/Samples/cpp/RHI/Sample017_MultiQueue/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample017_MultiQueue) diff --git a/Samples/cpp/RHI/Sample017_MultiQueue/Main.cpp b/Samples/cpp/RHI/Sample017_MultiQueue/Main.cpp new file mode 100644 index 00000000..f6127229 --- /dev/null +++ b/Samples/cpp/RHI/Sample017_MultiQueue/Main.cpp @@ -0,0 +1,279 @@ +#include +/// A compute shader generates an animated vertex grid on the compute queue, +/// then the graphics queue waits on the compute fence and renders the result. + +#include +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; +using draco::math::Matrix4; + +class MultiQueueSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample017 - MultiQueue (Async Compute)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateDepth(w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kComputeSrc[] = u8R"( + cbuffer Params : register(b0, space0) { float Time; uint NumPoints; float Spacing; float Padding; }; + struct Vertex { float PosX, PosY, PosZ, ColR, ColG, ColB; }; + RWStructuredBuffer gVertices : register(u0, space0); + [numthreads(64, 1, 1)] + void CSMain(uint3 dtid : SV_DispatchThreadID) { + uint idx = dtid.x; if (idx >= NumPoints) return; + uint gridSize = (uint)sqrt((float)NumPoints); + uint row = idx / gridSize, col = idx % gridSize; + float fx = ((float)col / (float)(gridSize-1))*2.0 - 1.0; + float fz = ((float)row / (float)(gridSize-1))*2.0 - 1.0; + float dist = sqrt(fx*fx + fz*fz); + float fy = sin(dist*8.0 - Time*3.0) * 0.2; + gVertices[idx].PosX = fx; gVertices[idx].PosY = fy; gVertices[idx].PosZ = fz; + gVertices[idx].ColR = 0.5 + 0.5*sin(Time + fx*3.0); + gVertices[idx].ColG = 0.5 + 0.5*cos(Time + fz*3.0); + gVertices[idx].ColB = 0.5 + 0.5*sin(Time*0.7 + dist*4.0); + } + )"; + static constexpr const char8_t kRenderSrc[] = u8R"( + cbuffer ViewProj : register(b0, space0) { row_major float4x4 VP; }; + struct VSInput { float3 Position : TEXCOORD0; float3 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Color : COLOR0; + [[vk::builtin("PointSize")]] float PointSize : PSIZE; }; + PSInput VSMain(VSInput i) { PSInput o; o.Position = mul(float4(i.Position,1), VP); o.Color = i.Color; o.PointSize = 1.0; return o; } + float4 PSMain(PSInput i) : SV_TARGET { return float4(i.Color, 1.0); } + )"; + + static constexpr draco::u32 kGrid = 64, kNumPts = kGrid*kGrid, kVertSz = 24, kBufSz = kNumPts*kVertSz; + + void recreateDepth(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_cs = nullptr, *m_vs = nullptr, *m_ps = nullptr; + + // Compute resources. + rhi::Queue* m_computeQueue = nullptr; + rhi::CommandPool *m_computePool = nullptr; + rhi::BindGroupLayout *m_compBgl = nullptr; rhi::BindGroup *m_compBg = nullptr; + rhi::PipelineLayout *m_compPl = nullptr; rhi::ComputePipeline *m_compPipe = nullptr; + rhi::Buffer *m_paramsBuf = nullptr; void *m_paramsMapped = nullptr; + + // Graphics resources. + rhi::CommandPool *m_gfxPool = nullptr; + rhi::BindGroupLayout *m_renBgl = nullptr; rhi::BindGroup *m_renBg = nullptr; + rhi::PipelineLayout *m_renPl = nullptr; rhi::RenderPipeline *m_renPipe = nullptr; + rhi::Buffer *m_vpBuf = nullptr; void *m_vpMapped = nullptr; + + // Shared. + rhi::Buffer *m_vtxBuf = nullptr; + sf::DepthBuffer m_depthBuf; + + // Synchronization. + rhi::Fence *m_compFence = nullptr, *m_gfxFence = nullptr; + draco::u64 m_compFenceVal = 0, m_gfxFenceVal = 0; + bool m_hasDedicatedCompute = false; + float m_lastReportTime = 0.0f; +}; + +void MultiQueueSample::recreateDepth(draco::u32 w, draco::u32 h) { + m_depthBuf.recreate(m_device, w, h); +} + +draco::Status MultiQueueSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + // Check for dedicated compute queue. + if (m_device->getQueueCount(rhi::QueueType::Compute) == 0) { + m_computeQueue = m_graphicsQueue; + m_hasDedicatedCompute = false; + std::printf("No dedicated compute queue - using graphics queue for both\n"); + } else { + m_computeQueue = m_device->getQueue(rhi::QueueType::Compute, 0); + m_hasDedicatedCompute = true; + std::printf("Using dedicated compute queue\n"); + } + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kComputeSrc, shaders::ShaderStage::Compute, u8"CSMain", u8"CS", m_cs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kRenderSrc, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kRenderSrc, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Shared vertex/storage buffer. + rhi::BufferDesc vbd{}; vbd.size = kBufSz; vbd.usage = rhi::BufferUsage::Storage | rhi::BufferUsage::Vertex; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vtxBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Compute params UBO. + rhi::BufferDesc pbd{}; pbd.size = 16; pbd.usage = rhi::BufferUsage::Uniform; pbd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(pbd, m_paramsBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_paramsMapped = m_paramsBuf->map(); + + // View-projection UBO. + rhi::BufferDesc vpd{}; vpd.size = 64; vpd.usage = rhi::BufferUsage::Uniform; vpd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(vpd, m_vpBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_vpMapped = m_vpBuf->map(); + + // Compute pipeline. + rhi::BindGroupLayoutEntry cE[2] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Compute), + rhi::BindGroupLayoutEntry::storageBuffer(0, rhi::ShaderStage::Compute, false) }; + rhi::BindGroupLayoutDesc cBgld{}; cBgld.entries = std::span(cE, 2); + if (m_device->createBindGroupLayout(cBgld, m_compBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry cBgE[2] = { rhi::BindGroupEntry::bufferEntry(m_paramsBuf, 0, 16), + rhi::BindGroupEntry::bufferEntry(m_vtxBuf, 0, kBufSz) }; + rhi::BindGroupDesc cBgd{}; cBgd.layout = m_compBgl; cBgd.entries = std::span(cBgE, 2); + if (m_device->createBindGroup(cBgd, m_compBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* cSets[1] = { m_compBgl }; + rhi::PipelineLayoutDesc cPld{}; cPld.bindGroupLayouts = std::span(cSets, 1); + if (m_device->createPipelineLayout(cPld, m_compPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::ComputePipelineDesc cpd{}; cpd.layout = m_compPl; cpd.compute = { m_cs, u8"CSMain", rhi::ShaderStage::Compute }; + if (m_device->createComputePipeline(cpd, m_compPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render pipeline. + rhi::BindGroupLayoutEntry rE[1] = { rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex) }; + rhi::BindGroupLayoutDesc rBgld{}; rBgld.entries = std::span(rE, 1); + if (m_device->createBindGroupLayout(rBgld, m_renBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupEntry rBgE[1] = { rhi::BindGroupEntry::bufferEntry(m_vpBuf, 0, 64) }; + rhi::BindGroupDesc rBgd{}; rBgd.layout = m_renBgl; rBgd.entries = std::span(rBgE, 1); + if (m_device->createBindGroup(rBgd, m_renBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BindGroupLayout* rSets[1] = { m_renBgl }; + rhi::PipelineLayoutDesc rPld{}; rPld.bindGroupLayouts = std::span(rSets, 1); + if (m_device->createPipelineLayout(rPld, m_renPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + m_depthBuf.recreate(m_device, m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x3, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = kVertSz; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_renPl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::PointList; + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthWriteEnabled = true; rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + if (m_device->createRenderPipeline(rpd, m_renPipe) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Command pools - one per queue type. + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_gfxPool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + auto compPoolType = m_hasDedicatedCompute ? rhi::QueueType::Compute : rhi::QueueType::Graphics; + if (m_device->createCommandPool(compPoolType, m_computePool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createFence(0, m_compFence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_gfxFence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void MultiQueueSample::onRender() { + using draco::u32, draco::f32, std::span; + if (m_gfxFenceVal > 0) m_gfxFence->wait(m_gfxFenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Update compute params. + u32 numPts = kNumPts; + f32 params[4] = { m_totalTime, 0, 1.0f, 0 }; + std::memcpy(¶ms[1], &numPts, 4); + std::memcpy(m_paramsMapped, params, 16); + + // Update VP. + f32 aspect = static_cast(m_width) / static_cast(m_height); + f32 camAngle = m_totalTime * 0.4f, camDist = 2.5f; + Matrix4 view = Matrix4::lookAtRH(draco::math::Vector3{std::sin(camAngle)*camDist, 1.2f, std::cos(camAngle)*camDist}, draco::math::Vector3{ 0,0,0}, draco::math::Vector3{0,1,0}); + Matrix4 proj = Matrix4::perspectiveFovRH(draco::math::degToRad(45.0f), aspect, 0.1f, 100.0f); + Matrix4 vp = view * proj; + std::memcpy(m_vpMapped, vp.data(), 64); + + // === Compute pass on compute queue === + m_computePool->reset(); + rhi::CommandEncoder* cEnc = nullptr; + if (m_computePool->createEncoder(cEnc) != draco::ErrorCode::Ok || !cEnc) return; + + rhi::BufferBarrier bb{}; bb.buffer = m_vtxBuf; bb.oldState = rhi::ResourceState::VertexBuffer; bb.newState = rhi::ResourceState::ShaderWrite; + rhi::BarrierGroup bg1{}; bg1.bufferBarriers = std::span(&bb, 1); + cEnc->barrier(bg1); + auto* cp = cEnc->beginComputePass(u8"AsyncCompute"); + cp->setPipeline(m_compPipe); cp->setBindGroup(0, m_compBg); + cp->dispatch((kNumPts + 63) / 64); cp->end(); + bb.oldState = rhi::ResourceState::ShaderWrite; bb.newState = rhi::ResourceState::VertexBuffer; + cEnc->barrier(bg1); + + rhi::CommandBuffer* cCb = cEnc->finish(); m_compFenceVal++; + rhi::CommandBuffer* cCbs[1] = { cCb }; + m_computeQueue->submit(std::span(cCbs, 1), m_compFence, m_compFenceVal); + m_computePool->destroyEncoder(cEnc); + + // === Graphics pass - waits on compute fence before executing === + m_gfxPool->reset(); + rhi::CommandEncoder* gEnc = nullptr; + if (m_gfxPool->createEncoder(gEnc) != draco::ErrorCode::Ok || !gEnc) return; + + gEnc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + gEnc->transitionTexture(m_depthBuf.texture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.03f, 0.03f, 0.06f, 1.0f); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthBuf.view; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = gEnc->beginRenderPass(rpd); + rp->setPipeline(m_renPipe); rp->setBindGroup(0, m_renBg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vtxBuf, 0); + rp->draw(kNumPts); rp->end(); + + gEnc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* gCb = gEnc->finish(); m_gfxFenceVal++; + rhi::CommandBuffer* gCbs[1] = { gCb }; + + // Submit graphics - wait on compute fence, signal graphics fence. + rhi::Fence* waitFences[1] = { m_compFence }; + draco::u64 waitValues[1] = { m_compFenceVal }; + m_graphicsQueue->submit(std::span(gCbs, 1), + std::span(waitFences, 1), + std::span(waitValues, 1), + m_gfxFence, m_gfxFenceVal); + m_swapChain->present(m_graphicsQueue); + m_gfxPool->destroyEncoder(gEnc); + + if (m_totalTime - m_lastReportTime >= 3.0f) { + std::printf("MultiQueue: compute fence=%llu, graphics fence=%llu, dt=%.2fms\n", + static_cast(m_compFenceVal), + static_cast(m_gfxFenceVal), + m_deltaTime * 1000.0f); + m_lastReportTime = m_totalTime; + } +} + +void MultiQueueSample::onShutdown() { + if (m_paramsBuf && m_paramsMapped) m_paramsBuf->unmap(); + if (m_vpBuf && m_vpMapped) m_vpBuf->unmap(); + m_depthBuf.destroy(m_device); + if (m_gfxFence) m_device->destroyFence(m_gfxFence); if (m_compFence) m_device->destroyFence(m_compFence); + if (m_gfxPool) m_device->destroyCommandPool(m_gfxPool); if (m_computePool) m_device->destroyCommandPool(m_computePool); + if (m_renPipe) m_device->destroyRenderPipeline(m_renPipe); if (m_renPl) m_device->destroyPipelineLayout(m_renPl); + if (m_renBg) m_device->destroyBindGroup(m_renBg); if (m_renBgl) m_device->destroyBindGroupLayout(m_renBgl); + if (m_compPipe) m_device->destroyComputePipeline(m_compPipe); if (m_compPl) m_device->destroyPipelineLayout(m_compPl); + if (m_compBg) m_device->destroyBindGroup(m_compBg); if (m_compBgl) m_device->destroyBindGroupLayout(m_compBgl); + if (m_vpBuf) m_device->destroyBuffer(m_vpBuf); if (m_paramsBuf) m_device->destroyBuffer(m_paramsBuf); + if (m_vtxBuf) m_device->destroyBuffer(m_vtxBuf); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_cs) m_device->destroyShaderModule(m_cs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MultiQueueSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample018_Bindless/CMakeLists.txt b/Samples/cpp/RHI/Sample018_Bindless/CMakeLists.txt new file mode 100644 index 00000000..187c8951 --- /dev/null +++ b/Samples/cpp/RHI/Sample018_Bindless/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample018_Bindless) diff --git a/Samples/cpp/RHI/Sample018_Bindless/Main.cpp b/Samples/cpp/RHI/Sample018_Bindless/Main.cpp new file mode 100644 index 00000000..bc360e74 --- /dev/null +++ b/Samples/cpp/RHI/Sample018_Bindless/Main.cpp @@ -0,0 +1,339 @@ +#include +/// Demonstrates bindless texture arrays with material index via push constants. +/// Creates 4 procedural textures, binds them in a bindless array, and renders +/// 4 quads each selecting a different texture via push constant index. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class BindlessSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample018 - Bindless Textures"; } + rhi::DeviceFeatures requiredFeatures() const override { + rhi::DeviceFeatures f{}; f.bindlessDescriptors = true; return f; + } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + draco::Status createTextures(); + void generatePixel(draco::u32 texIndex, draco::u32 x, draco::u32 y, draco::u8* rgba); + + static constexpr const char8_t kShader[] = u8R"( + Texture2D gTextures[] : register(t0, space0); + SamplerState gSampler : register(s0, space1); + + struct PushData + { + uint TextureIndex; + float OffsetX; + float OffsetY; + float Padding; + }; + + [[vk::push_constant]] ConstantBuffer gPush : register(b0, space2); + + struct PSInput + { + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; + }; + + PSInput VSMain(uint vertexID : SV_VertexID) + { + // Fullscreen-quad-style: 4 vertices for a unit quad + float2 positions[4] = { + float2(-0.4, 0.4), + float2( 0.4, 0.4), + float2(-0.4,-0.4), + float2( 0.4,-0.4) + }; + float2 uvs[4] = { + float2(0, 0), float2(1, 0), + float2(0, 1), float2(1, 1) + }; + + PSInput output; + float2 pos = positions[vertexID]; + pos.x += gPush.OffsetX; + pos.y += gPush.OffsetY; + output.Position = float4(pos, 0.0, 1.0); + output.TexCoord = uvs[vertexID]; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return gTextures[gPush.TextureIndex].Sample(gSampler, input.TexCoord); + } + )"; + + static constexpr draco::u32 kTexSize = 64; + static constexpr draco::u32 kNumTextures = 4; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + + // Textures + rhi::Texture* m_textures[kNumTextures] = {}; + rhi::TextureView* m_textureViews[kNumTextures] = {}; + rhi::Sampler* m_sampler = nullptr; + + // Bindless bind group (space0: bindless textures) + rhi::BindGroupLayout* m_bindlessBgl = nullptr; + rhi::BindGroup* m_bindlessBg = nullptr; + + // Sampler bind group (space1: sampler) + rhi::BindGroupLayout* m_samplerBgl = nullptr; + rhi::BindGroup* m_samplerBg = nullptr; + + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status BindlessSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"BindlessVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"BindlessPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create 4 procedural textures with different patterns + if (createTextures() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Sampler + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Linear; sd.magFilter = rhi::FilterMode::Linear; + sd.addressU = rhi::AddressMode::Repeat; sd.addressV = rhi::AddressMode::Repeat; + sd.label = u8"BindlessSampler"; + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bindless BGL (space0): unbounded texture array + rhi::BindGroupLayoutEntry bindlessEntry{}; + bindlessEntry.binding = 0; + bindlessEntry.visibility = rhi::ShaderStage::Fragment; + bindlessEntry.type = rhi::BindingType::BindlessTextures; + bindlessEntry.textureDimension = rhi::TextureViewDimension::Texture2D; + bindlessEntry.count = 0xFFFFFFFF; + rhi::BindGroupLayoutEntry blEntries[1] = { bindlessEntry }; + rhi::BindGroupLayoutDesc blBgld{}; blBgld.entries = std::span(blEntries, 1); + blBgld.label = u8"BindlessBGL"; + if (m_device->createBindGroupLayout(blBgld, m_bindlessBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create bindless bind group (no entries at creation - populated via updateBindless) + rhi::BindGroupDesc blBgd{}; blBgd.layout = m_bindlessBgl; blBgd.label = u8"BindlessBG"; + if (m_device->createBindGroup(blBgd, m_bindlessBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Populate bindless slots + rhi::BindlessUpdateEntry bindlessUpdates[kNumTextures]; + for (u32 i = 0; i < kNumTextures; ++i) { + bindlessUpdates[i] = {}; + bindlessUpdates[i].layoutIndex = 0; + bindlessUpdates[i].arrayIndex = i; + bindlessUpdates[i].textureView = m_textureViews[i]; + } + m_bindlessBg->updateBindless(std::span(bindlessUpdates, kNumTextures)); + + // Sampler BGL (space1) + rhi::BindGroupLayoutEntry samplerEntry = rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment); + rhi::BindGroupLayoutEntry sEntries[1] = { samplerEntry }; + rhi::BindGroupLayoutDesc sBgld{}; sBgld.entries = std::span(sEntries, 1); + sBgld.label = u8"SamplerBGL"; + if (m_device->createBindGroupLayout(sBgld, m_samplerBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupEntry sBgEntries[1] = { rhi::BindGroupEntry::samplerEntry(m_sampler) }; + rhi::BindGroupDesc sBgd{}; sBgd.layout = m_samplerBgl; + sBgd.entries = std::span(sBgEntries, 1); sBgd.label = u8"SamplerBG"; + if (m_device->createBindGroup(sBgd, m_samplerBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout: group 0 = bindless textures, group 1 = sampler, push constants + rhi::BindGroupLayout* sets[2] = { m_bindlessBgl, m_samplerBgl }; + rhi::PushConstantRange pcr{}; pcr.stages = rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment; + pcr.offset = 0; pcr.size = 16; + rhi::PushConstantRange pushRanges[1] = { pcr }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 2); + pld.pushConstantRanges = std::span(pushRanges, 1); + pld.label = u8"BindlessPL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render pipeline (no vertex buffers - SV_VertexID driven) + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleStrip; + rpd.label = u8"BindlessPipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +draco::Status BindlessSample::createTextures() { + using draco::Status, std::span, draco::u8, draco::u32; + + constexpr u32 rowBytes = kTexSize * 4; + constexpr u32 texBytes = rowBytes * kTexSize; + u8 pixels[texBytes]; + + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + + for (u32 t = 0; t < kNumTextures; ++t) { + // Generate pattern + for (u32 y = 0; y < kTexSize; ++y) { + for (u32 x = 0; x < kTexSize; ++x) { + u32 offset = (y * kTexSize + x) * 4; + generatePixel(t, x, y, &pixels[offset]); + } + } + + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = kTexSize; td.height = kTexSize; + td.mipLevelCount = 1; td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + td.label = u8"BindlessTex"; + if (m_device->createTexture(td, m_textures[t]) != draco::ErrorCode::Ok) { + m_graphicsQueue->destroyTransferBatch(batch); return draco::ErrorCode::Unknown; + } + + rhi::TextureDataLayout layout{}; layout.bytesPerRow = rowBytes; layout.rowsPerImage = kTexSize; + batch->writeTexture(m_textures[t], std::span(pixels, texBytes), + layout, rhi::Extent3D{kTexSize, kTexSize, 1}); + + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; + tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_textures[t], tvd, m_textureViews[t]) != draco::ErrorCode::Ok) { + m_graphicsQueue->destroyTransferBatch(batch); return draco::ErrorCode::Unknown; + } + } + + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + return draco::ErrorCode::Ok; +} + +void BindlessSample::generatePixel(draco::u32 texIndex, draco::u32 x, draco::u32 y, draco::u8* rgba) { + float fx = static_cast(x) / static_cast(kTexSize); + float fy = static_cast(y) / static_cast(kTexSize); + + switch (texIndex) { + case 0: { // Red/white checkerboard + bool check = ((x / 8) + (y / 8)) % 2 == 0; + rgba[0] = check ? 220 : 255; + rgba[1] = check ? 30 : 255; + rgba[2] = check ? 30 : 255; + rgba[3] = 255; + break; + } + case 1: { // Green gradient with stripes + auto g = static_cast(fx * 255.0f); + bool stripe = (y % 16) < 8; + rgba[0] = stripe ? 30 : 10; + rgba[1] = stripe ? g : static_cast(g / 2); + rgba[2] = stripe ? 50 : 30; + rgba[3] = 255; + break; + } + case 2: { // Blue circles + float cx = fx - 0.5f, cy = fy - 0.5f; + float dist = std::sqrt(cx * cx + cy * cy); + float rings = std::sin(dist * 30.0f) * 0.5f + 0.5f; + rgba[0] = static_cast(rings * 60); + rgba[1] = static_cast(rings * 100); + rgba[2] = static_cast(rings * 255); + rgba[3] = 255; + break; + } + default: { // Yellow/purple diagonal + float diag = std::sin((fx + fy) * 10.0f) * 0.5f + 0.5f; + rgba[0] = static_cast(diag * 255 + (1.0f - diag) * 120); + rgba[1] = static_cast(diag * 220); + rgba[2] = static_cast((1.0f - diag) * 200); + rgba[3] = 255; + break; + } + } +} + +void BindlessSample::onRender() { + using draco::f32, draco::u32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.06f, 0.12f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setBindGroup(0, m_bindlessBg); + rp->setBindGroup(1, m_samplerBg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + + // Draw 4 quads, each with a different texture index via push constants + // Layout: 2x2 grid + float offsets[8] = { -0.45f, 0.45f, 0.45f, 0.45f, -0.45f, -0.45f, 0.45f, -0.45f }; + + for (u32 i = 0; i < kNumTextures; ++i) { + u32 pushData[4] = { i, 0, 0, 0 }; + std::memcpy(&pushData[1], &offsets[i * 2], 4); + std::memcpy(&pushData[2], &offsets[i * 2 + 1], 4); + rp->setPushConstants(rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, 0, 16, pushData); + rp->draw(4); + } + + rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void BindlessSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_samplerBg) m_device->destroyBindGroup(m_samplerBg); + if (m_samplerBgl) m_device->destroyBindGroupLayout(m_samplerBgl); + if (m_bindlessBg) m_device->destroyBindGroup(m_bindlessBg); + if (m_bindlessBgl) m_device->destroyBindGroupLayout(m_bindlessBgl); + if (m_sampler) m_device->destroySampler(m_sampler); + for (int i = kNumTextures - 1; i >= 0; --i) { + if (m_textureViews[i]) m_device->destroyTextureView(m_textureViews[i]); + if (m_textures[i]) m_device->destroyTexture(m_textures[i]); + } + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BindlessSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample019_BatchUpload/CMakeLists.txt b/Samples/cpp/RHI/Sample019_BatchUpload/CMakeLists.txt new file mode 100644 index 00000000..7ed0d6b5 --- /dev/null +++ b/Samples/cpp/RHI/Sample019_BatchUpload/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample019_BatchUpload) diff --git a/Samples/cpp/RHI/Sample019_BatchUpload/Main.cpp b/Samples/cpp/RHI/Sample019_BatchUpload/Main.cpp new file mode 100644 index 00000000..e1ea07dd --- /dev/null +++ b/Samples/cpp/RHI/Sample019_BatchUpload/Main.cpp @@ -0,0 +1,329 @@ +#include +/// Demonstrates batched GPU uploads using TransferBatch with async fence signaling. +/// Uploads a vertex buffer, index buffer, and a procedural texture in a single +/// batched transfer with submitAsync, then renders a textured quad once the +/// upload fence signals completion. + +#include +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class BatchUploadSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample019 - Batch Upload (Async Transfer)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + draco::Status doBatchUpload(); + + static constexpr const char8_t kShader[] = u8R"( + Texture2D gTexture : register(t0, space0); + SamplerState gSampler : register(s0, space0); + + struct VSInput + { + float3 Position : TEXCOORD0; + float2 TexCoord : TEXCOORD1; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; + }; + + cbuffer Transform : register(b0, space0) + { + float Time; + float Pad0; + float Pad1; + float Pad2; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + // Gentle rotation + float c = cos(Time * 0.5); + float s = sin(Time * 0.5); + float3 p = input.Position; + float x = p.x * c - p.y * s; + float y = p.x * s + p.y * c; + output.Position = float4(x, y, p.z, 1.0); + output.TexCoord = input.TexCoord; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return gTexture.Sample(gSampler, input.TexCoord); + } + )"; + + static constexpr draco::u32 kTexSize = 128; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + + rhi::Buffer* m_vb = nullptr; + rhi::Buffer* m_ib = nullptr; + rhi::Texture* m_tex = nullptr; + rhi::TextureView* m_texView = nullptr; + rhi::Sampler* m_sampler = nullptr; + + rhi::Buffer* m_transformBuf = nullptr; + void* m_transformMapped = nullptr; + + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::BindGroup* m_bg = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_frameFence = nullptr; + draco::u64 m_frameFenceVal = 0; + + // Upload tracking + rhi::Fence* m_uploadFence = nullptr; + draco::u64 m_uploadFenceVal = 0; + bool m_uploadComplete = false; + float m_uploadStartTime = 0.0f; +}; + +draco::Status BatchUploadSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"BatchVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"BatchPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex buffer: 4 vertices x (pos3 + uv2) x 4 = 80 bytes + rhi::BufferDesc vbd{}; vbd.size = 80; vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; vbd.label = u8"BatchVB"; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Index buffer: 6 uint16 = 12 bytes + rhi::BufferDesc ibd{}; ibd.size = 12; ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; ibd.label = u8"BatchIB"; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Texture + rhi::TextureDesc td{}; td.format = rhi::TextureFormat::RGBA8Unorm; td.width = kTexSize; td.height = kTexSize; + td.mipLevelCount = 1; td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; td.label = u8"BatchTex"; + if (m_device->createTexture(td, m_tex) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::RGBA8Unorm; tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + if (m_device->createTextureView(m_tex, tvd, m_texView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::SamplerDesc sd{}; sd.minFilter = rhi::FilterMode::Linear; sd.magFilter = rhi::FilterMode::Linear; + sd.addressU = rhi::AddressMode::Repeat; sd.addressV = rhi::AddressMode::Repeat; sd.label = u8"BatchSampler"; + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Transform UBO + rhi::BufferDesc tbd{}; tbd.size = 16; tbd.usage = rhi::BufferUsage::Uniform; tbd.memory = rhi::MemoryLocation::CpuToGpu; tbd.label = u8"BatchTransform"; + if (m_device->createBuffer(tbd, m_transformBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + m_transformMapped = m_transformBuf->map(); + + // Bind group layout: UBO + texture + sampler + rhi::BindGroupLayoutEntry bglEntries[3] = { + rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex), + rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment), + }; + rhi::BindGroupLayoutDesc bgld{}; bgld.entries = std::span(bglEntries, 3); bgld.label = u8"BatchBGL"; + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BindGroupEntry bgEntries[3] = { + rhi::BindGroupEntry::bufferEntry(m_transformBuf, 0, 16), + rhi::BindGroupEntry::textureEntry(m_texView), + rhi::BindGroupEntry::samplerEntry(m_sampler), + }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_bgl; bgd.entries = std::span(bgEntries, 3); bgd.label = u8"BatchBG"; + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout + rhi::BindGroupLayout* bgls[1] = { m_bgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(bgls, 1); pld.label = u8"BatchPL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Render pipeline + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x2, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 20; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"BatchPipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_frameFence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Upload fence + if (m_device->createFence(0, m_uploadFence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // === Batch upload: VB + IB + texture in one submission === + if (doBatchUpload() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + return draco::ErrorCode::Ok; +} + +draco::Status BatchUploadSample::doBatchUpload() { + using draco::Status, std::span, draco::u8, draco::u32; + + m_uploadStartTime = m_totalTime; + + rhi::TransferBatch* transfer = nullptr; + if (m_graphicsQueue->createTransferBatch(transfer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex data: quad + float verts[20] = { + -0.6f, 0.6f, 0.0f, 0.0f, 0.0f, + 0.6f, 0.6f, 0.0f, 1.0f, 0.0f, + 0.6f, -0.6f, 0.0f, 1.0f, 1.0f, + -0.6f, -0.6f, 0.0f, 0.0f, 1.0f, + }; + transfer->writeBuffer(m_vb, 0, std::span(reinterpret_cast(verts), 80)); + + // Index data + draco::u16 indices[6] = { 0, 1, 2, 0, 2, 3 }; + transfer->writeBuffer(m_ib, 0, std::span(reinterpret_cast(indices), 12)); + + // Texture data: procedural mandelbrot-ish pattern + u32 texBytes = kTexSize * kTexSize * 4; + auto* pixels = new u8[texBytes]; + + for (u32 y = 0; y < kTexSize; y++) { + for (u32 x = 0; x < kTexSize; x++) { + float cr = static_cast(x) / static_cast(kTexSize) * 3.0f - 2.0f; + float ci = static_cast(y) / static_cast(kTexSize) * 2.4f - 1.2f; + float zr = 0, zi = 0; + int iter = 0; + for (iter = 0; iter < 64; iter++) { + float zr2 = zr * zr - zi * zi + cr; + float zi2 = 2.0f * zr * zi + ci; + zr = zr2; zi = zi2; + if (zr * zr + zi * zi > 4.0f) break; + } + + u32 off = (y * kTexSize + x) * 4; + if (iter == 64) { + pixels[off] = 10; pixels[off + 1] = 10; pixels[off + 2] = 30; pixels[off + 3] = 255; + } else { + float t = static_cast(iter) / 64.0f; + pixels[off] = static_cast(t * 200 + 55); + pixels[off + 1] = static_cast(t * t * 255); + pixels[off + 2] = static_cast(std::sqrt(t) * 255); + pixels[off + 3] = 255; + } + } + } + + rhi::TextureDataLayout layout{}; layout.bytesPerRow = kTexSize * 4; layout.rowsPerImage = kTexSize; + transfer->writeTexture(m_tex, std::span(pixels, texBytes), layout, rhi::Extent3D{kTexSize, kTexSize, 1}); + + delete[] pixels; + + // Async submit - signals fence when GPU transfer completes + m_uploadFenceVal = 1; + if (transfer->submitAsync(m_uploadFence, m_uploadFenceVal) != draco::ErrorCode::Ok) { + m_graphicsQueue->destroyTransferBatch(transfer); + return draco::ErrorCode::Unknown; + } + + std::printf("Batch upload submitted asynchronously (VB: 80B, IB: 12B, Tex: %uB)\n", texBytes); + m_graphicsQueue->destroyTransferBatch(transfer); + return draco::ErrorCode::Ok; +} + +void BatchUploadSample::onRender() { + using draco::f32, std::span; + + if (m_frameFenceVal > 0) m_frameFence->wait(m_frameFenceVal, ~0ull); + + // Check if async upload has completed + if (!m_uploadComplete) { + if (m_uploadFence->completedValue() >= m_uploadFenceVal) { + m_uploadComplete = true; + std::printf("Batch upload completed! Rendering enabled.\n"); + } + } + + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Update transform + float transform[4] = { m_totalTime, 0, 0, 0 }; + std::memcpy(m_transformMapped, transform, 16); + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + if (m_uploadComplete) { + rp->setPipeline(m_pipeline); + rp->setBindGroup(0, m_bg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(6); + } + + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_frameFenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_frameFence, m_frameFenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void BatchUploadSample::onShutdown() { + if (m_transformBuf && m_transformMapped) m_transformBuf->unmap(); + + if (m_uploadFence) m_device->destroyFence(m_uploadFence); + if (m_frameFence) m_device->destroyFence(m_frameFence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_sampler) m_device->destroySampler(m_sampler); + if (m_texView) m_device->destroyTextureView(m_texView); + if (m_tex) m_device->destroyTexture(m_tex); + if (m_transformBuf) m_device->destroyBuffer(m_transformBuf); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { BatchUploadSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample020_MeshShaders/CMakeLists.txt b/Samples/cpp/RHI/Sample020_MeshShaders/CMakeLists.txt new file mode 100644 index 00000000..9bbc62b4 --- /dev/null +++ b/Samples/cpp/RHI/Sample020_MeshShaders/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample020_MeshShaders) diff --git a/Samples/cpp/RHI/Sample020_MeshShaders/Main.cpp b/Samples/cpp/RHI/Sample020_MeshShaders/Main.cpp new file mode 100644 index 00000000..04938690 --- /dev/null +++ b/Samples/cpp/RHI/Sample020_MeshShaders/Main.cpp @@ -0,0 +1,236 @@ +#include +/// Demonstrates mesh shader pipeline: a rotating triangle generated entirely in the mesh shader. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +struct PushData { + float time; + float aspectRatio; + float pad0; + float pad1; +}; + +class MeshShaderSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample020 - Mesh Shaders (Rotating Triangle)"; } +protected: + rhi::DeviceFeatures requiredFeatures() const override { + rhi::DeviceFeatures f{}; + f.meshShaders = true; + return f; + } + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kMeshShaderSource[] = u8R"( + struct PushConstants + { + float Time; + float AspectRatio; + float Pad0, Pad1; + }; + + [[vk::push_constant]] ConstantBuffer pc : register(b0, space0); + + struct MeshOutput + { + float4 Position : SV_POSITION; + float3 Color : TEXCOORD0; + }; + + [outputtopology("triangle")] + [numthreads(1, 1, 1)] + void MSMain(out vertices MeshOutput verts[3], out indices uint3 tris[1]) + { + SetMeshOutputCounts(3, 1); + + float angle = pc.Time * 0.5; + float c = cos(angle); + float s = sin(angle); + + float2 positions[3] = { + float2( 0.0, 0.5), + float2(-0.5, -0.5), + float2( 0.5, -0.5) + }; + + float3 colors[3] = { + float3(1.0, 0.0, 0.0), + float3(0.0, 1.0, 0.0), + float3(0.0, 0.0, 1.0) + }; + + for (uint i = 0; i < 3; i++) + { + float2 p = positions[i]; + float2 rotated = float2(p.x * c - p.y * s, p.x * s + p.y * c); + rotated.x /= pc.AspectRatio; + + verts[i].Position = float4(rotated, 0.0, 1.0); + verts[i].Color = colors[i]; + } + + tris[0] = uint3(0, 1, 2); + } + )"; + + static constexpr const char8_t kFragmentShaderSource[] = u8R"( + struct PSInput + { + float4 Position : SV_POSITION; + float3 Color : TEXCOORD0; + }; + + float4 PSMain(PSInput input) : SV_TARGET + { + return float4(input.Color, 1.0); + } + )"; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule* m_meshModule = nullptr; + rhi::ShaderModule* m_fragModule = nullptr; + rhi::PipelineLayout* m_pipelineLayout = nullptr; + rhi::MeshPipeline* m_meshPipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status MeshShaderSample::onInit() { + using draco::Status, std::span; + + // Check mesh shader support. + if (!m_device->features.meshShaders) { + std::fprintf(stderr, "ERROR: Mesh shaders are not supported by this device/backend\n"); + return draco::ErrorCode::Unknown; + } + + // Shader compiler. + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Compile mesh shader (SM 6.5 required for mesh shaders). + if (sf::compileToModule(m_compiler, m_device, kMeshShaderSource, shaders::ShaderStage::Mesh, + u8"MSMain", u8"MeshShader", u8"6_5", m_meshModule) != draco::ErrorCode::Ok) + return draco::ErrorCode::Unknown; + + // Compile fragment shader. + if (sf::compileToModule(m_compiler, m_device, kFragmentShaderSource, shaders::ShaderStage::Fragment, + u8"PSMain", u8"FragmentShader", m_fragModule) != draco::ErrorCode::Ok) + return draco::ErrorCode::Unknown; + + // Pipeline layout with push constants. + rhi::PushConstantRange pushRange{}; + pushRange.stages = rhi::ShaderStage::Mesh; + pushRange.offset = 0; + pushRange.size = sizeof(PushData); + + rhi::PipelineLayoutDesc pld{}; + pld.pushConstantRanges = std::span(&pushRange, 1); + pld.label = u8"MeshPipelineLayout"; + if (m_device->createPipelineLayout(pld, m_pipelineLayout) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create mesh pipeline. + rhi::ColorTargetState ct{}; + ct.format = m_swapChain->format(); + ct.writeMask = rhi::ColorWriteMask::All; + + rhi::MeshPipelineDesc mpd{}; + mpd.layout = m_pipelineLayout; + mpd.mesh = { m_meshModule, u8"MSMain", rhi::ShaderStage::Mesh }; + mpd.fragment = rhi::FragmentState{}; + mpd.fragment->shader = { m_fragModule, u8"PSMain", rhi::ShaderStage::Fragment }; + mpd.fragment->targets = std::span(&ct, 1); + mpd.colorTargets = std::span(&ct, 1); + mpd.label = u8"MeshShaderPipeline"; + if (m_device->createMeshPipeline(mpd, m_meshPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Command pool and fence. + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + return draco::ErrorCode::Ok; +} + +void MeshShaderSample::onRender() { + using draco::f32, std::span; + + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Barrier: present -> render target. + enc->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + // Begin render pass. + rhi::ColorAttachment ca{}; + ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; + ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1.0f); + + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + + // Draw with mesh shader. + if (auto* meshPass = rp->asMeshShaderExt()) { + meshPass->setMeshPipeline(m_meshPipeline); + + // Push constants (must be after pipeline is bound). + PushData pushData{}; + pushData.time = m_totalTime; + pushData.aspectRatio = static_cast(m_width) / static_cast(m_height); + pushData.pad0 = 0.0f; + pushData.pad1 = 0.0f; + rp->setPushConstants(rhi::ShaderStage::Mesh, 0, sizeof(PushData), &pushData); + + meshPass->drawMeshTasks(1); + } + + rp->end(); + + // Barrier: render target -> present. + enc->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void MeshShaderSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_meshPipeline) m_device->destroyMeshPipeline(m_meshPipeline); + if (m_pipelineLayout) m_device->destroyPipelineLayout(m_pipelineLayout); + if (m_fragModule) m_device->destroyShaderModule(m_fragModule); + if (m_meshModule) m_device->destroyShaderModule(m_meshModule); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MeshShaderSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample021_RayTracing/CMakeLists.txt b/Samples/cpp/RHI/Sample021_RayTracing/CMakeLists.txt new file mode 100644 index 00000000..f9c9d6e4 --- /dev/null +++ b/Samples/cpp/RHI/Sample021_RayTracing/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample021_RayTracing) diff --git a/Samples/cpp/RHI/Sample021_RayTracing/Main.cpp b/Samples/cpp/RHI/Sample021_RayTracing/Main.cpp new file mode 100644 index 00000000..05854a3a --- /dev/null +++ b/Samples/cpp/RHI/Sample021_RayTracing/Main.cpp @@ -0,0 +1,564 @@ +#include +/// Demonstrates hardware ray tracing: builds a BLAS/TLAS for a single triangle, +/// dispatches rays via TraceRays, and copies the RT output to the swap chain. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class RayTracingSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample021 - Ray Tracing (TraceRays)"; } +protected: + rhi::DeviceFeatures requiredFeatures() const override { + rhi::DeviceFeatures f{}; + f.rayTracing = true; + return f; + } + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override; + void onShutdown() override; +private: + // Ray tracing shader library (compiled as lib_6_3). + // All RT entry points are in a single source compiled once as a library. + static constexpr const char8_t kRtShaderSource[] = u8R"( + [[vk::image_format("rgba8")]] RWTexture2D gOutput : register(u0, space0); + RaytracingAccelerationStructure gScene : register(t0, space0); + + struct RayPayload + { + float3 Color; + }; + + [shader("raygeneration")] + void RayGen() + { + uint2 launchIndex = DispatchRaysIndex().xy; + uint2 launchDim = DispatchRaysDimensions().xy; + + float2 uv = (float2(launchIndex) + 0.5) / float2(launchDim); + float2 ndc = uv * 2.0 - 1.0; + ndc.y = -ndc.y; + + RayDesc ray; + ray.Origin = float3(ndc.x, ndc.y, -1.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.001; + ray.TMax = 100.0; + + RayPayload payload; + payload.Color = float3(0.0, 0.0, 0.0); + + TraceRay(gScene, RAY_FLAG_FORCE_OPAQUE, 0xFF, 0, 0, 0, ray, payload); + + gOutput[launchIndex] = float4(payload.Color, 1.0); + } + + [shader("closesthit")] + void ClosestHit(inout RayPayload payload, BuiltInTriangleIntersectionAttributes attribs) + { + float3 bary = float3(1.0 - attribs.barycentrics.x - attribs.barycentrics.y, + attribs.barycentrics.x, + attribs.barycentrics.y); + + payload.Color = float3(bary.x, bary.y, bary.z); + } + + [shader("miss")] + void Miss(inout RayPayload payload) + { + float2 uv = (float2(DispatchRaysIndex().xy) + 0.5) / float2(DispatchRaysDimensions().xy); + payload.Color = float3(0.1, 0.1, 0.2) + float3(0.0, 0.0, 0.3) * uv.y; + } + )"; + + // BLAS triangle positions only (float3 per vertex, no color). + static constexpr float kBlasVertexData[9] = { + 0.0f, 0.5f, 0.0f, + -0.5f, -0.5f, 0.0f, + 0.5f, -0.5f, 0.0f + }; + + shaders::Compiler* m_compiler = nullptr; + + // RT resources. + rhi::ShaderModule* m_rtShaderModule = nullptr; + rhi::RayTracingPipeline* m_rtPipeline = nullptr; + rhi::AccelStruct* m_blas = nullptr; + rhi::AccelStruct* m_tlas = nullptr; + rhi::Buffer* m_scratchBuffer = nullptr; + rhi::Buffer* m_rtVertexBuffer = nullptr; // Triangle for BLAS. + rhi::Buffer* m_instanceBuffer = nullptr; // TLAS instance data. + rhi::Buffer* m_sbtBuffer = nullptr; // Shader binding table. + rhi::PipelineLayout* m_rtPipelineLayout = nullptr; + rhi::BindGroupLayout* m_rtBindGroupLayout = nullptr; + rhi::BindGroup* m_rtBindGroup = nullptr; + + // RT output texture. + rhi::Texture* m_outputTexture = nullptr; + rhi::TextureView* m_outputTextureView = nullptr; + rhi::ResourceState m_outputTextureState = rhi::ResourceState::Undefined; + + // SBT layout info (cached for traceRays). + draco::u32 m_sbtAlignedStride = 0; + + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status RayTracingSample::onInit() { + using draco::Status, std::span; + + // ---- Check ray tracing support ---- + if (!m_device->features.rayTracing) { + std::fprintf(stderr, "ERROR: Ray tracing is not supported by this device/backend\n"); + return draco::ErrorCode::Unknown; + } + + std::printf("Ray tracing extension available:\n"); + std::printf(" shaderGroupHandleSize: %u\n", m_device->shaderGroupHandleSize); + std::printf(" shaderGroupHandleAlignment: %u\n", m_device->shaderGroupHandleAlignment); + std::printf(" shaderGroupBaseAlignment: %u\n", m_device->shaderGroupBaseAlignment); + + // ---- Shader compiler ---- + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // ---- Compile RT shader library (lib_6_3) ---- + // Use ShaderStage::RayGen so stagePrefix yields "lib"; SM 6_3 for RT. + if (sf::compileToModule(m_compiler, m_device, kRtShaderSource, shaders::ShaderStage::RayGen, + u8"", u8"RTShaderLib", u8"6_3", m_rtShaderModule) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "ERROR: RT shader library compilation failed\n"); + return draco::ErrorCode::Unknown; + } + + // ---- Command pool and fence ---- + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // ---- Create RT output texture (storage + copy source) ---- + { + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = m_width; + td.height = m_height; + td.arrayLayerCount = 1; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Storage | rhi::TextureUsage::CopySrc; + td.label = u8"RTOutputTex"; + if (m_device->createTexture(td, m_outputTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; + tvd.label = u8"RTOutputView"; + if (m_device->createTextureView(m_outputTexture, tvd, m_outputTextureView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create BLAS vertex buffer (3 vertices * 12 bytes = 36 bytes) ---- + { + rhi::BufferDesc bd{}; + bd.size = 36; + bd.usage = rhi::BufferUsage::AccelStructInput | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; + bd.label = u8"BLAS_VB"; + if (m_device->createBuffer(bd, m_rtVertexBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Upload BLAS vertex data. + { + rhi::TransferBatch* transfer = nullptr; + if (m_graphicsQueue->createTransferBatch(transfer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + transfer->writeBuffer(m_rtVertexBuffer, 0, + std::span(reinterpret_cast(kBlasVertexData), 36)); + transfer->submit(); + m_graphicsQueue->destroyTransferBatch(transfer); + } + + // ---- Create acceleration structures ---- + { + rhi::AccelStructDesc asd{}; + asd.type = rhi::AccelStructType::BottomLevel; + asd.label = u8"BLAS"; + if (m_device->createAccelStruct(asd, m_blas) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + asd.type = rhi::AccelStructType::TopLevel; + asd.label = u8"TLAS"; + if (m_device->createAccelStruct(asd, m_tlas) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create scratch buffer (256 KB) ---- + { + rhi::BufferDesc bd{}; + bd.size = 256 * 1024; + bd.usage = rhi::BufferUsage::AccelStructScratch; + bd.memory = rhi::MemoryLocation::GpuOnly; + bd.label = u8"ScratchBuffer"; + if (m_device->createBuffer(bd, m_scratchBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create instance buffer (64 bytes = sizeof(VkAccelerationStructureInstanceKHR)) ---- + { + rhi::BufferDesc bd{}; + bd.size = 64; + bd.usage = rhi::BufferUsage::AccelStructInput; + bd.memory = rhi::MemoryLocation::CpuToGpu; + bd.label = u8"InstanceBuffer"; + if (m_device->createBuffer(bd, m_instanceBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Fill instance data. + { + auto* ptr = static_cast(m_instanceBuffer->map()); + if (!ptr) { std::fprintf(stderr, "ERROR: Failed to map instance buffer\n"); return draco::ErrorCode::Unknown; } + std::memset(ptr, 0, 64); + + // Identity transform (3x4 row-major float matrix). + auto* transform = reinterpret_cast(ptr); + transform[0] = 1.0f; // row 0, col 0 + transform[5] = 1.0f; // row 1, col 1 + transform[10] = 1.0f; // row 2, col 2 + + // instanceCustomIndex (24 bit) + mask (8 bit) at offset 48. + ptr[48] = 0; ptr[49] = 0; ptr[50] = 0; // customIndex = 0 + ptr[51] = 0xFF; // mask + + // SBT offset (24 bit) + flags (8 bit) at offset 52. + ptr[52] = 0; ptr[53] = 0; ptr[54] = 0; // sbtOffset = 0 + ptr[55] = 0x04; // VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR + + // accelerationStructureReference at offset 56. + *reinterpret_cast(ptr + 56) = m_blas->deviceAddress(); + + m_instanceBuffer->unmap(); + } + + // ---- Build BLAS and TLAS ---- + { + rhi::CommandEncoder* encoder = nullptr; + if (m_pool->createEncoder(encoder) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (auto* rtEnc = encoder->asRayTracingExt()) { + // Build BLAS from triangle geometry. + rhi::AccelStructGeometryTriangles triGeom{}; + triGeom.vertexBuffer = m_rtVertexBuffer; + triGeom.vertexOffset = 0; + triGeom.vertexCount = 3; + triGeom.vertexStride = 12; + triGeom.vertexFormat = rhi::VertexFormat::Float32x3; + triGeom.flags = rhi::GeometryFlags::Opaque; + + rtEnc->buildBottomLevelAccelStruct(m_blas, m_scratchBuffer, 0, + std::span(&triGeom, 1), + std::span{}); + + // Barrier between BLAS and TLAS build. + rhi::MemoryBarrier mb{}; + mb.oldState = rhi::ResourceState::AccelStructWrite; + mb.newState = rhi::ResourceState::AccelStructRead; + rhi::BarrierGroup bg{}; + bg.memoryBarriers = std::span(&mb, 1); + encoder->barrier(bg); + + // Build TLAS from instances. + rtEnc->buildTopLevelAccelStruct(m_tlas, m_scratchBuffer, 0, + m_instanceBuffer, 0, 1); + } else { + std::fprintf(stderr, "ERROR: Command encoder does not support ray tracing\n"); + m_pool->destroyEncoder(encoder); + return draco::ErrorCode::Unknown; + } + + rhi::CommandBuffer* cb = encoder->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + // Wait for build to complete. + m_fence->wait(m_fenceVal); + m_pool->reset(); + m_pool->destroyEncoder(encoder); + } + + std::printf("BLAS and TLAS built successfully.\n"); + std::printf(" BLAS DeviceAddress: 0x%llx\n", static_cast(m_blas->deviceAddress())); + std::printf(" TLAS DeviceAddress: 0x%llx\n", static_cast(m_tlas->deviceAddress())); + + // ---- Create RT bind group layout and bind group ---- + { + // binding 0 (u0): RWTexture2D - storage texture, read-write + // binding 0 (t0): RaytracingAccelerationStructure - TLAS + // Both use register 0 in different HLSL spaces (u vs t), + // mapped to different Vulkan bindings via shifts (UAV=2000, SRV=1000). + rhi::BindGroupLayoutEntry layoutEntries[2]{}; + + // Storage texture (read-write). + layoutEntries[0].binding = 0; + layoutEntries[0].visibility = rhi::ShaderStage::RayGen; + layoutEntries[0].type = rhi::BindingType::StorageTextureReadWrite; + layoutEntries[0].storageTextureFormat = rhi::TextureFormat::RGBA8Unorm; + layoutEntries[0].count = 1; + + // Acceleration structure. + layoutEntries[1].binding = 0; + layoutEntries[1].visibility = rhi::ShaderStage::RayGen; + layoutEntries[1].type = rhi::BindingType::AccelerationStructure; + layoutEntries[1].count = 1; + + rhi::BindGroupLayoutDesc bgld{}; + bgld.entries = std::span(layoutEntries, 2); + bgld.label = u8"RTBindGroupLayout"; + if (m_device->createBindGroupLayout(bgld, m_rtBindGroupLayout) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create bind group with output texture + TLAS. + rhi::BindGroupEntry bgEntries[2]{}; + bgEntries[0] = rhi::BindGroupEntry::textureEntry(m_outputTextureView); + bgEntries[1] = rhi::BindGroupEntry::accelStructEntry(m_tlas); + + rhi::BindGroupDesc bgd{}; + bgd.layout = m_rtBindGroupLayout; + bgd.entries = std::span(bgEntries, 2); + bgd.label = u8"RTBindGroup"; + if (m_device->createBindGroup(bgd, m_rtBindGroup) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create RT pipeline layout with bind group ---- + { + rhi::BindGroupLayout* bglArr[1] = { m_rtBindGroupLayout }; + + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(bglArr, 1); + pld.label = u8"RTPipelineLayout"; + if (m_device->createPipelineLayout(pld, m_rtPipelineLayout) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // 3 stages: RayGen, ClosestHit, Miss - all from the same shader module. + rhi::ProgrammableStage stages[3]{}; + stages[0] = { m_rtShaderModule, u8"RayGen", rhi::ShaderStage::RayGen }; + stages[1] = { m_rtShaderModule, u8"ClosestHit", rhi::ShaderStage::ClosestHit }; + stages[2] = { m_rtShaderModule, u8"Miss", rhi::ShaderStage::Miss }; + + // 3 groups: raygen (general), hit group (triangles), miss (general). + rhi::RayTracingShaderGroup groups[3]{}; + groups[0].type = rhi::RayTracingShaderGroup::Type::General; + groups[0].generalShaderIndex = 0; + + groups[1].type = rhi::RayTracingShaderGroup::Type::TrianglesHitGroup; + groups[1].closestHitShaderIndex = 1; + + groups[2].type = rhi::RayTracingShaderGroup::Type::General; + groups[2].generalShaderIndex = 2; + + rhi::RayTracingPipelineDesc rtpd{}; + rtpd.layout = m_rtPipelineLayout; + rtpd.stages = std::span(stages, 3); + rtpd.groups = std::span(groups, 3); + rtpd.maxRecursionDepth = 1; + rtpd.label = u8"RTPipeline"; + if (m_device->createRayTracingPipeline(rtpd, m_rtPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + std::printf("Ray tracing pipeline created successfully.\n"); + + // ---- Build Shader Binding Table ---- + { + draco::u32 handleSize = m_device->shaderGroupHandleSize; + draco::u32 baseAlignment = m_device->shaderGroupBaseAlignment; + draco::u32 groupCount = 3; + + // Aligned handle stride (round up to base alignment). + m_sbtAlignedStride = (handleSize + baseAlignment - 1) & ~(baseAlignment - 1); + + // Get shader group handles. + draco::u8 handleData[128]; // Enough for 3 handles (max ~32 bytes each). + if (m_device->getShaderGroupHandles(m_rtPipeline, 0, groupCount, + std::span(handleData, handleSize * groupCount)) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "ERROR: getShaderGroupHandles failed\n"); + return draco::ErrorCode::Unknown; + } + + // Create SBT buffer: 3 entries, each aligned to baseAlignment. + draco::u64 sbtSize = static_cast(m_sbtAlignedStride) * groupCount; + rhi::BufferDesc sbd{}; + sbd.size = sbtSize; + sbd.usage = rhi::BufferUsage::ShaderBindingTable; + sbd.memory = rhi::MemoryLocation::CpuToGpu; + sbd.label = u8"SBTBuffer"; + if (m_device->createBuffer(sbd, m_sbtBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Copy handles into SBT with proper alignment. + auto* sbtPtr = static_cast(m_sbtBuffer->map()); + if (!sbtPtr) { std::fprintf(stderr, "ERROR: Failed to map SBT buffer\n"); return draco::ErrorCode::Unknown; } + std::memset(sbtPtr, 0, static_cast(sbtSize)); + + for (draco::u32 i = 0; i < groupCount; i++) { + std::memcpy(sbtPtr + (i * m_sbtAlignedStride), + handleData + (i * handleSize), + handleSize); + } + m_sbtBuffer->unmap(); + + std::printf("SBT built: handleSize=%u, baseAlignment=%u, alignedStride=%u, totalSize=%llu\n", + handleSize, baseAlignment, m_sbtAlignedStride, static_cast(sbtSize)); + } + + std::printf("RT sample ready - TraceRays rendering active.\n"); + + return draco::ErrorCode::Ok; +} + +void RayTracingSample::onRender() { + using std::span; + + // Wait for previous frame. + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + + // Acquire next swap chain image. + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Reset and create encoder. + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // ---- Transition output texture to ShaderWrite for TraceRays ---- + enc->transitionTexture(m_outputTexture, m_outputTextureState, rhi::ResourceState::ShaderWrite); + + // ---- Dispatch TraceRays ---- + if (auto* rtEnc = enc->asRayTracingExt()) { + rtEnc->setRayTracingPipeline(m_rtPipeline); + rtEnc->setBindGroup(0, m_rtBindGroup); + + // SBT layout: [0] = raygen, [1] = hit, [2] = miss. + draco::u64 raygenOffset = 0; + draco::u64 hitOffset = static_cast(1) * m_sbtAlignedStride; + draco::u64 missOffset = static_cast(2) * m_sbtAlignedStride; + draco::u64 stride = static_cast(m_sbtAlignedStride); + + rtEnc->traceRays( + m_sbtBuffer, raygenOffset, stride, + m_sbtBuffer, missOffset, stride, + m_sbtBuffer, hitOffset, stride, + m_width, m_height); + } + + // ---- Transition: output texture ShaderWrite -> CopySrc, swapchain Present -> CopyDst ---- + { + rhi::TextureBarrier texBarriers[2]{}; + texBarriers[0].texture = m_outputTexture; + texBarriers[0].oldState = rhi::ResourceState::ShaderWrite; + texBarriers[0].newState = rhi::ResourceState::CopySrc; + + texBarriers[1].texture = m_swapChain->currentTexture(); + texBarriers[1].oldState = rhi::ResourceState::Present; + texBarriers[1].newState = rhi::ResourceState::CopyDst; + + rhi::BarrierGroup bg{}; + bg.textureBarriers = std::span(texBarriers, 2); + enc->barrier(bg); + } + + // ---- Copy RT output to swapchain ---- + m_outputTextureState = rhi::ResourceState::CopySrc; + { + rhi::TextureCopyRegion region{}; + region.extent = rhi::Extent3D{ m_width, m_height, 1 }; + enc->copyTextureToTexture(m_outputTexture, m_swapChain->currentTexture(), region); + } + + // ---- Transition swapchain CopyDst -> Present ---- + enc->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::CopyDst, rhi::ResourceState::Present); + + // Finish and submit. + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + // Present. + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void RayTracingSample::onResize(draco::u32 w, draco::u32 h) { + using draco::Status, std::span; + // Wait for GPU idle before destroying resources. + if (m_fence) m_fence->wait(m_fenceVal, ~0ull); + + // Destroy old bind group, view, and texture. + if (m_rtBindGroup) m_device->destroyBindGroup(m_rtBindGroup); + if (m_outputTextureView) m_device->destroyTextureView(m_outputTextureView); + if (m_outputTexture) m_device->destroyTexture(m_outputTexture); + m_rtBindGroup = nullptr; m_outputTextureView = nullptr; m_outputTexture = nullptr; + + // Recreate output texture at new size. + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = w; + td.height = h; + td.arrayLayerCount = 1; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Storage | rhi::TextureUsage::CopySrc; + td.label = u8"RTOutputTex"; + m_device->createTexture(td, m_outputTexture); + + rhi::TextureViewDesc tvd{}; tvd.label = u8"RTOutputView"; + m_device->createTextureView(m_outputTexture, tvd, m_outputTextureView); + + // Recreate bind group with new texture view + same TLAS. + rhi::BindGroupEntry bgEntries[2]{}; + bgEntries[0] = rhi::BindGroupEntry::textureEntry(m_outputTextureView); + bgEntries[1] = rhi::BindGroupEntry::accelStructEntry(m_tlas); + rhi::BindGroupDesc bgd{}; + bgd.layout = m_rtBindGroupLayout; + bgd.entries = std::span(bgEntries, 2); + bgd.label = u8"RTBindGroup"; + m_device->createBindGroup(bgd, m_rtBindGroup); + + m_outputTextureState = rhi::ResourceState::Undefined; +} + +void RayTracingSample::onShutdown() { + if (m_fence) m_fence->wait(m_fenceVal, ~0ull); + + // RT bind group. + if (m_rtBindGroup) m_device->destroyBindGroup(m_rtBindGroup); + if (m_rtBindGroupLayout) m_device->destroyBindGroupLayout(m_rtBindGroupLayout); + + // RT output texture. + if (m_outputTextureView) m_device->destroyTextureView(m_outputTextureView); + if (m_outputTexture) m_device->destroyTexture(m_outputTexture); + + // RT resources. + if (m_sbtBuffer) m_device->destroyBuffer(m_sbtBuffer); + if (m_rtPipeline) m_device->destroyRayTracingPipeline(m_rtPipeline); + if (m_rtPipelineLayout) m_device->destroyPipelineLayout(m_rtPipelineLayout); + if (m_instanceBuffer) m_device->destroyBuffer(m_instanceBuffer); + if (m_scratchBuffer) m_device->destroyBuffer(m_scratchBuffer); + if (m_tlas) m_device->destroyAccelStruct(m_tlas); + if (m_blas) m_device->destroyAccelStruct(m_blas); + if (m_rtVertexBuffer) m_device->destroyBuffer(m_rtVertexBuffer); + if (m_rtShaderModule) m_device->destroyShaderModule(m_rtShaderModule); + + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { RayTracingSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample022_StencilOutline/CMakeLists.txt b/Samples/cpp/RHI/Sample022_StencilOutline/CMakeLists.txt new file mode 100644 index 00000000..f16971de --- /dev/null +++ b/Samples/cpp/RHI/Sample022_StencilOutline/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample022_StencilOutline) diff --git a/Samples/cpp/RHI/Sample022_StencilOutline/Main.cpp b/Samples/cpp/RHI/Sample022_StencilOutline/Main.cpp new file mode 100644 index 00000000..e600934a --- /dev/null +++ b/Samples/cpp/RHI/Sample022_StencilOutline/Main.cpp @@ -0,0 +1,275 @@ +#include +/// Demonstrates stencil buffer operations for object outlining. +/// Pass 1: Draw solid hexagon, write stencil = 1. +/// Pass 2: Draw scaled-up hexagon, only where stencil != 1 (outline effect). + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class StencilOutlineSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample022 - Stencil Outline"; } + +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateDepthStencil(w, h); } + void onShutdown() override; + +private: + static constexpr const char8_t kShader[] = u8R"( + struct PushConstants + { + float Scale; + float AspectRatio; + float Time; + float _pad; + }; + + [[vk::push_constant]] ConstantBuffer pc : register(b0, space0); + + struct VSInput + { + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + float2 pos = input.Position.xy * pc.Scale; + pos.x /= pc.AspectRatio; + // Gentle rotation + float c = cos(pc.Time * 0.5); + float s = sin(pc.Time * 0.5); + float2 rotated = float2(pos.x * c - pos.y * s, pos.x * s + pos.y * c); + output.Position = float4(rotated, input.Position.z, 1.0); + output.Color = input.Color; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + )"; + + struct PushData { + float scale; + float aspectRatio; + float time; + float _pad; + }; + + // Hexagon: center + 6 outer vertices. + // Stride: 7 floats per vertex (pos xyz + color rgba). + static constexpr float kVerts[] = { + // Center + 0.0f, 0.0f, 0.5f, 0.9f, 0.9f, 0.9f, 1.0f, + // Outer vertices (radius 0.6) + 0.6f, 0.0f, 0.5f, 0.3f, 0.6f, 1.0f, 1.0f, + 0.3f, 0.52f, 0.5f, 0.3f, 1.0f, 0.6f, 1.0f, + -0.3f, 0.52f, 0.5f, 1.0f, 1.0f, 0.3f, 1.0f, + -0.6f, 0.0f, 0.5f, 1.0f, 0.6f, 0.3f, 1.0f, + -0.3f, -0.52f, 0.5f, 1.0f, 0.3f, 0.6f, 1.0f, + 0.3f, -0.52f, 0.5f, 0.6f, 0.3f, 1.0f, 1.0f, + }; + static constexpr draco::u16 kIdx[] = { + 0, 1, 2, + 0, 2, 3, + 0, 3, 4, + 0, 4, 5, + 0, 5, 6, + 0, 6, 1, + }; + + void recreateDepthStencil(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_stencilWritePipeline = nullptr; + rhi::RenderPipeline* m_stencilTestPipeline = nullptr; + rhi::Texture* m_depthStencilTex = nullptr; + rhi::TextureView* m_depthStencilView = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +void StencilOutlineSample::recreateDepthStencil(draco::u32 w, draco::u32 h) { + if (m_depthStencilView) { m_device->destroyTextureView(m_depthStencilView); m_depthStencilView = nullptr; } + if (m_depthStencilTex) { m_device->destroyTexture(m_depthStencilTex); m_depthStencilTex = nullptr; } + + rhi::TextureDesc td = rhi::TextureDesc::depthBuffer(rhi::TextureFormat::Depth24PlusStencil8, w, h, 1, u8"StencilDSTex"); + m_device->createTexture(td, m_depthStencilTex); + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::Depth24PlusStencil8; tvd.dimension = rhi::TextureViewDimension::Texture2D; + tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_depthStencilTex, tvd, m_depthStencilView); +} + +draco::Status StencilOutlineSample::onInit() { + using draco::Status, std::span, draco::u8; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"StencilVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"StencilPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex & index buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Pipeline layout with push constants (no bind groups). + rhi::PushConstantRange pcRange{ rhi::ShaderStage::Vertex, 0, sizeof(PushData) }; + rhi::PipelineLayoutDesc pld{}; + pld.pushConstantRanges = std::span(&pcRange, 1); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + recreateDepthStencil(m_width, m_height); + + // Shared vertex layout. + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + + // Pipeline 1: Stencil write - draw solid, always pass depth, write stencil = ref (1). + { + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.depthStencil = rhi::DepthStencilState{}; + rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthWriteEnabled = true; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Always; + rpd.depthStencil->stencilEnabled = true; + rpd.depthStencil->stencilReadMask = 0xFF; + rpd.depthStencil->stencilWriteMask = 0xFF; + rpd.depthStencil->stencilFront = { rhi::CompareFunction::Always, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep, rhi::StencilOperation::Replace }; + rpd.depthStencil->stencilBack = { rhi::CompareFunction::Always, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep, rhi::StencilOperation::Replace }; + if (m_device->createRenderPipeline(rpd, m_stencilWritePipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Pipeline 2: Stencil test - draw outline, only where stencil != 1. + { + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.depthStencil = rhi::DepthStencilState{}; + rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthWriteEnabled = false; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Always; + rpd.depthStencil->stencilEnabled = true; + rpd.depthStencil->stencilReadMask = 0xFF; + rpd.depthStencil->stencilWriteMask = 0x00; + rpd.depthStencil->stencilFront = { rhi::CompareFunction::NotEqual, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep }; + rpd.depthStencil->stencilBack = { rhi::CompareFunction::NotEqual, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep, rhi::StencilOperation::Keep }; + if (m_device->createRenderPipeline(rpd, m_stencilTestPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void StencilOutlineSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthStencilTex, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthStencilView; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + dsa.stencilLoadOp = rhi::LoadOp::Clear; dsa.stencilStoreOp = rhi::StoreOp::Store; dsa.stencilClearValue = 0; + + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + + f32 aspect = static_cast(m_width) / static_cast(m_height); + + // Pass 1: Draw solid hexagon, write stencil = 1. + rp->setPipeline(m_stencilWritePipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->setStencilReference(1); + PushData pc1{ 1.0f, aspect, m_totalTime, 0.0f }; + rp->setPushConstants(rhi::ShaderStage::Vertex, 0, sizeof(PushData), &pc1); + rp->drawIndexed(18); + + // Pass 2: Draw scaled-up hexagon, only where stencil != 1 (outline ring). + rp->setPipeline(m_stencilTestPipeline); + rp->setStencilReference(1); + PushData pc2{ 1.15f, aspect, m_totalTime, 0.0f }; + rp->setPushConstants(rhi::ShaderStage::Vertex, 0, sizeof(PushData), &pc2); + rp->drawIndexed(18); + + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void StencilOutlineSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_stencilTestPipeline) m_device->destroyRenderPipeline(m_stencilTestPipeline); + if (m_stencilWritePipeline) m_device->destroyRenderPipeline(m_stencilWritePipeline); + if (m_depthStencilView) m_device->destroyTextureView(m_depthStencilView); + if (m_depthStencilTex) m_device->destroyTexture(m_depthStencilTex); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { StencilOutlineSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample023_CubeMap/CMakeLists.txt b/Samples/cpp/RHI/Sample023_CubeMap/CMakeLists.txt new file mode 100644 index 00000000..f755bff2 --- /dev/null +++ b/Samples/cpp/RHI/Sample023_CubeMap/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample023_CubeMap) diff --git a/Samples/cpp/RHI/Sample023_CubeMap/Main.cpp b/Samples/cpp/RHI/Sample023_CubeMap/Main.cpp new file mode 100644 index 00000000..1c2f058e --- /dev/null +++ b/Samples/cpp/RHI/Sample023_CubeMap/Main.cpp @@ -0,0 +1,561 @@ +#include +/// Demonstrates cube map textures and comparison samplers. +/// Renders a fullscreen quad that samples a procedural cube map (skybox), +/// plus a second pass with a depth texture sampled via comparison sampler +/// to demonstrate shadow-map-style sampling. + +#include +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class CubeMapSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample023 - Cube Map & Comparison Sampler"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + draco::Status createCubeMap(); + draco::Status createDepthTexture(); + + // Skybox shader: fullscreen quad -> ray direction -> cube map lookup + static constexpr const char8_t kSkyboxShader[] = u8R"( + TextureCube gCubeMap : register(t0, space0); + SamplerState gSampler : register(s0, space0); + + struct PushConstants + { + float Time; + float AspectRatio; + float2 _pad; + }; + + [[vk::push_constant]] ConstantBuffer pc : register(b0, space1); + + struct PSInput + { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; + }; + + PSInput VSMain(uint vertexID : SV_VertexID) + { + PSInput output; + // Fullscreen triangle + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(uv * 2.0 - 1.0, 0.5, 1.0); + output.UV = uv; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + // Convert UV to ray direction + float2 ndc = input.UV * 2.0 - 1.0; + ndc.x *= pc.AspectRatio; + ndc.y = -ndc.y; + + // Simple rotation around Y + float c = cos(pc.Time * 0.3); + float s = sin(pc.Time * 0.3); + + float3 dir = normalize(float3(ndc.x, ndc.y, 1.0)); + float3 rotDir = float3(dir.x * c + dir.z * s, dir.y, -dir.x * s + dir.z * c); + + return gCubeMap.Sample(gSampler, rotDir); + } + )"; + + // Shadow test shader: renders a quad, samples a depth texture with comparison sampler + static constexpr const char8_t kShadowShader[] = u8R"( + Texture2D gShadowMap : register(t0, space0); + SamplerComparisonState gShadowSampler : register(s0, space0); + + struct PushConstants + { + float Time; + float AspectRatio; + float2 _pad; + }; + + [[vk::push_constant]] ConstantBuffer pc : register(b0, space1); + + struct VSInput + { + float3 Position : TEXCOORD0; + float2 TexCoord : TEXCOORD1; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.TexCoord = input.TexCoord; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + // Compare at varying depth based on time for animated shadow boundary + float compareValue = 0.5 + 0.4 * sin(pc.Time); + float shadow = gShadowMap.SampleCmpLevelZero(gShadowSampler, input.TexCoord, compareValue); + float3 litColor = float3(0.9, 0.85, 0.7); + float3 shadowColor = float3(0.1, 0.1, 0.2); + float3 color = lerp(shadowColor, litColor, shadow); + return float4(color, 1.0); + } + )"; + + struct PushData { + float time; + float aspectRatio; + float _pad0; + float _pad1; + }; + + shaders::Compiler* m_compiler = nullptr; + + // Skybox resources + rhi::ShaderModule* m_skyboxVs = nullptr; + rhi::ShaderModule* m_skyboxPs = nullptr; + rhi::Texture* m_cubeTexture = nullptr; + rhi::TextureView* m_cubeView = nullptr; + rhi::Sampler* m_linearSampler = nullptr; + rhi::BindGroupLayout* m_skyboxBgl = nullptr; + rhi::BindGroup* m_skyboxBg = nullptr; + rhi::PipelineLayout* m_skyboxPl = nullptr; + rhi::RenderPipeline* m_skyboxPipeline = nullptr; + + // Shadow comparison resources + rhi::ShaderModule* m_shadowVs = nullptr; + rhi::ShaderModule* m_shadowPs = nullptr; + rhi::Texture* m_depthTexture = nullptr; + rhi::TextureView* m_depthView = nullptr; + rhi::Sampler* m_comparisonSampler = nullptr; + rhi::Buffer* m_quadVb = nullptr; + rhi::BindGroupLayout* m_shadowBgl = nullptr; + rhi::BindGroup* m_shadowBg = nullptr; + rhi::PipelineLayout* m_shadowPl = nullptr; + rhi::RenderPipeline* m_shadowPipeline = nullptr; + + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status CubeMapSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Compile skybox shaders + if (sf::compileToModule(m_compiler, m_device, kSkyboxShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"SkyboxVS", m_skyboxVs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kSkyboxShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"SkyboxPS", m_skyboxPs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Compile shadow shaders + if (sf::compileToModule(m_compiler, m_device, kShadowShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"ShadowVS", m_shadowVs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShadowShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"ShadowPS", m_shadowPs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create procedural cube map (6 faces, 64x64, each a solid color) + if (createCubeMap() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create depth texture for comparison sampler (gradient) + if (createDepthTexture() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create samplers + { + rhi::SamplerDesc sd{}; + sd.minFilter = rhi::FilterMode::Linear; + sd.magFilter = rhi::FilterMode::Linear; + sd.label = u8"LinearSampler"; + if (m_device->createSampler(sd, m_linearSampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + { + rhi::SamplerDesc sd{}; + sd.minFilter = rhi::FilterMode::Linear; + sd.magFilter = rhi::FilterMode::Linear; + sd.compare = rhi::CompareFunction::LessEqual; + sd.label = u8"ComparisonSampler"; + if (m_device->createSampler(sd, m_comparisonSampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Skybox bind group layout: cube texture + sampler + { + rhi::BindGroupLayoutEntry entries[2] = { + rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, rhi::TextureViewDimension::TextureCube), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment) + }; + rhi::BindGroupLayoutDesc bgld{}; + bgld.entries = std::span(entries, 2); + bgld.label = u8"SkyboxBGL"; + if (m_device->createBindGroupLayout(bgld, m_skyboxBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Skybox bind group + { + rhi::BindGroupEntry entries[2] = { + rhi::BindGroupEntry::textureEntry(m_cubeView), + rhi::BindGroupEntry::samplerEntry(m_linearSampler) + }; + rhi::BindGroupDesc bgd{}; + bgd.layout = m_skyboxBgl; + bgd.entries = std::span(entries, 2); + bgd.label = u8"SkyboxBG"; + if (m_device->createBindGroup(bgd, m_skyboxBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Skybox pipeline layout + { + rhi::BindGroupLayout* sets[1] = { m_skyboxBgl }; + rhi::PushConstantRange pcr{}; + pcr.stages = rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment; + pcr.offset = 0; + pcr.size = sizeof(PushData); + rhi::PushConstantRange pushRanges[1] = { pcr }; + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(sets, 1); + pld.pushConstantRanges = std::span(pushRanges, 1); + pld.label = u8"SkyboxPL"; + if (m_device->createPipelineLayout(pld, m_skyboxPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Skybox pipeline (fullscreen triangle, no vertex input) + { + rhi::ColorTargetState ct{}; + ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_skyboxPl; + rpd.vertex.shader = { m_skyboxVs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.fragment = rhi::FragmentState{}; + rpd.fragment->shader = { m_skyboxPs, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"SkyboxPipeline"; + if (m_device->createRenderPipeline(rpd, m_skyboxPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Shadow test quad vertices (bottom-right corner overlay) + { + float quadVerts[] = { + // pos xyz, uv + 0.3f, -0.9f, 0.0f, 0.0f, 1.0f, + 0.9f, -0.9f, 0.0f, 1.0f, 1.0f, + 0.9f, -0.3f, 0.0f, 1.0f, 0.0f, + 0.3f, -0.9f, 0.0f, 0.0f, 1.0f, + 0.9f, -0.3f, 0.0f, 1.0f, 0.0f, + 0.3f, -0.3f, 0.0f, 0.0f, 0.0f + }; + + u32 vbSize = sizeof(quadVerts); + rhi::BufferDesc bd{}; + bd.size = vbSize; + bd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; + bd.label = u8"ShadowQuadVB"; + if (m_device->createBuffer(bd, m_quadVb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_quadVb, 0, std::span(reinterpret_cast(quadVerts), vbSize)); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + } + + // Shadow bind group layout: depth texture + comparison sampler + { + rhi::BindGroupLayoutEntry entries[2]; + entries[0] = rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment, rhi::TextureViewDimension::Texture2D); + entries[1] = {}; + entries[1].binding = 0; + entries[1].visibility = rhi::ShaderStage::Fragment; + entries[1].type = rhi::BindingType::ComparisonSampler; + rhi::BindGroupLayoutDesc bgld{}; + bgld.entries = std::span(entries, 2); + bgld.label = u8"ShadowBGL"; + if (m_device->createBindGroupLayout(bgld, m_shadowBgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Shadow bind group + { + rhi::BindGroupEntry entries[2] = { + rhi::BindGroupEntry::textureEntry(m_depthView), + rhi::BindGroupEntry::samplerEntry(m_comparisonSampler) + }; + rhi::BindGroupDesc bgd{}; + bgd.layout = m_shadowBgl; + bgd.entries = std::span(entries, 2); + bgd.label = u8"ShadowBG"; + if (m_device->createBindGroup(bgd, m_shadowBg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Shadow pipeline layout + { + rhi::BindGroupLayout* sets[1] = { m_shadowBgl }; + rhi::PushConstantRange pcr{}; + pcr.stages = rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment; + pcr.offset = 0; + pcr.size = sizeof(PushData); + rhi::PushConstantRange pushRanges[1] = { pcr }; + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(sets, 1); + pld.pushConstantRanges = std::span(pushRanges, 1); + pld.label = u8"ShadowPL"; + if (m_device->createPipelineLayout(pld, m_shadowPl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Shadow pipeline + { + rhi::VertexAttribute attrs[2] = { + { rhi::VertexFormat::Float32x3, 0, 0 }, + { rhi::VertexFormat::Float32x2, 12, 1 } + }; + rhi::VertexBufferLayout vbl{}; + vbl.stride = 20; + vbl.attributes = std::span(attrs, 2); + + rhi::ColorTargetState ct{}; + ct.format = m_swapChain->format(); + + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_shadowPl; + rpd.vertex.shader = { m_shadowVs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; + rpd.fragment->shader = { m_shadowPs, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"ShadowPipeline"; + if (m_device->createRenderPipeline(rpd, m_shadowPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +draco::Status CubeMapSample::createCubeMap() { + using draco::Status, std::span, draco::u8, draco::u32; + + constexpr u32 faceSize = 64; + constexpr u32 BytesPerPixel = 4; + constexpr u32 faceBytes = faceSize * faceSize * BytesPerPixel; + + // Create cube map texture: 2D with 6 array layers + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::RGBA8UnormSrgb; + td.width = faceSize; + td.height = faceSize; + td.arrayLayerCount = 6; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + td.label = u8"CubeMapTex"; + if (m_device->createTexture(td, m_cubeTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create cube view + rhi::TextureViewDesc tvd{}; + tvd.format = rhi::TextureFormat::RGBA8UnormSrgb; + tvd.dimension = rhi::TextureViewDimension::TextureCube; + tvd.baseMipLevel = 0; + tvd.mipLevelCount = 1; + tvd.baseArrayLayer = 0; + tvd.arrayLayerCount = 6; + if (m_device->createTextureView(m_cubeTexture, tvd, m_cubeView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Generate 6 face colors: +X red, -X cyan, +Y green, -Y magenta, +Z blue, -Z yellow + u8 faceColors[6][4] = { + {200, 60, 60, 255}, // +X: red + {60, 200, 200, 255}, // -X: cyan + {60, 200, 60, 255}, // +Y: green + {200, 60, 200, 255}, // -Y: magenta + {60, 60, 200, 255}, // +Z: blue + {200, 200, 60, 255} // -Z: yellow + }; + + u8 stagingBuf[faceBytes]; + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + + for (int face = 0; face < 6; face++) { + // Fill face with gradient from face color to white at center + for (u32 y = 0; y < faceSize; y++) { + for (u32 x = 0; x < faceSize; x++) { + float fx = (static_cast(x) / static_cast(faceSize)) * 2.0f - 1.0f; + float fy = (static_cast(y) / static_cast(faceSize)) * 2.0f - 1.0f; + float dist = std::min(1.0f, std::sqrt(fx * fx + fy * fy)); + float t = 1.0f - dist * 0.5f; + + u32 idx = (y * faceSize + x) * BytesPerPixel; + stagingBuf[idx + 0] = static_cast(faceColors[face][0] * t + 40 * (1.0f - t)); + stagingBuf[idx + 1] = static_cast(faceColors[face][1] * t + 40 * (1.0f - t)); + stagingBuf[idx + 2] = static_cast(faceColors[face][2] * t + 40 * (1.0f - t)); + stagingBuf[idx + 3] = 255; + } + } + + rhi::TextureDataLayout layout{}; + layout.bytesPerRow = faceSize * BytesPerPixel; + layout.rowsPerImage = faceSize; + batch->writeTexture(m_cubeTexture, + std::span(stagingBuf, faceBytes), + layout, rhi::Extent3D{faceSize, faceSize, 1}, + 0, static_cast(face)); + } + + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + return draco::ErrorCode::Ok; +} + +draco::Status CubeMapSample::createDepthTexture() { + using draco::Status; + + constexpr draco::u32 texSize = 64; + + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::Depth32Float; + td.width = texSize; + td.height = texSize; + td.arrayLayerCount = 1; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::DepthStencil | rhi::TextureUsage::Sampled; + td.label = u8"ShadowDepthTex"; + if (m_device->createTexture(td, m_depthTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; + tvd.format = rhi::TextureFormat::Depth32Float; + tvd.dimension = rhi::TextureViewDimension::Texture2D; + if (m_device->createTextureView(m_depthTexture, tvd, m_depthView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // We'll render a gradient depth in a render pass + // For simplicity, just clear to 0.5 so the comparison sampler has something to compare against + // (A real sample would render shadow casters here) + + return draco::ErrorCode::Ok; +} + +void CubeMapSample::onRender() { + using draco::f32, draco::u32, std::span; + + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + f32 aspect = static_cast(m_width) / static_cast(m_height); + + // Render a depth value into the shadow depth texture + enc->transitionTexture(m_depthTexture, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + { + rhi::DepthStencilAttachment dsa{}; + dsa.view = m_depthView; + dsa.depthLoadOp = rhi::LoadOp::Clear; + dsa.depthStoreOp = rhi::StoreOp::Store; + dsa.depthClearValue = 0.5f; + rhi::RenderPassDesc rpd{}; + rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + rp->end(); + } + + // Transition depth texture from DepthStencilWrite -> ShaderRead for sampling + enc->transitionTexture(m_depthTexture, rhi::ResourceState::DepthStencilWrite, rhi::ResourceState::ShaderRead); + + // Transition swapchain + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; + ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; + ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.05f, 0.05f, 0.08f, 1.0f); + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + + // Draw skybox (fullscreen triangle, no VB needed - SV_VertexID) + rp->setPipeline(m_skyboxPipeline); + rp->setBindGroup(0, m_skyboxBg); + PushData pc{}; + pc.time = m_totalTime; + pc.aspectRatio = aspect; + rp->setPushConstants(rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, 0, sizeof(PushData), &pc); + rp->draw(3); + + // Draw shadow comparison overlay quad + rp->setPipeline(m_shadowPipeline); + rp->setBindGroup(0, m_shadowBg); + rp->setPushConstants(rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment, 0, sizeof(PushData), &pc); + rp->setVertexBuffer(0, m_quadVb, 0); + rp->draw(6); + + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + // Transition depth texture back to DepthStencilWrite for next frame + enc->transitionTexture(m_depthTexture, rhi::ResourceState::ShaderRead, rhi::ResourceState::DepthStencilWrite); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void CubeMapSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_shadowPipeline) m_device->destroyRenderPipeline(m_shadowPipeline); + if (m_skyboxPipeline) m_device->destroyRenderPipeline(m_skyboxPipeline); + if (m_shadowPl) m_device->destroyPipelineLayout(m_shadowPl); + if (m_skyboxPl) m_device->destroyPipelineLayout(m_skyboxPl); + if (m_shadowBg) m_device->destroyBindGroup(m_shadowBg); + if (m_skyboxBg) m_device->destroyBindGroup(m_skyboxBg); + if (m_shadowBgl) m_device->destroyBindGroupLayout(m_shadowBgl); + if (m_skyboxBgl) m_device->destroyBindGroupLayout(m_skyboxBgl); + if (m_comparisonSampler) m_device->destroySampler(m_comparisonSampler); + if (m_linearSampler) m_device->destroySampler(m_linearSampler); + if (m_quadVb) m_device->destroyBuffer(m_quadVb); + if (m_depthView) m_device->destroyTextureView(m_depthView); + if (m_depthTexture) m_device->destroyTexture(m_depthTexture); + if (m_cubeView) m_device->destroyTextureView(m_cubeView); + if (m_cubeTexture) m_device->destroyTexture(m_cubeTexture); + if (m_shadowPs) m_device->destroyShaderModule(m_shadowPs); + if (m_shadowVs) m_device->destroyShaderModule(m_shadowVs); + if (m_skyboxPs) m_device->destroyShaderModule(m_skyboxPs); + if (m_skyboxVs) m_device->destroyShaderModule(m_skyboxVs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { CubeMapSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample024_OcclusionQuery/CMakeLists.txt b/Samples/cpp/RHI/Sample024_OcclusionQuery/CMakeLists.txt new file mode 100644 index 00000000..d3b1ee3c --- /dev/null +++ b/Samples/cpp/RHI/Sample024_OcclusionQuery/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample024_OcclusionQuery) diff --git a/Samples/cpp/RHI/Sample024_OcclusionQuery/Main.cpp b/Samples/cpp/RHI/Sample024_OcclusionQuery/Main.cpp new file mode 100644 index 00000000..5299d334 --- /dev/null +++ b/Samples/cpp/RHI/Sample024_OcclusionQuery/Main.cpp @@ -0,0 +1,260 @@ +#include +/// Demonstrates occlusion queries and debug labels. +/// Renders an occluder quad, then two test quads behind it with occlusion queries. +/// Prints pixel counts to console. Uses debug labels to mark render sections. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class OcclusionQuerySample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample024 - Occlusion Queries & Debug Labels"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateDepth(w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput + { + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + )"; + + // Geometry: 3 quads + // Quad 0: Occluder (opaque gray, z=0.3, center) + // Quad 1: Test A (red, z=0.7, partially behind occluder) + // Quad 2: Test B (blue, z=0.7, fully behind occluder) + static constexpr float kVerts[] = { + // Quad 0: Occluder - center, near + -0.3f, -0.4f, 0.3f, 0.4f, 0.4f, 0.4f, 1.0f, + 0.3f, -0.4f, 0.3f, 0.4f, 0.4f, 0.4f, 1.0f, + 0.3f, 0.4f, 0.3f, 0.5f, 0.5f, 0.5f, 1.0f, + -0.3f, 0.4f, 0.3f, 0.5f, 0.5f, 0.5f, 1.0f, + + // Quad 1: Test A - partially occluded (left side visible) + -0.7f, -0.3f, 0.7f, 1.0f, 0.3f, 0.3f, 1.0f, + 0.0f, -0.3f, 0.7f, 1.0f, 0.3f, 0.3f, 1.0f, + 0.0f, 0.3f, 0.7f, 1.0f, 0.5f, 0.5f, 1.0f, + -0.7f, 0.3f, 0.7f, 1.0f, 0.5f, 0.5f, 1.0f, + + // Quad 2: Test B - fully occluded (behind occluder) + -0.15f, -0.2f, 0.7f, 0.3f, 0.3f, 1.0f, 1.0f, + 0.15f, -0.2f, 0.7f, 0.3f, 0.3f, 1.0f, 1.0f, + 0.15f, 0.2f, 0.7f, 0.5f, 0.5f, 1.0f, 1.0f, + -0.15f, 0.2f, 0.7f, 0.5f, 0.5f, 1.0f, 1.0f, + }; + static constexpr draco::u16 kIdx[] = { + 0, 1, 2, 0, 2, 3, + 4, 5, 6, 4, 6, 7, + 8, 9, 10, 8, 10, 11, + }; + + void recreateDepth(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::Texture *m_depthTex = nullptr; + rhi::TextureView *m_depthView = nullptr; + rhi::QuerySet *m_occlusionQuerySet = nullptr; + rhi::Buffer *m_queryResultBuf = nullptr; + rhi::CommandPool *m_pool = nullptr; + rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; + int m_frameCount = 0; + float m_lastReportTime = 0.0f; +}; + +void OcclusionQuerySample::recreateDepth(draco::u32 w, draco::u32 h) { + if (m_depthView) { m_device->destroyTextureView(m_depthView); m_depthView = nullptr; } + if (m_depthTex) { m_device->destroyTexture(m_depthTex); m_depthTex = nullptr; } + + rhi::TextureDesc td = rhi::TextureDesc::depthBuffer(rhi::TextureFormat::Depth24PlusStencil8, w, h, 1, u8"OccDepthTex"); + m_device->createTexture(td, m_depthTex); + rhi::TextureViewDesc tvd{}; tvd.format = rhi::TextureFormat::Depth24PlusStencil8; tvd.dimension = rhi::TextureViewDimension::Texture2D; + tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_depthTex, tvd, m_depthView); +} + +draco::Status OcclusionQuerySample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"OccVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"OccPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; vbd.label = u8"OccVB"; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; ibd.label = u8"OccIB"; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + rhi::PipelineLayoutDesc pld{}; pld.label = u8"OccPL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + recreateDepth(m_width, m_height); + + rhi::VertexAttribute attrs[2] = { {rhi::VertexFormat::Float32x3, 0, 0}, {rhi::VertexFormat::Float32x4, 12, 1} }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.depthStencil = rhi::DepthStencilState{}; rpd.depthStencil->format = rhi::TextureFormat::Depth24PlusStencil8; + rpd.depthStencil->depthWriteEnabled = true; + rpd.depthStencil->depthCompare = rhi::CompareFunction::Less; + rpd.label = u8"OccPipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Occlusion query set: 2 queries (one per test quad). + rhi::QuerySetDesc qsd{}; qsd.type = rhi::QueryType::Occlusion; qsd.count = 2; qsd.label = u8"OcclusionQS"; + if (m_device->createQuerySet(qsd, m_occlusionQuerySet) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Buffer for query results (2 * uint64 = 16 bytes). + rhi::BufferDesc qbd{}; qbd.size = 16; qbd.usage = rhi::BufferUsage::CopyDst; qbd.memory = rhi::MemoryLocation::GpuToCpu; qbd.label = u8"OccResultBuf"; + if (m_device->createBuffer(qbd, m_queryResultBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void OcclusionQuerySample::onRender() { + using draco::f32, draco::u64, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + + // Read back previous frame's occlusion results (after fence wait ensures GPU is done). + if (m_frameCount > 1) { + void* mapped = m_queryResultBuf->map(); + if (mapped) { + auto* results = static_cast(mapped); + u64 pixelsA = results[0]; + u64 pixelsB = results[1]; + + if (m_totalTime - m_lastReportTime >= 2.0f) { + std::printf("Occlusion: QuadA=%llu pixels, QuadB=%llu pixels (B should be ~0)\n", + static_cast(pixelsA), static_cast(pixelsB)); + m_lastReportTime = m_totalTime; + } + m_queryResultBuf->unmap(); + } + } + + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // Debug label: frame start. + enc->insertDebugLabel(u8"Frame Start", 0, 1, 0); + + // Reset queries for this frame. + enc->resetQuerySet(m_occlusionQuerySet, 0, 2); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + enc->transitionTexture(m_depthTex, rhi::ResourceState::Undefined, rhi::ResourceState::DepthStencilWrite); + + // Debug label: render pass. + enc->beginDebugLabel(u8"Main Render Pass", 0.2f, 0.5f, 1.0f); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + rhi::DepthStencilAttachment dsa{}; dsa.view = m_depthView; + dsa.depthLoadOp = rhi::LoadOp::Clear; dsa.depthStoreOp = rhi::StoreOp::Store; dsa.depthClearValue = 1.0f; + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); rpd.depthStencilAttachment = dsa; + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + + // Draw occluder first (writes depth). + rp->drawIndexed(6, 1, 0, 0, 0); + + // Draw test quad A with occlusion query 0. + rp->beginOcclusionQuery(m_occlusionQuerySet, 0); + rp->drawIndexed(6, 1, 6, 0, 0); + rp->endOcclusionQuery(m_occlusionQuerySet, 0); + + // Draw test quad B with occlusion query 1. + rp->beginOcclusionQuery(m_occlusionQuerySet, 1); + rp->drawIndexed(6, 1, 12, 0, 0); + rp->endOcclusionQuery(m_occlusionQuerySet, 1); + + rp->end(); + + enc->endDebugLabel(); + + // Resolve occlusion queries to buffer. + enc->resolveQuerySet(m_occlusionQuerySet, 0, 2, m_queryResultBuf, 0); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); + + m_frameCount++; +} + +void OcclusionQuerySample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_queryResultBuf) m_device->destroyBuffer(m_queryResultBuf); + if (m_occlusionQuerySet) m_device->destroyQuerySet(m_occlusionQuerySet); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_depthView) m_device->destroyTextureView(m_depthView); + if (m_depthTex) m_device->destroyTexture(m_depthTex); + if (m_ib) m_device->destroyBuffer(m_ib); if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { OcclusionQuerySample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample025_MultiDrawIndirect/CMakeLists.txt b/Samples/cpp/RHI/Sample025_MultiDrawIndirect/CMakeLists.txt new file mode 100644 index 00000000..79eb4aaf --- /dev/null +++ b/Samples/cpp/RHI/Sample025_MultiDrawIndirect/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample025_MultiDrawIndirect) diff --git a/Samples/cpp/RHI/Sample025_MultiDrawIndirect/Main.cpp b/Samples/cpp/RHI/Sample025_MultiDrawIndirect/Main.cpp new file mode 100644 index 00000000..6af39600 --- /dev/null +++ b/Samples/cpp/RHI/Sample025_MultiDrawIndirect/Main.cpp @@ -0,0 +1,325 @@ +#include +/// Renders 4 colored quads using a single drawIndexedIndirect call with drawCount=4, +/// then overlays white line wireframes using LineList topology. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +struct DrawIndexedIndirectArgs { + draco::u32 indexCountPerInstance; + draco::u32 instanceCount; + draco::u32 startIndexLocation; + draco::i32 baseVertexLocation; + draco::u32 startInstanceLocation; +}; + +class MultiDrawIndirectSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample025 - Multi-Draw Indirect & Lines"; } +protected: + rhi::DeviceFeatures requiredFeatures() const override { + rhi::DeviceFeatures f{}; + f.multiDrawIndirect = true; + return f; + } + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput + { + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + )"; + + draco::Status createGeometry(); + draco::Status createIndirectBuffer(); + draco::Status createLineGeometry(); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_indirectBuf = nullptr, *m_lineVb = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_fillPipeline = nullptr, *m_linePipeline = nullptr; + rhi::CommandPool *m_pool = nullptr; + rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status MultiDrawIndirectSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"VS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"PS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (createGeometry() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (createIndirectBuffer() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (createLineGeometry() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout (empty - no bind groups needed). + rhi::PipelineLayoutDesc pld{}; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex layout: pos float3 + color float4, stride 28. + rhi::VertexAttribute attrs[2] = { + {rhi::VertexFormat::Float32x3, 0, 0}, + {rhi::VertexFormat::Float32x4, 12, 1} + }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.stepMode = rhi::VertexStepMode::Vertex; + vbl.attributes = std::span(attrs, 2); + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + + // Fill pipeline (TriangleList). + { + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + if (m_device->createRenderPipeline(rpd, m_fillPipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Line pipeline (LineList). + { + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::LineList; + if (m_device->createRenderPipeline(rpd, m_linePipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +draco::Status MultiDrawIndirectSample::createGeometry() { + using draco::Status, std::span, draco::u8, draco::u32; + + // 4 quads at different positions. + static constexpr float verts[] = { + // Quad 0: top-left (red) + -0.9f, 0.1f, 0.5f, 0.8f, 0.2f, 0.2f, 1.0f, + -0.1f, 0.1f, 0.5f, 0.8f, 0.2f, 0.2f, 1.0f, + -0.1f, 0.9f, 0.5f, 1.0f, 0.4f, 0.4f, 1.0f, + -0.9f, 0.9f, 0.5f, 1.0f, 0.4f, 0.4f, 1.0f, + + // Quad 1: top-right (green) + 0.1f, 0.1f, 0.5f, 0.2f, 0.8f, 0.2f, 1.0f, + 0.9f, 0.1f, 0.5f, 0.2f, 0.8f, 0.2f, 1.0f, + 0.9f, 0.9f, 0.5f, 0.4f, 1.0f, 0.4f, 1.0f, + 0.1f, 0.9f, 0.5f, 0.4f, 1.0f, 0.4f, 1.0f, + + // Quad 2: bottom-left (blue) + -0.9f, -0.9f, 0.5f, 0.2f, 0.2f, 0.8f, 1.0f, + -0.1f, -0.9f, 0.5f, 0.2f, 0.2f, 0.8f, 1.0f, + -0.1f, -0.1f, 0.5f, 0.4f, 0.4f, 1.0f, 1.0f, + -0.9f, -0.1f, 0.5f, 0.4f, 0.4f, 1.0f, 1.0f, + + // Quad 3: bottom-right (yellow) + 0.1f, -0.9f, 0.5f, 0.8f, 0.8f, 0.2f, 1.0f, + 0.9f, -0.9f, 0.5f, 0.8f, 0.8f, 0.2f, 1.0f, + 0.9f, -0.1f, 0.5f, 1.0f, 1.0f, 0.4f, 1.0f, + 0.1f, -0.1f, 0.5f, 1.0f, 1.0f, 0.4f, 1.0f, + }; + + static constexpr draco::u16 indices[] = { + 0, 1, 2, 0, 2, 3, // Quad 0 + 4, 5, 6, 4, 6, 7, // Quad 1 + 8, 9, 10, 8, 10, 11, // Quad 2 + 12, 13, 14, 12, 14, 15, // Quad 3 + }; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(verts); + vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; + vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc ibd{}; ibd.size = sizeof(indices); + ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; + ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(verts), sizeof(verts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(indices), sizeof(indices))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + return draco::ErrorCode::Ok; +} + +draco::Status MultiDrawIndirectSample::createIndirectBuffer() { + using draco::Status, std::span, draco::u8; + + // 4 indirect draw commands - one per quad. + DrawIndexedIndirectArgs args[4] = { + { 6, 1, 0, 0, 0 }, + { 6, 1, 6, 0, 0 }, + { 6, 1, 12, 0, 0 }, + { 6, 1, 18, 0, 0 }, + }; + + rhi::BufferDesc bd{}; bd.size = sizeof(args); + bd.usage = rhi::BufferUsage::Indirect | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(bd, m_indirectBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_indirectBuf, 0, std::span(reinterpret_cast(args), sizeof(args))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + return draco::ErrorCode::Ok; +} + +draco::Status MultiDrawIndirectSample::createLineGeometry() { + using draco::Status, std::span, draco::u8; + + // Line wireframes for each quad: 4 edges per quad = 8 verts per quad, white lines. + static constexpr float lineVerts[] = { + // Quad 0 edges + -0.9f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + + // Quad 1 edges + 0.1f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, 0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, 0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + + // Quad 2 edges + -0.9f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.1f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + -0.9f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + + // Quad 3 edges + 0.1f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.9f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, -0.1f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.1f, -0.9f, 0.4f, 1.0f, 1.0f, 1.0f, 1.0f, + }; + + rhi::BufferDesc bd{}; bd.size = sizeof(lineVerts); + bd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(bd, m_lineVb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_lineVb, 0, std::span(reinterpret_cast(lineVerts), sizeof(lineVerts))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + return draco::ErrorCode::Ok; +} + +void MultiDrawIndirectSample::onRender() { + using draco::f32, std::span, draco::u32; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.06f, 0.06f, 0.1f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + + // Pass 1: Draw all 4 quads with a single multi-draw indirect call. + rp->setPipeline(m_fillPipeline); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexedIndirect(m_indirectBuf, 0, 4, static_cast(sizeof(DrawIndexedIndirectArgs))); + + // Pass 2: Draw line wireframes. + rp->setPipeline(m_linePipeline); + rp->setVertexBuffer(0, m_lineVb, 0); + rp->draw(32); // 4 quads * 4 edges * 2 verts = 32 line verts + + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void MultiDrawIndirectSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_linePipeline) m_device->destroyRenderPipeline(m_linePipeline); + if (m_fillPipeline) m_device->destroyRenderPipeline(m_fillPipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_lineVb) m_device->destroyBuffer(m_lineVb); + if (m_indirectBuf) m_device->destroyBuffer(m_indirectBuf); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { MultiDrawIndirectSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample026_DynamicOffsets/CMakeLists.txt b/Samples/cpp/RHI/Sample026_DynamicOffsets/CMakeLists.txt new file mode 100644 index 00000000..03735310 --- /dev/null +++ b/Samples/cpp/RHI/Sample026_DynamicOffsets/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample026_DynamicOffsets) diff --git a/Samples/cpp/RHI/Sample026_DynamicOffsets/Main.cpp b/Samples/cpp/RHI/Sample026_DynamicOffsets/Main.cpp new file mode 100644 index 00000000..3f8479f2 --- /dev/null +++ b/Samples/cpp/RHI/Sample026_DynamicOffsets/Main.cpp @@ -0,0 +1,237 @@ +#include +/// Demonstrates dynamic uniform buffer offsets and blend constants. +/// Draws 4 quads, each reading from a different offset in one shared UBO. +/// Uses setBlendConstant with BlendFactor::Constant for per-frame color modulation. + +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class DynamicOffsetSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample026 - Dynamic Offsets & Blend Constants"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + void updateUBO(); + + static constexpr const char8_t kShader[] = u8R"( + cbuffer ObjectData : register(b0, space0) + { + float4 TintColor; + float4 OffsetScale; // xy=offset, zw=scale + }; + + struct VSInput + { + float3 Position : TEXCOORD0; + }; + + struct PSInput + { + float4 Position : SV_POSITION; + }; + + PSInput VSMain(VSInput input) + { + PSInput output; + float2 pos = input.Position.xy * OffsetScale.zw + OffsetScale.xy; + output.Position = float4(pos, input.Position.z, 1.0); + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return TintColor; + } + )"; + + struct ObjectData { + float tintColor[4]; + float offsetScale[4]; + // Pad to 256-byte alignment (D3D12 CBV minimum). + float _pad[56]; + }; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr, *m_ub = nullptr; + rhi::BindGroupLayout *m_bgl = nullptr; + rhi::BindGroup *m_bg = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::CommandPool *m_pool = nullptr; + rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status DynamicOffsetSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"DynVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"DynPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Unit quad vertices (will be transformed by UBO data). + static constexpr float verts[] = { + -0.5f, -0.5f, 0.5f, + 0.5f, -0.5f, 0.5f, + 0.5f, 0.5f, 0.5f, + -0.5f, 0.5f, 0.5f, + }; + static constexpr draco::u16 indices[] = { 0, 1, 2, 0, 2, 3 }; + + rhi::BufferDesc vbd{}; vbd.size = sizeof(verts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(indices); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(verts), sizeof(verts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(indices), sizeof(indices))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Uniform buffer: 4 ObjectData structs (256 bytes each = 1024 total). + rhi::BufferDesc ubd{}; ubd.size = 256 * 4; ubd.usage = rhi::BufferUsage::Uniform; ubd.memory = rhi::MemoryLocation::CpuToGpu; + if (m_device->createBuffer(ubd, m_ub) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Initialize UBO data. + updateUBO(); + + // Bind group layout with dynamic offset UBO. + rhi::BindGroupLayoutEntry entry = rhi::BindGroupLayoutEntry::uniformBuffer(0, rhi::ShaderStage::Vertex | rhi::ShaderStage::Fragment); + entry.hasDynamicOffset = true; + rhi::BindGroupLayoutEntry entries[1] = { entry }; + rhi::BindGroupLayoutDesc bgld{}; bgld.entries = std::span(entries, 1); + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Bind group (bind the whole buffer, dynamic offset selects the slice). + rhi::BindGroupEntry bgEntries[1] = { rhi::BindGroupEntry::bufferEntry(m_ub, 0, 256) }; + rhi::BindGroupDesc bgd{}; bgd.layout = m_bgl; bgd.entries = std::span(bgEntries, 1); + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline layout. + rhi::BindGroupLayout* sets[1] = { m_bgl }; + rhi::PipelineLayoutDesc pld{}; pld.bindGroupLayouts = std::span(sets, 1); + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Pipeline with blend constant support. + rhi::VertexAttribute attrs[1] = { { rhi::VertexFormat::Float32x3, 0, 0 } }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 12; vbl.attributes = std::span(attrs, 1); + + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); ct.writeMask = rhi::ColorWriteMask::All; + ct.blend = rhi::BlendState{ + { rhi::BlendFactor::Constant, rhi::BlendFactor::OneMinusConstant, rhi::BlendOperation::Add }, + { rhi::BlendFactor::One, rhi::BlendFactor::Zero, rhi::BlendOperation::Add } + }; + + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void DynamicOffsetSample::updateUBO() { + void* mapped = m_ub->map(); + if (!mapped) return; + + // 4 objects at different positions with different colors. + ObjectData objs[4] = {}; + + // Red, top-left. + objs[0].tintColor[0] = 1.0f; objs[0].tintColor[1] = 0.2f; objs[0].tintColor[2] = 0.2f; objs[0].tintColor[3] = 1.0f; + objs[0].offsetScale[0] = -0.45f; objs[0].offsetScale[1] = 0.45f; objs[0].offsetScale[2] = 0.4f; objs[0].offsetScale[3] = 0.4f; + + // Green, top-right. + objs[1].tintColor[0] = 0.2f; objs[1].tintColor[1] = 1.0f; objs[1].tintColor[2] = 0.2f; objs[1].tintColor[3] = 1.0f; + objs[1].offsetScale[0] = 0.45f; objs[1].offsetScale[1] = 0.45f; objs[1].offsetScale[2] = 0.4f; objs[1].offsetScale[3] = 0.4f; + + // Blue, bottom-left. + objs[2].tintColor[0] = 0.2f; objs[2].tintColor[1] = 0.3f; objs[2].tintColor[2] = 1.0f; objs[2].tintColor[3] = 1.0f; + objs[2].offsetScale[0] = -0.45f; objs[2].offsetScale[1] = -0.45f; objs[2].offsetScale[2] = 0.4f; objs[2].offsetScale[3] = 0.4f; + + // Yellow, bottom-right. + objs[3].tintColor[0] = 1.0f; objs[3].tintColor[1] = 1.0f; objs[3].tintColor[2] = 0.2f; objs[3].tintColor[3] = 1.0f; + objs[3].offsetScale[0] = 0.45f; objs[3].offsetScale[1] = -0.45f; objs[3].offsetScale[2] = 0.4f; objs[3].offsetScale[3] = 0.4f; + + std::memcpy(mapped, objs, sizeof(objs)); + m_ub->unmap(); +} + +void DynamicOffsetSample::onRender() { + using draco::f32, draco::u32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + + // Animate blend constant: pulsing between full visibility and half. + f32 pulse = 0.5f + 0.5f * std::sin(m_totalTime * 2.0f); + rp->setBlendConstant(pulse, pulse, pulse, 1.0f); + + // Draw 4 objects, each at a different dynamic offset. + for (u32 i = 0; i < 4; i++) { + u32 off[1] = { i * 256 }; + rp->setBindGroup(0, m_bg, std::span(off, 1)); + rp->drawIndexed(6); + } + + rp->end(); + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void DynamicOffsetSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_ub) m_device->destroyBuffer(m_ub); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { DynamicOffsetSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample027_3DTexture/CMakeLists.txt b/Samples/cpp/RHI/Sample027_3DTexture/CMakeLists.txt new file mode 100644 index 00000000..8fd5b546 --- /dev/null +++ b/Samples/cpp/RHI/Sample027_3DTexture/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample027_3DTexture) diff --git a/Samples/cpp/RHI/Sample027_3DTexture/Main.cpp b/Samples/cpp/RHI/Sample027_3DTexture/Main.cpp new file mode 100644 index 00000000..3afd4464 --- /dev/null +++ b/Samples/cpp/RHI/Sample027_3DTexture/Main.cpp @@ -0,0 +1,379 @@ +#include +/// Demonstrates 3D textures and 1D textures. +/// Generates a 3D noise volume, renders slices animated over time. +/// Uses a 1D gradient LUT for color mapping. + +#include +#include +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class Texture3DSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample027 - 3D Texture & 1D LUT"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; +private: + draco::Status createVolumeTexture(); + draco::Status createLUTTexture(); + + static constexpr const char8_t kShader[] = u8R"( + Texture3D gVolume : register(t0, space0); + Texture1D gLUT : register(t1, space0); + SamplerState gSampler : register(s0, space0); + + struct PushConstants + { + float SliceZ; + float Time; + float2 _pad; + }; + + [[vk::push_constant]] ConstantBuffer pc : register(b0, space1); + + struct PSInput + { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; + }; + + PSInput VSMain(uint vertexID : SV_VertexID) + { + PSInput output; + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(uv * 2.0 - 1.0, 0.5, 1.0); + output.UV = uv; + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + // Sample 3D volume at current slice + float3 uvw = float3(input.UV, pc.SliceZ); + float density = gVolume.Sample(gSampler, uvw).r; + + // Map density through 1D LUT + float4 color = gLUT.Sample(gSampler, density); + return color; + } + )"; + + struct PushData { + float sliceZ; + float time; + float _pad0; + float _pad1; + }; + + static constexpr draco::u32 kVolumeSize = 32; + static constexpr draco::u32 kLUTSize = 64; + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + + // 3D volume texture + rhi::Texture* m_volumeTexture = nullptr; + rhi::TextureView* m_volumeView = nullptr; + + // 1D LUT texture + rhi::Texture* m_lutTexture = nullptr; + rhi::TextureView* m_lutView = nullptr; + + rhi::Sampler* m_sampler = nullptr; + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::BindGroup* m_bg = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status Texture3DSample::onInit() { + using draco::Status, std::span, draco::u8, draco::u32; + + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"Vol3DVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"Vol3DPS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (createVolumeTexture() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (createLUTTexture() != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Sampler + { + rhi::SamplerDesc sd{}; + sd.minFilter = rhi::FilterMode::Linear; + sd.magFilter = rhi::FilterMode::Linear; + sd.addressU = rhi::AddressMode::Repeat; + sd.addressV = rhi::AddressMode::Repeat; + sd.addressW = rhi::AddressMode::Repeat; + sd.label = u8"VolSampler"; + if (m_device->createSampler(sd, m_sampler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Bind group layout: 3D tex, 1D tex, sampler + { + rhi::BindGroupLayoutEntry entries[3] = { + rhi::BindGroupLayoutEntry::sampledTexture(0, rhi::ShaderStage::Fragment, rhi::TextureViewDimension::Texture3D), + rhi::BindGroupLayoutEntry::sampledTexture(1, rhi::ShaderStage::Fragment, rhi::TextureViewDimension::Texture1D), + rhi::BindGroupLayoutEntry::sampler(0, rhi::ShaderStage::Fragment) + }; + rhi::BindGroupLayoutDesc bgld{}; + bgld.entries = std::span(entries, 3); + bgld.label = u8"VolBGL"; + if (m_device->createBindGroupLayout(bgld, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Bind group + { + rhi::BindGroupEntry entries[3] = { + rhi::BindGroupEntry::textureEntry(m_volumeView), + rhi::BindGroupEntry::textureEntry(m_lutView), + rhi::BindGroupEntry::samplerEntry(m_sampler) + }; + rhi::BindGroupDesc bgd{}; + bgd.layout = m_bgl; + bgd.entries = std::span(entries, 3); + bgd.label = u8"VolBG"; + if (m_device->createBindGroup(bgd, m_bg) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Pipeline layout with push constants + { + rhi::BindGroupLayout* sets[1] = { m_bgl }; + rhi::PushConstantRange pcr{}; + pcr.stages = rhi::ShaderStage::Fragment; + pcr.offset = 0; + pcr.size = sizeof(PushData); + rhi::PushConstantRange pushRanges[1] = { pcr }; + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(sets, 1); + pld.pushConstantRanges = std::span(pushRanges, 1); + pld.label = u8"VolPL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Render pipeline (fullscreen triangle, no vertex input) + { + rhi::ColorTargetState ct{}; + ct.format = m_swapChain->format(); + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.fragment = rhi::FragmentState{}; + rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"VolPipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +draco::Status Texture3DSample::createVolumeTexture() { + using draco::Status, std::span, draco::u8, draco::u32; + + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture3D; + td.format = rhi::TextureFormat::R8Unorm; + td.width = kVolumeSize; + td.height = kVolumeSize; + td.depth = kVolumeSize; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + td.label = u8"VolumeTex3D"; + if (m_device->createTexture(td, m_volumeTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; + tvd.format = rhi::TextureFormat::R8Unorm; + tvd.dimension = rhi::TextureViewDimension::Texture3D; + if (m_device->createTextureView(m_volumeTexture, tvd, m_volumeView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Generate procedural 3D noise data + constexpr u32 dataSize = kVolumeSize * kVolumeSize * kVolumeSize; + u8 data[dataSize]; + + for (u32 z = 0; z < kVolumeSize; z++) { + for (u32 y = 0; y < kVolumeSize; y++) { + for (u32 x = 0; x < kVolumeSize; x++) { + float fx = static_cast(x) / static_cast(kVolumeSize); + float fy = static_cast(y) / static_cast(kVolumeSize); + float fz = static_cast(z) / static_cast(kVolumeSize); + + // Simple 3D pattern: spherical blobs + frequency pattern + float cx = fx - 0.5f, cy = fy - 0.5f, cz = fz - 0.5f; + float dist = std::sqrt(cx * cx + cy * cy + cz * cz); + float sphere = std::max(0.0f, 1.0f - dist * 3.0f); + float pattern = std::sin(fx * 12.0f) * std::sin(fy * 12.0f) * std::sin(fz * 12.0f); + float v = std::clamp(sphere + pattern * 0.3f, 0.0f, 1.0f); + + u32 idx = z * kVolumeSize * kVolumeSize + y * kVolumeSize + x; + data[idx] = static_cast(v * 255.0f); + } + } + } + + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + rhi::TextureDataLayout layout{}; + layout.bytesPerRow = kVolumeSize; + layout.rowsPerImage = kVolumeSize; + batch->writeTexture(m_volumeTexture, + std::span(data, dataSize), + layout, rhi::Extent3D{kVolumeSize, kVolumeSize, kVolumeSize}); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + + return draco::ErrorCode::Ok; +} + +draco::Status Texture3DSample::createLUTTexture() { + using draco::Status, std::span, draco::u8, draco::u32; + + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture1D; + td.format = rhi::TextureFormat::RGBA8UnormSrgb; + td.width = kLUTSize; + td.height = 1; + td.arrayLayerCount = 1; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Sampled | rhi::TextureUsage::CopyDst; + td.label = u8"LUTTex1D"; + if (m_device->createTexture(td, m_lutTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; + tvd.format = rhi::TextureFormat::RGBA8UnormSrgb; + tvd.dimension = rhi::TextureViewDimension::Texture1D; + if (m_device->createTextureView(m_lutTexture, tvd, m_lutView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Generate gradient LUT: dark blue -> cyan -> green -> yellow -> red -> white + u8 data[kLUTSize * 4]; + for (u32 i = 0; i < kLUTSize; i++) { + float t = static_cast(i) / static_cast(kLUTSize - 1); + float r, g, b; + if (t < 0.2f) { + float s = t / 0.2f; + r = 0.05f; g = 0.05f + s * 0.4f; b = 0.3f + s * 0.5f; + } else if (t < 0.4f) { + float s = (t - 0.2f) / 0.2f; + r = 0.05f; g = 0.45f + s * 0.5f; b = 0.8f - s * 0.5f; + } else if (t < 0.6f) { + float s = (t - 0.4f) / 0.2f; + r = s * 0.8f; g = 0.95f; b = 0.3f - s * 0.3f; + } else if (t < 0.8f) { + float s = (t - 0.6f) / 0.2f; + r = 0.8f + s * 0.2f; g = 0.95f - s * 0.6f; b = 0.0f; + } else { + float s = (t - 0.8f) / 0.2f; + r = 1.0f; g = 0.35f + s * 0.65f; b = s * 0.8f; + } + + u32 idx = i * 4; + data[idx + 0] = static_cast(r * 255.0f); + data[idx + 1] = static_cast(g * 255.0f); + data[idx + 2] = static_cast(b * 255.0f); + data[idx + 3] = 255; + } + + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + rhi::TextureDataLayout layout{}; + layout.bytesPerRow = kLUTSize * 4; + layout.rowsPerImage = 1; + batch->writeTexture(m_lutTexture, + std::span(data, kLUTSize * 4), + layout, rhi::Extent3D{kLUTSize, 1, 1}); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + + return draco::ErrorCode::Ok; +} + +void Texture3DSample::onRender() { + using draco::f32, std::span; + + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; + ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; + ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.02f, 0.02f, 0.05f, 1.0f); + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + + auto* rp = enc->beginRenderPass(rpd); + + rp->setPipeline(m_pipeline); + rp->setBindGroup(0, m_bg); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0.0f, 1.0f); + rp->setScissor(0, 0, m_width, m_height); + + // Animate slice through 3D volume + float sliceZ = 0.5f + 0.5f * std::sin(m_totalTime * 0.5f); + PushData pc{}; + pc.sliceZ = sliceZ; + pc.time = m_totalTime; + rp->setPushConstants(rhi::ShaderStage::Fragment, 0, sizeof(PushData), &pc); + + rp->draw(3); // Fullscreen triangle + + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void Texture3DSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bg) m_device->destroyBindGroup(m_bg); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_sampler) m_device->destroySampler(m_sampler); + if (m_lutView) m_device->destroyTextureView(m_lutView); + if (m_lutTexture) m_device->destroyTexture(m_lutTexture); + if (m_volumeView) m_device->destroyTextureView(m_volumeView); + if (m_volumeTexture) m_device->destroyTexture(m_volumeTexture); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { Texture3DSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample028_ProceduralRT/CMakeLists.txt b/Samples/cpp/RHI/Sample028_ProceduralRT/CMakeLists.txt new file mode 100644 index 00000000..dabeca4f --- /dev/null +++ b/Samples/cpp/RHI/Sample028_ProceduralRT/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample028_ProceduralRT) diff --git a/Samples/cpp/RHI/Sample028_ProceduralRT/Main.cpp b/Samples/cpp/RHI/Sample028_ProceduralRT/Main.cpp new file mode 100644 index 00000000..3e193320 --- /dev/null +++ b/Samples/cpp/RHI/Sample028_ProceduralRT/Main.cpp @@ -0,0 +1,618 @@ +#include +/// Demonstrates procedural ray tracing geometry using AABBs. +/// Renders spheres via intersection shaders inside axis-aligned bounding boxes. +/// Tests GeometryType.AABBs, ProceduralHitGroup, IntersectionShaderIndex. + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class ProceduralRTSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample028 - Procedural RT (AABB Spheres)"; } +protected: + rhi::DeviceFeatures requiredFeatures() const override { + rhi::DeviceFeatures f{}; + f.rayTracing = true; + return f; + } + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override; + void onShutdown() override; +private: + // Ray tracing shader library (compiled as lib_6_3). + // All RT entry points are in a single source compiled once as a library. + static constexpr const char8_t kRtShaderSource[] = u8R"( + [[vk::image_format("rgba8")]] RWTexture2D gOutput : register(u0, space0); + RaytracingAccelerationStructure gScene : register(t0, space0); + + struct RayPayload + { + float3 Color; + float HitT; + float2 UV; + float2 _pad; + }; + + struct SphereAttribs + { + float3 Normal; + float HitDist; + }; + + [shader("raygeneration")] + void RayGen() + { + uint2 launchIndex = DispatchRaysIndex().xy; + uint2 launchDim = DispatchRaysDimensions().xy; + + float2 uv = (float2(launchIndex) + 0.5) / float2(launchDim); + float2 ndc = uv * 2.0 - 1.0; + ndc.y = -ndc.y; + float aspect = float(launchDim.x) / float(launchDim.y); + ndc.x *= aspect; + + RayDesc ray; + ray.Origin = float3(ndc.x * 2.0, ndc.y * 2.0, -3.0); + ray.Direction = float3(0.0, 0.0, 1.0); + ray.TMin = 0.001; + ray.TMax = 100.0; + + RayPayload payload; + payload.Color = float3(0.0, 0.0, 0.0); + payload.HitT = -1.0; + payload.UV = uv; + payload._pad = float2(0, 0); + + TraceRay(gScene, RAY_FLAG_FORCE_OPAQUE, 0xFF, 0, 0, 0, ray, payload); + + gOutput[launchIndex] = float4(payload.Color, 1.0); + } + + [shader("intersection")] + void SphereIntersection() + { + // AABB center is at origin of the geometry instance, radius 0.5 + float3 center = float3(0, 0, 0); + float radius = 0.45; + + float3 origin = ObjectRayOrigin(); + float3 dir = ObjectRayDirection(); + float3 oc = origin - center; + + float a = dot(dir, dir); + float b = 2.0 * dot(oc, dir); + float c = dot(oc, oc) - radius * radius; + float discriminant = b * b - 4.0 * a * c; + + if (discriminant >= 0.0) + { + float t = (-b - sqrt(discriminant)) / (2.0 * a); + if (t >= RayTMin() && t <= RayTCurrent()) + { + float3 hitPos = origin + t * dir; + float3 normal = normalize(hitPos - center); + + SphereAttribs attribs; + attribs.Normal = normal; + attribs.HitDist = t; + ReportHit(t, 0, attribs); + } + } + } + + [shader("closesthit")] + void ClosestHit(inout RayPayload payload, SphereAttribs attribs) + { + // Simple diffuse shading + float3 lightDir = normalize(float3(0.5, 1.0, -0.5)); + float3 normal = normalize(mul((float3x3)ObjectToWorld3x4(), attribs.Normal)); + float ndotl = max(0.0, dot(normal, lightDir)); + float ambient = 0.15; + + // Color based on instance index + uint instID = InstanceIndex(); + float3 baseColor; + if (instID == 0) baseColor = float3(1.0, 0.3, 0.3); + else if (instID == 1) baseColor = float3(0.3, 1.0, 0.3); + else if (instID == 2) baseColor = float3(0.3, 0.3, 1.0); + else baseColor = float3(1.0, 1.0, 0.3); + + payload.Color = baseColor * (ndotl + ambient); + payload.HitT = attribs.HitDist; + } + + [shader("miss")] + void Miss(inout RayPayload payload) + { + payload.Color = float3(0.05, 0.05, 0.1) + float3(0.0, 0.0, 0.15) * payload.UV.y; + payload.HitT = -1.0; + } + )"; + + static constexpr int kSphereCount = 4; + + shaders::Compiler* m_compiler = nullptr; + + // RT resources. + rhi::ShaderModule* m_rtShaderModule = nullptr; + rhi::RayTracingPipeline* m_rtPipeline = nullptr; + rhi::AccelStruct* m_blas = nullptr; + rhi::AccelStruct* m_tlas = nullptr; + rhi::Buffer* m_scratchBuffer = nullptr; + rhi::Buffer* m_aabbBuffer = nullptr; // AABB for BLAS. + rhi::Buffer* m_instanceBuffer = nullptr; // TLAS instance data. + rhi::Buffer* m_sbtBuffer = nullptr; // Shader binding table. + rhi::PipelineLayout* m_rtPipelineLayout = nullptr; + rhi::BindGroupLayout* m_rtBindGroupLayout = nullptr; + rhi::BindGroup* m_rtBindGroup = nullptr; + + // RT output texture. + rhi::Texture* m_outputTexture = nullptr; + rhi::TextureView* m_outputTextureView = nullptr; + rhi::ResourceState m_outputTextureState = rhi::ResourceState::Undefined; + + // SBT layout info (cached for traceRays). + draco::u32 m_sbtAlignedStride = 0; + + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status ProceduralRTSample::onInit() { + using draco::Status, std::span; + + // ---- Check ray tracing support ---- + if (!m_device->features.rayTracing) { + std::fprintf(stderr, "ERROR: Ray tracing is not supported by this device/backend\n"); + return draco::ErrorCode::Unknown; + } + + std::printf("Ray tracing extension available:\n"); + std::printf(" shaderGroupHandleSize: %u\n", m_device->shaderGroupHandleSize); + std::printf(" shaderGroupHandleAlignment: %u\n", m_device->shaderGroupHandleAlignment); + std::printf(" shaderGroupBaseAlignment: %u\n", m_device->shaderGroupBaseAlignment); + + // ---- Shader compiler ---- + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // ---- Compile RT shader library (lib_6_3) ---- + if (sf::compileToModule(m_compiler, m_device, kRtShaderSource, shaders::ShaderStage::RayGen, + u8"", u8"ProcRTLib", u8"6_3", m_rtShaderModule) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "ERROR: RT shader library compilation failed\n"); + return draco::ErrorCode::Unknown; + } + + // ---- Command pool and fence ---- + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // ---- Create RT output texture (storage + copy source) ---- + { + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; + td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = m_width; + td.height = m_height; + td.arrayLayerCount = 1; + td.mipLevelCount = 1; + td.sampleCount = 1; + td.usage = rhi::TextureUsage::Storage | rhi::TextureUsage::CopySrc; + td.label = u8"ProcRTOutput"; + if (m_device->createTexture(td, m_outputTexture) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TextureViewDesc tvd{}; + tvd.label = u8"ProcRTOutputView"; + if (m_device->createTextureView(m_outputTexture, tvd, m_outputTextureView) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- AABB buffer: one AABB per sphere (6 floats: minX, minY, minZ, maxX, maxY, maxZ) ---- + // All AABBs are unit cubes [-0.5, 0.5] centered at origin - instance transforms position them + { + float aabbs[6] = { + -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f + }; + draco::u32 aabbSize = sizeof(aabbs); + + rhi::BufferDesc bd{}; + bd.size = aabbSize; + bd.usage = rhi::BufferUsage::AccelStructInput | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; + bd.label = u8"AABBBuffer"; + if (m_device->createBuffer(bd, m_aabbBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* transfer = nullptr; + if (m_graphicsQueue->createTransferBatch(transfer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + transfer->writeBuffer(m_aabbBuffer, 0, + std::span(reinterpret_cast(aabbs), aabbSize)); + transfer->submit(); + m_graphicsQueue->destroyTransferBatch(transfer); + } + + // ---- Create acceleration structures ---- + { + rhi::AccelStructDesc asd{}; + asd.type = rhi::AccelStructType::BottomLevel; + asd.label = u8"ProcBLAS"; + if (m_device->createAccelStruct(asd, m_blas) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + asd.type = rhi::AccelStructType::TopLevel; + asd.label = u8"ProcTLAS"; + if (m_device->createAccelStruct(asd, m_tlas) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create scratch buffer (256 KB) ---- + { + rhi::BufferDesc bd{}; + bd.size = 256 * 1024; + bd.usage = rhi::BufferUsage::AccelStructScratch; + bd.memory = rhi::MemoryLocation::GpuOnly; + bd.label = u8"ProcScratch"; + if (m_device->createBuffer(bd, m_scratchBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create instance buffer (64 bytes * 4 spheres) ---- + { + rhi::BufferDesc bd{}; + bd.size = 64 * kSphereCount; + bd.usage = rhi::BufferUsage::AccelStructInput; + bd.memory = rhi::MemoryLocation::CpuToGpu; + bd.label = u8"ProcInstances"; + if (m_device->createBuffer(bd, m_instanceBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // Fill instance data: 4 spheres at different positions. + { + auto* ptr = static_cast(m_instanceBuffer->map()); + if (!ptr) { std::fprintf(stderr, "ERROR: Failed to map instance buffer\n"); return draco::ErrorCode::Unknown; } + + float positions[4][3] = { + {-1.0f, 0.5f, 0.0f}, + { 1.0f, 0.5f, 0.0f}, + {-1.0f, -0.5f, 0.0f}, + { 1.0f, -0.5f, 0.0f} + }; + + for (int i = 0; i < kSphereCount; i++) { + draco::u8* inst = ptr + i * 64; + std::memset(inst, 0, 64); + + // 3x4 row-major transform. + auto* xform = reinterpret_cast(inst); + xform[0] = 1.0f; xform[3] = positions[i][0]; + xform[5] = 1.0f; xform[7] = positions[i][1]; + xform[10] = 1.0f; xform[11] = positions[i][2]; + + // instanceCustomIndex (24 bit) + mask (8 bit) at offset 48. + inst[48] = 0; inst[49] = 0; inst[50] = 0; + inst[51] = 0xFF; // mask + + // SBT offset (24 bit) + flags (8 bit) at offset 52. + inst[52] = 0; inst[53] = 0; inst[54] = 0; + inst[55] = 0x04; // VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR + + // accelerationStructureReference at offset 56. + *reinterpret_cast(inst + 56) = m_blas->deviceAddress(); + } + + m_instanceBuffer->unmap(); + } + + // ---- Build BLAS from AABB geometry, then TLAS ---- + { + rhi::CommandEncoder* encoder = nullptr; + if (m_pool->createEncoder(encoder) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (auto* rtEnc = encoder->asRayTracingExt()) { + // Build BLAS from AABB geometry. + rhi::AccelStructGeometryAABBs aabbGeom{}; + aabbGeom.aabbBuffer = m_aabbBuffer; + aabbGeom.offset = 0; + aabbGeom.count = 1; + aabbGeom.stride = 24; + aabbGeom.flags = rhi::GeometryFlags::Opaque; + + rtEnc->buildBottomLevelAccelStruct(m_blas, m_scratchBuffer, 0, + std::span{}, + std::span(&aabbGeom, 1)); + + // Barrier between BLAS and TLAS build. + rhi::MemoryBarrier mb{}; + mb.oldState = rhi::ResourceState::AccelStructWrite; + mb.newState = rhi::ResourceState::AccelStructRead; + rhi::BarrierGroup bg{}; + bg.memoryBarriers = std::span(&mb, 1); + encoder->barrier(bg); + + // Build TLAS from instances. + rtEnc->buildTopLevelAccelStruct(m_tlas, m_scratchBuffer, 0, + m_instanceBuffer, 0, kSphereCount); + } else { + std::fprintf(stderr, "ERROR: Command encoder does not support ray tracing\n"); + m_pool->destroyEncoder(encoder); + return draco::ErrorCode::Unknown; + } + + rhi::CommandBuffer* cb = encoder->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + // Wait for build to complete. + m_fence->wait(m_fenceVal); + m_pool->reset(); + m_pool->destroyEncoder(encoder); + } + + std::printf("Procedural BLAS/TLAS built.\n"); + + // ---- Create RT bind group layout and bind group ---- + { + rhi::BindGroupLayoutEntry layoutEntries[2]{}; + + // Storage texture (read-write). + layoutEntries[0].binding = 0; + layoutEntries[0].visibility = rhi::ShaderStage::RayGen; + layoutEntries[0].type = rhi::BindingType::StorageTextureReadWrite; + layoutEntries[0].storageTextureFormat = rhi::TextureFormat::RGBA8Unorm; + layoutEntries[0].count = 1; + + // Acceleration structure. + layoutEntries[1].binding = 0; + layoutEntries[1].visibility = rhi::ShaderStage::RayGen | rhi::ShaderStage::ClosestHit; + layoutEntries[1].type = rhi::BindingType::AccelerationStructure; + layoutEntries[1].count = 1; + + rhi::BindGroupLayoutDesc bgld{}; + bgld.entries = std::span(layoutEntries, 2); + bgld.label = u8"ProcRTBGL"; + if (m_device->createBindGroupLayout(bgld, m_rtBindGroupLayout) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Create bind group with output texture + TLAS. + rhi::BindGroupEntry bgEntries[2]{}; + bgEntries[0] = rhi::BindGroupEntry::textureEntry(m_outputTextureView); + bgEntries[1] = rhi::BindGroupEntry::accelStructEntry(m_tlas); + + rhi::BindGroupDesc bgd{}; + bgd.layout = m_rtBindGroupLayout; + bgd.entries = std::span(bgEntries, 2); + bgd.label = u8"ProcRTBG"; + if (m_device->createBindGroup(bgd, m_rtBindGroup) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + } + + // ---- Create RT pipeline layout and pipeline ---- + { + rhi::BindGroupLayout* bglArr[1] = { m_rtBindGroupLayout }; + + rhi::PipelineLayoutDesc pld{}; + pld.bindGroupLayouts = std::span(bglArr, 1); + pld.label = u8"ProcRTPL"; + if (m_device->createPipelineLayout(pld, m_rtPipelineLayout) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // 4 stages: RayGen, Intersection, ClosestHit, Miss - all from the same shader module. + rhi::ProgrammableStage stages[4]{}; + stages[0] = { m_rtShaderModule, u8"RayGen", rhi::ShaderStage::RayGen }; + stages[1] = { m_rtShaderModule, u8"SphereIntersection", rhi::ShaderStage::Intersection }; + stages[2] = { m_rtShaderModule, u8"ClosestHit", rhi::ShaderStage::ClosestHit }; + stages[3] = { m_rtShaderModule, u8"Miss", rhi::ShaderStage::Miss }; + + // 3 groups: raygen (general), procedural hit group (intersection + closest hit), miss (general). + rhi::RayTracingShaderGroup groups[3]{}; + groups[0].type = rhi::RayTracingShaderGroup::Type::General; + groups[0].generalShaderIndex = 0; + + groups[1].type = rhi::RayTracingShaderGroup::Type::ProceduralHitGroup; + groups[1].intersectionShaderIndex = 1; + groups[1].closestHitShaderIndex = 2; + + groups[2].type = rhi::RayTracingShaderGroup::Type::General; + groups[2].generalShaderIndex = 3; + + rhi::RayTracingPipelineDesc rtpd{}; + rtpd.layout = m_rtPipelineLayout; + rtpd.stages = std::span(stages, 4); + rtpd.groups = std::span(groups, 3); + rtpd.maxRecursionDepth = 1; + rtpd.maxPayloadSize = 32; // RayPayload: float3 + float + float2 + float2 + rtpd.maxAttributeSize = 16; // SphereAttribs: float3 Normal + float HitDist + rtpd.label = u8"ProcRTPipeline"; + if (m_device->createRayTracingPipeline(rtpd, m_rtPipeline) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "ERROR: CreateRayTracingPipeline failed\n"); + return draco::ErrorCode::Unknown; + } + } + + std::printf("Procedural RT pipeline created successfully.\n"); + + // ---- Build Shader Binding Table ---- + { + draco::u32 handleSize = m_device->shaderGroupHandleSize; + draco::u32 baseAlignment = m_device->shaderGroupBaseAlignment; + draco::u32 groupCount = 3; + + // Aligned handle stride (round up to base alignment). + m_sbtAlignedStride = (handleSize + baseAlignment - 1) & ~(baseAlignment - 1); + + // Get shader group handles. + draco::u8 handleData[128]; // Enough for 3 handles (max ~32 bytes each). + if (m_device->getShaderGroupHandles(m_rtPipeline, 0, groupCount, + std::span(handleData, handleSize * groupCount)) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "ERROR: getShaderGroupHandles failed\n"); + return draco::ErrorCode::Unknown; + } + + // Create SBT buffer: 3 entries, each aligned to baseAlignment. + draco::u64 sbtSize = static_cast(m_sbtAlignedStride) * groupCount; + rhi::BufferDesc sbd{}; + sbd.size = sbtSize; + sbd.usage = rhi::BufferUsage::ShaderBindingTable; + sbd.memory = rhi::MemoryLocation::CpuToGpu; + sbd.label = u8"ProcSBT"; + if (m_device->createBuffer(sbd, m_sbtBuffer) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Copy handles into SBT with proper alignment. + auto* sbtPtr = static_cast(m_sbtBuffer->map()); + if (!sbtPtr) { std::fprintf(stderr, "ERROR: Failed to map SBT buffer\n"); return draco::ErrorCode::Unknown; } + std::memset(sbtPtr, 0, static_cast(sbtSize)); + + for (draco::u32 i = 0; i < groupCount; i++) { + std::memcpy(sbtPtr + (i * m_sbtAlignedStride), + handleData + (i * handleSize), + handleSize); + } + m_sbtBuffer->unmap(); + + std::printf("SBT built: handleSize=%u, baseAlignment=%u, alignedStride=%u, totalSize=%llu\n", + handleSize, baseAlignment, m_sbtAlignedStride, static_cast(sbtSize)); + } + + std::printf("Procedural RT sample ready.\n"); + + return draco::ErrorCode::Ok; +} + +void ProceduralRTSample::onRender() { + using std::span; + + // Wait for previous frame. + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + + // Acquire next swap chain image. + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + // Reset and create encoder. + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // ---- Transition output texture to ShaderWrite for TraceRays ---- + enc->transitionTexture(m_outputTexture, m_outputTextureState, rhi::ResourceState::ShaderWrite); + + // ---- Dispatch TraceRays ---- + if (auto* rtEnc = enc->asRayTracingExt()) { + rtEnc->setRayTracingPipeline(m_rtPipeline); + rtEnc->setBindGroup(0, m_rtBindGroup); + + // SBT layout: [0] = raygen, [1] = hit, [2] = miss. + draco::u64 raygenOffset = 0; + draco::u64 hitOffset = static_cast(1) * m_sbtAlignedStride; + draco::u64 missOffset = static_cast(2) * m_sbtAlignedStride; + draco::u64 stride = static_cast(m_sbtAlignedStride); + + rtEnc->traceRays( + m_sbtBuffer, raygenOffset, stride, + m_sbtBuffer, missOffset, stride, + m_sbtBuffer, hitOffset, stride, + m_width, m_height); + } + + // ---- Transition: output texture ShaderWrite -> CopySrc, swapchain Present -> CopyDst ---- + { + rhi::TextureBarrier texBarriers[2]{}; + texBarriers[0].texture = m_outputTexture; + texBarriers[0].oldState = rhi::ResourceState::ShaderWrite; + texBarriers[0].newState = rhi::ResourceState::CopySrc; + + texBarriers[1].texture = m_swapChain->currentTexture(); + texBarriers[1].oldState = rhi::ResourceState::Present; + texBarriers[1].newState = rhi::ResourceState::CopyDst; + + rhi::BarrierGroup bg{}; + bg.textureBarriers = std::span(texBarriers, 2); + enc->barrier(bg); + } + + // ---- Copy RT output to swapchain ---- + m_outputTextureState = rhi::ResourceState::CopySrc; + { + rhi::TextureCopyRegion region{}; + region.extent = rhi::Extent3D{ m_width, m_height, 1 }; + enc->copyTextureToTexture(m_outputTexture, m_swapChain->currentTexture(), region); + } + + // ---- Transition swapchain CopyDst -> Present ---- + enc->transitionTexture(m_swapChain->currentTexture(), + rhi::ResourceState::CopyDst, rhi::ResourceState::Present); + + // Finish and submit. + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + // Present. + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void ProceduralRTSample::onResize(draco::u32 w, draco::u32 h) { + using std::span; + if (m_fence) m_fence->wait(m_fenceVal, ~0ull); + + if (m_rtBindGroup) m_device->destroyBindGroup(m_rtBindGroup); + if (m_outputTextureView) m_device->destroyTextureView(m_outputTextureView); + if (m_outputTexture) m_device->destroyTexture(m_outputTexture); + m_rtBindGroup = nullptr; m_outputTextureView = nullptr; m_outputTexture = nullptr; + + rhi::TextureDesc td{}; + td.dimension = rhi::TextureDimension::Texture2D; td.format = rhi::TextureFormat::RGBA8Unorm; + td.width = w; td.height = h; td.arrayLayerCount = 1; td.mipLevelCount = 1; td.sampleCount = 1; + td.usage = rhi::TextureUsage::Storage | rhi::TextureUsage::CopySrc; td.label = u8"RTOutputTex"; + m_device->createTexture(td, m_outputTexture); + + rhi::TextureViewDesc tvd{}; tvd.label = u8"RTOutputView"; + m_device->createTextureView(m_outputTexture, tvd, m_outputTextureView); + + rhi::BindGroupEntry bgEntries[2]{}; + bgEntries[0] = rhi::BindGroupEntry::textureEntry(m_outputTextureView); + bgEntries[1] = rhi::BindGroupEntry::accelStructEntry(m_tlas); + rhi::BindGroupDesc bgd{}; bgd.layout = m_rtBindGroupLayout; + bgd.entries = std::span(bgEntries, 2); bgd.label = u8"RTBindGroup"; + m_device->createBindGroup(bgd, m_rtBindGroup); + + m_outputTextureState = rhi::ResourceState::Undefined; +} + +void ProceduralRTSample::onShutdown() { + if (m_fence) m_fence->wait(m_fenceVal, ~0ull); + + // RT bind group. + if (m_rtBindGroup) m_device->destroyBindGroup(m_rtBindGroup); + if (m_rtBindGroupLayout) m_device->destroyBindGroupLayout(m_rtBindGroupLayout); + + // RT output texture. + if (m_outputTextureView) m_device->destroyTextureView(m_outputTextureView); + if (m_outputTexture) m_device->destroyTexture(m_outputTexture); + + // RT resources. + if (m_sbtBuffer) m_device->destroyBuffer(m_sbtBuffer); + if (m_rtPipeline) m_device->destroyRayTracingPipeline(m_rtPipeline); + if (m_rtPipelineLayout) m_device->destroyPipelineLayout(m_rtPipelineLayout); + if (m_instanceBuffer) m_device->destroyBuffer(m_instanceBuffer); + if (m_scratchBuffer) m_device->destroyBuffer(m_scratchBuffer); + if (m_tlas) m_device->destroyAccelStruct(m_tlas); + if (m_blas) m_device->destroyAccelStruct(m_blas); + if (m_aabbBuffer) m_device->destroyBuffer(m_aabbBuffer); + if (m_rtShaderModule) m_device->destroyShaderModule(m_rtShaderModule); + + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { ProceduralRTSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample029_ResolveTexture/CMakeLists.txt b/Samples/cpp/RHI/Sample029_ResolveTexture/CMakeLists.txt new file mode 100644 index 00000000..327f0ab3 --- /dev/null +++ b/Samples/cpp/RHI/Sample029_ResolveTexture/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample029_ResolveTexture) diff --git a/Samples/cpp/RHI/Sample029_ResolveTexture/Main.cpp b/Samples/cpp/RHI/Sample029_ResolveTexture/Main.cpp new file mode 100644 index 00000000..c83039e9 --- /dev/null +++ b/Samples/cpp/RHI/Sample029_ResolveTexture/Main.cpp @@ -0,0 +1,210 @@ +#include +/// Demonstrates explicit MSAA resolve via CommandEncoder::resolveTexture(). +/// Unlike Sample010 (which uses ColorAttachment.resolveTarget for automatic +/// render-pass resolve), this sample renders to a 4x MSAA target and then +/// manually resolves to the swapchain using the resolveTexture command. + +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class ResolveTextureSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample029 - ResolveTexture (Explicit 4x MSAA)"; } +protected: + draco::Status onInit() override; + void onRender() override; + void onResize(draco::u32 w, draco::u32 h) override { recreateMsaaTarget(w, h); } + void onShutdown() override; +private: + static constexpr const char8_t kShader[] = u8R"( + struct VSInput { + float3 Position : TEXCOORD0; + float4 Color : TEXCOORD1; + }; + struct PSInput { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + }; + PSInput VSMain(VSInput input) { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET { + return input.Color; + } + )"; + + // Star shape: great for showing MSAA on diagonal edges. + // Each vertex: float3 Position, float4 Color (stride = 28). + static constexpr float kVerts[] = { + // Center + 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, + // Outer tips (radius 0.7) + 0.0f, 0.7f, 0.0f, 1.0f, 0.2f, 0.2f, 1.0f, + 0.665f, 0.216f, 0.0f, 0.2f, 1.0f, 0.2f, 1.0f, + 0.411f, -0.566f, 0.0f, 0.2f, 0.3f, 1.0f, 1.0f, + -0.411f, -0.566f, 0.0f, 1.0f, 1.0f, 0.2f, 1.0f, + -0.665f, 0.216f, 0.0f, 1.0f, 0.2f, 1.0f, 1.0f, + // Inner notches (radius 0.25) + 0.238f, 0.327f, 0.0f, 0.8f, 0.7f, 0.5f, 1.0f, + 0.385f, -0.125f, 0.0f, 0.5f, 0.8f, 0.7f, 1.0f, + 0.0f, -0.405f, 0.0f, 0.5f, 0.5f, 0.9f, 1.0f, + -0.385f, -0.125f, 0.0f, 0.9f, 0.8f, 0.5f, 1.0f, + -0.238f, 0.327f, 0.0f, 0.9f, 0.5f, 0.8f, 1.0f, + }; + + static constexpr draco::u16 kIdx[] = { + 0, 1, 6, 0, 6, 2, + 0, 2, 7, 0, 7, 3, + 0, 3, 8, 0, 8, 4, + 0, 4, 9, 0, 9, 5, + 0, 5, 10, 0, 10, 1, + }; + + static constexpr draco::u32 kSamples = 4; + + void recreateMsaaTarget(draco::u32 w, draco::u32 h); + + shaders::Compiler* m_compiler = nullptr; + rhi::ShaderModule *m_vs = nullptr, *m_ps = nullptr; + rhi::Buffer *m_vb = nullptr, *m_ib = nullptr; + rhi::PipelineLayout *m_pl = nullptr; + rhi::RenderPipeline *m_pipeline = nullptr; + rhi::Texture *m_msaaTex = nullptr; + rhi::TextureView *m_msaaView = nullptr; + rhi::CommandPool *m_pool = nullptr; + rhi::Fence *m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +void ResolveTextureSample::recreateMsaaTarget(draco::u32 w, draco::u32 h) { + if (m_msaaView) { m_device->destroyTextureView(m_msaaView); m_msaaView = nullptr; } + if (m_msaaTex) { m_device->destroyTexture(m_msaaTex); m_msaaTex = nullptr; } + // Need RenderTarget (to draw into) and CopySrc (source for resolveTexture). + rhi::TextureDesc td{}; + td.format = m_swapChain->format(); td.width = w; td.height = h; td.sampleCount = kSamples; + td.usage = rhi::TextureUsage::RenderTarget | rhi::TextureUsage::CopySrc; + td.label = u8"MsaaRT"; + m_device->createTexture(td, m_msaaTex); + rhi::TextureViewDesc tvd{}; tvd.format = m_swapChain->format(); tvd.mipLevelCount = 1; tvd.arrayLayerCount = 1; + m_device->createTextureView(m_msaaTex, tvd, m_msaaView); +} + +draco::Status ResolveTextureSample::onInit() { + using draco::Status, std::span, draco::u8; + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Vertex, u8"VSMain", u8"ResolveVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShader, shaders::ShaderStage::Fragment, u8"PSMain", u8"ResolvePS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // Vertex and index buffers. + rhi::BufferDesc vbd{}; vbd.size = sizeof(kVerts); vbd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; vbd.memory = rhi::MemoryLocation::GpuOnly; vbd.label = u8"ResolveVB"; + if (m_device->createBuffer(vbd, m_vb) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::BufferDesc ibd{}; ibd.size = sizeof(kIdx); ibd.usage = rhi::BufferUsage::Index | rhi::BufferUsage::CopyDst; ibd.memory = rhi::MemoryLocation::GpuOnly; ibd.label = u8"ResolveIB"; + if (m_device->createBuffer(ibd, m_ib) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vb, 0, std::span(reinterpret_cast(kVerts), sizeof(kVerts))); + batch->writeBuffer(m_ib, 0, std::span(reinterpret_cast(kIdx), sizeof(kIdx))); + batch->submit(); m_graphicsQueue->destroyTransferBatch(batch); + + // Pipeline layout (no bind groups). + rhi::PipelineLayoutDesc pld{}; pld.label = u8"ResolvePL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + // MSAA render target. + recreateMsaaTarget(m_width, m_height); + + // Render pipeline with multisample count = 4. + rhi::VertexAttribute attrs[2] = { + { rhi::VertexFormat::Float32x3, 0, 0 }, + { rhi::VertexFormat::Float32x4, 12, 1 }, + }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 28; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); + + rhi::RenderPipelineDesc rpd{}; rpd.layout = m_pl; rpd.label = u8"ResolvePipeline"; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.multisample.count = kSamples; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void ResolveTextureSample::onRender() { + using draco::f32, std::span; + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + // === Step 1: Render star into MSAA texture === + enc->transitionTexture(m_msaaTex, rhi::ResourceState::CopySrc, rhi::ResourceState::RenderTarget); + + rhi::ColorAttachment ca{}; + ca.view = m_msaaView; + ca.resolveTarget = nullptr; // No auto-resolve -- we do it manually. + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.08f, 0.08f, 0.12f, 1.0f); + rhi::RenderPassDesc rpd{}; rpd.colorAttachments.push_back(ca); + auto* rp = enc->beginRenderPass(rpd); + rp->setPipeline(m_pipeline); + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + rp->setVertexBuffer(0, m_vb, 0); + rp->setIndexBuffer(m_ib, rhi::IndexFormat::UInt16, 0); + rp->drawIndexed(30); + rp->end(); + + // === Step 2: Transition for resolve === + // MSAA texture: RenderTarget -> CopySrc + enc->transitionTexture(m_msaaTex, rhi::ResourceState::RenderTarget, rhi::ResourceState::CopySrc); + // Swapchain: Present -> CopyDst + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Present, rhi::ResourceState::CopyDst); + + // === Step 3: Explicit resolve MSAA -> swapchain === + enc->resolveTexture(m_msaaTex, m_swapChain->currentTexture()); + + // === Step 4: Transition swapchain back to Present === + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::CopyDst, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + m_swapChain->present(m_graphicsQueue); m_pool->destroyEncoder(enc); +} + +void ResolveTextureSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_msaaView) m_device->destroyTextureView(m_msaaView); + if (m_msaaTex) m_device->destroyTexture(m_msaaTex); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_ib) m_device->destroyBuffer(m_ib); + if (m_vb) m_device->destroyBuffer(m_vb); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { ResolveTextureSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Sample030_RenderBundles/CMakeLists.txt b/Samples/cpp/RHI/Sample030_RenderBundles/CMakeLists.txt new file mode 100644 index 00000000..ceb06d8f --- /dev/null +++ b/Samples/cpp/RHI/Sample030_RenderBundles/CMakeLists.txt @@ -0,0 +1 @@ +draco_add_rhi_sample(Sample030_RenderBundles) diff --git a/Samples/cpp/RHI/Sample030_RenderBundles/Main.cpp b/Samples/cpp/RHI/Sample030_RenderBundles/Main.cpp new file mode 100644 index 00000000..5c374068 --- /dev/null +++ b/Samples/cpp/RHI/Sample030_RenderBundles/Main.cpp @@ -0,0 +1,174 @@ +#include +/// Sample030 - Render Bundles. Records the triangle's draw commands into a render bundle +/// (a reusable, off-thread-recordable command sequence) and replays it into the frame's render +/// pass via ExecuteBundles. The pass is begun with RenderPassContents::SecondaryCommandBuffers. +/// Exercises the RHI render-bundle path (Vulkan secondary command buffers / DX12 bundles). + +#include +#include +#include +#include + +import core; +import rhi; +import shaders; +import samples.rhi.framework; +import rhi.vk; + +namespace sf = draco::samples::framework; +namespace rhi = draco::rhi; +namespace shaders = draco::shaders; + +class RenderBundlesSample : public sf::SampleApp { +public: + using sf::SampleApp::SampleApp; + std::u8string_view title() const override { return u8"Sample030 - Render Bundles"; } + +protected: + draco::Status onInit() override; + void onRender() override; + void onShutdown() override; + +private: + static constexpr const char8_t kShaderSource[] = u8R"( + struct VSInput { float3 Position : TEXCOORD0; float3 Color : TEXCOORD1; }; + struct PSInput { float4 Position : SV_POSITION; float3 Color : TEXCOORD0; }; + PSInput VSMain(VSInput input) { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + return output; + } + float4 PSMain(PSInput input) : SV_TARGET { return float4(input.Color, 1.0); } + )"; + + static constexpr float kVertexData[] = { + 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, + }; + + shaders::Compiler* m_compiler = nullptr; + rhi::Buffer* m_vertexBuf = nullptr; + rhi::ShaderModule* m_vs = nullptr; + rhi::ShaderModule* m_ps = nullptr; + rhi::BindGroupLayout* m_bgl = nullptr; + rhi::PipelineLayout* m_pl = nullptr; + rhi::RenderPipeline* m_pipeline = nullptr; + rhi::CommandPool* m_pool = nullptr; + rhi::Fence* m_fence = nullptr; + draco::u64 m_fenceVal = 0; +}; + +draco::Status RenderBundlesSample::onInit() { + if (shaders::createCompiler(shaders::CompilerDesc{}, m_compiler) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Vertex, u8"VSMain", u8"BundleVS", m_vs) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (sf::compileToModule(m_compiler, m_device, kShaderSource, shaders::ShaderStage::Fragment, u8"PSMain", u8"BundlePS", m_ps) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::BufferDesc bd{}; bd.size = sizeof(kVertexData); bd.usage = rhi::BufferUsage::Vertex | rhi::BufferUsage::CopyDst; + bd.memory = rhi::MemoryLocation::GpuOnly; bd.label = u8"BundleVB"; + if (m_device->createBuffer(bd, m_vertexBuf) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::TransferBatch* batch = nullptr; + m_graphicsQueue->createTransferBatch(batch); + batch->writeBuffer(m_vertexBuf, 0, std::span(reinterpret_cast(kVertexData), sizeof(kVertexData))); + batch->submit(); + m_graphicsQueue->destroyTransferBatch(batch); + + rhi::BindGroupLayoutDesc bglDesc{}; bglDesc.label = u8"EmptyBGL"; + if (m_device->createBindGroupLayout(bglDesc, m_bgl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + rhi::PipelineLayoutDesc pld{}; + rhi::BindGroupLayout* sets[1] = { m_bgl }; + pld.bindGroupLayouts = std::span(sets, 1); + pld.label = u8"BundlePL"; + if (m_device->createPipelineLayout(pld, m_pl) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + rhi::VertexAttribute attrs[2] = { { rhi::VertexFormat::Float32x3, 0, 0 }, { rhi::VertexFormat::Float32x3, 12, 1 } }; + rhi::VertexBufferLayout vbl{}; vbl.stride = 24; vbl.attributes = std::span(attrs, 2); + rhi::ColorTargetState ct{}; ct.format = m_swapChain->format(); ct.writeMask = rhi::ColorWriteMask::All; + + rhi::RenderPipelineDesc rpd{}; + rpd.layout = m_pl; + rpd.vertex.shader = { m_vs, u8"VSMain", rhi::ShaderStage::Vertex }; + rpd.vertex.buffers = std::span(&vbl, 1); + rpd.fragment = rhi::FragmentState{}; + rpd.fragment->shader = { m_ps, u8"PSMain", rhi::ShaderStage::Fragment }; + rpd.fragment->targets = std::span(&ct, 1); + rpd.primitive.topology = rhi::PrimitiveTopology::TriangleList; + rpd.label = u8"BundlePipeline"; + if (m_device->createRenderPipeline(rpd, m_pipeline) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + + if (m_device->createCommandPool(rhi::QueueType::Graphics, m_pool) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + if (m_device->createFence(0, m_fence) != draco::ErrorCode::Ok) return draco::ErrorCode::Unknown; + return draco::ErrorCode::Ok; +} + +void RenderBundlesSample::onRender() { + if (m_fenceVal > 0) m_fence->wait(m_fenceVal, ~0ull); + if (m_swapChain->acquireNextImage() != draco::ErrorCode::Ok) return; + + m_pool->reset(); + rhi::CommandEncoder* enc = nullptr; + if (m_pool->createEncoder(enc) != draco::ErrorCode::Ok || !enc) return; + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::Undefined, rhi::ResourceState::RenderTarget); + + // Record the triangle's draw into a reusable bundle (could be done off-thread). + rhi::RenderBundleDesc bdesc{}; + bdesc.colorFormats[0] = m_swapChain->format(); + bdesc.colorFormatCount = 1; + bdesc.width = m_width; + bdesc.height = m_height; + bdesc.label = u8"TriangleBundle"; + rhi::RenderBundleEncoder* be = enc->createRenderBundleEncoder(bdesc); + rhi::RenderBundle* bundle = nullptr; + if (be) { + be->setPipeline(m_pipeline); + be->setVertexBuffer(0, m_vertexBuf, 0); + be->draw(3); + bundle = be->finish(); + } + + rhi::ColorAttachment ca{}; + ca.view = m_swapChain->currentTextureView(); + ca.loadOp = rhi::LoadOp::Clear; ca.storeOp = rhi::StoreOp::Store; + ca.clearValue = rhi::ClearColor(0.1f, 0.1f, 0.15f, 1.0f); + + rhi::RenderPassDesc rpd{}; + rpd.colorAttachments.push_back(ca); + rpd.contents = rhi::RenderPassContents::SecondaryCommandBuffers; // pass body is supplied by bundles + + auto* rp = enc->beginRenderPass(rpd); + // Viewport/scissor must be set on the parent pass - DX12 bundles inherit these. + rp->setViewport(0, 0, static_cast(m_width), static_cast(m_height), 0, 1); + rp->setScissor(0, 0, m_width, m_height); + if (bundle) { + rhi::RenderBundle* bundles[1] = { bundle }; + rp->executeBundles(std::span(bundles, 1)); + } + rp->end(); + + enc->transitionTexture(m_swapChain->currentTexture(), rhi::ResourceState::RenderTarget, rhi::ResourceState::Present); + + rhi::CommandBuffer* cb = enc->finish(); + m_fenceVal++; + rhi::CommandBuffer* cbs[1] = { cb }; + m_graphicsQueue->submit(std::span(cbs, 1), m_fence, m_fenceVal); + + m_swapChain->present(m_graphicsQueue); + m_pool->destroyEncoder(enc); +} + +void RenderBundlesSample::onShutdown() { + if (m_fence) m_device->destroyFence(m_fence); + if (m_pool) m_device->destroyCommandPool(m_pool); + if (m_pipeline) m_device->destroyRenderPipeline(m_pipeline); + if (m_pl) m_device->destroyPipelineLayout(m_pl); + if (m_bgl) m_device->destroyBindGroupLayout(m_bgl); + if (m_ps) m_device->destroyShaderModule(m_ps); + if (m_vs) m_device->destroyShaderModule(m_vs); + if (m_vertexBuf) m_device->destroyBuffer(m_vertexBuf); + if (m_compiler) { m_compiler->destroy(); delete m_compiler; } +} + +int main(int argc, char** argv) { RenderBundlesSample app; return app.run(argc, argv); } diff --git a/Samples/cpp/RHI/Smoketest/CMakeLists.txt b/Samples/cpp/RHI/Smoketest/CMakeLists.txt new file mode 100644 index 00000000..f353c61f --- /dev/null +++ b/Samples/cpp/RHI/Smoketest/CMakeLists.txt @@ -0,0 +1,2 @@ +draco_add_rhi_sample(Smoketest) +target_link_libraries(Smoketest PRIVATE Rendering_RHI_Null) diff --git a/Samples/cpp/RHI/Smoketest/Main.cpp b/Samples/cpp/RHI/Smoketest/Main.cpp new file mode 100644 index 00000000..bdbb8c9e --- /dev/null +++ b/Samples/cpp/RHI/Smoketest/Main.cpp @@ -0,0 +1,434 @@ +#include +// RHI Smoketest - low-level API tour exercising the VK backend directly. +// No framework dependency; useful for debugging the RHI itself. + +#include +#include +#include +#include +#include +#include + +import core; +import rhi; +import rhi.vk; +import rhi.null; +import rhi.validation; +import shell; +import shell.desktop; +#ifdef DRACONIC_HAS_SHADERS +import shaders; +#endif +#ifdef DRACONIC_HAS_DX12 +import rhi.dx12; +#endif + +static const char* adapterTypeStr(draco::rhi::AdapterType t) { + using draco::rhi::AdapterType; + switch (t) { + case AdapterType::DiscreteGpu: return "DiscreteGpu"; + case AdapterType::IntegratedGpu: return "IntegratedGpu"; + case AdapterType::Cpu: return "Cpu"; + default: return "Unknown"; + } +} + +int main(int /*argc*/, char** /*argv*/) { + using namespace draco; + using namespace draco::rhi; + namespace vk = draco::rhi::vk; + namespace shell = draco::shell; + + // ---- Shell: window via the desktop (SDL3) shell ---- + shell::WindowSettings ws{}; + ws.title = u8"Draconic Smoketest"; + ws.width = 1280; + ws.height = 720; + std::unique_ptr plat{ shell::createShell(ws), shell::destroyShell }; + if (!plat || plat->mainWindow() == nullptr) { + std::fprintf(stderr, "shell/window init failed\n"); + return 1; + } + shell::IWindow* window = plat->mainWindow(); + + const shell::NativeWindow nw = window->native(); + void* native = nw.window; + void* display = nw.display; + if (!native) { + std::fprintf(stderr, "no native handle on shell window\n"); + return 1; + } + + // ---- VK backend (wrapped in validation layer) ---- + vk::VkBackendDesc vkDesc{ .enableValidation = true }; + Backend* rawBackend = nullptr; + if (vk::createBackend(vkDesc, rawBackend) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createBackend failed\n"); + return 1; + } + Backend* backend = validation::createValidatedBackend(rawBackend); + + auto adapters = backend->enumerateAdapters(); + if (adapters.size() == 0) { backend->destroy(); return 1; } + + // Adapters are enumerated best-GPU-first (see Backend::enumerateAdapters). + Adapter* chosen = adapters[0]; + auto adapterInfo = chosen->info(); + const std::u8string adapterName = std::u8string(adapterInfo.name); + std::printf("adapter: %s (%s)\n", reinterpret_cast(adapterName.c_str()), adapterTypeStr(adapterInfo.type)); + + DeviceDesc dd{}; + dd.graphicsQueueCount = 1; + dd.computeQueueCount = 1; + dd.transferQueueCount = 1; + dd.requiredFeatures.meshShaders = adapterInfo.supportedFeatures.meshShaders; + Device* device = nullptr; + if (chosen->createDevice(dd, device) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createDevice failed\n"); + backend->destroy(); return 1; + } + + // ---- Surface + swap chain ---- + Surface* surface = nullptr; + if (backend->createSurface(native, display, surface) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createSurface failed\n"); + device->destroy(); return 1; + } + std::printf("surface created\n"); + + SwapChainDesc sd{}; + sd.width = 1280; sd.height = 720; + sd.format = TextureFormat::BGRA8UnormSrgb; + sd.presentMode = PresentMode::Fifo; + sd.bufferCount = 2; + sd.label = u8"main"; + SwapChain* swap = nullptr; + if (device->createSwapChain(surface, sd, swap) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createSwapChain failed\n"); + device->destroySurface(surface); + device->destroy(); return 1; + } + std::printf("swap chain: %ux%u, bufferCount=%u\n", swap->width(), swap->height(), swap->bufferCount()); + + // ---- Buffer / Sampler / ShaderModule ---- + BufferDesc ubDesc{}; + ubDesc.size = 1024; + ubDesc.usage = BufferUsage::Uniform | BufferUsage::CopyDst; + ubDesc.memory = MemoryLocation::CpuToGpu; + ubDesc.label = u8"smoketest_uniform"; + Buffer* ub = nullptr; + if (device->createBuffer(ubDesc, ub) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createBuffer failed\n"); + } else { + void* mapped = ub->map(); + std::printf("uniform buffer: size=%llu mapped=%p\n", + static_cast(ub->getSize()), mapped); + if (mapped) std::memset(mapped, 0xAB, 16); + ub->unmap(); + } + + SamplerDesc sampDesc{}; + sampDesc.maxAnisotropy = 16; + sampDesc.label = u8"smoketest_sampler"; + Sampler* samp = nullptr; + if (device->createSampler(sampDesc, samp) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createSampler failed\n"); + } else { + std::printf("sampler created (aniso=%u)\n", samp->desc.maxAnisotropy); + } + + // Minimal SPIR-V noop fragment shader. + static const u32 kSpvNoop[] = { + 0x07230203u, 0x00010000u, 0x00080001u, 0x00000005u, 0x00000000u, + 0x00020011u, 0x00000001u, + 0x0003000Eu, 0x00000000u, 0x00000001u, + 0x0005000Fu, 0x00000004u, 0x00000001u, 0x6E69616Du, 0x00000000u, + 0x00030010u, 0x00000001u, 0x00000007u, + 0x00020013u, 0x00000002u, + 0x00030021u, 0x00000003u, 0x00000002u, + 0x00050036u, 0x00000002u, 0x00000001u, 0x00000000u, 0x00000003u, + 0x000200F8u, 0x00000004u, + 0x000100FDu, 0x00010038u, + }; + ShaderModuleDesc shDesc{}; + shDesc.code = std::span(reinterpret_cast(kSpvNoop), sizeof(kSpvNoop)); + shDesc.label = u8"smoketest_noop_fs"; + ShaderModule* sh = nullptr; + if (device->createShaderModule(shDesc, sh) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createShaderModule failed\n"); + } else { + std::printf("shader module created (%zu bytes)\n", shDesc.code.size()); + device->destroyShaderModule(sh); + } + + // ---- BindGroupLayout / PipelineLayout / PipelineCache ---- + BindGroupLayoutEntry layoutEntries[2] = { + BindGroupLayoutEntry::uniformBuffer(0, ShaderStage::Vertex), + BindGroupLayoutEntry::sampledTexture(1, ShaderStage::Fragment), + }; + BindGroupLayoutDesc bglDesc{}; + bglDesc.entries = std::span(layoutEntries, 2); + bglDesc.label = u8"smoketest_bgl"; + BindGroupLayout* bgl = nullptr; + if (device->createBindGroupLayout(bglDesc, bgl) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createBindGroupLayout failed\n"); + } else { + std::printf("bind group layout: %zu entries\n", bgl->entries().size()); + } + + PipelineLayoutDesc plDesc{}; + BindGroupLayout* plSets[1] = { bgl }; + plDesc.bindGroupLayouts = std::span(plSets, 1); + plDesc.label = u8"smoketest_pl"; + PipelineLayout* pl = nullptr; + if (device->createPipelineLayout(plDesc, pl) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createPipelineLayout failed\n"); + } else { + std::printf("pipeline layout created\n"); + } + + PipelineCacheDesc pcDesc{}; + pcDesc.label = u8"smoketest_pc"; + PipelineCache* pc = nullptr; + if (device->createPipelineCache(pcDesc, pc) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createPipelineCache failed\n"); + } else { + std::printf("pipeline cache created (size=%u)\n", pc->getDataSize()); + } + + device->destroyPipelineCache(pc); + device->destroyPipelineLayout(pl); + device->destroyBindGroupLayout(bgl); + device->destroySampler(samp); + device->destroyBuffer(ub); + + // ---- Command pool + fence ---- + CommandPool* pool = nullptr; + if (device->createCommandPool(QueueType::Graphics, pool) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createCommandPool failed\n"); + } else { + std::printf("command pool created\n"); + } + + Fence* fence = nullptr; + if (device->createFence(0, fence) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createFence failed\n"); + } else { + std::printf("fence created (initial=%llu)\n", + static_cast(fence->completedValue())); + } + + QuerySetDesc qsDesc{}; + qsDesc.type = QueryType::Timestamp; + qsDesc.count = 16; + qsDesc.label = u8"smoketest_qs"; + QuerySet* qs = nullptr; + if (device->createQuerySet(qsDesc, qs) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "createQuerySet failed\n"); + } else { + std::printf("query set created (type=%u count=%u)\n", + static_cast(qs->type), qs->count); + } + + // ---- Show window + acquire/present 3 frames ---- + + Queue* gfx = device->getQueue(QueueType::Graphics); + u64 fenceValue = 0; + for (int frame = 0; frame < 3; ++frame) { + if (swap->acquireNextImage() != draco::ErrorCode::Ok) { + std::fprintf(stderr, "acquireNextImage failed on frame %d\n", frame); + break; + } + + CommandEncoder* enc = nullptr; + if (pool && pool->createEncoder(enc) == draco::ErrorCode::Ok && enc) { + enc->transitionTexture(swap->currentTexture(), ResourceState::Undefined, ResourceState::Present); + + CommandBuffer* cb = enc->finish(); + CommandBuffer* cbs[1] = { cb }; + fenceValue++; + gfx->submit(std::span(cbs, 1), fence, fenceValue); + pool->destroyEncoder(enc); + } + + swap->present(gfx); + if (fence) fence->wait(fenceValue); + if (pool) pool->reset(); + std::printf("frame %d acquired image_index=%u fence=%llu\n", + frame, swap->currentImageIndex(), + static_cast(fenceValue)); + } + + // ---- Extension probes ---- + if (device->features.meshShaders) { + std::printf("mesh shaders: supported\n"); + } else { + std::printf("mesh shaders: not available\n"); + } + if (device->features.rayTracing) { + std::printf("ray tracing: supported (handle_size=%u)\n", device->shaderGroupHandleSize); + } else { + std::printf("ray tracing: not available\n"); + } + + // ---- Cleanup ---- + // ---- DXC shader compilation test ---- +#ifdef DRACONIC_HAS_SHADERS + { + namespace shaders = draco::shaders; + shaders::Compiler* shaderc = nullptr; + if (shaders::createCompiler(shaders::CompilerDesc{}, shaderc) != draco::ErrorCode::Ok) { + std::fprintf(stderr, "shaders: createCompiler failed\n"); + } else { + static const char kHlsl[] = + "float4 main(float2 uv : TEXCOORD0) : SV_Target {\n" + " return float4(uv, 0.0, 1.0);\n" + "}\n"; + shaders::CompileOptions opts{}; + opts.shaderModel = u8"6_0"; + opts.optimizationLevel = 3; + shaders::CompileResult cr{}; + draco::Status r = shaderc->compile( + reinterpret_cast(kHlsl), sizeof(kHlsl) - 1, + shaders::ShaderStage::Fragment, u8"main", shaders::ShaderTarget::SPIRV, opts, cr); + if (r == draco::ErrorCode::Ok) { + u32 magic = cr.bytecodeSize >= 4 ? *reinterpret_cast(cr.bytecode) : 0u; + std::printf("HLSL->SPIR-V: %zu bytes, magic=0x%08x %s\n", + cr.bytecodeSize, magic, magic == 0x07230203u ? "(SPIR-V OK)" : "(unexpected)"); + } else { + std::fprintf(stderr, "shaders: compile failed: %s\n", cr.messages ? cr.messages : "(no messages)"); + } + shaderc->freeResult(cr); + shaderc->destroy(); + delete shaderc; + } + } +#endif + + if (qs) device->destroyQuerySet(qs); + if (fence) device->destroyFence(fence); + if (pool) device->destroyCommandPool(pool); + + device->waitIdle(); + device->destroySwapChain(swap); + device->destroySurface(surface); + device->destroy(); + backend->destroy(); + + + + // ---- Null backend exercise ---- + std::printf("\n=== Null Backend ===\n"); + { + namespace null = draco::rhi::null; + Backend* nullBackend = nullptr; + null::createNullBackend(nullBackend); + + auto nullAdapters = nullBackend->enumerateAdapters(); + std::printf("null adapters: %zu\n", nullAdapters.size()); + + Device* nullDevice = nullptr; + nullAdapters[0]->createDevice(DeviceDesc{}, nullDevice); + + Surface* nullSurface = nullptr; + nullBackend->createSurface(nullptr, nullSurface); + + SwapChainDesc nullSd{}; nullSd.width = 800; nullSd.height = 600; nullSd.bufferCount = 2; + SwapChain* nullSwap = nullptr; + nullDevice->createSwapChain(nullSurface, nullSd, nullSwap); + std::printf("null swap chain: %ux%u\n", nullSwap->width(), nullSwap->height()); + + Buffer* nullBuf = nullptr; + BufferDesc nbd{}; nbd.size = 256; nbd.usage = BufferUsage::Uniform; nbd.memory = MemoryLocation::CpuToGpu; + nullDevice->createBuffer(nbd, nullBuf); + void* mapped = nullBuf->map(); + std::printf("null buffer mapped: %s\n", mapped ? "yes" : "no"); + nullBuf->unmap(); + + CommandPool* nullPool = nullptr; + nullDevice->createCommandPool(QueueType::Graphics, nullPool); + CommandEncoder* nullEnc = nullptr; + nullPool->createEncoder(nullEnc); + nullSwap->acquireNextImage(); + nullEnc->transitionTexture(nullSwap->currentTexture(), ResourceState::Undefined, ResourceState::Present); + CommandBuffer* nullCb = nullEnc->finish(); + Fence* nullFence = nullptr; + nullDevice->createFence(0, nullFence); + CommandBuffer* nullCbs[1] = { nullCb }; + nullDevice->getQueue(QueueType::Graphics)->submit(std::span(nullCbs, 1), nullFence, 1); + nullFence->wait(1, ~0ull); + nullSwap->present(nullDevice->getQueue(QueueType::Graphics)); + std::printf("null frame completed\n"); + + nullPool->destroyEncoder(nullEnc); + nullDevice->destroyFence(nullFence); + nullDevice->destroyCommandPool(nullPool); + nullDevice->destroyBuffer(nullBuf); + nullDevice->destroySwapChain(nullSwap); + nullDevice->destroySurface(nullSurface); + nullDevice->destroy(); + nullBackend->destroy(); + std::printf("null backend: OK\n"); + } + + // ===== DX12 backend (Windows only) ===== +#ifdef DRACONIC_HAS_DX12 + { + namespace dx12 = draco::rhi::dx12; + std::printf("\n=== DX12 Backend ===\n"); + + Backend* dx12Backend = nullptr; + dx12::DxBackendDesc dx12Desc{}; + dx12Desc.enableValidation = true; + if (dx12::createDxBackend(dx12Desc, dx12Backend) != ErrorCode::Ok) { + std::printf("DX12 backend: FAILED to create\n"); + } else { + auto dx12Adapters = dx12Backend->enumerateAdapters(); + std::printf("DX12 adapters: %zu\n", dx12Adapters.size()); + for (usize i = 0; i < dx12Adapters.size(); ++i) { + AdapterInfo ai = dx12Adapters[i]->info(); + const std::u8string name8 = std::u8string(ai.name); + std::printf(" [%zu] %s (%s)\n", i, + reinterpret_cast(name8.c_str()), + adapterTypeStr(ai.type)); + } + + if (dx12Adapters.size() > 0) { + Device* dx12Device = nullptr; + DeviceDesc dx12dd{}; dx12dd.graphicsQueueCount = 1; + if (dx12Adapters[0]->createDevice(dx12dd, dx12Device) == ErrorCode::Ok) { + std::printf("DX12 device created (type=%d)\n", static_cast(dx12Device->type)); + + // Create and destroy a buffer. + Buffer* dx12Buf = nullptr; + BufferDesc bd{}; bd.size = 256; bd.usage = BufferUsage::Uniform; + bd.memory = MemoryLocation::CpuToGpu; + dx12Device->createBuffer(bd, dx12Buf); + if (dx12Buf) { + void* mapped = dx12Buf->map(); + std::printf("DX12 buffer mapped: %s\n", mapped ? "OK" : "FAIL"); + if (mapped) dx12Buf->unmap(); + dx12Device->destroyBuffer(dx12Buf); + } + + // Create and destroy a fence. + Fence* dx12Fence = nullptr; + dx12Device->createFence(0, dx12Fence); + if (dx12Fence) { + std::printf("DX12 fence completed value: %llu\n", + static_cast(dx12Fence->completedValue())); + dx12Device->destroyFence(dx12Fence); + } + + dx12Device->destroy(); + } + } + + dx12Backend->destroy(); + std::printf("DX12 backend: OK\n"); + } + } +#endif + + return 0; +} diff --git a/Samples/cpp/Render/CMakeLists.txt b/Samples/cpp/Render/CMakeLists.txt index 5b4652c0..64b8beab 100644 --- a/Samples/cpp/Render/CMakeLists.txt +++ b/Samples/cpp/Render/CMakeLists.txt @@ -1,22 +1,14 @@ # -# Rendering Sample +# Render Sample - shell-only placeholder (no renderer yet) # add_executable(RenderSample RenderSample.cpp) -# Everything the sample needs comes through Runtime (which links the single Shell -# library, whose SDL3 dependency propagates for the createShell() backend). -target_link_libraries(RenderSample - PRIVATE - Runtime - bgfx - bx - bimg -) +# The single Shell library owns windowing + input; its SDL3 dependency is PRIVATE, so +# force-load SDL below to satisfy the backend at final link. +target_link_libraries(RenderSample PRIVATE Shell) -# SDL is pulled in (statically) through the Shell library; force-load the whole archive -# at the executable so SDL's constructor-registered video/input drivers are not stripped -# by the linker. This stays on the final binary rather than on Shell, so it does not also -# force a full SDL copy into the intermediate Runtime shared library. +# SDL is statically linked via Shell; force-load the whole archive so its +# constructor-registered video/input drivers are not stripped by the linker. if(UNIX AND NOT APPLE) target_link_libraries(RenderSample PRIVATE Threads::Threads dl) target_link_options(RenderSample PRIVATE -Wl,--whole-archive $ -Wl,--no-whole-archive) @@ -25,19 +17,3 @@ elseif(APPLE) elseif(MSVC) target_link_options(RenderSample PRIVATE /WHOLEARCHIVE:$) endif() - -compile_shaders(RenderSample) - -set(TEST_IMAGE - "${CMAKE_SOURCE_DIR}/Docs/assets/draconic_logo_no_text.png" -) - -add_custom_command( - TARGET RenderSample POST_BUILD - - COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${TEST_IMAGE} - $/test.png - - COMMENT "Copying test texture..." -) diff --git a/Samples/cpp/Render/RenderSample.cpp b/Samples/cpp/Render/RenderSample.cpp index 73431fa1..8596bff5 100644 --- a/Samples/cpp/Render/RenderSample.cpp +++ b/Samples/cpp/Render/RenderSample.cpp @@ -1,227 +1,32 @@ #include -#include #include +#include -#include - -import draconic; +import shell; +import shell.desktop; using namespace draco::shell; -namespace -{ - // Map the shell's window system onto the RHI's native window type so bgfx - // interprets the handles correctly (only Wayland needs explicit flagging). - draco::rendering::rhi::NativeWindowType toRhiWindowType(WindowSystem sys) - { - using RhiType = draco::rendering::rhi::NativeWindowType; - switch (sys) - { - case WindowSystem::Win32: return RhiType::Win32; - case WindowSystem::X11: return RhiType::X11; - case WindowSystem::Wayland: return RhiType::Wayland; - case WindowSystem::Cocoa: return RhiType::Cocoa; - default: return RhiType::Default; - } - } -} - int main(int, char*[]) { auto shell = createShell(WindowSettings{ - .title = u8"Draconic Engine Rendering Sample", + .title = u8"Draconic Engine Sample", .width = 1280, .height = 720, }); - IWindow* mainWindow = shell->mainWindow(); - if (mainWindow == nullptr) + if (shell->mainWindow() == nullptr) { std::println("Failed to create shell window"); - destroyShell(shell); - return -1; - } - - // Capture the mouse for camera control. - shell->input()->mouse()->setRelativeMode(true); - - const NativeWindow native = mainWindow->native(); - const draco::u32 startW = mainWindow->width(); - const draco::u32 startH = mainWindow->height(); - - if (!draco::rendering::rhi::init(native.display, native.window, toRhiWindowType(native.system), - static_cast(startW), static_cast(startH))) - { - std::println("RHI init failed"); - destroyShell(shell); return -1; } - draco::rendering::renderer::init(startW, startH); - - auto cube_mesh = draco::rendering::mesh::createCube(); - auto plane_mesh = draco::rendering::mesh::createPlane(5.0f); - auto sphere_mesh = draco::rendering::mesh::createSphere(24, 16); - auto cylinder_mesh = draco::rendering::mesh::createCylinder(24, 2.0f); - auto capsule_mesh = draco::rendering::mesh::createCapsule(24, 12, 2.0f); - - auto img = draco::core::io::loader::image::loadImage("test.png"); - - draco::rendering::rhi::TextureHandle tex = draco::rendering::rhi::InvalidTexture; - - if (img.isValid) { - tex = draco::rendering::rhi::createTexture(img.pixels.data(), img.width, img.height); - } - - auto s_texColor = draco::rendering::rhi::createUniform("s_texColor", draco::rendering::rhi::UniformType::Sampler); - - auto vs = draco::core::io::filesystem::loadBinary("vs.bin"); - auto fs = draco::core::io::filesystem::loadBinary("fs.bin"); - - auto vs_quad = draco::core::io::filesystem::loadBinary("vs_quad.bin"); - auto fs_quad = draco::core::io::filesystem::loadBinary("fs_quad.bin"); - - if (vs.empty() || fs.empty() || vs_quad.empty() || fs_quad.empty()) { - std::println("Shader load failed"); - draco::rendering::rhi::shutdown(); - destroyShell(shell); - return -1; - } - - auto vsh = draco::rendering::rhi::createShader(vs.data(), (draco::u32)vs.size()); - auto fsh = draco::rendering::rhi::createShader(fs.data(), (draco::u32)fs.size()); - - auto vsh_quad = draco::rendering::rhi::createShader(vs_quad.data(), (draco::u32)vs_quad.size()); - auto fsh_quad = draco::rendering::rhi::createShader(fs_quad.data(), (draco::u32)fs_quad.size()); - - auto pipeline = draco::rendering::rhi::createPipeline({vsh, fsh, draco::rendering::rhi::PipelineState::WriteRGB | draco::rendering::rhi::PipelineState::WriteAlpha | draco::rendering::rhi::PipelineState::MSAA, draco::rendering::rhi::BlendMode::None, draco::rendering::rhi::DepthTest::Less, draco::rendering::rhi::CullMode::CCW, true}); - - auto pipeline_quad = draco::rendering::rhi::createPipeline({vsh_quad, fsh_quad, draco::rendering::rhi::PipelineState::WriteRGB | draco::rendering::rhi::PipelineState::WriteAlpha | draco::rendering::rhi::PipelineState::MSAA, draco::rendering::rhi::BlendMode::Alpha, draco::rendering::rhi::DepthTest::None, draco::rendering::rhi::CullMode::None, true}); - - draco::rendering::quad::QuadRenderer quad_renderer; - quad_renderer.init(pipeline_quad); - - draco::scene::CameraController camera; - camera.init(); - - auto u_tint = draco::rendering::rhi::createUniform("u_tint", draco::rendering::rhi::UniformType::Vec4); - auto u_offset = draco::rendering::rhi::createUniform("u_offset", draco::rendering::rhi::UniformType::Vec4); - - draco::rendering::rhi::registerUniform(draco::rendering::rhi::hashUniform("u_tint"), u_tint); - - draco::rendering::rhi::registerUniform(draco::rendering::rhi::hashUniform("u_offset"), u_offset); - - draco::f32 tint[4] = {1,1,1,1}; - draco::f32 offset[4] = {0,0,0,0}; - - bool mouse_captured = true; - - draco::rendering::material::Material mat{}; - mat.pipeline = pipeline; - mat.texture = tex; - mat.sampler = s_texColor; - - mat.uniforms.push_back({.nameHash = draco::rendering::rhi::hashUniform("u_tint"), .data = tint, .count = 1}); - mat.uniforms.push_back({.nameHash = draco::rendering::rhi::hashUniform("u_offset"), .data = offset, .count = 1}); - - draco::scene::Scene scene; - - static constexpr draco::math::Transform tr; - - scene.renderables.push_back({cube_mesh, tr, mat}); - scene.renderables.push_back({plane_mesh, tr, mat}); - scene.renderables.push_back({sphere_mesh, tr, mat}); - scene.renderables.push_back({cylinder_mesh, tr, mat}); - scene.renderables.push_back({capsule_mesh, tr, mat}); - - scene.renderables[0].transform.setPosition(-12.0f, 0.0f, 0.0f); - scene.renderables[1].transform.setPosition(-6.0f, 0.0f, 0.0f); - scene.renderables[2].transform.setPosition(0.0f, 0.0f, 0.0f); - scene.renderables[3].transform.setPosition(6.0f, 0.0f, 0.0f); - scene.renderables[4].transform.setPosition(12.0f, 0.0f, 0.0f); - - scene.renderables[1].transform.setRotation(-bx::kPiHalf, 0.0f, 0.0f); - - using clock = std::chrono::steady_clock; - const auto startTime = clock::now(); - auto lastTime = startTime; - + // No renderer yet: pump the shell each frame until the window is closed. while (shell->isRunning()) { shell->processEvents(); - - const auto nowTime = clock::now(); - const draco::f32 dt = std::chrono::duration(nowTime - lastTime).count(); - lastTime = nowTime; - const draco::f32 elapsed = std::chrono::duration(nowTime - startTime).count(); - - IInputManager* input = shell->input(); - IKeyboard* kb = input->keyboard(); - IMouse* ms = input->mouse(); - - // Toggle mouse capture on Escape (edge-triggered). - if (kb->isKeyPressed(KeyCode::Escape)) - { - mouse_captured = !mouse_captured; - ms->setRelativeMode(mouse_captured); - } - - const draco::u32 w = mainWindow->width(); - const draco::u32 h = mainWindow->height(); - - if (w == 0 || h == 0 || mainWindow->isMinimized()) - { - continue; - } - - draco::rendering::rhi::resize((draco::u16)w, (draco::u16)h); - draco::rendering::renderer::resize((draco::u16)w, (draco::u16)h); - - draco::scene::CameraInput camInput{}; - camInput.mouseDx = ms->deltaX(); - camInput.mouseDy = ms->deltaY(); - camInput.moveForward = kb->isKeyDown(KeyCode::W); - camInput.moveBackward = kb->isKeyDown(KeyCode::S); - camInput.moveLeft = kb->isKeyDown(KeyCode::A); - camInput.moveRight = kb->isKeyDown(KeyCode::D); - camera.update(dt, camInput); - - auto cam = camera.getCamera(); - - draco::rendering::renderer::beginFrame(cam); - - for (const auto& renderable : scene.renderables) - { - draco::rendering::renderer::submitRenderable(renderable.transform, renderable.material, renderable.mesh); - } - - quad_renderer.begin(); - - static draco::f32 quad_base_x = 400.0f; - static draco::f32 quad_base_y = 300.0f; - - for (int i = 0; i < 50; i++) - { - draco::rendering::quad::QuadCommand q{}; - - q.texture = tex; - q.color = 0xffffffff; - q.x = quad_base_x + std::sin(elapsed + i) * 50.0f; - q.y = quad_base_y + i * 6.0f; - q.width = 50.0f; - q.height = 50.0f; - q.rotation = elapsed; - - quad_renderer.submit(q); - } - - draco::rendering::renderer::submitUI(quad_renderer); - - draco::rendering::renderer::endFrame(); + std::this_thread::sleep_for(std::chrono::milliseconds(16)); } - draco::rendering::rhi::shutdown(); - destroyShell(shell); // destroys the window and shuts SDL down - return 0; } diff --git a/cmake/Shaders.cmake b/cmake/Shaders.cmake deleted file mode 100644 index 1d638ed8..00000000 --- a/cmake/Shaders.cmake +++ /dev/null @@ -1,84 +0,0 @@ -include_guard(GLOBAL) - -set(SHADER_SRC_DIR "${CMAKE_SOURCE_DIR}/Engine/cpp/Runtime/Rendering/Shaders") -set(SHADER_BIN_DIR "${CMAKE_BINARY_DIR}") -set(BGFX_INCLUDE "${CMAKE_SOURCE_DIR}/Engine/cpp/ThirdParty/bgfx/src") - -function(compile_shaders TARGET_NAME) - file(MAKE_DIRECTORY ${SHADER_BIN_DIR}) - - if(APPLE) - set(VERTEX_PROFILE "metal") - set(FRAGMENT_PROFILE "metal") - set(SHADER_PLATFORM "osx") - elseif(WIN32) - set(VERTEX_PROFILE "s_5_0") - set(FRAGMENT_PROFILE "s_5_0") - set(SHADER_PLATFORM "windows") - else() - set(VERTEX_PROFILE "spirv") - set(FRAGMENT_PROFILE "spirv") - set(SHADER_PLATFORM "linux") - endif() - - set(VERTEX_INPUT "${SHADER_SRC_DIR}/vs.sc") - set(VERTEX_OUTPUT "${SHADER_BIN_DIR}/vs.bin") - - set(FRAGMENT_INPUT "${SHADER_SRC_DIR}/fs.sc") - set(FRAGMENT_OUTPUT "${SHADER_BIN_DIR}/fs.bin") - - set(QUAD_VERTEX_INPUT "${SHADER_SRC_DIR}/vs_quad.sc") - set(QUAD_VERTEX_OUTPUT "${SHADER_BIN_DIR}/vs_quad.bin") - - set(QUAD_FRAGMENT_INPUT "${SHADER_SRC_DIR}/fs_quad.sc") - set(QUAD_FRAGMENT_OUTPUT "${SHADER_BIN_DIR}/fs_quad.bin") - - set(VARYING_DEF "${SHADER_SRC_DIR}/varying.def.sc") - set(QUAD_VARYING_DEF "${SHADER_SRC_DIR}/varying_quad.def.sc") - - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMENT "Compiling asset pipelines and core engine shaders via native tools..." - - COMMAND $ - -f ${VERTEX_INPUT} - -o ${VERTEX_OUTPUT} - --type vertex - --platform ${SHADER_PLATFORM} - -p ${VERTEX_PROFILE} - --varyingdef ${VARYING_DEF} - -i ${BGFX_INCLUDE} - - COMMAND $ - -f ${FRAGMENT_INPUT} - -o ${FRAGMENT_OUTPUT} - --type fragment - --platform ${SHADER_PLATFORM} - -p ${FRAGMENT_PROFILE} - --varyingdef ${VARYING_DEF} - -i ${BGFX_INCLUDE} - - COMMAND $ - -f ${QUAD_VERTEX_INPUT} - -o ${QUAD_VERTEX_OUTPUT} - --type vertex - --platform ${SHADER_PLATFORM} - -p ${VERTEX_PROFILE} - --varyingdef ${QUAD_VARYING_DEF} - -i ${BGFX_INCLUDE} - - COMMAND $ - -f ${QUAD_FRAGMENT_INPUT} - -o ${QUAD_FRAGMENT_OUTPUT} - --type fragment - --platform ${SHADER_PLATFORM} - -p ${FRAGMENT_PROFILE} - --varyingdef ${QUAD_VARYING_DEF} - -i ${BGFX_INCLUDE} - - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${VERTEX_OUTPUT} $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FRAGMENT_OUTPUT} $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${QUAD_VERTEX_OUTPUT} $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${QUAD_FRAGMENT_OUTPUT} $ - ) -endfunction()