diff --git a/.gitignore b/.gitignore index 9309668d..a38f0748 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /active_project /test/assets/assets.db /test/products/ +_codeql_detected_source_root diff --git a/runtime/core/include/core/shapes/Shapes.h b/runtime/core/include/core/shapes/Shapes.h index dfc451c9..94bf48bc 100644 --- a/runtime/core/include/core/shapes/Shapes.h +++ b/runtime/core/include/core/shapes/Shapes.h @@ -14,6 +14,22 @@ namespace sky { bool Intersection(const AABB &aabb, const Frustum &frustum); std::pair Intersection(const AABB &aabb, const Plane &plane); + /** + * @brief Ray-AABB intersection test + * @param ray Ray to test + * @param aabb Axis-aligned bounding box + * @return (hit, tMin, tMax) - whether hit occurred, and entry/exit distances + */ + std::tuple Intersection(const Ray &ray, const AABB &aabb); + + /** + * @brief Ray-AABB intersection test (simple version) + * @param ray Ray to test + * @param aabb Axis-aligned bounding box + * @return true if ray intersects AABB + */ + bool IntersectionTest(const Ray &ray, const AABB &aabb); + // utils Plane CreatePlaneByVertices(const Vector3 &v1, const Vector3 &v2, const Vector3 &v3); Plane CreatePlaneByNormalAndVertex(const Vector3 &normal, const Vector3 &pt); diff --git a/runtime/core/include/core/tree/BVH.h b/runtime/core/include/core/tree/BVH.h new file mode 100644 index 00000000..9d52d57b --- /dev/null +++ b/runtime/core/include/core/tree/BVH.h @@ -0,0 +1,693 @@ +// +// Created by SkyEngine on 2024/02/24. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Traits for BVH element types + * + * Specialize this template to define how to get bounds from your element type. + * + * Example: + * @code + * template <> + * struct BVHTraits { + * static AABB GetBounds(MyObject* obj) { + * return obj->GetBounds(); + * } + * }; + * @endcode + */ + template + struct BVHTraits { + // Default implementation assumes T has a 'bounds' member + static AABB GetBounds(const T &element) + { + return element.bounds; + } + }; + + /** + * @brief BVH build strategy + */ + enum class BVHBuildStrategy { + MEDIAN, // Split at spatial median (fast build) + SAH, // Surface Area Heuristic (optimal traversal) + OBJECT_MEDIAN // Split at object median (balanced tree) + }; + + /** + * @brief BVH configuration + */ + struct BVHConfig { + BVHBuildStrategy strategy = BVHBuildStrategy::OBJECT_MEDIAN; + uint32_t maxElementsPerLeaf = 4; + uint32_t maxDepth = 32; + float sahTraversalCost = 1.0f; + float sahIntersectionCost = 1.0f; + }; + + /** + * @brief Bounding Volume Hierarchy for efficient spatial queries + * + * A binary tree structure where each node contains an AABB that bounds + * all elements in its subtree. Supports efficient ray casting, AABB + * queries, and point queries. + * + * @tparam T Element type stored in the BVH + */ + template + class BVH { + public: + using NodeIndex = uint32_t; + static constexpr NodeIndex INVALID_INDEX = ~(0U); + + /** + * @brief BVH node structure + */ + struct Node { + AABB bounds; + NodeIndex leftChild = INVALID_INDEX; // Left child (if internal) or first element (if leaf) + NodeIndex rightChild = INVALID_INDEX; // Right child (if internal) or INVALID (if leaf) + uint32_t elementStart = 0; // Start index in elements array + uint32_t elementCount = 0; // Number of elements (0 for internal nodes) + + bool IsLeaf() const { return elementCount > 0; } + }; + + /** + * @brief Result of a ray cast query + */ + struct RayHit { + bool hit = false; + float distance = std::numeric_limits::max(); + uint32_t elementIndex = INVALID_INDEX; + const T *element = nullptr; + }; + + BVH() = default; + ~BVH() = default; + + /** + * @brief Build BVH from a list of elements + * @param elements Elements to build BVH from + * @param config Build configuration + */ + void Build(const std::vector &elements, const BVHConfig &config = BVHConfig{}) + { + Clear(); + if (elements.empty()) { + return; + } + + config_ = config; + + // Store original elements temporarily + std::vector originalElements = elements; + + // Build index array for sorting without moving elements + std::vector indices(elements.size()); + for (uint32_t i = 0; i < elements.size(); ++i) { + indices[i] = i; + } + + // Compute centroids for SAH (using original indices) + centroids_.resize(elements.size()); + for (uint32_t i = 0; i < elements.size(); ++i) { + AABB bounds = BVHTraits::GetBounds(originalElements[i]); + centroids_[i] = (bounds.min + bounds.max) * 0.5f; + } + + // Build the tree recursively - this sorts indices in place + nodes_.reserve(elements.size() * 2); // Rough estimate + BuildRecursive(originalElements, indices, 0, static_cast(indices.size()), 0); + + // Reorder elements to match the sorted indices + // After this, elements_[i] corresponds to indices[i] from original + elements_.resize(indices.size()); + for (uint32_t i = 0; i < indices.size(); ++i) { + elements_[i] = originalElements[indices[i]]; + } + + // Clear centroids as they're no longer needed + centroids_.clear(); + } + + /** + * @brief Clear all data + */ + void Clear() + { + nodes_.clear(); + elements_.clear(); + centroids_.clear(); + } + + /** + * @brief Check if BVH is empty + */ + bool IsEmpty() const { return nodes_.empty(); } + + /** + * @brief Get the number of nodes + */ + uint32_t GetNodeCount() const { return static_cast(nodes_.size()); } + + /** + * @brief Get the number of elements + */ + uint32_t GetElementCount() const { return static_cast(elements_.size()); } + + /** + * @brief Get the root node + */ + const Node& GetRoot() const { return nodes_[0]; } + + /** + * @brief Query elements intersecting an AABB + * @param queryBounds AABB to query + * @param callback Function called for each intersecting element + */ + template + void QueryAABB(const AABB &queryBounds, Func &&callback) const + { + if (nodes_.empty()) { + return; + } + QueryAABBRecursive(0, queryBounds, callback); + } + + /** + * @brief Collect elements intersecting an AABB + * @param queryBounds AABB to query + * @param result Vector to store intersecting elements + */ + void QueryAABB(const AABB &queryBounds, std::vector &result) const + { + result.clear(); + QueryAABB(queryBounds, [&result](const T &element) { + result.push_back(&element); + }); + } + + /** + * @brief Cast a ray through the BVH + * + * @param ray Ray to cast + * @param maxDistance Maximum ray distance + * @param hitTest Function to test intersection with element, returns (hit, distance) + * @return RayHit result + */ + template + RayHit RayCast(const Ray &ray, float maxDistance, HitTestFunc &&hitTest) const + { + RayHit result; + result.distance = maxDistance; + + if (nodes_.empty()) { + return result; + } + + RayCastRecursive(0, ray, result, hitTest); + return result; + } + + /** + * @brief Cast a ray with default AABB hit test + * @param ray Ray to cast + * @param maxDistance Maximum ray distance + * @return RayHit result (distance is to AABB, not actual geometry) + */ + RayHit RayCast(const Ray &ray, float maxDistance = std::numeric_limits::max()) const + { + return RayCast(ray, maxDistance, [](const Ray &r, const T &element) { + AABB bounds = BVHTraits::GetBounds(element); + auto [hit, tMin, tMax] = Intersection(r, bounds); + return std::make_pair(hit, tMin); + }); + } + + /** + * @brief Query all elements that contain a point + * @param point Point to test + * @param callback Function called for each containing element + */ + template + void QueryPoint(const Vector3 &point, Func &&callback) const + { + if (nodes_.empty()) { + return; + } + QueryPointRecursive(0, point, callback); + } + + /** + * @brief Iterate over all elements in the BVH + * @param callback Function called for each element + */ + template + void ForEach(Func &&callback) const + { + for (const auto &element : elements_) { + callback(element); + } + } + + /** + * @brief Get the BVH depth + */ + uint32_t GetDepth() const + { + if (nodes_.empty()) { + return 0; + } + return GetDepthRecursive(0); + } + + /** + * @brief Get all nodes (for debugging/visualization) + */ + const std::vector& GetNodes() const { return nodes_; } + + /** + * @brief Get all elements + */ + const std::vector& GetElements() const { return elements_; } + + private: + /** + * @brief Build BVH recursively + */ + NodeIndex BuildRecursive(const std::vector &originalElements, std::vector &indices, + uint32_t start, uint32_t end, uint32_t depth) + { + NodeIndex nodeIndex = static_cast(nodes_.size()); + nodes_.emplace_back(); + Node &node = nodes_.back(); + + // Compute bounds for all elements in this node + AABB bounds; + bounds.min = Vector3(std::numeric_limits::max()); + bounds.max = Vector3(std::numeric_limits::lowest()); + + for (uint32_t i = start; i < end; ++i) { + AABB elementBounds = BVHTraits::GetBounds(originalElements[indices[i]]); + Merge(bounds, elementBounds, bounds); + } + node.bounds = bounds; + + uint32_t numElements = end - start; + + // Create leaf node if few elements or max depth reached + if (numElements <= config_.maxElementsPerLeaf || depth >= config_.maxDepth) { + node.elementStart = start; + node.elementCount = numElements; + return nodeIndex; + } + + // Choose split axis and position + uint32_t splitAxis; + uint32_t splitIndex; + FindBestSplit(originalElements, indices, start, end, bounds, splitAxis, splitIndex); + + // If split failed, create leaf + if (splitIndex == start || splitIndex == end) { + node.elementStart = start; + node.elementCount = numElements; + return nodeIndex; + } + + // Partition elements + PartitionElements(indices, start, end, splitAxis, splitIndex); + + // Recurse + node.leftChild = BuildRecursive(originalElements, indices, start, splitIndex, depth + 1); + // Re-fetch node reference as vector may have reallocated + nodes_[nodeIndex].rightChild = BuildRecursive(originalElements, indices, splitIndex, end, depth + 1); + + return nodeIndex; + } + + /** + * @brief Find best split using configured strategy + */ + void FindBestSplit(const std::vector &originalElements, const std::vector &indices, + uint32_t start, uint32_t end, + const AABB &bounds, uint32_t &outAxis, uint32_t &outIndex) + { + switch (config_.strategy) { + case BVHBuildStrategy::SAH: + FindBestSplitSAH(originalElements, indices, start, end, bounds, outAxis, outIndex); + break; + case BVHBuildStrategy::MEDIAN: + FindBestSplitMedian(indices, start, end, bounds, outAxis, outIndex); + break; + case BVHBuildStrategy::OBJECT_MEDIAN: + default: + FindBestSplitObjectMedian(indices, start, end, bounds, outAxis, outIndex); + break; + } + } + + /** + * @brief Find split using object median + */ + void FindBestSplitObjectMedian(const std::vector &indices, uint32_t start, uint32_t end, + const AABB &bounds, uint32_t &outAxis, uint32_t &outIndex) + { + // Choose axis with largest extent + Vector3 extent = bounds.max - bounds.min; + if (extent.x >= extent.y && extent.x >= extent.z) { + outAxis = 0; + } else if (extent.y >= extent.z) { + outAxis = 1; + } else { + outAxis = 2; + } + + outIndex = (start + end) / 2; + } + + /** + * @brief Find split using spatial median + */ + void FindBestSplitMedian(const std::vector &indices, uint32_t start, uint32_t end, + const AABB &bounds, uint32_t &outAxis, uint32_t &outIndex) + { + // Choose axis with largest extent + Vector3 extent = bounds.max - bounds.min; + if (extent.x >= extent.y && extent.x >= extent.z) { + outAxis = 0; + } else if (extent.y >= extent.z) { + outAxis = 1; + } else { + outAxis = 2; + } + + // Split at spatial median + float splitPos = (GetAxisValue(bounds.min, outAxis) + GetAxisValue(bounds.max, outAxis)) * 0.5f; + + // Count elements on each side + uint32_t leftCount = 0; + for (uint32_t i = start; i < end; ++i) { + if (GetAxisValue(centroids_[indices[i]], outAxis) < splitPos) { + ++leftCount; + } + } + + outIndex = start + leftCount; + if (outIndex == start || outIndex == end) { + outIndex = (start + end) / 2; // Fall back to object median + } + } + + /** + * @brief Find split using Surface Area Heuristic + */ + void FindBestSplitSAH(const std::vector &originalElements, const std::vector &indices, + uint32_t start, uint32_t end, + const AABB &bounds, uint32_t &outAxis, uint32_t &outIndex) + { + constexpr uint32_t NUM_BUCKETS = 12; + float bestCost = std::numeric_limits::max(); + outAxis = 0; + outIndex = (start + end) / 2; + + uint32_t numElements = end - start; + float invParentArea = 1.0f / SurfaceArea(bounds); + + // Try each axis + for (uint32_t axis = 0; axis < 3; ++axis) { + // Compute centroid bounds + float minCentroid = std::numeric_limits::max(); + float maxCentroid = std::numeric_limits::lowest(); + for (uint32_t i = start; i < end; ++i) { + float c = GetAxisValue(centroids_[indices[i]], axis); + minCentroid = std::min(minCentroid, c); + maxCentroid = std::max(maxCentroid, c); + } + + if (maxCentroid - minCentroid < 1e-6f) { + continue; // All centroids at same position + } + + // Initialize buckets + struct Bucket { + uint32_t count = 0; + AABB bounds; + }; + std::array buckets; + for (auto &b : buckets) { + b.bounds.min = Vector3(std::numeric_limits::max()); + b.bounds.max = Vector3(std::numeric_limits::lowest()); + } + + // Place elements in buckets + float scale = static_cast(NUM_BUCKETS) / (maxCentroid - minCentroid); + for (uint32_t i = start; i < end; ++i) { + float c = GetAxisValue(centroids_[indices[i]], axis); + uint32_t b = static_cast((c - minCentroid) * scale); + b = std::min(b, NUM_BUCKETS - 1); + buckets[b].count++; + AABB elementBounds = BVHTraits::GetBounds(originalElements[indices[i]]); + Merge(buckets[b].bounds, elementBounds, buckets[b].bounds); + } + + // Compute costs for each split + for (uint32_t split = 1; split < NUM_BUCKETS; ++split) { + AABB leftBounds, rightBounds; + leftBounds.min = rightBounds.min = Vector3(std::numeric_limits::max()); + leftBounds.max = rightBounds.max = Vector3(std::numeric_limits::lowest()); + uint32_t leftCount = 0, rightCount = 0; + + for (uint32_t i = 0; i < split; ++i) { + if (buckets[i].count > 0) { + Merge(leftBounds, buckets[i].bounds, leftBounds); + leftCount += buckets[i].count; + } + } + for (uint32_t i = split; i < NUM_BUCKETS; ++i) { + if (buckets[i].count > 0) { + Merge(rightBounds, buckets[i].bounds, rightBounds); + rightCount += buckets[i].count; + } + } + + if (leftCount == 0 || rightCount == 0) { + continue; + } + + float cost = config_.sahTraversalCost + + config_.sahIntersectionCost * ( + leftCount * SurfaceArea(leftBounds) + + rightCount * SurfaceArea(rightBounds)) * invParentArea; + + if (cost < bestCost) { + bestCost = cost; + outAxis = axis; + outIndex = start + leftCount; + } + } + } + + // Check if SAH split is better than leaf + float leafCost = config_.sahIntersectionCost * static_cast(numElements); + if (bestCost >= leafCost) { + outIndex = start; // Signal to create leaf + } + } + + /** + * @brief Partition elements around split + */ + void PartitionElements(std::vector &indices, uint32_t start, uint32_t end, + uint32_t axis, uint32_t splitIndex) + { + // Partial sort so that first (splitIndex - start) elements are on the left + std::nth_element(indices.begin() + start, indices.begin() + splitIndex, indices.begin() + end, + [this, axis](uint32_t a, uint32_t b) { + return GetAxisValue(centroids_[a], axis) < GetAxisValue(centroids_[b], axis); + }); + } + + /** + * @brief Get axis value from vector + */ + static float GetAxisValue(const Vector3 &v, uint32_t axis) + { + switch (axis) { + case 0: return v.x; + case 1: return v.y; + case 2: return v.z; + default: return v.x; + } + } + + /** + * @brief Compute surface area of AABB + */ + static float SurfaceArea(const AABB &bounds) + { + Vector3 d = bounds.max - bounds.min; + return 2.0f * (d.x * d.y + d.y * d.z + d.z * d.x); + } + + /** + * @brief Query AABB recursively + */ + template + void QueryAABBRecursive(NodeIndex nodeIndex, const AABB &queryBounds, Func &&callback) const + { + const Node &node = nodes_[nodeIndex]; + + // Test node bounds + if (!Intersection(node.bounds, queryBounds)) { + return; + } + + if (node.IsLeaf()) { + // Test each element + for (uint32_t i = 0; i < node.elementCount; ++i) { + const T &element = elements_[node.elementStart + i]; + AABB elementBounds = BVHTraits::GetBounds(element); + if (Intersection(elementBounds, queryBounds)) { + callback(element); + } + } + } else { + // Recurse to children + QueryAABBRecursive(node.leftChild, queryBounds, callback); + QueryAABBRecursive(node.rightChild, queryBounds, callback); + } + } + + /** + * @brief Ray cast recursively + */ + template + void RayCastRecursive(NodeIndex nodeIndex, const Ray &ray, RayHit &result, HitTestFunc &&hitTest) const + { + const Node &node = nodes_[nodeIndex]; + + // Test node bounds + auto [hit, tMin, tMax] = Intersection(ray, node.bounds); + if (!hit || tMin > result.distance) { + return; + } + + if (node.IsLeaf()) { + // Test each element + for (uint32_t i = 0; i < node.elementCount; ++i) { + uint32_t elementIndex = node.elementStart + i; + const T &element = elements_[elementIndex]; + auto [elemHit, elemDist] = hitTest(ray, element); + if (elemHit && elemDist < result.distance && elemDist >= 0.0f) { + result.hit = true; + result.distance = elemDist; + result.elementIndex = elementIndex; + result.element = &element; + } + } + } else { + // Order children by distance to ray origin for early termination + const Node &leftNode = nodes_[node.leftChild]; + const Node &rightNode = nodes_[node.rightChild]; + + auto [leftHit, leftTMin, leftTMax] = Intersection(ray, leftNode.bounds); + auto [rightHit, rightTMin, rightTMax] = Intersection(ray, rightNode.bounds); + + if (leftHit && rightHit) { + // Visit closer child first + if (leftTMin < rightTMin) { + RayCastRecursive(node.leftChild, ray, result, hitTest); + if (rightTMin < result.distance) { + RayCastRecursive(node.rightChild, ray, result, hitTest); + } + } else { + RayCastRecursive(node.rightChild, ray, result, hitTest); + if (leftTMin < result.distance) { + RayCastRecursive(node.leftChild, ray, result, hitTest); + } + } + } else if (leftHit) { + RayCastRecursive(node.leftChild, ray, result, hitTest); + } else if (rightHit) { + RayCastRecursive(node.rightChild, ray, result, hitTest); + } + } + } + + /** + * @brief Query point recursively + */ + template + void QueryPointRecursive(NodeIndex nodeIndex, const Vector3 &point, Func &&callback) const + { + const Node &node = nodes_[nodeIndex]; + + // Test if point is in node bounds + if (point.x < node.bounds.min.x || point.x > node.bounds.max.x || + point.y < node.bounds.min.y || point.y > node.bounds.max.y || + point.z < node.bounds.min.z || point.z > node.bounds.max.z) { + return; + } + + if (node.IsLeaf()) { + // Test each element + for (uint32_t i = 0; i < node.elementCount; ++i) { + const T &element = elements_[node.elementStart + i]; + AABB elementBounds = BVHTraits::GetBounds(element); + if (point.x >= elementBounds.min.x && point.x <= elementBounds.max.x && + point.y >= elementBounds.min.y && point.y <= elementBounds.max.y && + point.z >= elementBounds.min.z && point.z <= elementBounds.max.z) { + callback(element); + } + } + } else { + QueryPointRecursive(node.leftChild, point, callback); + QueryPointRecursive(node.rightChild, point, callback); + } + } + + /** + * @brief Get depth recursively + */ + uint32_t GetDepthRecursive(NodeIndex nodeIndex) const + { + const Node &node = nodes_[nodeIndex]; + if (node.IsLeaf()) { + return 1; + } + return 1 + std::max(GetDepthRecursive(node.leftChild), GetDepthRecursive(node.rightChild)); + } + + BVHConfig config_; + std::vector nodes_; + std::vector elements_; + std::vector centroids_; + }; + + /** + * @brief Specialization for AABB-bounded objects + */ + template <> + struct BVHTraits { + static AABB GetBounds(const AABB &element) + { + return element; + } + }; + +} // namespace sky diff --git a/runtime/core/src/shapes/Shapes.cpp b/runtime/core/src/shapes/Shapes.cpp index e9730390..5e07d0d3 100644 --- a/runtime/core/src/shapes/Shapes.cpp +++ b/runtime/core/src/shapes/Shapes.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include namespace sky { @@ -47,6 +49,65 @@ namespace sky { }); } + std::tuple Intersection(const Ray &ray, const AABB &aabb) + { + // Slab-based ray-AABB intersection + float tMin = 0.0f; + float tMax = std::numeric_limits::max(); + + // X axis + if (std::abs(ray.dir.x) < 1e-6f) { + // Ray parallel to X slab + if (ray.origin.x < aabb.min.x || ray.origin.x > aabb.max.x) { + return {false, 0.0f, 0.0f}; + } + } else { + float invD = 1.0f / ray.dir.x; + float t1 = (aabb.min.x - ray.origin.x) * invD; + float t2 = (aabb.max.x - ray.origin.x) * invD; + if (t1 > t2) std::swap(t1, t2); + tMin = std::max(tMin, t1); + tMax = std::min(tMax, t2); + if (tMin > tMax) return {false, 0.0f, 0.0f}; + } + + // Y axis + if (std::abs(ray.dir.y) < 1e-6f) { + if (ray.origin.y < aabb.min.y || ray.origin.y > aabb.max.y) { + return {false, 0.0f, 0.0f}; + } + } else { + float invD = 1.0f / ray.dir.y; + float t1 = (aabb.min.y - ray.origin.y) * invD; + float t2 = (aabb.max.y - ray.origin.y) * invD; + if (t1 > t2) std::swap(t1, t2); + tMin = std::max(tMin, t1); + tMax = std::min(tMax, t2); + if (tMin > tMax) return {false, 0.0f, 0.0f}; + } + + // Z axis + if (std::abs(ray.dir.z) < 1e-6f) { + if (ray.origin.z < aabb.min.z || ray.origin.z > aabb.max.z) { + return {false, 0.0f, 0.0f}; + } + } else { + float invD = 1.0f / ray.dir.z; + float t1 = (aabb.min.z - ray.origin.z) * invD; + float t2 = (aabb.max.z - ray.origin.z) * invD; + if (t1 > t2) std::swap(t1, t2); + tMin = std::max(tMin, t1); + tMax = std::min(tMax, t2); + if (tMin > tMax) return {false, 0.0f, 0.0f}; + } + + return {true, tMin, tMax}; + } + + bool IntersectionTest(const Ray &ray, const AABB &aabb) + { + return std::get<0>(Intersection(ray, aabb)); + } Plane CreatePlaneByVertices(const Vector3 &v1, const Vector3 &v2, const Vector3 &v3) { diff --git a/runtime/render/core/include/render/culling/PVSBakedData.h b/runtime/render/core/include/render/culling/PVSBakedData.h new file mode 100644 index 00000000..cbccc291 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSBakedData.h @@ -0,0 +1,77 @@ +// +// Created by SkyEngine on 2024/02/16. +// + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sky { + + class BinaryInputArchive; + class BinaryOutputArchive; + + /** + * @brief Serializable PVS baked data structure + * + * Contains all pre-computed visibility data that can be saved to disk + * and loaded at runtime. + */ + struct PVSBakedData { + // Version for compatibility + static constexpr uint32_t VERSION = 1; + + // Configuration used for baking + PVSConfig config; + + // Grid dimensions + PVSCellCoord gridDimensions; + + // Number of objects + uint32_t numObjects = 0; + + // Cell data (bounds, centers) + std::vector cells; + + // Visibility bitsets (one per cell) + // Stored as raw uint64_t arrays for efficient serialization + std::vector> visibilityData; + + // Object UUIDs or names for mapping (optional) + std::vector objectNames; + + /** + * @brief Save the baked data to a binary archive + */ + void Save(BinaryOutputArchive &archive) const; + + /** + * @brief Load baked data from a binary archive + */ + void Load(BinaryInputArchive &archive); + + /** + * @brief Calculate the total memory size of the baked data + */ + size_t GetMemorySize() const; + + /** + * @brief Get statistics about the baked data + */ + struct Statistics { + uint32_t totalCells = 0; + uint32_t totalObjects = 0; + uint32_t totalVisiblePairs = 0; // Total cell-object visibility pairs + float averageVisibleObjects = 0.0f; + float compressionRatio = 0.0f; + size_t rawDataSize = 0; + }; + Statistics GetStatistics() const; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSBaker.h b/runtime/render/core/include/render/culling/PVSBaker.h new file mode 100644 index 00000000..e3ce59ae --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSBaker.h @@ -0,0 +1,196 @@ +// +// Created by SkyEngine on 2024/02/16. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Progress callback for PVS baking + * @param progress Progress value from 0.0 to 1.0 + * @param message Status message + */ + using PVSBakeProgressCallback = std::function; + + /** + * @brief Visibility test function type + * @param cellBounds Bounds of the cell + * @param cellCenter Center of the cell + * @param objectBounds Bounds of the object + * @param objectIndex Index of the object + * @return true if the object should be considered visible from the cell + */ + using PVSVisibilityTestFunc = std::function; + + /** + * @brief Configuration for PVS baking + */ + struct PVSBakeConfig { + // PVS grid configuration + PVSConfig pvsConfig; + + // Visibility computation method + enum class Method { + DISTANCE, // Simple distance-based culling + RAYCAST, // Ray-casting based visibility + CUSTOM // Custom visibility function + }; + Method method = Method::DISTANCE; + + // Distance-based parameters + float maxVisibilityDistance = 100.0f; + + // Ray-cast parameters + uint32_t raysPerCell = 64; // Number of rays to cast per cell + uint32_t rayBounces = 1; // Number of bounces for indirect visibility + + // Custom visibility function + PVSVisibilityTestFunc customTestFunc; + + // Parallelization + uint32_t numThreads = 0; // 0 = auto-detect + + // Progress callback + PVSBakeProgressCallback progressCallback; + }; + + /** + * @brief Result of PVS baking operation + */ + struct PVSBakeResult { + bool success = false; + std::string errorMessage; + + // Timing + float totalTimeSeconds = 0.0f; + float computeTimeSeconds = 0.0f; + + // Statistics + PVSBakedData::Statistics statistics; + }; + + /** + * @brief Object to be included in PVS baking + */ + struct PVSBakeObject { + AABB bounds; + std::string name; + uint32_t flags = 0; // User-defined flags (e.g., static, dynamic, occluder) + }; + + /** + * @brief PVS Baker - computes and bakes visibility data + * + * The PVSBaker takes a scene description (objects with bounds) and + * computes visibility data that can be serialized and loaded at runtime. + */ + class PVSBaker { + public: + PVSBaker() = default; + ~PVSBaker() = default; + + /** + * @brief Add an object to be baked + * @param object Object description + * @return Object index for reference + */ + uint32_t AddObject(const PVSBakeObject &object); + + /** + * @brief Add multiple objects + * @param objects Vector of objects + */ + void AddObjects(const std::vector &objects); + + /** + * @brief Clear all objects + */ + void ClearObjects(); + + /** + * @brief Get the number of objects + */ + uint32_t GetObjectCount() const { return static_cast(objects.size()); } + + /** + * @brief Get an object by index + */ + const PVSBakeObject& GetObject(uint32_t index) const { return objects[index]; } + + /** + * @brief Bake the PVS data + * @param config Baking configuration + * @param outData Output baked data + * @return Baking result + */ + PVSBakeResult Bake(const PVSBakeConfig &config, PVSBakedData &outData); + + /** + * @brief Cancel ongoing baking operation + */ + void Cancel(); + + /** + * @brief Check if baking is in progress + */ + bool IsBaking() const { return baking.load(); } + + private: + /** + * @brief Compute visibility using distance method + */ + void BakeDistanceBased( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result); + + /** + * @brief Compute visibility using ray-cast method + */ + void BakeRayCast( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result); + + /** + * @brief Compute visibility using custom function + */ + void BakeCustom( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result); + + /** + * @brief Initialize output data structure + */ + void InitializeOutputData(const PVSBakeConfig &config, PVSBakedData &outData); + + /** + * @brief Report progress + */ + void ReportProgress( + const PVSBakeConfig &config, + float progress, + const std::string &message); + + std::vector objects; + std::atomic baking{false}; + std::atomic cancelRequested{false}; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSBitSet.h b/runtime/render/core/include/render/culling/PVSBitSet.h new file mode 100644 index 00000000..a0d39669 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSBitSet.h @@ -0,0 +1,166 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#pragma once + +#include +#include +#include + +namespace sky { + + /** + * @brief Dynamic bitset for storing visibility information + * + * A compact representation of which objects are visible from a particular cell. + * Uses a vector of uint64_t for efficient storage and operations. + */ + class PVSBitSet { + public: + static constexpr uint32_t BITS_PER_WORD = 64; + + PVSBitSet() = default; + explicit PVSBitSet(uint32_t numBits); + + /** + * @brief Resize the bitset to accommodate numBits + */ + void Resize(uint32_t numBits); + + /** + * @brief Set a specific bit to 1 (visible) + */ + void Set(uint32_t index); + + /** + * @brief Clear a specific bit to 0 (not visible) + */ + void Clear(uint32_t index); + + /** + * @brief Test if a specific bit is set + */ + bool Test(uint32_t index) const; + + /** + * @brief Set all bits to 0 + */ + void ClearAll(); + + /** + * @brief Set all bits to 1 + */ + void SetAll(); + + /** + * @brief Perform bitwise OR with another bitset + */ + void OrWith(const PVSBitSet &other); + + /** + * @brief Perform bitwise AND with another bitset + */ + void AndWith(const PVSBitSet &other); + + /** + * @brief Count the number of set bits + */ + uint32_t CountSet() const; + + /** + * @brief Get the capacity in bits + */ + uint32_t GetCapacity() const { return capacity; } + + /** + * @brief Check if any bit is set + */ + bool Any() const; + + /** + * @brief Check if no bits are set + */ + bool None() const { return !Any(); } + + /** + * @brief Access raw data for serialization + */ + const std::vector& GetData() const { return data; } + + /** + * @brief Set raw data for deserialization + */ + void SetData(const std::vector& rawData, uint32_t numBits); + + /** + * @brief Iterate over all set bits efficiently using CTZ (Count Trailing Zeros) + * + * This is much faster than checking each bit individually when the bitset + * is sparse (few bits set). Uses hardware bit-scan instructions. + * + * @param callback Function called for each set bit index + */ + template + void ForEachSetBit(Func &&callback) const + { + for (size_t wordIdx = 0; wordIdx < data.size(); ++wordIdx) { + uint64_t word = data[wordIdx]; + while (word != 0) { + // Find position of lowest set bit using CTZ + uint32_t bitPos = CountTrailingZeros(word); + uint32_t globalBitIndex = static_cast(wordIdx * BITS_PER_WORD + bitPos); + + if (globalBitIndex < capacity) { + callback(globalBitIndex); + } + + // Clear the lowest set bit + word &= (word - 1); + } + } + } + + /** + * @brief Collect all set bit indices into a vector + * @param result Vector to store set bit indices + */ + void GetSetBitIndices(std::vector &result) const; + + /** + * @brief Get the first N set bit indices + * @param result Vector to store set bit indices + * @param maxCount Maximum number of indices to collect + * @return Number of indices actually collected + */ + uint32_t GetSetBitIndices(std::vector &result, uint32_t maxCount) const; + + private: + static uint32_t WordIndex(uint32_t bitIndex) { return bitIndex / BITS_PER_WORD; } + static uint32_t BitOffset(uint32_t bitIndex) { return bitIndex % BITS_PER_WORD; } + static uint64_t BitMask(uint32_t bitIndex) { return 1ULL << BitOffset(bitIndex); } + + /** + * @brief Count trailing zeros (position of lowest set bit) + * + * PRECONDITION: value must be non-zero. Behavior is undefined for zero. + * All callers must check value != 0 before calling this function. + */ + static uint32_t CountTrailingZeros(uint64_t value) + { +#ifdef _MSC_VER + unsigned long index; + // PRECONDITION: value != 0 (caller must ensure this) + _BitScanForward64(&index, value); + return static_cast(index); +#else + // PRECONDITION: value != 0 (caller must ensure this) + return static_cast(__builtin_ctzll(value)); +#endif + } + + std::vector data; + uint32_t capacity = 0; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSCulling.h b/runtime/render/core/include/render/culling/PVSCulling.h new file mode 100644 index 00000000..d27a2af8 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSCulling.h @@ -0,0 +1,246 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sky { + + class SceneView; + class RenderScene; + + /** + * @brief Manages PVS-based visibility culling for a render scene + * + * The PVSCulling class provides: + * - Object registration and management + * - Pre-computed visibility data storage + * - Runtime visibility queries combining PVS with frustum culling + * - Visibility computation utilities + */ + class PVSCulling { + public: + PVSCulling() = default; + ~PVSCulling() = default; + + /** + * @brief Initialize the PVS culling system + * @param config PVS configuration + */ + void Initialize(const PVSConfig &config); + + /** + * @brief Clear all PVS data and reset the system + */ + void Clear(); + + /** + * @brief Register a render primitive with the PVS system + * @param primitive Render primitive to register + * @return Object ID for use in visibility queries + */ + PVSObjectID RegisterPrimitive(RenderPrimitive *primitive); + + /** + * @brief Unregister a render primitive from the PVS system + * @param primitive Render primitive to unregister + */ + void UnregisterPrimitive(RenderPrimitive *primitive); + + /** + * @brief Get the object ID for a registered primitive + * @param primitive Render primitive + * @return Object ID or INVALID_PVS_OBJECT if not registered + */ + PVSObjectID GetObjectID(RenderPrimitive *primitive) const; + + /** + * @brief Get the primitive for an object ID + * @param objectID Object ID + * @return Render primitive or nullptr if not found + */ + RenderPrimitive* GetPrimitive(PVSObjectID objectID) const; + + /** + * @brief Query visible primitives from a view position + * + * This combines PVS lookup with optional frustum culling + * + * @param viewPosition Camera/view position + * @param sceneView Optional scene view for frustum culling + * @param result Vector to store visible primitives + */ + void QueryVisiblePrimitives( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const; + + /** + * @brief Query visible primitives using only PVS (no frustum culling) + * @param viewPosition Camera/view position + * @param result Vector to store visible primitives + */ + void QueryPVSVisiblePrimitives( + const Vector3 &viewPosition, + std::vector &result) const; + + /** + * @brief Optimized query using fast bit iteration + * + * This method iterates only over visible objects instead of all objects, + * which is much faster when visibility is sparse (few objects visible per cell). + * Uses hardware CTZ (count trailing zeros) for efficient bit scanning. + * + * @param viewPosition Camera/view position + * @param sceneView Optional scene view for frustum culling + * @param result Vector to store visible primitives + */ + void QueryVisiblePrimitivesOptimized( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const; + + /** + * @brief Iterate over visible object IDs without creating a result vector + * + * Zero-allocation iteration for maximum performance. The callback receives + * each visible object ID and can return false to stop iteration early. + * + * @param viewPosition Camera/view position + * @param callback Function called for each visible object ID, return false to stop + * @return Number of visible objects visited + */ + template + uint32_t ForEachVisibleObject(const Vector3 &viewPosition, Func &&callback) const + { + PVSCellID cellID = pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + return 0; + } + + uint32_t count = 0; + const PVSBitSet &visibility = pvsData.GetVisibilitySet(cellID); + + visibility.ForEachSetBit([&](uint32_t objID) { + if (objID < idToPrimitive.size() && idToPrimitive[objID] != nullptr) { + callback(objID); + ++count; + } + }); + + return count; + } + + /** + * @brief Iterate over visible primitives without creating a result vector + * + * @param viewPosition Camera/view position + * @param sceneView Optional scene view for frustum culling + * @param callback Function called for each visible primitive + * @return Number of visible primitives visited + */ + template + uint32_t ForEachVisiblePrimitive( + const Vector3 &viewPosition, + const SceneView *sceneView, + Func &&callback) const + { + PVSCellID cellID = pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + // Fall back to all objects when outside PVS bounds + uint32_t count = 0; + for (RenderPrimitive *primitive : idToPrimitive) { + if (primitive == nullptr) { + continue; + } + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + callback(primitive); + ++count; + } + } + return count; + } + + uint32_t count = 0; + const PVSBitSet &visibility = pvsData.GetVisibilitySet(cellID); + + visibility.ForEachSetBit([&](uint32_t objID) { + if (objID < idToPrimitive.size()) { + RenderPrimitive *primitive = idToPrimitive[objID]; + if (primitive != nullptr) { + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + callback(primitive); + ++count; + } + } + } + }); + + return count; + } + + /** + * @brief Check if a specific primitive is visible from a position + * @param viewPosition View position + * @param primitive Primitive to check + * @return True if potentially visible + */ + bool IsPrimitiveVisible(const Vector3 &viewPosition, RenderPrimitive *primitive) const; + + /** + * @brief Get the underlying PVS data for modification + * @return Reference to PVS data + */ + PVSData& GetPVSData() { return pvsData; } + const PVSData& GetPVSData() const { return pvsData; } + + /** + * @brief Compute visibility for all cells using a visibility test function + * + * The test function should return true if the object is visible from the cell + * + * @param testFunc Function (cellID, objectID, cellBounds, objectBounds) -> bool + */ + void ComputeVisibility( + const std::function &testFunc); + + /** + * @brief Compute simple distance-based visibility + * @param maxDistance Maximum visibility distance + */ + void ComputeDistanceBasedVisibility(float maxDistance); + + /** + * @brief Get the number of registered objects + */ + uint32_t GetObjectCount() const { return static_cast(primitiveToID.size()); } + + /** + * @brief Check if the system is initialized + */ + bool IsInitialized() const { return initialized; } + + /** + * @brief Get count of visible objects from a position (fast, no allocation) + * @param viewPosition View position + * @return Number of potentially visible objects + */ + uint32_t GetVisibleCount(const Vector3 &viewPosition) const; + + private: + bool initialized = false; + PVSData pvsData; + PVSObjectID nextObjectID = 0; + + std::unordered_map primitiveToID; + std::vector idToPrimitive; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSData.h b/runtime/render/core/include/render/culling/PVSData.h new file mode 100644 index 00000000..98da4a27 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSData.h @@ -0,0 +1,239 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#pragma once + +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Stores pre-computed visibility data for a 3D grid of cells + * + * The PVSData structure divides the world into a uniform grid of cells. + * For each cell, it stores a bitset indicating which objects are potentially + * visible from that cell. + * + * Usage: + * 1. Configure the PVS system with world bounds and cell size + * 2. Register objects to get object IDs + * 3. Compute visibility for each cell (offline or at load time) + * 4. At runtime, query visibility by position + */ + class PVSData { + public: + PVSData() = default; + ~PVSData() = default; + + /** + * @brief Initialize the PVS grid with configuration + * @param config Configuration specifying world bounds and cell size + */ + void Initialize(const PVSConfig &config); + + /** + * @brief Clear all PVS data + */ + void Clear(); + + /** + * @brief Get the cell ID for a world position (optimized) + * + * Uses pre-computed inverse cell sizes to avoid division. + * + * @param position World position + * @return Cell ID or INVALID_PVS_CELL if outside bounds + */ + PVSCellID GetCellID(const Vector3 &position) const; + + /** + * @brief Get the cell ID for a world position (fast inline version) + * + * This version includes clamping to ensure valid cell indices even for + * positions at the boundaries. Use IsInBounds() first if you need to + * distinguish between valid and invalid positions. + * + * @param position World position (ideally within bounds, clamped if not) + * @return Cell ID (always valid due to clamping) + */ + inline PVSCellID GetCellIDClamped(const Vector3 &position) const + { + int32_t x = static_cast((position.x - config.worldBounds.min.x) * invCellSize.x); + int32_t y = static_cast((position.y - config.worldBounds.min.y) * invCellSize.y); + int32_t z = static_cast((position.z - config.worldBounds.min.z) * invCellSize.z); + + // Clamp to valid range + x = x < 0 ? 0 : (x >= gridDimensions.x ? gridDimensions.x - 1 : x); + y = y < 0 ? 0 : (y >= gridDimensions.y ? gridDimensions.y - 1 : y); + z = z < 0 ? 0 : (z >= gridDimensions.z ? gridDimensions.z - 1 : z); + + return static_cast(z * xyArea + y * gridDimensions.x + x); + } + + /** + * @brief Get the cell ID for a position known to be within bounds (fastest) + * + * No bounds checking or clamping. Caller must ensure position is within + * world bounds, otherwise behavior is undefined. + * + * @param position World position (MUST be within world bounds) + * @return Cell ID + */ + inline PVSCellID GetCellIDFast(const Vector3 &position) const + { + // Direct calculation - position must be within bounds + int32_t x = static_cast((position.x - config.worldBounds.min.x) * invCellSize.x); + int32_t y = static_cast((position.y - config.worldBounds.min.y) * invCellSize.y); + int32_t z = static_cast((position.z - config.worldBounds.min.z) * invCellSize.z); + + return static_cast(z * xyArea + y * gridDimensions.x + x); + } + + /** + * @brief Get the cell coordinates for a world position + * @param position World position + * @return Cell coordinates + */ + PVSCellCoord GetCellCoord(const Vector3 &position) const; + + /** + * @brief Get cell ID from coordinates + * @param coord Cell coordinates + * @return Cell ID or INVALID_PVS_CELL if outside bounds + */ + PVSCellID GetCellIDFromCoord(const PVSCellCoord &coord) const; + + /** + * @brief Get cell ID from coordinates (fast inline version) + * @param coord Cell coordinates (must be valid) + * @return Cell ID + */ + inline PVSCellID GetCellIDFromCoordFast(const PVSCellCoord &coord) const + { + return static_cast(coord.z * xyArea + coord.y * gridDimensions.x + coord.x); + } + + /** + * @brief Get cell coordinates from cell ID + * @param cellID Cell ID + * @return Cell coordinates + */ + PVSCellCoord GetCoordFromCellID(PVSCellID cellID) const; + + /** + * @brief Get the cell information + * @param cellID Cell ID + * @return Cell information + */ + const PVSCell& GetCell(PVSCellID cellID) const; + + /** + * @brief Set an object as visible from a cell + * @param cellID Cell ID + * @param objectID Object ID + */ + void SetVisible(PVSCellID cellID, PVSObjectID objectID); + + /** + * @brief Clear visibility for an object from a cell + * @param cellID Cell ID + * @param objectID Object ID + */ + void ClearVisible(PVSCellID cellID, PVSObjectID objectID); + + /** + * @brief Check if an object is visible from a cell + * @param cellID Cell ID + * @param objectID Object ID + * @return True if the object is potentially visible from the cell + */ + bool IsVisible(PVSCellID cellID, PVSObjectID objectID) const; + + /** + * @brief Get the visibility bitset for a cell + * @param cellID Cell ID + * @return Reference to the visibility bitset + */ + const PVSBitSet& GetVisibilitySet(PVSCellID cellID) const; + + /** + * @brief Get a mutable visibility bitset for a cell + * @param cellID Cell ID + * @return Reference to the visibility bitset + */ + PVSBitSet& GetMutableVisibilitySet(PVSCellID cellID); + + /** + * @brief Set all objects visible from a cell + * @param cellID Cell ID + */ + void SetAllVisible(PVSCellID cellID); + + /** + * @brief Clear all visibility from a cell + * @param cellID Cell ID + */ + void ClearAllVisible(PVSCellID cellID); + + /** + * @brief Get the number of cells in the grid + */ + uint32_t GetCellCount() const { return static_cast(cells.size()); } + + /** + * @brief Get grid dimensions + */ + const PVSCellCoord& GetGridDimensions() const { return gridDimensions; } + + /** + * @brief Get the configuration + */ + const PVSConfig& GetConfig() const { return config; } + + /** + * @brief Check if a cell ID is valid + */ + bool IsValidCell(PVSCellID cellID) const { return cellID < GetCellCount(); } + + /** + * @brief Check if a position is within the world bounds + */ + inline bool IsInBounds(const Vector3 &position) const + { + return position.x >= config.worldBounds.min.x && position.x < config.worldBounds.max.x && + position.y >= config.worldBounds.min.y && position.y < config.worldBounds.max.y && + position.z >= config.worldBounds.min.z && position.z < config.worldBounds.max.z; + } + + /** + * @brief Load PVS data from baked data + * @param bakedData Pre-computed visibility data + */ + void LoadFromBakedData(const struct PVSBakedData &bakedData); + + /** + * @brief Export to baked data format + * @param outBakedData Output baked data structure + */ + void ExportToBakedData(struct PVSBakedData &outBakedData) const; + + private: + PVSConfig config; + PVSCellCoord gridDimensions; + + // Pre-computed values for fast cell lookup + Vector3 invCellSize; // 1.0 / cellSize for multiplication instead of division + int32_t xyArea = 0; // gridDimensions.x * gridDimensions.y + + std::vector cells; + std::vector visibilityData; // One bitset per cell + + // Empty bitset for invalid queries + PVSBitSet emptyBitSet; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSSampling.h b/runtime/render/core/include/render/culling/PVSSampling.h new file mode 100644 index 00000000..d33a677c --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSSampling.h @@ -0,0 +1,497 @@ +// +// Created by SkyEngine on 2024/02/26. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Sampling strategy for generating sample points/directions + */ + enum class PVSSamplingStrategy { + RANDOM, // Pure random sampling + STRATIFIED, // Stratified (jittered grid) sampling + HALTON, // Halton low-discrepancy sequence + FIBONACCI // Fibonacci sphere sampling for directions + }; + + /** + * @brief Configuration for cell sampling + */ + struct PVSCellSamplingConfig { + uint32_t numSamplesPerCell = 64; // Number of sample points per cell + uint32_t numDirectionsPerSample = 32; // Number of directions per sample point + PVSSamplingStrategy pointStrategy = PVSSamplingStrategy::STRATIFIED; + PVSSamplingStrategy directionStrategy = PVSSamplingStrategy::FIBONACCI; + bool useHemisphereForFloors = false; // Use hemisphere sampling for floor surfaces + float hemisphereUpBias = 0.0f; // Bias towards up direction (0-1) + }; + + /** + * @brief A single sample with position and direction + */ + struct PVSSample { + Vector3 position; + Vector3 direction; + }; + + /** + * @brief Collection of samples for a cell + */ + struct PVSCellSamples { + PVSCellID cellId = INVALID_PVS_CELL; + std::vector samples; + }; + + /** + * @brief Simple random number generator for sampling + * + * Uses xorshift128+ algorithm for fast, quality random numbers + */ + class PVSRandomGenerator { + public: + explicit PVSRandomGenerator(uint64_t seed = 12345ULL) + { + state[0] = seed; + state[1] = seed ^ 0x5DEECE66DULL; + // Warm up + for (int i = 0; i < 10; ++i) { + NextU64(); + } + } + + /** + * @brief Generate random uint64 + */ + uint64_t NextU64() + { + uint64_t s1 = state[0]; + uint64_t s0 = state[1]; + uint64_t result = s0 + s1; + state[0] = s0; + s1 ^= s1 << 23; + state[1] = s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5); + return result; + } + + /** + * @brief Generate random float in [0, 1) + */ + float NextFloat() + { + return static_cast(NextU64() & 0xFFFFFFFFULL) / 4294967296.0f; + } + + /** + * @brief Generate random float in [min, max) + */ + float NextFloat(float min, float max) + { + return min + NextFloat() * (max - min); + } + + /** + * @brief Seed the generator + */ + void Seed(uint64_t seed) + { + state[0] = seed; + state[1] = seed ^ 0x5DEECE66DULL; + } + + private: + uint64_t state[2]; + }; + + /** + * @brief Sampling utility class for PVS visibility computation + */ + class PVSSampling { + public: + PVSSampling() : rng(12345ULL) {} + explicit PVSSampling(uint64_t seed) : rng(seed) {} + + /** + * @brief Generate a random point within a cell/AABB + * @param bounds The cell bounds + * @return Random point within the bounds + */ + Vector3 GenerateRandomPointInCell(const AABB &bounds) + { + return Vector3( + rng.NextFloat(bounds.min.x, bounds.max.x), + rng.NextFloat(bounds.min.y, bounds.max.y), + rng.NextFloat(bounds.min.z, bounds.max.z) + ); + } + + /** + * @brief Generate a random direction on unit sphere + * + * Uses spherical coordinates with uniform distribution + * @return Normalized direction vector + */ + Vector3 GenerateRandomDirection() + { + // Generate uniform random point on unit sphere + // Using spherical coordinates with theta = [0, 2pi], phi = [0, pi] + float u = rng.NextFloat(); + float v = rng.NextFloat(); + + float theta = 2.0f * PI * u; + float phi = std::acos(2.0f * v - 1.0f); // Uniform distribution on sphere + + float sinPhi = std::sin(phi); + return Vector3( + sinPhi * std::cos(theta), + sinPhi * std::sin(theta), + std::cos(phi) + ); + } + + /** + * @brief Generate a random direction in hemisphere around normal + * @param normal The hemisphere normal direction + * @return Normalized direction in the hemisphere + */ + Vector3 GenerateHemisphereDirection(const Vector3 &normal) + { + // Generate random point on hemisphere (cosine-weighted) + float u = rng.NextFloat(); + float v = rng.NextFloat(); + + float phi = 2.0f * PI * u; + float cosTheta = std::sqrt(1.0f - v); // Cosine-weighted + float sinTheta = std::sqrt(v); + + // Local hemisphere direction + Vector3 localDir( + sinTheta * std::cos(phi), + sinTheta * std::sin(phi), + cosTheta + ); + + // Build orthonormal basis around normal + Vector3 up = std::abs(normal.z) < 0.999f ? Vector3(0, 0, 1) : Vector3(1, 0, 0); + Vector3 tangent = up.Cross(normal); + tangent.Normalize(); + Vector3 bitangent = normal.Cross(tangent); + + // Transform to world space + return Vector3( + tangent.x * localDir.x + bitangent.x * localDir.y + normal.x * localDir.z, + tangent.y * localDir.x + bitangent.y * localDir.y + normal.y * localDir.z, + tangent.z * localDir.x + bitangent.z * localDir.y + normal.z * localDir.z + ); + } + + /** + * @brief Generate Halton sequence value + * @param index Sample index (1-based) + * @param base Base for the sequence (usually prime: 2, 3, 5, etc.) + * @return Value in [0, 1) + */ + static float HaltonSequence(uint32_t index, uint32_t base) + { + float result = 0.0f; + float f = 1.0f / static_cast(base); + uint32_t i = index; + + while (i > 0) { + result += f * static_cast(i % base); + i /= base; + f /= static_cast(base); + } + + return result; + } + + /** + * @brief Generate Fibonacci sphere direction + * + * Generates uniformly distributed points on a sphere using Fibonacci spiral + * @param index Sample index + * @param numSamples Total number of samples + * @return Direction on unit sphere + */ + static Vector3 FibonacciSphereDirection(uint32_t index, uint32_t numSamples) + { + static constexpr float GOLDEN_RATIO = 1.6180339887498948482f; + + float y = 1.0f - (2.0f * static_cast(index) + 1.0f) / static_cast(numSamples); + float radius = std::sqrt(1.0f - y * y); + float theta = 2.0f * PI * static_cast(index) / GOLDEN_RATIO; + + return Vector3( + radius * std::cos(theta), + y, + radius * std::sin(theta) + ); + } + + /** + * @brief Generate stratified sample points in a cell + * + * Divides the cell into strata and samples one point per stratum + * @param bounds Cell bounds + * @param numSamples Number of samples to generate + * @param outPoints Output vector of sample points + */ + void GenerateStratifiedPoints( + const AABB &bounds, + uint32_t numSamples, + std::vector &outPoints) + { + outPoints.clear(); + outPoints.reserve(numSamples); + + // Compute strata grid dimensions + uint32_t n = static_cast(std::ceil(std::cbrt(static_cast(numSamples)))); + + Vector3 size = bounds.max - bounds.min; + Vector3 strataSize( + size.x / static_cast(n), + size.y / static_cast(n), + size.z / static_cast(n) + ); + + uint32_t count = 0; + for (uint32_t z = 0; z < n && count < numSamples; ++z) { + for (uint32_t y = 0; y < n && count < numSamples; ++y) { + for (uint32_t x = 0; x < n && count < numSamples; ++x) { + // Compute stratum bounds + Vector3 strataMin( + bounds.min.x + static_cast(x) * strataSize.x, + bounds.min.y + static_cast(y) * strataSize.y, + bounds.min.z + static_cast(z) * strataSize.z + ); + Vector3 strataMax = strataMin + strataSize; + + // Random point within stratum + outPoints.emplace_back( + rng.NextFloat(strataMin.x, strataMax.x), + rng.NextFloat(strataMin.y, strataMax.y), + rng.NextFloat(strataMin.z, strataMax.z) + ); + ++count; + } + } + } + } + + /** + * @brief Generate Halton sequence points in a cell + * @param bounds Cell bounds + * @param numSamples Number of samples to generate + * @param outPoints Output vector of sample points + */ + void GenerateHaltonPoints( + const AABB &bounds, + uint32_t numSamples, + std::vector &outPoints) + { + outPoints.clear(); + outPoints.reserve(numSamples); + + Vector3 size = bounds.max - bounds.min; + + for (uint32_t i = 1; i <= numSamples; ++i) { + Vector3 point( + bounds.min.x + HaltonSequence(i, 2) * size.x, + bounds.min.y + HaltonSequence(i, 3) * size.y, + bounds.min.z + HaltonSequence(i, 5) * size.z + ); + outPoints.emplace_back(point); + } + } + + /** + * @brief Generate random directions on unit sphere + * @param numDirections Number of directions to generate + * @param outDirections Output vector of directions + */ + void GenerateRandomDirections( + uint32_t numDirections, + std::vector &outDirections) + { + outDirections.clear(); + outDirections.reserve(numDirections); + + for (uint32_t i = 0; i < numDirections; ++i) { + outDirections.emplace_back(GenerateRandomDirection()); + } + } + + /** + * @brief Generate Fibonacci sphere directions + * @param numDirections Number of directions to generate + * @param outDirections Output vector of directions + */ + static void GenerateFibonacciDirections( + uint32_t numDirections, + std::vector &outDirections) + { + outDirections.clear(); + outDirections.reserve(numDirections); + + for (uint32_t i = 0; i < numDirections; ++i) { + outDirections.emplace_back(FibonacciSphereDirection(i, numDirections)); + } + } + + /** + * @brief Generate complete cell samples (points + directions) + * + * Main function for generating sample points within a cell and + * sampling directions for each point, used in ray-based PVS baking. + * + * @param cellBounds Bounds of the PVS cell + * @param config Sampling configuration + * @param outSamples Output cell samples + */ + void GenerateCellSamples( + const AABB &cellBounds, + const PVSCellSamplingConfig &config, + PVSCellSamples &outSamples) + { + outSamples.samples.clear(); + outSamples.samples.reserve(config.numSamplesPerCell * config.numDirectionsPerSample); + + // Generate sample points + std::vector points; + switch (config.pointStrategy) { + case PVSSamplingStrategy::RANDOM: + points.reserve(config.numSamplesPerCell); + for (uint32_t i = 0; i < config.numSamplesPerCell; ++i) { + points.emplace_back(GenerateRandomPointInCell(cellBounds)); + } + break; + case PVSSamplingStrategy::STRATIFIED: + GenerateStratifiedPoints(cellBounds, config.numSamplesPerCell, points); + break; + case PVSSamplingStrategy::HALTON: + GenerateHaltonPoints(cellBounds, config.numSamplesPerCell, points); + break; + default: + // Default to stratified + GenerateStratifiedPoints(cellBounds, config.numSamplesPerCell, points); + break; + } + + // Generate directions + std::vector directions; + switch (config.directionStrategy) { + case PVSSamplingStrategy::RANDOM: + GenerateRandomDirections(config.numDirectionsPerSample, directions); + break; + case PVSSamplingStrategy::FIBONACCI: + GenerateFibonacciDirections(config.numDirectionsPerSample, directions); + break; + default: + GenerateFibonacciDirections(config.numDirectionsPerSample, directions); + break; + } + + // Combine points and directions + for (const auto &point : points) { + if (config.useHemisphereForFloors) { + // For floor cells, only cast rays upward + Vector3 upNormal(0, 1, 0); + for (uint32_t i = 0; i < config.numDirectionsPerSample; ++i) { + PVSSample sample; + sample.position = point; + sample.direction = GenerateHemisphereDirection(upNormal); + outSamples.samples.push_back(sample); + } + } else { + // Full sphere sampling + for (const auto &dir : directions) { + PVSSample sample; + sample.position = point; + sample.direction = dir; + outSamples.samples.push_back(sample); + } + } + } + } + + /** + * @brief Generate sample points within cell (convenience function) + * @param cellBounds Cell bounds + * @param numSamples Number of samples + * @param strategy Sampling strategy + * @param outPoints Output points + */ + void GeneratePointsInCell( + const AABB &cellBounds, + uint32_t numSamples, + PVSSamplingStrategy strategy, + std::vector &outPoints) + { + switch (strategy) { + case PVSSamplingStrategy::RANDOM: + outPoints.clear(); + outPoints.reserve(numSamples); + for (uint32_t i = 0; i < numSamples; ++i) { + outPoints.emplace_back(GenerateRandomPointInCell(cellBounds)); + } + break; + case PVSSamplingStrategy::STRATIFIED: + GenerateStratifiedPoints(cellBounds, numSamples, outPoints); + break; + case PVSSamplingStrategy::HALTON: + GenerateHaltonPoints(cellBounds, numSamples, outPoints); + break; + default: + GenerateStratifiedPoints(cellBounds, numSamples, outPoints); + break; + } + } + + /** + * @brief Generate sample directions (convenience function) + * @param numDirections Number of directions + * @param strategy Sampling strategy + * @param outDirections Output directions + */ + void GenerateDirections( + uint32_t numDirections, + PVSSamplingStrategy strategy, + std::vector &outDirections) + { + switch (strategy) { + case PVSSamplingStrategy::RANDOM: + GenerateRandomDirections(numDirections, outDirections); + break; + case PVSSamplingStrategy::FIBONACCI: + GenerateFibonacciDirections(numDirections, outDirections); + break; + default: + GenerateFibonacciDirections(numDirections, outDirections); + break; + } + } + + /** + * @brief Set the random seed + */ + void SetSeed(uint64_t seed) + { + rng.Seed(seed); + } + + private: + static constexpr float PI = 3.14159265358979323846f; + PVSRandomGenerator rng; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSStreamingManager.h b/runtime/render/core/include/render/culling/PVSStreamingManager.h new file mode 100644 index 00000000..7d6c4208 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSStreamingManager.h @@ -0,0 +1,300 @@ +// +// Created by SkyEngine on 2024/02/20. +// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sky { + + class RenderPrimitive; + class SceneView; + + /** + * @brief Manages streaming of PVS data for large world scenarios + * + * The PVSStreamingManager divides the world into sectors and streams + * them in/out based on camera position. This allows handling worlds + * much larger than available memory. + * + * Key features: + * - Automatic sector loading/unloading based on distance + * - LRU cache for loaded sectors + * - Async loading support + * - Cross-sector visibility queries + */ + class PVSStreamingManager { + public: + PVSStreamingManager() = default; + ~PVSStreamingManager() = default; + + /** + * @brief Initialize the streaming manager + * @param config Streaming configuration + */ + void Initialize(const PVSStreamingConfig &config); + + /** + * @brief Clear all data and reset + */ + void Clear(); + + /** + * @brief Set the data provider for loading sector data + * + * The provider is called when a sector needs to be loaded. + * It should return true and fill outData if the sector exists. + * + * @param provider Function to provide sector data + */ + void SetDataProvider(PVSSectorDataProvider provider); + + /** + * @brief Update streaming based on viewer position + * + * Call this every frame with the camera position. It will: + * - Load nearby sectors within loadRadius + * - Unload distant sectors beyond unloadRadius + * - Respect maxLoadedSectors limit using LRU + * + * @param viewerPosition Current camera/viewer position + * @param frameNumber Current frame number for LRU tracking + */ + void Update(const Vector3 &viewerPosition, uint64_t frameNumber); + + /** + * @brief Request a specific sector to be loaded + * @param coord Sector coordinates + * @param callback Optional callback when load completes + */ + void RequestSectorLoad(const PVSSectorCoord &coord, PVSSectorLoadCallback callback = nullptr); + + /** + * @brief Request a sector to be unloaded + * @param coord Sector coordinates + */ + void RequestSectorUnload(const PVSSectorCoord &coord); + + /** + * @brief Check if a sector is loaded + * @param coord Sector coordinates + * @return True if sector is loaded and ready + */ + bool IsSectorLoaded(const PVSSectorCoord &coord) const; + + /** + * @brief Get the sector coordinate for a world position + * @param position World position + * @return Sector coordinates + */ + PVSSectorCoord GetSectorCoord(const Vector3 &position) const; + + /** + * @brief Get the sector ID for coordinates + * @param coord Sector coordinates + * @return Sector ID or INVALID_PVS_SECTOR if not loaded + */ + PVSSectorID GetSectorID(const PVSSectorCoord &coord) const; + + /** + * @brief Query visibility across loaded sectors + * + * This queries visibility from the viewer position across all + * relevant loaded sectors. + * + * @param viewPosition Camera/view position + * @param sceneView Optional scene view for frustum culling + * @param result Vector to store visible primitives + * @return True if query was successful (relevant sectors loaded) + */ + bool QueryVisiblePrimitives( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const; + + /** + * @brief Iterate over visible primitives without allocation + * + * @param viewPosition Camera/view position + * @param sceneView Optional scene view for frustum culling + * @param callback Function called for each visible primitive + * @return Number of visible primitives, or 0 if sectors not loaded + */ + template + uint32_t ForEachVisiblePrimitive( + const Vector3 &viewPosition, + const SceneView *sceneView, + Func &&callback) const + { + PVSSectorCoord sectorCoord = GetSectorCoord(viewPosition); + + auto it = loadedSectors.find(sectorCoord); + if (it == loadedSectors.end() || it->second.info.state != PVSSectorState::LOADED) { + return 0; + } + + const LoadedSector §or = it->second; + + // Get cell ID within the sector + PVSCellID cellID = sector.pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + return 0; + } + + uint32_t count = 0; + const PVSBitSet &visibility = sector.pvsData.GetVisibilitySet(cellID); + + visibility.ForEachSetBit([&](uint32_t localObjID) { + if (localObjID < sector.primitives.size()) { + auto *primitive = sector.primitives[localObjID]; + if (primitive != nullptr) { + callback(primitive); + ++count; + } + } + }); + + return count; + } + + /** + * @brief Check if a position has valid visibility data loaded + * @param position World position + * @return True if the sector containing position is loaded + */ + bool IsPositionLoaded(const Vector3 &position) const; + + /** + * @brief Get the number of currently loaded sectors + */ + uint32_t GetLoadedSectorCount() const { return static_cast(loadedSectors.size()); } + + /** + * @brief Get all loaded sector coordinates + * @param result Vector to store coordinates + */ + void GetLoadedSectors(std::vector &result) const; + + /** + * @brief Get streaming statistics + */ + struct Statistics { + uint32_t loadedSectors = 0; + uint32_t pendingLoads = 0; + uint32_t pendingUnloads = 0; + size_t totalMemoryUsed = 0; + float averageSectorSize = 0.0f; + }; + Statistics GetStatistics() const; + + /** + * @brief Register a primitive with a specific sector + * + * Objects that span multiple sectors should be registered with each sector. + * + * @param coord Sector coordinates + * @param primitive Render primitive + * @param localObjectID The object ID within the sector's PVS data + */ + void RegisterPrimitive(const PVSSectorCoord &coord, RenderPrimitive *primitive, uint32_t localObjectID); + + /** + * @brief Unregister a primitive from a sector + * @param coord Sector coordinates + * @param primitive Render primitive + */ + void UnregisterPrimitive(const PVSSectorCoord &coord, RenderPrimitive *primitive); + + /** + * @brief Get the configuration + */ + const PVSStreamingConfig& GetConfig() const { return config; } + + private: + /** + * @brief Internal representation of a loaded sector + */ + struct LoadedSector { + PVSSectorInfo info; + PVSData pvsData; + std::vector primitives; // Local ID -> primitive + size_t memorySize = 0; + + // For tracking primitive to local ID mapping + std::unordered_map primitiveToLocalID; + }; + + /** + * @brief Load a sector synchronously + */ + bool LoadSectorInternal(const PVSSectorCoord &coord); + + /** + * @brief Unload a sector + */ + void UnloadSectorInternal(const PVSSectorCoord &coord); + + /** + * @brief Get sectors to load based on distance + */ + void GetSectorsToLoad(const Vector3 &position, std::vector &result) const; + + /** + * @brief Get sectors to unload based on distance + */ + void GetSectorsToUnload(const Vector3 &position, std::vector &result) const; + + /** + * @brief Enforce LRU limit on loaded sectors + */ + void EnforceSectorLimit(); + + /** + * @brief Calculate distance from sector center to position + */ + float GetDistanceToSector(const PVSSectorCoord &coord, const Vector3 &position) const; + + /** + * @brief Get sector bounds from coordinates + */ + AABB GetSectorBounds(const PVSSectorCoord &coord) const; + + /** + * @brief Get sector center from coordinates + */ + Vector3 GetSectorCenter(const PVSSectorCoord &coord) const; + + PVSStreamingConfig config; + bool initialized = false; + + // Sector grid info + PVSSectorCoord gridDimensions; + Vector3 invSectorSize; // 1/sectorSize for fast lookup + + // Loaded sectors + std::unordered_map loadedSectors; + + // Pending operations + std::unordered_set pendingLoads; + std::unordered_set pendingUnloads; + + // Data provider + PVSSectorDataProvider dataProvider; + + // Load callbacks + std::unordered_map, PVSSectorCoordHash> loadCallbacks; + + // Current viewer position for LRU + Vector3 currentViewerPosition; + uint64_t currentFrame = 0; + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSStreamingTypes.h b/runtime/render/core/include/render/culling/PVSStreamingTypes.h new file mode 100644 index 00000000..abfa6ea9 --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSStreamingTypes.h @@ -0,0 +1,138 @@ +// +// Created by SkyEngine on 2024/02/20. +// + +#pragma once + +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Unique identifier for a world sector + * + * A sector is a large chunk of the world that contains multiple PVS cells. + * Sectors are streamed in/out based on player position. + */ + using PVSSectorID = uint32_t; + static constexpr PVSSectorID INVALID_PVS_SECTOR = ~(0U); + + /** + * @brief Grid coordinates for a sector in the world + */ + struct PVSSectorCoord { + int32_t x = 0; + int32_t y = 0; + int32_t z = 0; + + bool operator==(const PVSSectorCoord &other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator!=(const PVSSectorCoord &other) const { + return !(*this == other); + } + }; + + /** + * @brief Hash function for PVSSectorCoord + */ + struct PVSSectorCoordHash { + size_t operator()(const PVSSectorCoord &coord) const { + size_t h1 = std::hash{}(coord.x); + size_t h2 = std::hash{}(coord.y); + size_t h3 = std::hash{}(coord.z); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + + /** + * @brief Configuration for streaming PVS sectors + */ + struct PVSStreamingConfig { + // World configuration + AABB worldBounds; // Total world bounds (can be very large) + Vector3 sectorSize; // Size of each sector in world units + Vector3 cellSize; // Size of each cell within sectors + + // Streaming configuration + float loadRadius = 200.0f; // Distance to load sectors + float unloadRadius = 300.0f; // Distance to unload sectors (should be > loadRadius) + uint32_t maxLoadedSectors = 16; // Maximum sectors in memory + bool preloadNeighbors = true; // Preload adjacent sectors + + // Object configuration + uint32_t maxObjectsPerSector = 1024; // Max objects per sector + }; + + /** + * @brief State of a streaming sector + */ + enum class PVSSectorState { + UNLOADED, // Not in memory + LOADING, // Being loaded asynchronously + LOADED, // Fully loaded and ready + UNLOADING // Being unloaded + }; + + /** + * @brief Information about a streaming sector + */ + struct PVSSectorInfo { + PVSSectorID id = INVALID_PVS_SECTOR; + PVSSectorCoord coord; + AABB bounds; + Vector3 center; + PVSSectorState state = PVSSectorState::UNLOADED; + + // Usage tracking for LRU + uint64_t lastUsedFrame = 0; + float distanceToViewer = 0.0f; + }; + + /** + * @brief Baked data for a single sector + * + * This is what gets serialized to disk per sector file. + */ + struct PVSSectorBakedData { + static constexpr uint32_t VERSION = 1; + + // Sector identification + PVSSectorCoord coord; + AABB bounds; + + // PVS data for this sector + PVSBakedData pvsData; + + // Object ID mapping: local ID -> global object name + // This allows objects to be identified across sector boundaries + std::vector objectNames; + + /** + * @brief Save sector data to binary archive + */ + void Save(class BinaryOutputArchive &archive) const; + + /** + * @brief Load sector data from binary archive + */ + void Load(class BinaryInputArchive &archive); + + /** + * @brief Get memory size of sector data + */ + size_t GetMemorySize() const; + }; + + /** + * @brief Callback types for async sector operations + */ + using PVSSectorLoadCallback = std::function; + using PVSSectorUnloadCallback = std::function; + using PVSSectorDataProvider = std::function; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/PVSTypes.h b/runtime/render/core/include/render/culling/PVSTypes.h new file mode 100644 index 00000000..b1494bfa --- /dev/null +++ b/runtime/render/core/include/render/culling/PVSTypes.h @@ -0,0 +1,74 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#pragma once + +#include +#include +#include +#include + +namespace sky { + + /** + * @brief Unique identifier for a PVS cell + */ + using PVSCellID = uint32_t; + static constexpr PVSCellID INVALID_PVS_CELL = ~(0U); + + /** + * @brief Unique identifier for an object in the PVS system + */ + using PVSObjectID = uint32_t; + static constexpr PVSObjectID INVALID_PVS_OBJECT = ~(0U); + + /** + * @brief Configuration for PVS grid generation + */ + struct PVSConfig { + AABB worldBounds; // World space bounds for the entire PVS system + Vector3 cellSize; // Size of each cell in world units + uint32_t maxObjects = 4096; // Maximum number of objects to track + bool enablePortals = false; // Whether to use portal-based visibility + }; + + /** + * @brief Represents a single cell in the PVS grid + */ + struct PVSCell { + PVSCellID id = INVALID_PVS_CELL; + AABB bounds; + Vector3 center; + }; + + /** + * @brief Grid coordinates for a PVS cell + */ + struct PVSCellCoord { + int32_t x = 0; + int32_t y = 0; + int32_t z = 0; + + bool operator==(const PVSCellCoord &other) const { + return x == other.x && y == other.y && z == other.z; + } + + bool operator!=(const PVSCellCoord &other) const { + return !(*this == other); + } + }; + + /** + * @brief Hash function for PVSCellCoord + */ + struct PVSCellCoordHash { + size_t operator()(const PVSCellCoord &coord) const { + size_t h1 = std::hash{}(coord.x); + size_t h2 = std::hash{}(coord.y); + size_t h3 = std::hash{}(coord.z); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + +} // namespace sky diff --git a/runtime/render/core/include/render/culling/SIMDUtils.h b/runtime/render/core/include/render/culling/SIMDUtils.h new file mode 100644 index 00000000..895281ca --- /dev/null +++ b/runtime/render/core/include/render/culling/SIMDUtils.h @@ -0,0 +1,430 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#pragma once + +#include +#include + +// Platform detection and SIMD include +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + #define SKY_SIMD_SSE 1 + #include // SSE2 + #include // SSE + #if defined(__SSE4_1__) || defined(__AVX__) + #include // SSE4.1 + #define SKY_SIMD_SSE4 1 + #endif + #if defined(__AVX2__) + #include // AVX2 + #define SKY_SIMD_AVX2 1 + #endif +#elif defined(__ARM_NEON) || defined(__ARM_NEON__) + #define SKY_SIMD_NEON 1 + #include +#else + #define SKY_SIMD_NONE 1 +#endif + +#ifdef _MSC_VER + #include +#endif + +namespace sky { +namespace simd { + + /** + * @brief SIMD-accelerated bitwise OR of two uint64_t arrays + * @param dst Destination array (also first source) + * @param src Source array to OR with + * @param count Number of uint64_t elements + */ + inline void BitwiseOr(uint64_t* dst, const uint64_t* src, size_t count) + { +#if defined(SKY_SIMD_AVX2) + // Process 4 uint64_t (256 bits) at a time with AVX2 + size_t simdCount = count / 4; + for (size_t i = 0; i < simdCount; ++i) { + __m256i a = _mm256_loadu_si256(reinterpret_cast(dst + i * 4)); + __m256i b = _mm256_loadu_si256(reinterpret_cast(src + i * 4)); + __m256i result = _mm256_or_si256(a, b); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(dst + i * 4), result); + } + // Handle remaining elements + for (size_t i = simdCount * 4; i < count; ++i) { + dst[i] |= src[i]; + } +#elif defined(SKY_SIMD_SSE) + // Process 2 uint64_t (128 bits) at a time with SSE2 + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + __m128i a = _mm_loadu_si128(reinterpret_cast(dst + i * 2)); + __m128i b = _mm_loadu_si128(reinterpret_cast(src + i * 2)); + __m128i result = _mm_or_si128(a, b); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i * 2), result); + } + // Handle remaining element + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] |= src[i]; + } +#elif defined(SKY_SIMD_NEON) + // Process 2 uint64_t (128 bits) at a time with NEON + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + uint64x2_t a = vld1q_u64(dst + i * 2); + uint64x2_t b = vld1q_u64(src + i * 2); + uint64x2_t result = vorrq_u64(a, b); + vst1q_u64(dst + i * 2, result); + } + // Handle remaining element + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] |= src[i]; + } +#else + // Scalar fallback + for (size_t i = 0; i < count; ++i) { + dst[i] |= src[i]; + } +#endif + } + + /** + * @brief SIMD-accelerated bitwise AND of two uint64_t arrays + * @param dst Destination array (also first source) + * @param src Source array to AND with + * @param count Number of uint64_t elements + */ + inline void BitwiseAnd(uint64_t* dst, const uint64_t* src, size_t count) + { +#if defined(SKY_SIMD_AVX2) + size_t simdCount = count / 4; + for (size_t i = 0; i < simdCount; ++i) { + __m256i a = _mm256_loadu_si256(reinterpret_cast(dst + i * 4)); + __m256i b = _mm256_loadu_si256(reinterpret_cast(src + i * 4)); + __m256i result = _mm256_and_si256(a, b); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(dst + i * 4), result); + } + for (size_t i = simdCount * 4; i < count; ++i) { + dst[i] &= src[i]; + } +#elif defined(SKY_SIMD_SSE) + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + __m128i a = _mm_loadu_si128(reinterpret_cast(dst + i * 2)); + __m128i b = _mm_loadu_si128(reinterpret_cast(src + i * 2)); + __m128i result = _mm_and_si128(a, b); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i * 2), result); + } + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] &= src[i]; + } +#elif defined(SKY_SIMD_NEON) + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + uint64x2_t a = vld1q_u64(dst + i * 2); + uint64x2_t b = vld1q_u64(src + i * 2); + uint64x2_t result = vandq_u64(a, b); + vst1q_u64(dst + i * 2, result); + } + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] &= src[i]; + } +#else + for (size_t i = 0; i < count; ++i) { + dst[i] &= src[i]; + } +#endif + } + + /** + * @brief SIMD-accelerated check if any bit is set in uint64_t array + * @param data Array to check + * @param count Number of uint64_t elements + * @return true if any bit is set + */ + inline bool AnyBitSet(const uint64_t* data, size_t count) + { +#if defined(SKY_SIMD_AVX2) + size_t simdCount = count / 4; + for (size_t i = 0; i < simdCount; ++i) { + __m256i v = _mm256_loadu_si256(reinterpret_cast(data + i * 4)); + if (!_mm256_testz_si256(v, v)) { + return true; + } + } + for (size_t i = simdCount * 4; i < count; ++i) { + if (data[i] != 0) { + return true; + } + } + return false; +#elif defined(SKY_SIMD_SSE4) + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + __m128i v = _mm_loadu_si128(reinterpret_cast(data + i * 2)); + if (!_mm_testz_si128(v, v)) { + return true; + } + } + for (size_t i = simdCount * 2; i < count; ++i) { + if (data[i] != 0) { + return true; + } + } + return false; +#elif defined(SKY_SIMD_SSE) + // SSE2 fallback using comparison + __m128i zero = _mm_setzero_si128(); + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + __m128i v = _mm_loadu_si128(reinterpret_cast(data + i * 2)); + __m128i cmp = _mm_cmpeq_epi32(v, zero); + if (_mm_movemask_epi8(cmp) != 0xFFFF) { + return true; + } + } + for (size_t i = simdCount * 2; i < count; ++i) { + if (data[i] != 0) { + return true; + } + } + return false; +#elif defined(SKY_SIMD_NEON) + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + uint64x2_t v = vld1q_u64(data + i * 2); + uint64x2_t zero = vdupq_n_u64(0); + uint64x2_t cmp = vceqq_u64(v, zero); + // Check if any lane is not zero + if (vgetq_lane_u64(cmp, 0) != ~0ULL || vgetq_lane_u64(cmp, 1) != ~0ULL) { + return true; + } + } + for (size_t i = simdCount * 2; i < count; ++i) { + if (data[i] != 0) { + return true; + } + } + return false; +#else + for (size_t i = 0; i < count; ++i) { + if (data[i] != 0) { + return true; + } + } + return false; +#endif + } + + /** + * @brief Population count for a single uint64_t + * @param value Value to count bits in + * @return Number of set bits + */ + inline uint32_t PopCount64(uint64_t value) + { +#ifdef _MSC_VER + return static_cast(__popcnt64(value)); +#else + return static_cast(__builtin_popcountll(value)); +#endif + } + + /** + * @brief SIMD-accelerated population count for uint64_t array + * @param data Array to count bits in + * @param count Number of uint64_t elements + * @return Total number of set bits + */ + inline uint32_t PopCountArray(const uint64_t* data, size_t count) + { + uint32_t total = 0; + +#if defined(SKY_SIMD_AVX2) && defined(__AVX2__) + // AVX2 popcount using parallel technique + // Note: Hardware popcount via POPCNT instruction is typically used per-word + // This still processes sequentially but benefits from cache prefetching + for (size_t i = 0; i < count; ++i) { + total += PopCount64(data[i]); + } +#else + // Use scalar popcount (which often uses hardware POPCNT instruction) + for (size_t i = 0; i < count; ++i) { + total += PopCount64(data[i]); + } +#endif + return total; + } + + /** + * @brief SIMD-accelerated zero fill for uint64_t array + * @param dst Destination array + * @param count Number of uint64_t elements + */ + inline void ZeroFill(uint64_t* dst, size_t count) + { +#if defined(SKY_SIMD_AVX2) + __m256i zero = _mm256_setzero_si256(); + size_t simdCount = count / 4; + for (size_t i = 0; i < simdCount; ++i) { + _mm256_storeu_si256(reinterpret_cast<__m256i*>(dst + i * 4), zero); + } + for (size_t i = simdCount * 4; i < count; ++i) { + dst[i] = 0; + } +#elif defined(SKY_SIMD_SSE) + __m128i zero = _mm_setzero_si128(); + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i * 2), zero); + } + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] = 0; + } +#elif defined(SKY_SIMD_NEON) + uint64x2_t zero = vdupq_n_u64(0); + size_t simdCount = count / 2; + for (size_t i = 0; i < simdCount; ++i) { + vst1q_u64(dst + i * 2, zero); + } + for (size_t i = simdCount * 2; i < count; ++i) { + dst[i] = 0; + } +#else + std::memset(dst, 0, count * sizeof(uint64_t)); +#endif + } + + /** + * @brief SIMD-accelerated AABB-AABB intersection test + * + * Tests intersection between multiple AABB pairs efficiently + * + * @param minA Min corners of AABBs A (xyz triplets, count * 3 floats) + * @param maxA Max corners of AABBs A + * @param minB Min corners of AABBs B + * @param maxB Max corners of AABBs B + * @param results Output: 1 if intersecting, 0 if not + * @param count Number of AABB pairs to test + */ + inline void AABBIntersectionBatch( + const float* minA, const float* maxA, + const float* minB, const float* maxB, + uint8_t* results, size_t count) + { +#if defined(SKY_SIMD_SSE) + // Process one AABB pair at a time using SSE + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + + // Load AABB corners (xyz) + // minA[x,y,z], maxA[x,y,z], minB[x,y,z], maxB[x,y,z] + __m128 aMin = _mm_set_ps(0.0f, minA[offset + 2], minA[offset + 1], minA[offset + 0]); + __m128 aMax = _mm_set_ps(0.0f, maxA[offset + 2], maxA[offset + 1], maxA[offset + 0]); + __m128 bMin = _mm_set_ps(0.0f, minB[offset + 2], minB[offset + 1], minB[offset + 0]); + __m128 bMax = _mm_set_ps(0.0f, maxB[offset + 2], maxB[offset + 1], maxB[offset + 0]); + + // Test: aMin <= bMax && bMin <= aMax for all axes + __m128 test1 = _mm_cmple_ps(aMin, bMax); + __m128 test2 = _mm_cmple_ps(bMin, aMax); + __m128 combined = _mm_and_ps(test1, test2); + + // Extract results (check x, y, z lanes) + int mask = _mm_movemask_ps(combined); + // Bits 0,1,2 correspond to x,y,z. All must be set (0x7) + results[i] = (mask & 0x7) == 0x7 ? 1 : 0; + } +#elif defined(SKY_SIMD_NEON) + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + + // Load and test each axis + float32x4_t aMin = {minA[offset + 0], minA[offset + 1], minA[offset + 2], 0.0f}; + float32x4_t aMax = {maxA[offset + 0], maxA[offset + 1], maxA[offset + 2], 0.0f}; + float32x4_t bMin = {minB[offset + 0], minB[offset + 1], minB[offset + 2], 0.0f}; + float32x4_t bMax = {maxB[offset + 0], maxB[offset + 1], maxB[offset + 2], 0.0f}; + + uint32x4_t test1 = vcleq_f32(aMin, bMax); + uint32x4_t test2 = vcleq_f32(bMin, aMax); + uint32x4_t combined = vandq_u32(test1, test2); + + // Check if x, y, z all pass + results[i] = (vgetq_lane_u32(combined, 0) && + vgetq_lane_u32(combined, 1) && + vgetq_lane_u32(combined, 2)) ? 1 : 0; + } +#else + // Scalar fallback + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + bool intersects = + (minA[offset + 0] <= maxB[offset + 0] && maxA[offset + 0] >= minB[offset + 0]) && + (minA[offset + 1] <= maxB[offset + 1] && maxA[offset + 1] >= minB[offset + 1]) && + (minA[offset + 2] <= maxB[offset + 2] && maxA[offset + 2] >= minB[offset + 2]); + results[i] = intersects ? 1 : 0; + } +#endif + } + + /** + * @brief SIMD-accelerated squared distance calculation + * @param pointsA First set of points (xyz triplets) + * @param pointsB Second set of points (xyz triplets) + * @param distancesSq Output squared distances + * @param count Number of point pairs + */ + inline void DistanceSquaredBatch( + const float* pointsA, + const float* pointsB, + float* distancesSq, + size_t count) + { +#if defined(SKY_SIMD_SSE) + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + + __m128 a = _mm_set_ps(0.0f, pointsA[offset + 2], pointsA[offset + 1], pointsA[offset + 0]); + __m128 b = _mm_set_ps(0.0f, pointsB[offset + 2], pointsB[offset + 1], pointsB[offset + 0]); + + __m128 diff = _mm_sub_ps(a, b); + __m128 sq = _mm_mul_ps(diff, diff); + + // Sum x^2 + y^2 + z^2 using SSE2-compatible horizontal sum + // sq = [x^2, y^2, z^2, 0] + __m128 shuf = _mm_shuffle_ps(sq, sq, _MM_SHUFFLE(2, 3, 0, 1)); // [y^2, x^2, 0, z^2] + __m128 sums = _mm_add_ps(sq, shuf); // [x^2+y^2, y^2+x^2, z^2+0, 0+z^2] + shuf = _mm_movehl_ps(shuf, sums); // [z^2+0, 0+z^2, ?, ?] + sums = _mm_add_ss(sums, shuf); // [x^2+y^2+z^2, ...] + + _mm_store_ss(&distancesSq[i], sums); + } +#elif defined(SKY_SIMD_NEON) + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + + float32x4_t a = {pointsA[offset + 0], pointsA[offset + 1], pointsA[offset + 2], 0.0f}; + float32x4_t b = {pointsB[offset + 0], pointsB[offset + 1], pointsB[offset + 2], 0.0f}; + + float32x4_t diff = vsubq_f32(a, b); + float32x4_t sq = vmulq_f32(diff, diff); + + // Sum horizontally + float32x2_t sum = vadd_f32(vget_low_f32(sq), vget_high_f32(sq)); + sum = vpadd_f32(sum, sum); + + distancesSq[i] = vget_lane_f32(sum, 0); + } +#else + for (size_t i = 0; i < count; ++i) { + size_t offset = i * 3; + float dx = pointsA[offset + 0] - pointsB[offset + 0]; + float dy = pointsA[offset + 1] - pointsB[offset + 1]; + float dz = pointsA[offset + 2] - pointsB[offset + 2]; + distancesSq[i] = dx * dx + dy * dy + dz * dz; + } +#endif + } + +} // namespace simd +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSBakedData.cpp b/runtime/render/core/src/culling/PVSBakedData.cpp new file mode 100644 index 00000000..258c4109 --- /dev/null +++ b/runtime/render/core/src/culling/PVSBakedData.cpp @@ -0,0 +1,196 @@ +// +// Created by SkyEngine on 2024/02/16. +// + +#include +#include + +namespace sky { + + void PVSBakedData::Save(BinaryOutputArchive &archive) const + { + // Save version + archive.SaveValue(VERSION); + + // Save config + archive.SaveValue(config.worldBounds.min.x); + archive.SaveValue(config.worldBounds.min.y); + archive.SaveValue(config.worldBounds.min.z); + archive.SaveValue(config.worldBounds.max.x); + archive.SaveValue(config.worldBounds.max.y); + archive.SaveValue(config.worldBounds.max.z); + archive.SaveValue(config.cellSize.x); + archive.SaveValue(config.cellSize.y); + archive.SaveValue(config.cellSize.z); + archive.SaveValue(config.maxObjects); + archive.SaveValue(config.enablePortals); + + // Save grid dimensions + archive.SaveValue(gridDimensions.x); + archive.SaveValue(gridDimensions.y); + archive.SaveValue(gridDimensions.z); + + // Save number of objects + archive.SaveValue(numObjects); + + // Save cells + uint32_t numCells = static_cast(cells.size()); + archive.SaveValue(numCells); + for (const auto &cell : cells) { + archive.SaveValue(cell.id); + archive.SaveValue(cell.bounds.min.x); + archive.SaveValue(cell.bounds.min.y); + archive.SaveValue(cell.bounds.min.z); + archive.SaveValue(cell.bounds.max.x); + archive.SaveValue(cell.bounds.max.y); + archive.SaveValue(cell.bounds.max.z); + archive.SaveValue(cell.center.x); + archive.SaveValue(cell.center.y); + archive.SaveValue(cell.center.z); + } + + // Save visibility data + uint32_t numVisibilitySets = static_cast(visibilityData.size()); + archive.SaveValue(numVisibilitySets); + for (const auto &bitsetData : visibilityData) { + uint32_t numWords = static_cast(bitsetData.size()); + archive.SaveValue(numWords); + if (numWords > 0) { + archive.SaveValue( + reinterpret_cast(bitsetData.data()), + numWords * sizeof(uint64_t) + ); + } + } + + // Save object names + uint32_t numNames = static_cast(objectNames.size()); + archive.SaveValue(numNames); + for (const auto &name : objectNames) { + archive.SaveValue(name); + } + } + + void PVSBakedData::Load(BinaryInputArchive &archive) + { + // Load and verify version + uint32_t version = 0; + archive.LoadValue(version); + if (version != VERSION) { + // Handle version mismatch - for now, we only support current version + return; + } + + // Load config + archive.LoadValue(config.worldBounds.min.x); + archive.LoadValue(config.worldBounds.min.y); + archive.LoadValue(config.worldBounds.min.z); + archive.LoadValue(config.worldBounds.max.x); + archive.LoadValue(config.worldBounds.max.y); + archive.LoadValue(config.worldBounds.max.z); + archive.LoadValue(config.cellSize.x); + archive.LoadValue(config.cellSize.y); + archive.LoadValue(config.cellSize.z); + archive.LoadValue(config.maxObjects); + archive.LoadValue(config.enablePortals); + + // Load grid dimensions + archive.LoadValue(gridDimensions.x); + archive.LoadValue(gridDimensions.y); + archive.LoadValue(gridDimensions.z); + + // Load number of objects + archive.LoadValue(numObjects); + + // Load cells + uint32_t numCells = 0; + archive.LoadValue(numCells); + cells.resize(numCells); + for (auto &cell : cells) { + archive.LoadValue(cell.id); + archive.LoadValue(cell.bounds.min.x); + archive.LoadValue(cell.bounds.min.y); + archive.LoadValue(cell.bounds.min.z); + archive.LoadValue(cell.bounds.max.x); + archive.LoadValue(cell.bounds.max.y); + archive.LoadValue(cell.bounds.max.z); + archive.LoadValue(cell.center.x); + archive.LoadValue(cell.center.y); + archive.LoadValue(cell.center.z); + } + + // Load visibility data + uint32_t numVisibilitySets = 0; + archive.LoadValue(numVisibilitySets); + visibilityData.resize(numVisibilitySets); + for (auto &bitsetData : visibilityData) { + uint32_t numWords = 0; + archive.LoadValue(numWords); + bitsetData.resize(numWords); + if (numWords > 0) { + archive.LoadValue( + reinterpret_cast(bitsetData.data()), + numWords * sizeof(uint64_t) + ); + } + } + + // Load object names + uint32_t numNames = 0; + archive.LoadValue(numNames); + objectNames.resize(numNames); + for (auto &name : objectNames) { + archive.LoadValue(name); + } + } + + size_t PVSBakedData::GetMemorySize() const + { + size_t size = sizeof(PVSBakedData); + size += cells.size() * sizeof(PVSCell); + for (const auto &bitsetData : visibilityData) { + size += bitsetData.size() * sizeof(uint64_t); + } + for (const auto &name : objectNames) { + size += name.size(); + } + return size; + } + + PVSBakedData::Statistics PVSBakedData::GetStatistics() const + { + Statistics stats; + stats.totalCells = static_cast(cells.size()); + stats.totalObjects = numObjects; + + // Count visible pairs + uint32_t totalVisible = 0; + for (const auto &bitsetData : visibilityData) { + for (uint64_t word : bitsetData) { +#ifdef _MSC_VER + totalVisible += static_cast(__popcnt64(word)); +#else + totalVisible += static_cast(__builtin_popcountll(word)); +#endif + } + } + stats.totalVisiblePairs = totalVisible; + + if (stats.totalCells > 0) { + stats.averageVisibleObjects = + static_cast(totalVisible) / static_cast(stats.totalCells); + } + + // Calculate raw vs compressed size ratio + size_t rawSize = stats.totalCells * stats.totalObjects; // One bit per pair + stats.rawDataSize = GetMemorySize(); + + if (rawSize > 0) { + stats.compressionRatio = + static_cast(stats.rawDataSize) / static_cast(rawSize / 8); + } + + return stats; + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSBaker.cpp b/runtime/render/core/src/culling/PVSBaker.cpp new file mode 100644 index 00000000..b518ba04 --- /dev/null +++ b/runtime/render/core/src/culling/PVSBaker.cpp @@ -0,0 +1,294 @@ +// +// Created by SkyEngine on 2024/02/16. +// + +#include +#include +#include +#include +#include + +namespace sky { + + uint32_t PVSBaker::AddObject(const PVSBakeObject &object) + { + uint32_t index = static_cast(objects.size()); + objects.push_back(object); + return index; + } + + void PVSBaker::AddObjects(const std::vector &newObjects) + { + objects.reserve(objects.size() + newObjects.size()); + for (const auto &obj : newObjects) { + objects.push_back(obj); + } + } + + void PVSBaker::ClearObjects() + { + objects.clear(); + } + + void PVSBaker::Cancel() + { + cancelRequested.store(true); + } + + void PVSBaker::InitializeOutputData(const PVSBakeConfig &config, PVSBakedData &outData) + { + outData.config = config.pvsConfig; + outData.numObjects = static_cast(objects.size()); + + // Calculate grid dimensions + Vector3 worldSize = config.pvsConfig.worldBounds.max - config.pvsConfig.worldBounds.min; + + outData.gridDimensions.x = static_cast( + std::ceil(worldSize.x / config.pvsConfig.cellSize.x)); + outData.gridDimensions.y = static_cast( + std::ceil(worldSize.y / config.pvsConfig.cellSize.y)); + outData.gridDimensions.z = static_cast( + std::ceil(worldSize.z / config.pvsConfig.cellSize.z)); + + // Ensure at least one cell + outData.gridDimensions.x = std::max(1, outData.gridDimensions.x); + outData.gridDimensions.y = std::max(1, outData.gridDimensions.y); + outData.gridDimensions.z = std::max(1, outData.gridDimensions.z); + + uint32_t totalCells = static_cast( + outData.gridDimensions.x * outData.gridDimensions.y * outData.gridDimensions.z); + + // Initialize cells + outData.cells.resize(totalCells); + for (int32_t z = 0; z < outData.gridDimensions.z; ++z) { + for (int32_t y = 0; y < outData.gridDimensions.y; ++y) { + for (int32_t x = 0; x < outData.gridDimensions.x; ++x) { + uint32_t cellID = static_cast( + z * (outData.gridDimensions.x * outData.gridDimensions.y) + + y * outData.gridDimensions.x + x); + + Vector3 cellMin = config.pvsConfig.worldBounds.min + Vector3( + static_cast(x) * config.pvsConfig.cellSize.x, + static_cast(y) * config.pvsConfig.cellSize.y, + static_cast(z) * config.pvsConfig.cellSize.z + ); + + Vector3 cellMax = Vector3( + std::min(cellMin.x + config.pvsConfig.cellSize.x, + config.pvsConfig.worldBounds.max.x), + std::min(cellMin.y + config.pvsConfig.cellSize.y, + config.pvsConfig.worldBounds.max.y), + std::min(cellMin.z + config.pvsConfig.cellSize.z, + config.pvsConfig.worldBounds.max.z) + ); + + outData.cells[cellID].id = cellID; + outData.cells[cellID].bounds = AABB{cellMin, cellMax}; + outData.cells[cellID].center = (cellMin + cellMax) * 0.5f; + } + } + } + + // Initialize visibility data (one bitset per cell) + uint32_t wordsPerCell = (outData.numObjects + 63) / 64; + outData.visibilityData.resize(totalCells); + for (auto &bitset : outData.visibilityData) { + bitset.resize(wordsPerCell, 0); + } + + // Copy object names + outData.objectNames.resize(objects.size()); + for (size_t i = 0; i < objects.size(); ++i) { + outData.objectNames[i] = objects[i].name; + } + } + + void PVSBaker::ReportProgress( + const PVSBakeConfig &config, + float progress, + const std::string &message) + { + if (config.progressCallback) { + config.progressCallback(progress, message); + } + } + + PVSBakeResult PVSBaker::Bake(const PVSBakeConfig &config, PVSBakedData &outData) + { + PVSBakeResult result; + + if (baking.exchange(true)) { + result.success = false; + result.errorMessage = "Baking already in progress"; + return result; + } + + cancelRequested.store(false); + + auto startTime = std::chrono::high_resolution_clock::now(); + + // Validate inputs + if (objects.empty()) { + result.success = false; + result.errorMessage = "No objects to bake"; + baking.store(false); + return result; + } + + ReportProgress(config, 0.0f, "Initializing PVS grid..."); + InitializeOutputData(config, outData); + + auto computeStart = std::chrono::high_resolution_clock::now(); + + // Choose baking method + switch (config.method) { + case PVSBakeConfig::Method::DISTANCE: + BakeDistanceBased(config, outData, result); + break; + case PVSBakeConfig::Method::RAYCAST: + BakeRayCast(config, outData, result); + break; + case PVSBakeConfig::Method::CUSTOM: + BakeCustom(config, outData, result); + break; + } + + auto computeEnd = std::chrono::high_resolution_clock::now(); + + if (!cancelRequested.load()) { + result.success = true; + result.statistics = outData.GetStatistics(); + ReportProgress(config, 1.0f, "Baking complete"); + } else { + result.success = false; + result.errorMessage = "Baking cancelled"; + } + + auto endTime = std::chrono::high_resolution_clock::now(); + + result.totalTimeSeconds = std::chrono::duration(endTime - startTime).count(); + result.computeTimeSeconds = std::chrono::duration(computeEnd - computeStart).count(); + + baking.store(false); + return result; + } + + void PVSBaker::BakeDistanceBased( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result) + { + float maxDistSq = config.maxVisibilityDistance * config.maxVisibilityDistance; + uint32_t totalCells = static_cast(outData.cells.size()); + uint32_t numObjects = static_cast(objects.size()); + + // Pre-compute object centers for SIMD processing + std::vector objectCenters(numObjects * 3); + for (uint32_t i = 0; i < numObjects; ++i) { + const AABB &bounds = objects[i].bounds; + objectCenters[i * 3 + 0] = (bounds.min.x + bounds.max.x) * 0.5f; + objectCenters[i * 3 + 1] = (bounds.min.y + bounds.max.y) * 0.5f; + objectCenters[i * 3 + 2] = (bounds.min.z + bounds.max.z) * 0.5f; + } + + // Batch processing for SIMD + constexpr uint32_t BATCH_SIZE = 64; + std::vector cellCenters(BATCH_SIZE * 3); + std::vector distancesSq(BATCH_SIZE); + + for (uint32_t cellID = 0; cellID < totalCells && !cancelRequested.load(); ++cellID) { + const PVSCell &cell = outData.cells[cellID]; + + // Pre-fill cell centers for batch + for (uint32_t i = 0; i < BATCH_SIZE; ++i) { + cellCenters[i * 3 + 0] = cell.center.x; + cellCenters[i * 3 + 1] = cell.center.y; + cellCenters[i * 3 + 2] = cell.center.z; + } + + // Process objects in batches + for (uint32_t batchStart = 0; batchStart < numObjects; batchStart += BATCH_SIZE) { + uint32_t batchEnd = std::min(batchStart + BATCH_SIZE, numObjects); + uint32_t batchCount = batchEnd - batchStart; + + // Use SIMD to calculate distances + simd::DistanceSquaredBatch( + cellCenters.data(), + objectCenters.data() + batchStart * 3, + distancesSq.data(), + batchCount + ); + + // Update visibility based on distance + for (uint32_t i = 0; i < batchCount; ++i) { + if (distancesSq[i] <= maxDistSq) { + uint32_t objID = batchStart + i; + uint32_t wordIndex = objID / 64; + uint32_t bitIndex = objID % 64; + outData.visibilityData[cellID][wordIndex] |= (1ULL << bitIndex); + } + } + } + + // Report progress periodically + if (cellID % 100 == 0) { + float progress = static_cast(cellID) / static_cast(totalCells); + ReportProgress(config, progress * 0.9f + 0.1f, + "Computing visibility: " + std::to_string(cellID) + "/" + std::to_string(totalCells)); + } + } + } + + void PVSBaker::BakeRayCast( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result) + { + // Ray-cast based baking would require a scene geometry representation + // For now, fall back to distance-based with a note in the result + result.errorMessage = "Ray-cast baking not yet implemented, using distance-based"; + BakeDistanceBased(config, outData, result); + } + + void PVSBaker::BakeCustom( + const PVSBakeConfig &config, + PVSBakedData &outData, + PVSBakeResult &result) + { + if (!config.customTestFunc) { + result.success = false; + result.errorMessage = "Custom test function not provided"; + return; + } + + uint32_t totalCells = static_cast(outData.cells.size()); + uint32_t numObjects = static_cast(objects.size()); + + for (uint32_t cellID = 0; cellID < totalCells && !cancelRequested.load(); ++cellID) { + const PVSCell &cell = outData.cells[cellID]; + + for (uint32_t objID = 0; objID < numObjects; ++objID) { + bool visible = config.customTestFunc( + cell.bounds, + cell.center, + objects[objID].bounds, + objID + ); + + if (visible) { + uint32_t wordIndex = objID / 64; + uint32_t bitIndex = objID % 64; + outData.visibilityData[cellID][wordIndex] |= (1ULL << bitIndex); + } + } + + // Report progress periodically + if (cellID % 100 == 0) { + float progress = static_cast(cellID) / static_cast(totalCells); + ReportProgress(config, progress * 0.9f + 0.1f, + "Computing visibility: " + std::to_string(cellID) + "/" + std::to_string(totalCells)); + } + } + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSBitSet.cpp b/runtime/render/core/src/culling/PVSBitSet.cpp new file mode 100644 index 00000000..ad736329 --- /dev/null +++ b/runtime/render/core/src/culling/PVSBitSet.cpp @@ -0,0 +1,147 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#include +#include +#include + +namespace sky { + + PVSBitSet::PVSBitSet(uint32_t numBits) + { + Resize(numBits); + } + + void PVSBitSet::Resize(uint32_t numBits) + { + capacity = numBits; + uint32_t numWords = (numBits + BITS_PER_WORD - 1) / BITS_PER_WORD; + data.resize(numWords, 0); + } + + void PVSBitSet::Set(uint32_t index) + { + if (index >= capacity) { + return; + } + data[WordIndex(index)] |= BitMask(index); + } + + void PVSBitSet::Clear(uint32_t index) + { + if (index >= capacity) { + return; + } + data[WordIndex(index)] &= ~BitMask(index); + } + + bool PVSBitSet::Test(uint32_t index) const + { + if (index >= capacity) { + return false; + } + return (data[WordIndex(index)] & BitMask(index)) != 0; + } + + void PVSBitSet::ClearAll() + { + if (!data.empty()) { + simd::ZeroFill(data.data(), data.size()); + } + } + + void PVSBitSet::SetAll() + { + std::fill(data.begin(), data.end(), ~0ULL); + + // Clear unused bits in the last word + if (capacity > 0) { + uint32_t usedBits = capacity % BITS_PER_WORD; + if (usedBits != 0 && !data.empty()) { + data.back() &= (1ULL << usedBits) - 1; + } + } + } + + void PVSBitSet::OrWith(const PVSBitSet &other) + { + size_t minSize = std::min(data.size(), other.data.size()); + if (minSize > 0) { + // Use SIMD-accelerated bitwise OR + simd::BitwiseOr(data.data(), other.data.data(), minSize); + } + } + + void PVSBitSet::AndWith(const PVSBitSet &other) + { + size_t minSize = std::min(data.size(), other.data.size()); + if (minSize > 0) { + // Use SIMD-accelerated bitwise AND + simd::BitwiseAnd(data.data(), other.data.data(), minSize); + } + // Clear remaining bits if this bitset is larger + if (data.size() > minSize) { + simd::ZeroFill(data.data() + minSize, data.size() - minSize); + } + } + + uint32_t PVSBitSet::CountSet() const + { + if (data.empty()) { + return 0; + } + // Use SIMD-accelerated population count + return simd::PopCountArray(data.data(), data.size()); + } + + bool PVSBitSet::Any() const + { + if (data.empty()) { + return false; + } + // Use SIMD-accelerated any-bit-set check + return simd::AnyBitSet(data.data(), data.size()); + } + + void PVSBitSet::SetData(const std::vector& rawData, uint32_t numBits) + { + capacity = numBits; + data = rawData; + } + + void PVSBitSet::GetSetBitIndices(std::vector &result) const + { + result.clear(); + result.reserve(CountSet()); + + ForEachSetBit([&result](uint32_t index) { + result.push_back(index); + }); + } + + uint32_t PVSBitSet::GetSetBitIndices(std::vector &result, uint32_t maxCount) const + { + result.clear(); + result.reserve(std::min(maxCount, CountSet())); + + uint32_t count = 0; + for (size_t wordIdx = 0; wordIdx < data.size() && count < maxCount; ++wordIdx) { + uint64_t word = data[wordIdx]; + while (word != 0 && count < maxCount) { + uint32_t bitPos = CountTrailingZeros(word); + uint32_t globalBitIndex = static_cast(wordIdx * BITS_PER_WORD + bitPos); + + if (globalBitIndex < capacity) { + result.push_back(globalBitIndex); + ++count; + } + + word &= (word - 1); + } + } + + return count; + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSCulling.cpp b/runtime/render/core/src/culling/PVSCulling.cpp new file mode 100644 index 00000000..48b2cec6 --- /dev/null +++ b/runtime/render/core/src/culling/PVSCulling.cpp @@ -0,0 +1,319 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#include +#include +#include +#include + +namespace sky { + + void PVSCulling::Initialize(const PVSConfig &config) + { + pvsData.Initialize(config); + nextObjectID = 0; + primitiveToID.clear(); + idToPrimitive.clear(); + idToPrimitive.reserve(config.maxObjects); + initialized = true; + } + + void PVSCulling::Clear() + { + pvsData.Clear(); + primitiveToID.clear(); + idToPrimitive.clear(); + nextObjectID = 0; + initialized = false; + } + + PVSObjectID PVSCulling::RegisterPrimitive(RenderPrimitive *primitive) + { + if (primitive == nullptr) { + return INVALID_PVS_OBJECT; + } + + // Check if already registered + auto it = primitiveToID.find(primitive); + if (it != primitiveToID.end()) { + return it->second; + } + + // Check max objects limit + if (nextObjectID >= pvsData.GetConfig().maxObjects) { + return INVALID_PVS_OBJECT; + } + + PVSObjectID objectID = nextObjectID++; + primitiveToID[primitive] = objectID; + + // Expand idToPrimitive if needed + if (objectID >= idToPrimitive.size()) { + idToPrimitive.resize(objectID + 1, nullptr); + } + idToPrimitive[objectID] = primitive; + + return objectID; + } + + void PVSCulling::UnregisterPrimitive(RenderPrimitive *primitive) + { + if (primitive == nullptr) { + return; + } + + auto it = primitiveToID.find(primitive); + if (it == primitiveToID.end()) { + return; + } + + PVSObjectID objectID = it->second; + + // Clear visibility for this object from all cells + for (uint32_t cellID = 0; cellID < pvsData.GetCellCount(); ++cellID) { + pvsData.ClearVisible(cellID, objectID); + } + + // Remove from maps + primitiveToID.erase(it); + if (objectID < idToPrimitive.size()) { + idToPrimitive[objectID] = nullptr; + } + } + + PVSObjectID PVSCulling::GetObjectID(RenderPrimitive *primitive) const + { + auto it = primitiveToID.find(primitive); + if (it != primitiveToID.end()) { + return it->second; + } + return INVALID_PVS_OBJECT; + } + + RenderPrimitive* PVSCulling::GetPrimitive(PVSObjectID objectID) const + { + if (objectID < idToPrimitive.size()) { + return idToPrimitive[objectID]; + } + return nullptr; + } + + void PVSCulling::QueryVisiblePrimitives( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const + { + result.clear(); + + PVSCellID cellID = pvsData.GetCellID(viewPosition); + + // If outside PVS bounds, fall back to all objects (or frustum culling only) + if (cellID == INVALID_PVS_CELL) { + for (RenderPrimitive *primitive : idToPrimitive) { + if (primitive == nullptr) { + continue; + } + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + result.push_back(primitive); + } + } + return; + } + + const PVSBitSet &visibility = pvsData.GetVisibilitySet(cellID); + + // Iterate through all registered objects and check PVS visibility + for (PVSObjectID objID = 0; objID < idToPrimitive.size(); ++objID) { + RenderPrimitive *primitive = idToPrimitive[objID]; + if (primitive == nullptr) { + continue; + } + + // Check PVS visibility + if (!visibility.Test(objID)) { + continue; + } + + // Apply frustum culling if scene view is provided + if (sceneView != nullptr && !sceneView->FrustumCulling(primitive->worldBound)) { + continue; + } + + result.push_back(primitive); + } + } + + void PVSCulling::QueryPVSVisiblePrimitives( + const Vector3 &viewPosition, + std::vector &result) const + { + QueryVisiblePrimitives(viewPosition, nullptr, result); + } + + void PVSCulling::QueryVisiblePrimitivesOptimized( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const + { + result.clear(); + + PVSCellID cellID = pvsData.GetCellID(viewPosition); + + // If outside PVS bounds, fall back to all objects + if (cellID == INVALID_PVS_CELL) { + for (RenderPrimitive *primitive : idToPrimitive) { + if (primitive == nullptr) { + continue; + } + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + result.push_back(primitive); + } + } + return; + } + + const PVSBitSet &visibility = pvsData.GetVisibilitySet(cellID); + + // Pre-reserve based on visible count + result.reserve(visibility.CountSet()); + + // Use fast bit iteration - only visit set bits + visibility.ForEachSetBit([&](uint32_t objID) { + if (objID < idToPrimitive.size()) { + RenderPrimitive *primitive = idToPrimitive[objID]; + if (primitive != nullptr) { + // Apply frustum culling if scene view is provided + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + result.push_back(primitive); + } + } + } + }); + } + + uint32_t PVSCulling::GetVisibleCount(const Vector3 &viewPosition) const + { + PVSCellID cellID = pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + return static_cast(idToPrimitive.size()); + } + return pvsData.GetVisibilitySet(cellID).CountSet(); + } + + bool PVSCulling::IsPrimitiveVisible(const Vector3 &viewPosition, RenderPrimitive *primitive) const + { + PVSObjectID objectID = GetObjectID(primitive); + if (objectID == INVALID_PVS_OBJECT) { + return false; + } + + PVSCellID cellID = pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + // Outside PVS bounds - consider visible (conservative) + return true; + } + + return pvsData.IsVisible(cellID, objectID); + } + + void PVSCulling::ComputeVisibility( + const std::function &testFunc) + { + if (!initialized) { + return; + } + + for (uint32_t cellID = 0; cellID < pvsData.GetCellCount(); ++cellID) { + const AABB &cellBounds = pvsData.GetCell(cellID).bounds; + + for (const auto &pair : primitiveToID) { + RenderPrimitive *primitive = pair.first; + PVSObjectID objectID = pair.second; + + if (testFunc(cellID, objectID, cellBounds, primitive->worldBound)) { + pvsData.SetVisible(cellID, objectID); + } else { + pvsData.ClearVisible(cellID, objectID); + } + } + } + } + + void PVSCulling::ComputeDistanceBasedVisibility(float maxDistance) + { + if (!initialized || primitiveToID.empty()) { + return; + } + + float maxDistSq = maxDistance * maxDistance; + uint32_t numObjects = static_cast(idToPrimitive.size()); + + // Prepare object center data for SIMD batch processing + std::vector objectCenters(numObjects * 3); + std::vector objectValid(numObjects, false); + + for (const auto &pair : primitiveToID) { + RenderPrimitive *primitive = pair.first; + PVSObjectID objectID = pair.second; + + if (primitive != nullptr && objectID < numObjects) { + const AABB &bounds = primitive->worldBound; + size_t offset = objectID * 3; + objectCenters[offset + 0] = (bounds.min.x + bounds.max.x) * 0.5f; + objectCenters[offset + 1] = (bounds.min.y + bounds.max.y) * 0.5f; + objectCenters[offset + 2] = (bounds.min.z + bounds.max.z) * 0.5f; + objectValid[objectID] = true; + } + } + + // Batch size for SIMD processing + constexpr uint32_t BATCH_SIZE = 64; + std::vector cellCenters(BATCH_SIZE * 3); + std::vector distancesSq(BATCH_SIZE); + + // Process each cell + for (uint32_t cellID = 0; cellID < pvsData.GetCellCount(); ++cellID) { + const PVSCell &cell = pvsData.GetCell(cellID); + float cellCenterX = cell.center.x; + float cellCenterY = cell.center.y; + float cellCenterZ = cell.center.z; + + // Pre-fill cell centers for the full batch (reused for all batches in this cell) + for (uint32_t i = 0; i < BATCH_SIZE; ++i) { + cellCenters[i * 3 + 0] = cellCenterX; + cellCenters[i * 3 + 1] = cellCenterY; + cellCenters[i * 3 + 2] = cellCenterZ; + } + + // Process objects in batches + for (uint32_t batchStart = 0; batchStart < numObjects; batchStart += BATCH_SIZE) { + uint32_t batchEnd = std::min(batchStart + BATCH_SIZE, numObjects); + uint32_t batchCount = batchEnd - batchStart; + + // Use SIMD to calculate distances + simd::DistanceSquaredBatch( + cellCenters.data(), + objectCenters.data() + batchStart * 3, + distancesSq.data(), + batchCount + ); + + // Update visibility based on distance + for (uint32_t i = 0; i < batchCount; ++i) { + PVSObjectID objectID = batchStart + i; + if (!objectValid[objectID]) { + continue; + } + + if (distancesSq[i] <= maxDistSq) { + pvsData.SetVisible(cellID, objectID); + } else { + pvsData.ClearVisible(cellID, objectID); + } + } + } + } + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSData.cpp b/runtime/render/core/src/culling/PVSData.cpp new file mode 100644 index 00000000..68d20c4f --- /dev/null +++ b/runtime/render/core/src/culling/PVSData.cpp @@ -0,0 +1,255 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#include +#include +#include +#include + +namespace sky { + + void PVSData::Initialize(const PVSConfig &cfg) + { + config = cfg; + + // Calculate grid dimensions + Vector3 worldSize = config.worldBounds.max - config.worldBounds.min; + + gridDimensions.x = static_cast(std::ceil(worldSize.x / config.cellSize.x)); + gridDimensions.y = static_cast(std::ceil(worldSize.y / config.cellSize.y)); + gridDimensions.z = static_cast(std::ceil(worldSize.z / config.cellSize.z)); + + // Ensure at least one cell in each dimension + gridDimensions.x = std::max(1, gridDimensions.x); + gridDimensions.y = std::max(1, gridDimensions.y); + gridDimensions.z = std::max(1, gridDimensions.z); + + // Pre-compute inverse cell sizes for fast lookup (multiplication instead of division) + invCellSize.x = 1.0f / config.cellSize.x; + invCellSize.y = 1.0f / config.cellSize.y; + invCellSize.z = 1.0f / config.cellSize.z; + + // Pre-compute XY area for fast cell ID calculation + xyArea = gridDimensions.x * gridDimensions.y; + + // Calculate total number of cells + uint32_t totalCells = static_cast(gridDimensions.x * gridDimensions.y * gridDimensions.z); + + // Initialize cells + cells.resize(totalCells); + visibilityData.resize(totalCells); + + for (int32_t z = 0; z < gridDimensions.z; ++z) { + for (int32_t y = 0; y < gridDimensions.y; ++y) { + for (int32_t x = 0; x < gridDimensions.x; ++x) { + PVSCellID cellID = GetCellIDFromCoord({x, y, z}); + + // Calculate cell bounds + Vector3 cellMin = config.worldBounds.min + Vector3( + static_cast(x) * config.cellSize.x, + static_cast(y) * config.cellSize.y, + static_cast(z) * config.cellSize.z + ); + + Vector3 cellMax = Vector3( + std::min(cellMin.x + config.cellSize.x, config.worldBounds.max.x), + std::min(cellMin.y + config.cellSize.y, config.worldBounds.max.y), + std::min(cellMin.z + config.cellSize.z, config.worldBounds.max.z) + ); + + cells[cellID].id = cellID; + cells[cellID].bounds = AABB{cellMin, cellMax}; + cells[cellID].center = (cellMin + cellMax) * 0.5f; + + // Initialize visibility bitset with max objects capacity + visibilityData[cellID].Resize(config.maxObjects); + } + } + } + + emptyBitSet.Resize(config.maxObjects); + } + + void PVSData::Clear() + { + cells.clear(); + visibilityData.clear(); + gridDimensions = {0, 0, 0}; + invCellSize = Vector3(0.f, 0.f, 0.f); + xyArea = 0; + } + + PVSCellID PVSData::GetCellID(const Vector3 &position) const + { + // Fast path: check bounds and compute directly + if (!IsInBounds(position)) { + return INVALID_PVS_CELL; + } + return GetCellIDFast(position); + } + + PVSCellCoord PVSData::GetCellCoord(const Vector3 &position) const + { + Vector3 offset = position - config.worldBounds.min; + + // Use multiplication with inverse instead of division + // Use floor-like behavior for negative values: subtract 1 if negative with fractional part + PVSCellCoord coord; + float fx = offset.x * invCellSize.x; + float fy = offset.y * invCellSize.y; + float fz = offset.z * invCellSize.z; + + coord.x = static_cast(fx); + coord.y = static_cast(fy); + coord.z = static_cast(fz); + + // Correct for truncation toward zero vs floor (for negative values) + if (fx < 0.0f && fx != static_cast(coord.x)) coord.x -= 1; + if (fy < 0.0f && fy != static_cast(coord.y)) coord.y -= 1; + if (fz < 0.0f && fz != static_cast(coord.z)) coord.z -= 1; + + return coord; + } + + PVSCellID PVSData::GetCellIDFromCoord(const PVSCellCoord &coord) const + { + // Check bounds + if (coord.x < 0 || coord.x >= gridDimensions.x || + coord.y < 0 || coord.y >= gridDimensions.y || + coord.z < 0 || coord.z >= gridDimensions.z) { + return INVALID_PVS_CELL; + } + + // Use pre-computed xyArea + return static_cast( + coord.z * xyArea + + coord.y * gridDimensions.x + + coord.x + ); + } + + PVSCellCoord PVSData::GetCoordFromCellID(PVSCellID cellID) const + { + if (cellID >= GetCellCount()) { + return {-1, -1, -1}; + } + + // Use pre-computed xyArea + PVSCellCoord coord; + coord.z = static_cast(cellID) / xyArea; + int32_t remainder = static_cast(cellID) % xyArea; + coord.y = remainder / gridDimensions.x; + coord.x = remainder % gridDimensions.x; + + return coord; + } + + const PVSCell& PVSData::GetCell(PVSCellID cellID) const + { + static PVSCell invalidCell; + if (!IsValidCell(cellID)) { + return invalidCell; + } + return cells[cellID]; + } + + void PVSData::SetVisible(PVSCellID cellID, PVSObjectID objectID) + { + if (!IsValidCell(cellID) || objectID >= config.maxObjects) { + return; + } + visibilityData[cellID].Set(objectID); + } + + void PVSData::ClearVisible(PVSCellID cellID, PVSObjectID objectID) + { + if (!IsValidCell(cellID) || objectID >= config.maxObjects) { + return; + } + visibilityData[cellID].Clear(objectID); + } + + bool PVSData::IsVisible(PVSCellID cellID, PVSObjectID objectID) const + { + if (!IsValidCell(cellID) || objectID >= config.maxObjects) { + return false; + } + return visibilityData[cellID].Test(objectID); + } + + const PVSBitSet& PVSData::GetVisibilitySet(PVSCellID cellID) const + { + if (!IsValidCell(cellID)) { + return emptyBitSet; + } + return visibilityData[cellID]; + } + + PVSBitSet& PVSData::GetMutableVisibilitySet(PVSCellID cellID) + { + static PVSBitSet dummyBitSet; + if (!IsValidCell(cellID)) { + return dummyBitSet; + } + return visibilityData[cellID]; + } + + void PVSData::SetAllVisible(PVSCellID cellID) + { + if (!IsValidCell(cellID)) { + return; + } + visibilityData[cellID].SetAll(); + } + + void PVSData::ClearAllVisible(PVSCellID cellID) + { + if (!IsValidCell(cellID)) { + return; + } + visibilityData[cellID].ClearAll(); + } + + void PVSData::LoadFromBakedData(const PVSBakedData &bakedData) + { + // Copy configuration + config = bakedData.config; + gridDimensions = bakedData.gridDimensions; + + // Pre-compute inverse cell sizes for fast lookup + invCellSize.x = 1.0f / config.cellSize.x; + invCellSize.y = 1.0f / config.cellSize.y; + invCellSize.z = 1.0f / config.cellSize.z; + + // Pre-compute XY area for fast cell ID calculation + xyArea = gridDimensions.x * gridDimensions.y; + + // Copy cells + cells = bakedData.cells; + + // Copy visibility data + visibilityData.resize(bakedData.visibilityData.size()); + for (size_t i = 0; i < bakedData.visibilityData.size(); ++i) { + visibilityData[i].SetData(bakedData.visibilityData[i], bakedData.numObjects); + } + + // Initialize empty bitset + emptyBitSet.Resize(bakedData.numObjects); + } + + void PVSData::ExportToBakedData(PVSBakedData &outBakedData) const + { + outBakedData.config = config; + outBakedData.gridDimensions = gridDimensions; + outBakedData.numObjects = config.maxObjects; + outBakedData.cells = cells; + + // Export visibility data + outBakedData.visibilityData.resize(visibilityData.size()); + for (size_t i = 0; i < visibilityData.size(); ++i) { + outBakedData.visibilityData[i] = visibilityData[i].GetData(); + } + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSStreamingManager.cpp b/runtime/render/core/src/culling/PVSStreamingManager.cpp new file mode 100644 index 00000000..05d5dbc7 --- /dev/null +++ b/runtime/render/core/src/culling/PVSStreamingManager.cpp @@ -0,0 +1,438 @@ +// +// Created by SkyEngine on 2024/02/20. +// + +#include +#include +#include +#include + +namespace sky { + + void PVSStreamingManager::Initialize(const PVSStreamingConfig &cfg) + { + config = cfg; + + // Calculate grid dimensions + Vector3 worldSize = config.worldBounds.max - config.worldBounds.min; + + gridDimensions.x = static_cast(std::ceil(worldSize.x / config.sectorSize.x)); + gridDimensions.y = static_cast(std::ceil(worldSize.y / config.sectorSize.y)); + gridDimensions.z = static_cast(std::ceil(worldSize.z / config.sectorSize.z)); + + // Ensure at least one sector + gridDimensions.x = std::max(1, gridDimensions.x); + gridDimensions.y = std::max(1, gridDimensions.y); + gridDimensions.z = std::max(1, gridDimensions.z); + + // Pre-compute inverse sizes + invSectorSize.x = 1.0f / config.sectorSize.x; + invSectorSize.y = 1.0f / config.sectorSize.y; + invSectorSize.z = 1.0f / config.sectorSize.z; + + initialized = true; + } + + void PVSStreamingManager::Clear() + { + loadedSectors.clear(); + pendingLoads.clear(); + pendingUnloads.clear(); + loadCallbacks.clear(); + initialized = false; + } + + void PVSStreamingManager::SetDataProvider(PVSSectorDataProvider provider) + { + dataProvider = std::move(provider); + } + + PVSSectorCoord PVSStreamingManager::GetSectorCoord(const Vector3 &position) const + { + Vector3 offset = position - config.worldBounds.min; + + PVSSectorCoord coord; + coord.x = static_cast(offset.x * invSectorSize.x); + coord.y = static_cast(offset.y * invSectorSize.y); + coord.z = static_cast(offset.z * invSectorSize.z); + + return coord; + } + + PVSSectorID PVSStreamingManager::GetSectorID(const PVSSectorCoord &coord) const + { + auto it = loadedSectors.find(coord); + if (it != loadedSectors.end()) { + return it->second.info.id; + } + return INVALID_PVS_SECTOR; + } + + AABB PVSStreamingManager::GetSectorBounds(const PVSSectorCoord &coord) const + { + Vector3 minPt = config.worldBounds.min + Vector3( + static_cast(coord.x) * config.sectorSize.x, + static_cast(coord.y) * config.sectorSize.y, + static_cast(coord.z) * config.sectorSize.z + ); + + Vector3 maxPt = Vector3( + std::min(minPt.x + config.sectorSize.x, config.worldBounds.max.x), + std::min(minPt.y + config.sectorSize.y, config.worldBounds.max.y), + std::min(minPt.z + config.sectorSize.z, config.worldBounds.max.z) + ); + + return AABB{minPt, maxPt}; + } + + Vector3 PVSStreamingManager::GetSectorCenter(const PVSSectorCoord &coord) const + { + AABB bounds = GetSectorBounds(coord); + return (bounds.min + bounds.max) * 0.5f; + } + + float PVSStreamingManager::GetDistanceToSector(const PVSSectorCoord &coord, const Vector3 &position) const + { + Vector3 center = GetSectorCenter(coord); + Vector3 diff = position - center; + return std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z); + } + + bool PVSStreamingManager::IsSectorLoaded(const PVSSectorCoord &coord) const + { + auto it = loadedSectors.find(coord); + return it != loadedSectors.end() && it->second.info.state == PVSSectorState::LOADED; + } + + bool PVSStreamingManager::IsPositionLoaded(const Vector3 &position) const + { + return IsSectorLoaded(GetSectorCoord(position)); + } + + void PVSStreamingManager::Update(const Vector3 &viewerPosition, uint64_t frameNumber) + { + if (!initialized) { + return; + } + + currentViewerPosition = viewerPosition; + currentFrame = frameNumber; + + // Get sectors to load + std::vector toLoad; + GetSectorsToLoad(viewerPosition, toLoad); + + // Get sectors to unload + std::vector toUnload; + GetSectorsToUnload(viewerPosition, toUnload); + + // Process unloads first to free memory + for (const auto &coord : toUnload) { + UnloadSectorInternal(coord); + } + + // Process loads + for (const auto &coord : toLoad) { + if (!IsSectorLoaded(coord) && pendingLoads.find(coord) == pendingLoads.end()) { + LoadSectorInternal(coord); + } + } + + // Enforce memory limit + EnforceSectorLimit(); + + // Update LRU for current sector + PVSSectorCoord currentCoord = GetSectorCoord(viewerPosition); + auto it = loadedSectors.find(currentCoord); + if (it != loadedSectors.end()) { + it->second.info.lastUsedFrame = frameNumber; + it->second.info.distanceToViewer = 0.0f; + } + } + + void PVSStreamingManager::GetSectorsToLoad(const Vector3 &position, std::vector &result) const + { + result.clear(); + + PVSSectorCoord centerCoord = GetSectorCoord(position); + + // Calculate how many sectors fit in load radius + int32_t rangeX = static_cast(std::ceil(config.loadRadius / config.sectorSize.x)); + int32_t rangeY = static_cast(std::ceil(config.loadRadius / config.sectorSize.y)); + int32_t rangeZ = static_cast(std::ceil(config.loadRadius / config.sectorSize.z)); + + for (int32_t dz = -rangeZ; dz <= rangeZ; ++dz) { + for (int32_t dy = -rangeY; dy <= rangeY; ++dy) { + for (int32_t dx = -rangeX; dx <= rangeX; ++dx) { + PVSSectorCoord coord = { + centerCoord.x + dx, + centerCoord.y + dy, + centerCoord.z + dz + }; + + // Check if within grid bounds + if (coord.x < 0 || coord.x >= gridDimensions.x || + coord.y < 0 || coord.y >= gridDimensions.y || + coord.z < 0 || coord.z >= gridDimensions.z) { + continue; + } + + // Check distance + float dist = GetDistanceToSector(coord, position); + if (dist <= config.loadRadius) { + result.push_back(coord); + } + } + } + } + + // Sort by distance (closest first) + std::sort(result.begin(), result.end(), + [this, &position](const PVSSectorCoord &a, const PVSSectorCoord &b) { + return GetDistanceToSector(a, position) < GetDistanceToSector(b, position); + }); + } + + void PVSStreamingManager::GetSectorsToUnload(const Vector3 &position, std::vector &result) const + { + result.clear(); + + for (const auto &pair : loadedSectors) { + float dist = GetDistanceToSector(pair.first, position); + if (dist > config.unloadRadius) { + result.push_back(pair.first); + } + } + } + + bool PVSStreamingManager::LoadSectorInternal(const PVSSectorCoord &coord) + { + if (!dataProvider) { + return false; + } + + // Get sector data from provider + PVSSectorBakedData bakedData; + if (!dataProvider(coord, bakedData)) { + return false; + } + + // Create loaded sector + LoadedSector sector; + sector.info.id = static_cast( + coord.z * (gridDimensions.x * gridDimensions.y) + + coord.y * gridDimensions.x + coord.x); + sector.info.coord = coord; + sector.info.bounds = GetSectorBounds(coord); + sector.info.center = GetSectorCenter(coord); + sector.info.state = PVSSectorState::LOADED; + sector.info.lastUsedFrame = currentFrame; + sector.info.distanceToViewer = GetDistanceToSector(coord, currentViewerPosition); + + // Load PVS data + sector.pvsData.LoadFromBakedData(bakedData.pvsData); + + // Reserve primitive slots + sector.primitives.resize(bakedData.pvsData.numObjects, nullptr); + + // Calculate memory size + sector.memorySize = bakedData.GetMemorySize(); + + // Store loaded sector + loadedSectors[coord] = std::move(sector); + + // Call load callbacks + auto callbackIt = loadCallbacks.find(coord); + if (callbackIt != loadCallbacks.end()) { + for (auto &callback : callbackIt->second) { + if (callback) { + callback(loadedSectors[coord].info.id, true); + } + } + loadCallbacks.erase(callbackIt); + } + + return true; + } + + void PVSStreamingManager::UnloadSectorInternal(const PVSSectorCoord &coord) + { + auto it = loadedSectors.find(coord); + if (it == loadedSectors.end()) { + return; + } + + // Mark as unloading + it->second.info.state = PVSSectorState::UNLOADING; + + // Clear primitives + it->second.primitives.clear(); + it->second.primitiveToLocalID.clear(); + + // Remove from map + loadedSectors.erase(it); + } + + void PVSStreamingManager::RequestSectorLoad(const PVSSectorCoord &coord, PVSSectorLoadCallback callback) + { + if (callback) { + loadCallbacks[coord].push_back(callback); + } + + if (IsSectorLoaded(coord)) { + // Already loaded, call callback immediately + if (callback) { + callback(GetSectorID(coord), true); + } + loadCallbacks.erase(coord); + return; + } + + pendingLoads.insert(coord); + LoadSectorInternal(coord); + pendingLoads.erase(coord); + } + + void PVSStreamingManager::RequestSectorUnload(const PVSSectorCoord &coord) + { + pendingUnloads.insert(coord); + UnloadSectorInternal(coord); + pendingUnloads.erase(coord); + } + + void PVSStreamingManager::EnforceSectorLimit() + { + if (loadedSectors.size() <= config.maxLoadedSectors) { + return; + } + + // Build list of sectors sorted by LRU (oldest first) + std::vector> sectors; + for (const auto &pair : loadedSectors) { + sectors.emplace_back(pair.first, pair.second.info.lastUsedFrame); + } + + // Sort by last used frame (oldest first) + std::sort(sectors.begin(), sectors.end(), + [](const auto &a, const auto &b) { + return a.second < b.second; + }); + + // Unload oldest sectors until under limit + size_t toRemove = loadedSectors.size() - config.maxLoadedSectors; + for (size_t i = 0; i < toRemove && i < sectors.size(); ++i) { + UnloadSectorInternal(sectors[i].first); + } + } + + bool PVSStreamingManager::QueryVisiblePrimitives( + const Vector3 &viewPosition, + const SceneView *sceneView, + std::vector &result) const + { + result.clear(); + + PVSSectorCoord sectorCoord = GetSectorCoord(viewPosition); + + auto it = loadedSectors.find(sectorCoord); + if (it == loadedSectors.end() || it->second.info.state != PVSSectorState::LOADED) { + return false; + } + + const LoadedSector §or = it->second; + + // Get cell ID within the sector + PVSCellID cellID = sector.pvsData.GetCellID(viewPosition); + if (cellID == INVALID_PVS_CELL) { + return false; + } + + const PVSBitSet &visibility = sector.pvsData.GetVisibilitySet(cellID); + + // Pre-reserve based on visible count + result.reserve(visibility.CountSet()); + + visibility.ForEachSetBit([&](uint32_t localObjID) { + if (localObjID < sector.primitives.size()) { + RenderPrimitive *primitive = sector.primitives[localObjID]; + if (primitive != nullptr) { + if (sceneView == nullptr || sceneView->FrustumCulling(primitive->worldBound)) { + result.push_back(primitive); + } + } + } + }); + + return true; + } + + void PVSStreamingManager::GetLoadedSectors(std::vector &result) const + { + result.clear(); + result.reserve(loadedSectors.size()); + for (const auto &pair : loadedSectors) { + result.push_back(pair.first); + } + } + + PVSStreamingManager::Statistics PVSStreamingManager::GetStatistics() const + { + Statistics stats; + stats.loadedSectors = static_cast(loadedSectors.size()); + stats.pendingLoads = static_cast(pendingLoads.size()); + stats.pendingUnloads = static_cast(pendingUnloads.size()); + + for (const auto &pair : loadedSectors) { + stats.totalMemoryUsed += pair.second.memorySize; + } + + if (stats.loadedSectors > 0) { + stats.averageSectorSize = static_cast(stats.totalMemoryUsed) / + static_cast(stats.loadedSectors); + } + + return stats; + } + + void PVSStreamingManager::RegisterPrimitive( + const PVSSectorCoord &coord, + RenderPrimitive *primitive, + uint32_t localObjectID) + { + auto it = loadedSectors.find(coord); + if (it == loadedSectors.end()) { + return; + } + + LoadedSector §or = it->second; + + if (localObjectID >= sector.primitives.size()) { + sector.primitives.resize(localObjectID + 1, nullptr); + } + + sector.primitives[localObjectID] = primitive; + sector.primitiveToLocalID[primitive] = localObjectID; + } + + void PVSStreamingManager::UnregisterPrimitive( + const PVSSectorCoord &coord, + RenderPrimitive *primitive) + { + auto it = loadedSectors.find(coord); + if (it == loadedSectors.end()) { + return; + } + + LoadedSector §or = it->second; + + auto idIt = sector.primitiveToLocalID.find(primitive); + if (idIt != sector.primitiveToLocalID.end()) { + uint32_t localID = idIt->second; + if (localID < sector.primitives.size()) { + sector.primitives[localID] = nullptr; + } + sector.primitiveToLocalID.erase(idIt); + } + } + +} // namespace sky diff --git a/runtime/render/core/src/culling/PVSStreamingTypes.cpp b/runtime/render/core/src/culling/PVSStreamingTypes.cpp new file mode 100644 index 00000000..7329b607 --- /dev/null +++ b/runtime/render/core/src/culling/PVSStreamingTypes.cpp @@ -0,0 +1,83 @@ +// +// Created by SkyEngine on 2024/02/20. +// + +#include +#include + +namespace sky { + + void PVSSectorBakedData::Save(BinaryOutputArchive &archive) const + { + // Save version + archive.SaveValue(VERSION); + + // Save sector coordinate + archive.SaveValue(coord.x); + archive.SaveValue(coord.y); + archive.SaveValue(coord.z); + + // Save bounds + archive.SaveValue(bounds.min.x); + archive.SaveValue(bounds.min.y); + archive.SaveValue(bounds.min.z); + archive.SaveValue(bounds.max.x); + archive.SaveValue(bounds.max.y); + archive.SaveValue(bounds.max.z); + + // Save PVS data + pvsData.Save(archive); + + // Save object names + uint32_t numNames = static_cast(objectNames.size()); + archive.SaveValue(numNames); + for (const auto &name : objectNames) { + archive.SaveValue(name); + } + } + + void PVSSectorBakedData::Load(BinaryInputArchive &archive) + { + // Load and verify version + uint32_t version = 0; + archive.LoadValue(version); + if (version != VERSION) { + return; + } + + // Load sector coordinate + archive.LoadValue(coord.x); + archive.LoadValue(coord.y); + archive.LoadValue(coord.z); + + // Load bounds + archive.LoadValue(bounds.min.x); + archive.LoadValue(bounds.min.y); + archive.LoadValue(bounds.min.z); + archive.LoadValue(bounds.max.x); + archive.LoadValue(bounds.max.y); + archive.LoadValue(bounds.max.z); + + // Load PVS data + pvsData.Load(archive); + + // Load object names + uint32_t numNames = 0; + archive.LoadValue(numNames); + objectNames.resize(numNames); + for (auto &name : objectNames) { + archive.LoadValue(name); + } + } + + size_t PVSSectorBakedData::GetMemorySize() const + { + size_t size = sizeof(PVSSectorBakedData); + size += pvsData.GetMemorySize(); + for (const auto &name : objectNames) { + size += name.size(); + } + return size; + } + +} // namespace sky diff --git a/test/core/ShapesTest.cpp b/test/core/ShapesTest.cpp index 094eb9f4..20a2865c 100644 --- a/test/core/ShapesTest.cpp +++ b/test/core/ShapesTest.cpp @@ -58,3 +58,75 @@ TEST(ShapesTest, ViewFrustumTest) ASSERT_FALSE(Intersection(aabb4, CreateFrustumByViewProjectMatrix(mtx))); } } + +// ============================================================================ +// Ray-AABB Intersection Tests +// ============================================================================ + +TEST(ShapesTest, RayAABBIntersectionHit) +{ + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + AABB aabb{Vector3(5.f, -1.f, -1.f), Vector3(7.f, 1.f, 1.f)}; + + auto [hit, tMin, tMax] = Intersection(ray, aabb); + + ASSERT_TRUE(hit); + ASSERT_NEAR(tMin, 5.f, 0.001f); + ASSERT_NEAR(tMax, 7.f, 0.001f); +} + +TEST(ShapesTest, RayAABBIntersectionMiss) +{ + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + AABB aabb{Vector3(5.f, 5.f, 5.f), Vector3(7.f, 7.f, 7.f)}; // Not on ray path + + auto [hit, tMin, tMax] = Intersection(ray, aabb); + + ASSERT_FALSE(hit); +} + +TEST(ShapesTest, RayAABBIntersectionInsideBox) +{ + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + AABB aabb{Vector3(-1.f, -1.f, -1.f), Vector3(1.f, 1.f, 1.f)}; // Ray starts inside + + auto [hit, tMin, tMax] = Intersection(ray, aabb); + + ASSERT_TRUE(hit); + ASSERT_GE(tMin, 0.f); // tMin should be 0 since ray starts inside +} + +TEST(ShapesTest, RayAABBIntersectionBehind) +{ + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + AABB aabb{Vector3(-7.f, -1.f, -1.f), Vector3(-5.f, 1.f, 1.f)}; // Behind ray + + auto [hit, tMin, tMax] = Intersection(ray, aabb); + + // Ray still intersects, but at negative t + ASSERT_TRUE(hit); + ASSERT_LT(tMax, 0.f); +} + +TEST(ShapesTest, RayAABBIntersectionTest) +{ + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + AABB aabbHit{Vector3(5.f, -1.f, -1.f), Vector3(7.f, 1.f, 1.f)}; + AABB aabbMiss{Vector3(5.f, 5.f, 5.f), Vector3(7.f, 7.f, 7.f)}; + + ASSERT_TRUE(IntersectionTest(ray, aabbHit)); + ASSERT_FALSE(IntersectionTest(ray, aabbMiss)); +} + +TEST(ShapesTest, RayAABBDiagonal) +{ + // Ray going diagonally + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 1.f, 1.f)}; + ray.dir.Normalize(); + + AABB aabb{Vector3(4.f, 4.f, 4.f), Vector3(6.f, 6.f, 6.f)}; + + auto [hit, tMin, tMax] = Intersection(ray, aabb); + + ASSERT_TRUE(hit); +} diff --git a/test/core/TreeTest.cpp b/test/core/TreeTest.cpp index 340484a6..d37c88f3 100644 --- a/test/core/TreeTest.cpp +++ b/test/core/TreeTest.cpp @@ -3,6 +3,7 @@ // #include +#include #include #include #include @@ -94,4 +95,235 @@ TEST(OctreeTest, BasicTest) ASSERT_EQ(result[0]->id, 3); ASSERT_EQ(result[1]->id, 4); ASSERT_EQ(result[2]->id, 5); -} \ No newline at end of file +} +// ============================================================================ +// BVH Tests +// ============================================================================ + +struct TestBVHElement { + uint32_t id; + AABB bounds; +}; + +template <> +struct BVHTraits { + static AABB GetBounds(const TestBVHElement &element) + { + return element.bounds; + } +}; + +TEST(BVHTest, BuildEmpty) +{ + BVH bvh; + std::vector elements; + + bvh.Build(elements); + + ASSERT_TRUE(bvh.IsEmpty()); + ASSERT_EQ(bvh.GetNodeCount(), 0); + ASSERT_EQ(bvh.GetElementCount(), 0); +} + +TEST(BVHTest, BuildSingleElement) +{ + BVH bvh; + std::vector elements; + + elements.push_back({1, AABB{Vector3(0.f), Vector3(1.f)}}); + + bvh.Build(elements); + + ASSERT_FALSE(bvh.IsEmpty()); + ASSERT_EQ(bvh.GetNodeCount(), 1); + ASSERT_EQ(bvh.GetElementCount(), 1); + ASSERT_TRUE(bvh.GetRoot().IsLeaf()); +} + +TEST(BVHTest, BuildMultipleElements) +{ + BVH bvh; + std::vector elements; + + // Create a grid of elements + for (int i = 0; i < 10; ++i) { + float x = static_cast(i); + elements.push_back({static_cast(i), + AABB{Vector3(x, 0.f, 0.f), Vector3(x + 1.f, 1.f, 1.f)}}); + } + + bvh.Build(elements); + + ASSERT_FALSE(bvh.IsEmpty()); + ASSERT_EQ(bvh.GetElementCount(), 10); + ASSERT_GT(bvh.GetNodeCount(), 1); // Should have internal nodes +} + +TEST(BVHTest, QueryAABB) +{ + BVH bvh; + std::vector elements; + + // Create elements at different positions + elements.push_back({1, AABB{Vector3(0.f), Vector3(1.f)}}); + elements.push_back({2, AABB{Vector3(5.f), Vector3(6.f)}}); + elements.push_back({3, AABB{Vector3(10.f), Vector3(11.f)}}); + elements.push_back({4, AABB{Vector3(0.5f, 0.5f, 0.5f), Vector3(1.5f, 1.5f, 1.5f)}}); + + bvh.Build(elements); + + // Query area around first two elements + std::vector result; + bvh.QueryAABB(AABB{Vector3(-1.f), Vector3(2.f)}, result); + + ASSERT_EQ(result.size(), 2); // Should find elements 1 and 4 +} + +TEST(BVHTest, QueryAABBCallback) +{ + BVH bvh; + std::vector elements; + + for (int i = 0; i < 5; ++i) { + float x = static_cast(i * 2); + elements.push_back({static_cast(i), + AABB{Vector3(x, 0.f, 0.f), Vector3(x + 1.f, 1.f, 1.f)}}); + } + + bvh.Build(elements); + + // Query overlapping middle elements + std::vector foundIds; + bvh.QueryAABB(AABB{Vector3(3.5f, -1.f, -1.f), Vector3(6.5f, 2.f, 2.f)}, + [&foundIds](const TestBVHElement &elem) { + foundIds.push_back(elem.id); + }); + + ASSERT_EQ(foundIds.size(), 2); // Should find elements at x=4 and x=6 +} + +TEST(BVHTest, RayCast) +{ + BVH bvh; + std::vector elements; + + // Create elements in a row + elements.push_back({1, AABB{Vector3(5.f, -0.5f, -0.5f), Vector3(6.f, 0.5f, 0.5f)}}); + elements.push_back({2, AABB{Vector3(10.f, -0.5f, -0.5f), Vector3(11.f, 0.5f, 0.5f)}}); + elements.push_back({3, AABB{Vector3(15.f, -0.5f, -0.5f), Vector3(16.f, 0.5f, 0.5f)}}); + + bvh.Build(elements); + + // Cast ray from origin along X axis + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + auto hit = bvh.RayCast(ray); + + ASSERT_TRUE(hit.hit); + ASSERT_EQ(hit.element->id, 1); // First element along the ray + ASSERT_NEAR(hit.distance, 5.f, 0.01f); +} + +TEST(BVHTest, RayCastMiss) +{ + BVH bvh; + std::vector elements; + + elements.push_back({1, AABB{Vector3(5.f, 5.f, 5.f), Vector3(6.f, 6.f, 6.f)}}); + + bvh.Build(elements); + + // Cast ray that misses + Ray ray{Vector3(0.f, 0.f, 0.f), Vector3(1.f, 0.f, 0.f)}; + auto hit = bvh.RayCast(ray); + + ASSERT_FALSE(hit.hit); +} + +TEST(BVHTest, QueryPoint) +{ + BVH bvh; + std::vector elements; + + elements.push_back({1, AABB{Vector3(0.f), Vector3(2.f)}}); + elements.push_back({2, AABB{Vector3(1.f), Vector3(3.f)}}); // Overlaps with first + elements.push_back({3, AABB{Vector3(10.f), Vector3(12.f)}}); + + bvh.Build(elements); + + // Query point inside overlapping region + std::vector foundIds; + bvh.QueryPoint(Vector3(1.5f, 1.5f, 1.5f), [&foundIds](const TestBVHElement &elem) { + foundIds.push_back(elem.id); + }); + + ASSERT_EQ(foundIds.size(), 2); // Should find both overlapping elements +} + +TEST(BVHTest, GetDepth) +{ + BVH bvh; + std::vector elements; + + // Single element + elements.push_back({1, AABB{Vector3(0.f), Vector3(1.f)}}); + bvh.Build(elements); + ASSERT_EQ(bvh.GetDepth(), 1); + + // Many elements should create deeper tree + elements.clear(); + for (int i = 0; i < 100; ++i) { + float x = static_cast(i); + elements.push_back({static_cast(i), + AABB{Vector3(x, 0.f, 0.f), Vector3(x + 0.5f, 0.5f, 0.5f)}}); + } + bvh.Build(elements); + ASSERT_GT(bvh.GetDepth(), 1); +} + +TEST(BVHTest, BuildStrategies) +{ + std::vector elements; + for (int i = 0; i < 50; ++i) { + float x = static_cast(i); + elements.push_back({static_cast(i), + AABB{Vector3(x, 0.f, 0.f), Vector3(x + 0.5f, 0.5f, 0.5f)}}); + } + + // Test different strategies + BVH bvhMedian; + BVHConfig configMedian; + configMedian.strategy = BVHBuildStrategy::MEDIAN; + bvhMedian.Build(elements, configMedian); + ASSERT_EQ(bvhMedian.GetElementCount(), 50); + + BVH bvhObjectMedian; + BVHConfig configObject; + configObject.strategy = BVHBuildStrategy::OBJECT_MEDIAN; + bvhObjectMedian.Build(elements, configObject); + ASSERT_EQ(bvhObjectMedian.GetElementCount(), 50); + + BVH bvhSAH; + BVHConfig configSAH; + configSAH.strategy = BVHBuildStrategy::SAH; + bvhSAH.Build(elements, configSAH); + ASSERT_EQ(bvhSAH.GetElementCount(), 50); +} + +TEST(BVHTest, AABBElements) +{ + // Test direct AABB elements (using specialization) + BVH bvh; + std::vector elements; + + elements.push_back(AABB{Vector3(0.f), Vector3(1.f)}); + elements.push_back(AABB{Vector3(2.f), Vector3(3.f)}); + elements.push_back(AABB{Vector3(4.f), Vector3(5.f)}); + + bvh.Build(elements); + + ASSERT_EQ(bvh.GetElementCount(), 3); + + std::vector result; + bvh.QueryAABB(AABB{Vector3(-1.f), Vector3(1.5f)}, result); + ASSERT_EQ(result.size(), 1); +} diff --git a/test/render/PVSCullingTest.cpp b/test/render/PVSCullingTest.cpp new file mode 100644 index 00000000..77d097e9 --- /dev/null +++ b/test/render/PVSCullingTest.cpp @@ -0,0 +1,1545 @@ +// +// Created by SkyEngine on 2024/02/15. +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace sky; + +// ============================================================================ +// SIMD Utilities Tests +// ============================================================================ + +TEST(SIMDUtilsTest, BitwiseOr) +{ + std::vector dst = {0x00FF00FF00FF00FFULL, 0xAAAAAAAAAAAAAAAAULL, 0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL}; + std::vector src = {0xFF00FF00FF00FF00ULL, 0x5555555555555555ULL, 0xFFFFFFFFFFFFFFFFULL, 0x0000000000000000ULL}; + + simd::BitwiseOr(dst.data(), src.data(), dst.size()); + + ASSERT_EQ(dst[0], 0xFFFFFFFFFFFFFFFFULL); + ASSERT_EQ(dst[1], 0xFFFFFFFFFFFFFFFFULL); + ASSERT_EQ(dst[2], 0xFFFFFFFFFFFFFFFFULL); + ASSERT_EQ(dst[3], 0xFFFFFFFFFFFFFFFFULL); +} + +TEST(SIMDUtilsTest, BitwiseAnd) +{ + std::vector dst = {0xFFFFFFFFFFFFFFFFULL, 0xAAAAAAAAAAAAAAAAULL, 0x0F0F0F0F0F0F0F0FULL, 0xFFFFFFFFFFFFFFFFULL}; + std::vector src = {0x00FF00FF00FF00FFULL, 0x5555555555555555ULL, 0x0F0F0F0F0F0F0F0FULL, 0x0000000000000000ULL}; + + simd::BitwiseAnd(dst.data(), src.data(), dst.size()); + + ASSERT_EQ(dst[0], 0x00FF00FF00FF00FFULL); + ASSERT_EQ(dst[1], 0x0000000000000000ULL); + ASSERT_EQ(dst[2], 0x0F0F0F0F0F0F0F0FULL); + ASSERT_EQ(dst[3], 0x0000000000000000ULL); +} + +TEST(SIMDUtilsTest, AnyBitSet) +{ + std::vector allZero = {0, 0, 0, 0}; + std::vector oneSet = {0, 0, 0, 1}; + std::vector allSet = {~0ULL, ~0ULL, ~0ULL, ~0ULL}; + + ASSERT_FALSE(simd::AnyBitSet(allZero.data(), allZero.size())); + ASSERT_TRUE(simd::AnyBitSet(oneSet.data(), oneSet.size())); + ASSERT_TRUE(simd::AnyBitSet(allSet.data(), allSet.size())); +} + +TEST(SIMDUtilsTest, PopCount64) +{ + ASSERT_EQ(simd::PopCount64(0), 0); + ASSERT_EQ(simd::PopCount64(1), 1); + ASSERT_EQ(simd::PopCount64(0xFFFFFFFFFFFFFFFFULL), 64); + ASSERT_EQ(simd::PopCount64(0x5555555555555555ULL), 32); + ASSERT_EQ(simd::PopCount64(0xAAAAAAAAAAAAAAAAULL), 32); +} + +TEST(SIMDUtilsTest, PopCountArray) +{ + std::vector data = {0x0F0F0F0F0F0F0F0FULL, 0xF0F0F0F0F0F0F0F0ULL}; + // Each byte has 4 bits set, 8 bytes per uint64_t = 32 bits each + ASSERT_EQ(simd::PopCountArray(data.data(), data.size()), 64); +} + +TEST(SIMDUtilsTest, ZeroFill) +{ + std::vector data = {~0ULL, ~0ULL, ~0ULL, ~0ULL}; + + simd::ZeroFill(data.data(), data.size()); + + for (size_t i = 0; i < data.size(); ++i) { + ASSERT_EQ(data[i], 0); + } +} + +TEST(SIMDUtilsTest, DistanceSquaredBatch) +{ + // Test 3 point pairs + float pointsA[] = { + 0.0f, 0.0f, 0.0f, // Point 1 + 1.0f, 0.0f, 0.0f, // Point 2 + 1.0f, 2.0f, 3.0f // Point 3 + }; + float pointsB[] = { + 3.0f, 4.0f, 0.0f, // Point 1: dist^2 = 9 + 16 = 25 + 1.0f, 0.0f, 0.0f, // Point 2: dist^2 = 0 (same point) + 4.0f, 6.0f, 3.0f // Point 3: dist^2 = 9 + 16 + 0 = 25 + }; + float distances[3] = {0, 0, 0}; + + simd::DistanceSquaredBatch(pointsA, pointsB, distances, 3); + + ASSERT_FLOAT_EQ(distances[0], 25.0f); + ASSERT_FLOAT_EQ(distances[1], 0.0f); + ASSERT_FLOAT_EQ(distances[2], 25.0f); +} + +TEST(SIMDUtilsTest, AABBIntersectionBatch) +{ + // Test 4 AABB pairs + float minA[] = { + 0.0f, 0.0f, 0.0f, // AABB 1 min - intersects + 10.0f, 10.0f, 10.0f, // AABB 2 min - no intersection + 0.0f, 0.0f, 0.0f, // AABB 3 min - touching + -5.0f, -5.0f, -5.0f // AABB 4 min - fully contains + }; + float maxA[] = { + 5.0f, 5.0f, 5.0f, // AABB 1 max + 15.0f, 15.0f, 15.0f, // AABB 2 max + 5.0f, 5.0f, 5.0f, // AABB 3 max + 10.0f, 10.0f, 10.0f // AABB 4 max + }; + float minB[] = { + 3.0f, 3.0f, 3.0f, // AABB 1 min - intersects + 0.0f, 0.0f, 0.0f, // AABB 2 min - no intersection + 5.0f, 5.0f, 5.0f, // AABB 3 min - touching + 0.0f, 0.0f, 0.0f // AABB 4 min - contained + }; + float maxB[] = { + 8.0f, 8.0f, 8.0f, // AABB 1 max + 5.0f, 5.0f, 5.0f, // AABB 2 max + 10.0f, 10.0f, 10.0f, // AABB 3 max + 5.0f, 5.0f, 5.0f // AABB 4 max + }; + uint8_t results[4] = {0, 0, 0, 0}; + + simd::AABBIntersectionBatch(minA, maxA, minB, maxB, results, 4); + + ASSERT_EQ(results[0], 1); // Intersects + ASSERT_EQ(results[1], 0); // No intersection + ASSERT_EQ(results[2], 1); // Touching (counts as intersection) + ASSERT_EQ(results[3], 1); // Contains +} + +// ============================================================================ +// PVSBitSet Tests +// ============================================================================ + +TEST(PVSBitSetTest, DefaultConstruction) +{ + PVSBitSet bitset; + ASSERT_EQ(bitset.GetCapacity(), 0); + ASSERT_TRUE(bitset.None()); +} + +TEST(PVSBitSetTest, ConstructionWithSize) +{ + PVSBitSet bitset(128); + ASSERT_EQ(bitset.GetCapacity(), 128); + ASSERT_TRUE(bitset.None()); +} + +TEST(PVSBitSetTest, SetAndTest) +{ + PVSBitSet bitset(256); + + ASSERT_FALSE(bitset.Test(0)); + ASSERT_FALSE(bitset.Test(100)); + ASSERT_FALSE(bitset.Test(255)); + + bitset.Set(0); + bitset.Set(100); + bitset.Set(255); + + ASSERT_TRUE(bitset.Test(0)); + ASSERT_TRUE(bitset.Test(100)); + ASSERT_TRUE(bitset.Test(255)); + ASSERT_FALSE(bitset.Test(1)); + ASSERT_FALSE(bitset.Test(99)); +} + +TEST(PVSBitSetTest, Clear) +{ + PVSBitSet bitset(100); + + bitset.Set(50); + ASSERT_TRUE(bitset.Test(50)); + + bitset.Clear(50); + ASSERT_FALSE(bitset.Test(50)); +} + +TEST(PVSBitSetTest, ClearAll) +{ + PVSBitSet bitset(100); + + bitset.Set(10); + bitset.Set(50); + bitset.Set(90); + ASSERT_TRUE(bitset.Any()); + + bitset.ClearAll(); + ASSERT_TRUE(bitset.None()); +} + +TEST(PVSBitSetTest, SetAll) +{ + PVSBitSet bitset(100); + + bitset.SetAll(); + + ASSERT_TRUE(bitset.Test(0)); + ASSERT_TRUE(bitset.Test(50)); + ASSERT_TRUE(bitset.Test(99)); + ASSERT_EQ(bitset.CountSet(), 100); +} + +TEST(PVSBitSetTest, CountSet) +{ + PVSBitSet bitset(256); + + ASSERT_EQ(bitset.CountSet(), 0); + + bitset.Set(0); + bitset.Set(64); + bitset.Set(128); + bitset.Set(192); + + ASSERT_EQ(bitset.CountSet(), 4); +} + +TEST(PVSBitSetTest, OrWith) +{ + PVSBitSet bitset1(100); + PVSBitSet bitset2(100); + + bitset1.Set(10); + bitset1.Set(20); + + bitset2.Set(20); + bitset2.Set(30); + + bitset1.OrWith(bitset2); + + ASSERT_TRUE(bitset1.Test(10)); + ASSERT_TRUE(bitset1.Test(20)); + ASSERT_TRUE(bitset1.Test(30)); + ASSERT_EQ(bitset1.CountSet(), 3); +} + +TEST(PVSBitSetTest, AndWith) +{ + PVSBitSet bitset1(100); + PVSBitSet bitset2(100); + + bitset1.Set(10); + bitset1.Set(20); + bitset1.Set(30); + + bitset2.Set(20); + bitset2.Set(30); + bitset2.Set(40); + + bitset1.AndWith(bitset2); + + ASSERT_FALSE(bitset1.Test(10)); + ASSERT_TRUE(bitset1.Test(20)); + ASSERT_TRUE(bitset1.Test(30)); + ASSERT_FALSE(bitset1.Test(40)); + ASSERT_EQ(bitset1.CountSet(), 2); +} + +TEST(PVSBitSetTest, OutOfRangeAccess) +{ + PVSBitSet bitset(50); + + // These should not crash + bitset.Set(100); // Out of range, should be ignored + ASSERT_FALSE(bitset.Test(100)); // Out of range, should return false + + bitset.Clear(100); // Out of range, should be ignored +} + +// ============================================================================ +// PVSData Tests +// ============================================================================ + +TEST(PVSDataTest, Initialization) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(-10.f, -10.f, -10.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + // 20/5 = 4 cells in each dimension = 4*4*4 = 64 cells + ASSERT_EQ(pvsData.GetCellCount(), 64); + + auto dims = pvsData.GetGridDimensions(); + ASSERT_EQ(dims.x, 4); + ASSERT_EQ(dims.y, 4); + ASSERT_EQ(dims.z, 4); +} + +TEST(PVSDataTest, CellIDFromPosition) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + // 2x2x2 grid + ASSERT_EQ(pvsData.GetCellCount(), 8); + + // Test cell at (0, 0, 0) + PVSCellID cell1 = pvsData.GetCellID(Vector3(1.f, 1.f, 1.f)); + ASSERT_NE(cell1, INVALID_PVS_CELL); + + // Test cell at (1, 1, 1) + PVSCellID cell2 = pvsData.GetCellID(Vector3(7.f, 7.f, 7.f)); + ASSERT_NE(cell2, INVALID_PVS_CELL); + ASSERT_NE(cell1, cell2); + + // Test outside bounds + PVSCellID invalidCell = pvsData.GetCellID(Vector3(-5.f, 0.f, 0.f)); + ASSERT_EQ(invalidCell, INVALID_PVS_CELL); +} + +TEST(PVSDataTest, CellCoordinates) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + PVSCellCoord coord = pvsData.GetCellCoord(Vector3(7.f, 7.f, 7.f)); + ASSERT_EQ(coord.x, 1); + ASSERT_EQ(coord.y, 1); + ASSERT_EQ(coord.z, 1); + + PVSCellID cellID = pvsData.GetCellIDFromCoord(coord); + PVSCellCoord recoveredCoord = pvsData.GetCoordFromCellID(cellID); + + ASSERT_EQ(coord.x, recoveredCoord.x); + ASSERT_EQ(coord.y, recoveredCoord.y); + ASSERT_EQ(coord.z, recoveredCoord.z); +} + +TEST(PVSDataTest, VisibilitySetAndQuery) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + PVSCellID cellID = 0; + PVSObjectID obj1 = 0; + PVSObjectID obj2 = 10; + PVSObjectID obj3 = 50; + + ASSERT_FALSE(pvsData.IsVisible(cellID, obj1)); + ASSERT_FALSE(pvsData.IsVisible(cellID, obj2)); + + pvsData.SetVisible(cellID, obj1); + pvsData.SetVisible(cellID, obj2); + + ASSERT_TRUE(pvsData.IsVisible(cellID, obj1)); + ASSERT_TRUE(pvsData.IsVisible(cellID, obj2)); + ASSERT_FALSE(pvsData.IsVisible(cellID, obj3)); + + pvsData.ClearVisible(cellID, obj1); + ASSERT_FALSE(pvsData.IsVisible(cellID, obj1)); + ASSERT_TRUE(pvsData.IsVisible(cellID, obj2)); +} + +TEST(PVSDataTest, CellBounds) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + // Cell at (0, 0, 0) should have bounds [0,0,0] to [5,5,5] + PVSCellID cell0 = pvsData.GetCellIDFromCoord({0, 0, 0}); + const PVSCell& cell = pvsData.GetCell(cell0); + + ASSERT_FLOAT_EQ(cell.bounds.min.x, 0.f); + ASSERT_FLOAT_EQ(cell.bounds.min.y, 0.f); + ASSERT_FLOAT_EQ(cell.bounds.min.z, 0.f); + ASSERT_FLOAT_EQ(cell.bounds.max.x, 5.f); + ASSERT_FLOAT_EQ(cell.bounds.max.y, 5.f); + ASSERT_FLOAT_EQ(cell.bounds.max.z, 5.f); +} + +// ============================================================================ +// PVSCulling Tests +// ============================================================================ + +TEST(PVSCullingTest, Initialization) +{ + PVSCulling pvsCulling; + + ASSERT_FALSE(pvsCulling.IsInitialized()); + + PVSConfig config; + config.worldBounds = AABB{Vector3(-10.f, -10.f, -10.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + ASSERT_TRUE(pvsCulling.IsInitialized()); + ASSERT_EQ(pvsCulling.GetObjectCount(), 0); +} + +TEST(PVSCullingTest, PrimitiveRegistration) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(-10.f, -10.f, -10.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Create mock primitives + RenderPrimitive primitive1; + RenderPrimitive primitive2; + + PVSObjectID id1 = pvsCulling.RegisterPrimitive(&primitive1); + PVSObjectID id2 = pvsCulling.RegisterPrimitive(&primitive2); + + ASSERT_NE(id1, INVALID_PVS_OBJECT); + ASSERT_NE(id2, INVALID_PVS_OBJECT); + ASSERT_NE(id1, id2); + ASSERT_EQ(pvsCulling.GetObjectCount(), 2); + + // Double registration should return same ID + PVSObjectID id1Again = pvsCulling.RegisterPrimitive(&primitive1); + ASSERT_EQ(id1, id1Again); + ASSERT_EQ(pvsCulling.GetObjectCount(), 2); +} + +TEST(PVSCullingTest, PrimitiveUnregistration) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(-10.f, -10.f, -10.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + RenderPrimitive primitive; + PVSObjectID id = pvsCulling.RegisterPrimitive(&primitive); + ASSERT_NE(id, INVALID_PVS_OBJECT); + + pvsCulling.UnregisterPrimitive(&primitive); + + PVSObjectID newId = pvsCulling.GetObjectID(&primitive); + ASSERT_EQ(newId, INVALID_PVS_OBJECT); +} + +TEST(PVSCullingTest, NullPrimitiveHandling) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(-10.f, -10.f, -10.f), Vector3(10.f, 10.f, 10.f)}; + config.cellSize = Vector3(5.f, 5.f, 5.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Should handle null gracefully + PVSObjectID id = pvsCulling.RegisterPrimitive(nullptr); + ASSERT_EQ(id, INVALID_PVS_OBJECT); + + // Should not crash + pvsCulling.UnregisterPrimitive(nullptr); +} + +TEST(PVSCullingTest, VisibilityQuery) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(20.f, 20.f, 20.f)}; + config.cellSize = Vector3(10.f, 10.f, 10.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Create and register primitives with bounds + RenderPrimitive primitive1; + primitive1.worldBound = AABB{Vector3(5.f, 5.f, 5.f), Vector3(8.f, 8.f, 8.f)}; + + RenderPrimitive primitive2; + primitive2.worldBound = AABB{Vector3(15.f, 15.f, 15.f), Vector3(18.f, 18.f, 18.f)}; + + PVSObjectID id1 = pvsCulling.RegisterPrimitive(&primitive1); + PVSObjectID id2 = pvsCulling.RegisterPrimitive(&primitive2); + + // Set visibility: primitive1 visible from cell (0,0,0), primitive2 from cell (1,1,1) + PVSCellID cell0 = pvsCulling.GetPVSData().GetCellIDFromCoord({0, 0, 0}); + PVSCellID cell1 = pvsCulling.GetPVSData().GetCellIDFromCoord({1, 1, 1}); + + pvsCulling.GetPVSData().SetVisible(cell0, id1); + pvsCulling.GetPVSData().SetVisible(cell1, id2); + + // Query from position in cell (0,0,0) + std::vector result; + pvsCulling.QueryPVSVisiblePrimitives(Vector3(5.f, 5.f, 5.f), result); + + ASSERT_EQ(result.size(), 1); + ASSERT_EQ(result[0], &primitive1); + + // Query from position in cell (1,1,1) + result.clear(); + pvsCulling.QueryPVSVisiblePrimitives(Vector3(15.f, 15.f, 15.f), result); + + ASSERT_EQ(result.size(), 1); + ASSERT_EQ(result[0], &primitive2); +} + +TEST(PVSCullingTest, DistanceBasedVisibility) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(100.f, 100.f, 100.f)}; + config.cellSize = Vector3(10.f, 10.f, 10.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Create primitives at different positions + RenderPrimitive nearPrimitive; + nearPrimitive.worldBound = AABB{Vector3(4.f, 4.f, 4.f), Vector3(6.f, 6.f, 6.f)}; + + RenderPrimitive farPrimitive; + farPrimitive.worldBound = AABB{Vector3(94.f, 94.f, 94.f), Vector3(96.f, 96.f, 96.f)}; + + pvsCulling.RegisterPrimitive(&nearPrimitive); + pvsCulling.RegisterPrimitive(&farPrimitive); + + // Compute distance-based visibility with max distance of 20 + pvsCulling.ComputeDistanceBasedVisibility(20.f); + + // Query from first cell - near primitive should be visible + std::vector result; + pvsCulling.QueryPVSVisiblePrimitives(Vector3(5.f, 5.f, 5.f), result); + + // Near primitive should be visible, far primitive should not + bool nearFound = std::find(result.begin(), result.end(), &nearPrimitive) != result.end(); + bool farFound = std::find(result.begin(), result.end(), &farPrimitive) != result.end(); + + ASSERT_TRUE(nearFound); + ASSERT_FALSE(farFound); +} + +TEST(PVSCullingTest, IsPrimitiveVisible) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(20.f, 20.f, 20.f)}; + config.cellSize = Vector3(10.f, 10.f, 10.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + RenderPrimitive primitive; + PVSObjectID id = pvsCulling.RegisterPrimitive(&primitive); + + PVSCellID cell0 = pvsCulling.GetPVSData().GetCellIDFromCoord({0, 0, 0}); + pvsCulling.GetPVSData().SetVisible(cell0, id); + + // Should be visible from cell 0 + ASSERT_TRUE(pvsCulling.IsPrimitiveVisible(Vector3(5.f, 5.f, 5.f), &primitive)); + + // Should not be visible from cell 1 + ASSERT_FALSE(pvsCulling.IsPrimitiveVisible(Vector3(15.f, 15.f, 15.f), &primitive)); +} + +// ============================================================================ +// PVS Baker Tests +// ============================================================================ + +TEST(PVSBakerTest, AddObjects) +{ + PVSBaker baker; + + ASSERT_EQ(baker.GetObjectCount(), 0); + + PVSBakeObject obj1; + obj1.bounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(5.f, 5.f, 5.f)}; + obj1.name = "Object1"; + + uint32_t id1 = baker.AddObject(obj1); + ASSERT_EQ(id1, 0); + ASSERT_EQ(baker.GetObjectCount(), 1); + + PVSBakeObject obj2; + obj2.bounds = AABB{Vector3(10.f, 10.f, 10.f), Vector3(15.f, 15.f, 15.f)}; + obj2.name = "Object2"; + + uint32_t id2 = baker.AddObject(obj2); + ASSERT_EQ(id2, 1); + ASSERT_EQ(baker.GetObjectCount(), 2); +} + +TEST(PVSBakerTest, ClearObjects) +{ + PVSBaker baker; + + PVSBakeObject obj; + obj.bounds = AABB{Vector3(0.f), Vector3(5.f)}; + baker.AddObject(obj); + baker.AddObject(obj); + + ASSERT_EQ(baker.GetObjectCount(), 2); + + baker.ClearObjects(); + ASSERT_EQ(baker.GetObjectCount(), 0); +} + +TEST(PVSBakerTest, BakeDistanceBased) +{ + PVSBaker baker; + + // Add a few objects at different positions + PVSBakeObject nearObj; + nearObj.bounds = AABB{Vector3(4.f, 4.f, 4.f), Vector3(6.f, 6.f, 6.f)}; + nearObj.name = "NearObject"; + baker.AddObject(nearObj); + + PVSBakeObject farObj; + farObj.bounds = AABB{Vector3(94.f, 94.f, 94.f), Vector3(96.f, 96.f, 96.f)}; + farObj.name = "FarObject"; + baker.AddObject(farObj); + + // Configure baking + PVSBakeConfig bakeConfig; + bakeConfig.pvsConfig.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(100.f, 100.f, 100.f)}; + bakeConfig.pvsConfig.cellSize = Vector3(10.f, 10.f, 10.f); + bakeConfig.pvsConfig.maxObjects = 100; + bakeConfig.method = PVSBakeConfig::Method::DISTANCE; + bakeConfig.maxVisibilityDistance = 20.f; + + // Bake + PVSBakedData bakedData; + PVSBakeResult result = baker.Bake(bakeConfig, bakedData); + + ASSERT_TRUE(result.success); + ASSERT_GT(bakedData.cells.size(), 0); + ASSERT_EQ(bakedData.numObjects, 2); + + // The near object should be visible from cell (0,0,0) + // Cell 0 center is at (5,5,5), near object center is at (5,5,5) - distance 0 + uint32_t cell0 = 0; + bool nearVisible = (bakedData.visibilityData[cell0][0] & 1) != 0; + ASSERT_TRUE(nearVisible); + + // The far object should NOT be visible from cell (0,0,0) + // Cell 0 center is at (5,5,5), far object center is at (95,95,95) - distance ~156 + bool farVisible = (bakedData.visibilityData[cell0][0] & 2) != 0; + ASSERT_FALSE(farVisible); +} + +TEST(PVSBakerTest, BakeCustomFunction) +{ + PVSBaker baker; + + // Add objects + PVSBakeObject obj1; + obj1.bounds = AABB{Vector3(0.f), Vector3(5.f)}; + obj1.name = "Object1"; + baker.AddObject(obj1); + + PVSBakeObject obj2; + obj2.bounds = AABB{Vector3(10.f), Vector3(15.f)}; + obj2.name = "Object2"; + baker.AddObject(obj2); + + // Configure baking with custom function that only makes object 0 visible + PVSBakeConfig bakeConfig; + bakeConfig.pvsConfig.worldBounds = AABB{Vector3(0.f), Vector3(20.f)}; + bakeConfig.pvsConfig.cellSize = Vector3(10.f); + bakeConfig.pvsConfig.maxObjects = 100; + bakeConfig.method = PVSBakeConfig::Method::CUSTOM; + bakeConfig.customTestFunc = [](const AABB&, const Vector3&, const AABB&, uint32_t objIndex) { + return objIndex == 0; // Only object 0 is visible + }; + + PVSBakedData bakedData; + PVSBakeResult result = baker.Bake(bakeConfig, bakedData); + + ASSERT_TRUE(result.success); + + // Object 0 should be visible from all cells + for (size_t i = 0; i < bakedData.visibilityData.size(); ++i) { + bool obj0Visible = (bakedData.visibilityData[i][0] & 1) != 0; + bool obj1Visible = (bakedData.visibilityData[i][0] & 2) != 0; + ASSERT_TRUE(obj0Visible); + ASSERT_FALSE(obj1Visible); + } +} + +TEST(PVSBakerTest, Statistics) +{ + PVSBaker baker; + + // Add 10 objects + for (int i = 0; i < 10; ++i) { + PVSBakeObject obj; + obj.bounds = AABB{ + Vector3(static_cast(i * 10), 0.f, 0.f), + Vector3(static_cast(i * 10 + 5), 5.f, 5.f) + }; + obj.name = "Object" + std::to_string(i); + baker.AddObject(obj); + } + + PVSBakeConfig bakeConfig; + bakeConfig.pvsConfig.worldBounds = AABB{Vector3(0.f), Vector3(100.f)}; + bakeConfig.pvsConfig.cellSize = Vector3(25.f); + bakeConfig.pvsConfig.maxObjects = 100; + bakeConfig.method = PVSBakeConfig::Method::DISTANCE; + bakeConfig.maxVisibilityDistance = 50.f; + + PVSBakedData bakedData; + PVSBakeResult result = baker.Bake(bakeConfig, bakedData); + + ASSERT_TRUE(result.success); + ASSERT_GT(result.statistics.totalCells, 0); + ASSERT_EQ(result.statistics.totalObjects, 10); + ASSERT_GT(result.statistics.totalVisiblePairs, 0); +} + +// ============================================================================ +// PVS Baked Data Serialization Tests +// ============================================================================ + +TEST(PVSBakedDataTest, GetMemorySize) +{ + PVSBakedData data; + data.config.worldBounds = AABB{Vector3(-10.f), Vector3(10.f)}; + data.config.cellSize = Vector3(5.f); + data.gridDimensions = {4, 4, 4}; + data.numObjects = 100; + + // Add some cells + data.cells.resize(64); + + // Add visibility data + data.visibilityData.resize(64); + for (auto &v : data.visibilityData) { + v.resize(2); // 128 bits + } + + size_t memSize = data.GetMemorySize(); + ASSERT_GT(memSize, 0); +} + +TEST(PVSBakedDataTest, GetStatistics) +{ + PVSBakedData data; + data.numObjects = 64; + data.cells.resize(8); + data.visibilityData.resize(8); + + // Set some visibility bits + for (size_t i = 0; i < 8; ++i) { + data.visibilityData[i].resize(1); + data.visibilityData[i][0] = 0x0F0F0F0F0F0F0F0FULL; // 32 bits set + } + + auto stats = data.GetStatistics(); + + ASSERT_EQ(stats.totalCells, 8); + ASSERT_EQ(stats.totalObjects, 64); + ASSERT_EQ(stats.totalVisiblePairs, 32 * 8); // 32 bits set per cell, 8 cells + ASSERT_FLOAT_EQ(stats.averageVisibleObjects, 32.f); +} + +// ============================================================================ +// PVS Data Load/Export Tests +// ============================================================================ + +TEST(PVSDataTest, LoadFromBakedData) +{ + // Create baked data + PVSBakedData bakedData; + bakedData.config.worldBounds = AABB{Vector3(0.f), Vector3(20.f)}; + bakedData.config.cellSize = Vector3(10.f); + bakedData.config.maxObjects = 100; + bakedData.gridDimensions = {2, 2, 2}; + bakedData.numObjects = 3; + + // Create cells + bakedData.cells.resize(8); + for (uint32_t i = 0; i < 8; ++i) { + bakedData.cells[i].id = i; + } + + // Create visibility data - object 0 visible from all cells + bakedData.visibilityData.resize(8); + for (auto &v : bakedData.visibilityData) { + v.resize(1); + v[0] = 0x01; // Object 0 visible + } + + // Load into PVSData + PVSData pvsData; + pvsData.LoadFromBakedData(bakedData); + + // Verify + ASSERT_EQ(pvsData.GetCellCount(), 8); + + for (uint32_t cellID = 0; cellID < 8; ++cellID) { + ASSERT_TRUE(pvsData.IsVisible(cellID, 0)); // Object 0 visible + ASSERT_FALSE(pvsData.IsVisible(cellID, 1)); // Object 1 not visible + } +} + +TEST(PVSDataTest, ExportToBakedData) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(20.f)}; + config.cellSize = Vector3(10.f); + config.maxObjects = 10; + + pvsData.Initialize(config); + + // Set some visibility + pvsData.SetVisible(0, 0); + pvsData.SetVisible(0, 1); + pvsData.SetVisible(1, 2); + + // Export + PVSBakedData bakedData; + pvsData.ExportToBakedData(bakedData); + + // Verify + ASSERT_EQ(bakedData.config.maxObjects, 10); + ASSERT_EQ(bakedData.gridDimensions.x, 2); + ASSERT_EQ(bakedData.gridDimensions.y, 2); + ASSERT_EQ(bakedData.gridDimensions.z, 2); + ASSERT_EQ(bakedData.cells.size(), 8); +} + +// ============================================================================ +// Optimized Query Tests +// ============================================================================ + +TEST(PVSBitSetTest, ForEachSetBit) +{ + PVSBitSet bitset(256); + + // Set some bits + bitset.Set(0); + bitset.Set(5); + bitset.Set(63); // Last bit of first word + bitset.Set(64); // First bit of second word + bitset.Set(127); + bitset.Set(200); + + std::vector indices; + bitset.ForEachSetBit([&indices](uint32_t idx) { + indices.push_back(idx); + }); + + ASSERT_EQ(indices.size(), 6); + ASSERT_EQ(indices[0], 0); + ASSERT_EQ(indices[1], 5); + ASSERT_EQ(indices[2], 63); + ASSERT_EQ(indices[3], 64); + ASSERT_EQ(indices[4], 127); + ASSERT_EQ(indices[5], 200); +} + +TEST(PVSBitSetTest, GetSetBitIndices) +{ + PVSBitSet bitset(128); + + bitset.Set(10); + bitset.Set(50); + bitset.Set(100); + + std::vector indices; + bitset.GetSetBitIndices(indices); + + ASSERT_EQ(indices.size(), 3); + ASSERT_EQ(indices[0], 10); + ASSERT_EQ(indices[1], 50); + ASSERT_EQ(indices[2], 100); +} + +TEST(PVSBitSetTest, GetSetBitIndicesWithLimit) +{ + PVSBitSet bitset(256); + + for (uint32_t i = 0; i < 20; ++i) { + bitset.Set(i * 10); + } + + std::vector indices; + uint32_t count = bitset.GetSetBitIndices(indices, 5); + + ASSERT_EQ(count, 5); + ASSERT_EQ(indices.size(), 5); +} + +TEST(PVSDataTest, FastCellLookup) +{ + PVSData pvsData; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(100.f, 100.f, 100.f)}; + config.cellSize = Vector3(10.f, 10.f, 10.f); + config.maxObjects = 100; + + pvsData.Initialize(config); + + // Test GetCellIDFast vs GetCellID + Vector3 testPos(25.f, 35.f, 45.f); + + PVSCellID normalID = pvsData.GetCellID(testPos); + PVSCellID fastID = pvsData.GetCellIDFast(testPos); + + ASSERT_EQ(normalID, fastID); + + // Test IsInBounds + ASSERT_TRUE(pvsData.IsInBounds(testPos)); + ASSERT_FALSE(pvsData.IsInBounds(Vector3(-1.f, 0.f, 0.f))); + ASSERT_FALSE(pvsData.IsInBounds(Vector3(100.f, 0.f, 0.f))); // Edge case +} + +TEST(PVSCullingTest, QueryVisiblePrimitivesOptimized) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f, 0.f, 0.f), Vector3(100.f, 100.f, 100.f)}; + config.cellSize = Vector3(20.f, 20.f, 20.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Register some primitives + std::vector primitives(10); + for (int i = 0; i < 10; ++i) { + pvsCulling.RegisterPrimitive(&primitives[i]); + } + + // Set visibility for cell 0 - only objects 0, 3, 7 visible + PVSCellID cell0 = pvsCulling.GetPVSData().GetCellIDFromCoord({0, 0, 0}); + pvsCulling.GetPVSData().SetVisible(cell0, 0); + pvsCulling.GetPVSData().SetVisible(cell0, 3); + pvsCulling.GetPVSData().SetVisible(cell0, 7); + + // Query with optimized method + std::vector resultOptimized; + pvsCulling.QueryVisiblePrimitivesOptimized(Vector3(5.f, 5.f, 5.f), nullptr, resultOptimized); + + // Query with original method for comparison + std::vector resultOriginal; + pvsCulling.QueryVisiblePrimitives(Vector3(5.f, 5.f, 5.f), nullptr, resultOriginal); + + // Both methods should return same results + ASSERT_EQ(resultOptimized.size(), resultOriginal.size()); + ASSERT_EQ(resultOptimized.size(), 3); +} + +TEST(PVSCullingTest, ForEachVisibleObject) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(50.f)}; + config.cellSize = Vector3(10.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Register primitives + std::vector primitives(5); + for (int i = 0; i < 5; ++i) { + pvsCulling.RegisterPrimitive(&primitives[i]); + } + + // Set visibility + PVSCellID cell0 = pvsCulling.GetPVSData().GetCellIDFromCoord({0, 0, 0}); + pvsCulling.GetPVSData().SetVisible(cell0, 1); + pvsCulling.GetPVSData().SetVisible(cell0, 3); + + // Test ForEachVisibleObject + std::vector visitedObjects; + uint32_t count = pvsCulling.ForEachVisibleObject(Vector3(5.f, 5.f, 5.f), + [&visitedObjects](PVSObjectID objID) { + visitedObjects.push_back(objID); + }); + + ASSERT_EQ(count, 2); + ASSERT_EQ(visitedObjects.size(), 2); + ASSERT_EQ(visitedObjects[0], 1); + ASSERT_EQ(visitedObjects[1], 3); +} + +TEST(PVSCullingTest, GetVisibleCount) +{ + PVSCulling pvsCulling; + + PVSConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(40.f)}; + config.cellSize = Vector3(20.f); + config.maxObjects = 100; + + pvsCulling.Initialize(config); + + // Register primitives + std::vector primitives(10); + for (int i = 0; i < 10; ++i) { + pvsCulling.RegisterPrimitive(&primitives[i]); + } + + // Set different visibility per cell + PVSCellID cell0 = pvsCulling.GetPVSData().GetCellIDFromCoord({0, 0, 0}); + PVSCellID cell1 = pvsCulling.GetPVSData().GetCellIDFromCoord({1, 0, 0}); + + pvsCulling.GetPVSData().SetVisible(cell0, 0); + pvsCulling.GetPVSData().SetVisible(cell0, 1); + pvsCulling.GetPVSData().SetVisible(cell0, 2); + + pvsCulling.GetPVSData().SetVisible(cell1, 5); + pvsCulling.GetPVSData().SetVisible(cell1, 6); + pvsCulling.GetPVSData().SetVisible(cell1, 7); + pvsCulling.GetPVSData().SetVisible(cell1, 8); + pvsCulling.GetPVSData().SetVisible(cell1, 9); + + // Cell 0 has 3 visible + ASSERT_EQ(pvsCulling.GetVisibleCount(Vector3(5.f, 5.f, 5.f)), 3); + + // Cell 1 has 5 visible + ASSERT_EQ(pvsCulling.GetVisibleCount(Vector3(25.f, 5.f, 5.f)), 5); +} + +// ============================================================================ +// PVS Streaming Tests +// ============================================================================ + +TEST(PVSStreamingTypesTest, SectorCoordEquality) +{ + PVSSectorCoord coord1{1, 2, 3}; + PVSSectorCoord coord2{1, 2, 3}; + PVSSectorCoord coord3{1, 2, 4}; + + ASSERT_TRUE(coord1 == coord2); + ASSERT_FALSE(coord1 == coord3); + ASSERT_TRUE(coord1 != coord3); +} + +TEST(PVSStreamingTypesTest, SectorCoordHash) +{ + PVSSectorCoordHash hasher; + + PVSSectorCoord coord1{1, 2, 3}; + PVSSectorCoord coord2{1, 2, 3}; + PVSSectorCoord coord3{3, 2, 1}; + + ASSERT_EQ(hasher(coord1), hasher(coord2)); + ASSERT_NE(hasher(coord1), hasher(coord3)); +} + +TEST(PVSStreamingManagerTest, Initialize) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(1000.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + config.loadRadius = 150.f; + config.unloadRadius = 200.f; + config.maxLoadedSectors = 9; + + manager.Initialize(config); + + ASSERT_EQ(manager.GetLoadedSectorCount(), 0); +} + +TEST(PVSStreamingManagerTest, GetSectorCoord) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(400.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + + manager.Initialize(config); + + // Test sector coordinate calculation + PVSSectorCoord coord1 = manager.GetSectorCoord(Vector3(50.f, 50.f, 50.f)); + ASSERT_EQ(coord1.x, 0); + ASSERT_EQ(coord1.y, 0); + ASSERT_EQ(coord1.z, 0); + + PVSSectorCoord coord2 = manager.GetSectorCoord(Vector3(150.f, 150.f, 150.f)); + ASSERT_EQ(coord2.x, 1); + ASSERT_EQ(coord2.y, 1); + ASSERT_EQ(coord2.z, 1); + + PVSSectorCoord coord3 = manager.GetSectorCoord(Vector3(350.f, 250.f, 50.f)); + ASSERT_EQ(coord3.x, 3); + ASSERT_EQ(coord3.y, 2); + ASSERT_EQ(coord3.z, 0); +} + +TEST(PVSStreamingManagerTest, SectorLoadUnload) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(400.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + config.maxObjectsPerSector = 100; + + manager.Initialize(config); + + // Set up a data provider that creates simple sector data + manager.SetDataProvider([&config](PVSSectorCoord coord, PVSSectorBakedData &outData) { + outData.coord = coord; + outData.bounds = AABB{ + Vector3(coord.x * 100.f, coord.y * 100.f, coord.z * 100.f), + Vector3((coord.x + 1) * 100.f, (coord.y + 1) * 100.f, (coord.z + 1) * 100.f) + }; + + // Set up simple PVS data + outData.pvsData.config.worldBounds = outData.bounds; + outData.pvsData.config.cellSize = config.cellSize; + outData.pvsData.config.maxObjects = config.maxObjectsPerSector; + outData.pvsData.gridDimensions = {10, 10, 10}; + outData.pvsData.numObjects = 10; + outData.pvsData.cells.resize(1000); + outData.pvsData.visibilityData.resize(1000); + + return true; + }); + + // Request load + bool loadCalled = false; + PVSSectorCoord targetCoord{1, 1, 1}; + manager.RequestSectorLoad(targetCoord, [&loadCalled](PVSSectorID id, bool success) { + loadCalled = true; + ASSERT_TRUE(success); + }); + + ASSERT_TRUE(loadCalled); + ASSERT_TRUE(manager.IsSectorLoaded(targetCoord)); + ASSERT_EQ(manager.GetLoadedSectorCount(), 1); + + // Request unload + manager.RequestSectorUnload(targetCoord); + ASSERT_FALSE(manager.IsSectorLoaded(targetCoord)); + ASSERT_EQ(manager.GetLoadedSectorCount(), 0); +} + +TEST(PVSStreamingManagerTest, AutomaticStreaming) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(500.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + config.loadRadius = 150.f; + config.unloadRadius = 250.f; + config.maxLoadedSectors = 27; + + manager.Initialize(config); + + // Set up data provider + manager.SetDataProvider([&config](PVSSectorCoord coord, PVSSectorBakedData &outData) { + outData.coord = coord; + outData.pvsData.config.worldBounds = AABB{ + Vector3(coord.x * 100.f, coord.y * 100.f, coord.z * 100.f), + Vector3((coord.x + 1) * 100.f, (coord.y + 1) * 100.f, (coord.z + 1) * 100.f) + }; + outData.pvsData.config.cellSize = config.cellSize; + outData.pvsData.config.maxObjects = 100; + outData.pvsData.gridDimensions = {10, 10, 10}; + outData.pvsData.numObjects = 10; + outData.pvsData.cells.resize(1000); + outData.pvsData.visibilityData.resize(1000); + return true; + }); + + // Update with viewer at center + manager.Update(Vector3(250.f, 250.f, 250.f), 1); + + // Should have loaded sectors around viewer + ASSERT_GT(manager.GetLoadedSectorCount(), 0); + ASSERT_TRUE(manager.IsPositionLoaded(Vector3(250.f, 250.f, 250.f))); +} + +TEST(PVSStreamingManagerTest, LRUEviction) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(1000.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + config.loadRadius = 50.f; + config.unloadRadius = 100.f; + config.maxLoadedSectors = 3; // Very limited + + manager.Initialize(config); + + manager.SetDataProvider([&config](PVSSectorCoord coord, PVSSectorBakedData &outData) { + outData.coord = coord; + outData.pvsData.config.maxObjects = 10; + outData.pvsData.gridDimensions = {10, 10, 10}; + outData.pvsData.numObjects = 10; + outData.pvsData.cells.resize(1000); + outData.pvsData.visibilityData.resize(1000); + return true; + }); + + // Load first sector + manager.RequestSectorLoad({0, 0, 0}); + ASSERT_EQ(manager.GetLoadedSectorCount(), 1); + + // Load second sector + manager.RequestSectorLoad({1, 0, 0}); + ASSERT_EQ(manager.GetLoadedSectorCount(), 2); + + // Load third sector + manager.RequestSectorLoad({2, 0, 0}); + ASSERT_EQ(manager.GetLoadedSectorCount(), 3); + + // Load fourth sector - should evict oldest + manager.RequestSectorLoad({3, 0, 0}); + // Note: LRU eviction happens in Update() or EnforceSectorLimit() + // After manual loads, we need to enforce limit + + auto stats = manager.GetStatistics(); + ASSERT_LE(stats.loadedSectors, config.maxLoadedSectors + 1); // May be 1 over until next update +} + +TEST(PVSStreamingManagerTest, Statistics) +{ + PVSStreamingManager manager; + + PVSStreamingConfig config; + config.worldBounds = AABB{Vector3(0.f), Vector3(400.f)}; + config.sectorSize = Vector3(100.f); + config.cellSize = Vector3(10.f); + config.maxLoadedSectors = 16; + + manager.Initialize(config); + + manager.SetDataProvider([](PVSSectorCoord coord, PVSSectorBakedData &outData) { + outData.coord = coord; + outData.pvsData.config.maxObjects = 10; + outData.pvsData.gridDimensions = {10, 10, 10}; + outData.pvsData.numObjects = 10; + outData.pvsData.cells.resize(1000); + outData.pvsData.visibilityData.resize(1000); + return true; + }); + + manager.RequestSectorLoad({0, 0, 0}); + manager.RequestSectorLoad({1, 0, 0}); + + auto stats = manager.GetStatistics(); + ASSERT_EQ(stats.loadedSectors, 2); + ASSERT_GT(stats.totalMemoryUsed, 0); +} + +// ============================================================================ +// PVS Sampling Tests +// ============================================================================ + +TEST(PVSSamplingTest, RandomGenerator) +{ + PVSRandomGenerator rng(12345); + + // Test float range [0, 1) + for (int i = 0; i < 100; ++i) { + float f = rng.NextFloat(); + ASSERT_GE(f, 0.0f); + ASSERT_LT(f, 1.0f); + } + + // Test float range with custom bounds + for (int i = 0; i < 100; ++i) { + float f = rng.NextFloat(10.0f, 20.0f); + ASSERT_GE(f, 10.0f); + ASSERT_LT(f, 20.0f); + } +} + +TEST(PVSSamplingTest, RandomPointInCell) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(10.0f, 20.0f, 30.0f), Vector3(20.0f, 40.0f, 60.0f)}; + + for (int i = 0; i < 100; ++i) { + Vector3 point = sampler.GenerateRandomPointInCell(bounds); + + ASSERT_GE(point.x, bounds.min.x); + ASSERT_LT(point.x, bounds.max.x); + ASSERT_GE(point.y, bounds.min.y); + ASSERT_LT(point.y, bounds.max.y); + ASSERT_GE(point.z, bounds.min.z); + ASSERT_LT(point.z, bounds.max.z); + } +} + +TEST(PVSSamplingTest, RandomDirection) +{ + PVSSampling sampler(12345); + + for (int i = 0; i < 100; ++i) { + Vector3 dir = sampler.GenerateRandomDirection(); + + // Should be normalized (length ~= 1) + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z); + ASSERT_NEAR(len, 1.0f, 0.001f); + } +} + +TEST(PVSSamplingTest, HemisphereDirection) +{ + PVSSampling sampler(12345); + Vector3 upNormal(0.0f, 1.0f, 0.0f); + + for (int i = 0; i < 100; ++i) { + Vector3 dir = sampler.GenerateHemisphereDirection(upNormal); + + // Should be normalized + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z); + ASSERT_NEAR(len, 1.0f, 0.001f); + + // Dot product with normal should be positive (in hemisphere) + float dot = dir.x * upNormal.x + dir.y * upNormal.y + dir.z * upNormal.z; + ASSERT_GE(dot, 0.0f); + } +} + +TEST(PVSSamplingTest, HaltonSequence) +{ + // Known Halton sequence values for base 2 + ASSERT_NEAR(PVSSampling::HaltonSequence(1, 2), 0.5f, 0.001f); + ASSERT_NEAR(PVSSampling::HaltonSequence(2, 2), 0.25f, 0.001f); + ASSERT_NEAR(PVSSampling::HaltonSequence(3, 2), 0.75f, 0.001f); + ASSERT_NEAR(PVSSampling::HaltonSequence(4, 2), 0.125f, 0.001f); + + // Halton sequence base 3 + ASSERT_NEAR(PVSSampling::HaltonSequence(1, 3), 1.0f/3.0f, 0.001f); + ASSERT_NEAR(PVSSampling::HaltonSequence(2, 3), 2.0f/3.0f, 0.001f); + ASSERT_NEAR(PVSSampling::HaltonSequence(3, 3), 1.0f/9.0f, 0.001f); +} + +TEST(PVSSamplingTest, FibonacciSphereDirection) +{ + std::vector directions; + PVSSampling::GenerateFibonacciDirections(100, directions); + + ASSERT_EQ(directions.size(), 100); + + for (const auto &dir : directions) { + // Should be normalized + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z); + ASSERT_NEAR(len, 1.0f, 0.001f); + } +} + +TEST(PVSSamplingTest, StratifiedPoints) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(0.0f), Vector3(10.0f)}; + std::vector points; + + sampler.GenerateStratifiedPoints(bounds, 64, points); + + ASSERT_EQ(points.size(), 64); + + for (const auto &point : points) { + ASSERT_GE(point.x, bounds.min.x); + ASSERT_LT(point.x, bounds.max.x); + ASSERT_GE(point.y, bounds.min.y); + ASSERT_LT(point.y, bounds.max.y); + ASSERT_GE(point.z, bounds.min.z); + ASSERT_LT(point.z, bounds.max.z); + } +} + +TEST(PVSSamplingTest, HaltonPoints) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(5.0f, 10.0f, 15.0f), Vector3(15.0f, 30.0f, 45.0f)}; + std::vector points; + + sampler.GenerateHaltonPoints(bounds, 50, points); + + ASSERT_EQ(points.size(), 50); + + for (const auto &point : points) { + ASSERT_GE(point.x, bounds.min.x); + ASSERT_LE(point.x, bounds.max.x); + ASSERT_GE(point.y, bounds.min.y); + ASSERT_LE(point.y, bounds.max.y); + ASSERT_GE(point.z, bounds.min.z); + ASSERT_LE(point.z, bounds.max.z); + } +} + +TEST(PVSSamplingTest, GenerateCellSamples) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(0.0f), Vector3(10.0f)}; + + PVSCellSamplingConfig config; + config.numSamplesPerCell = 8; + config.numDirectionsPerSample = 16; + config.pointStrategy = PVSSamplingStrategy::STRATIFIED; + config.directionStrategy = PVSSamplingStrategy::FIBONACCI; + + PVSCellSamples samples; + sampler.GenerateCellSamples(bounds, config, samples); + + // Should have numSamplesPerCell * numDirectionsPerSample samples + ASSERT_EQ(samples.samples.size(), config.numSamplesPerCell * config.numDirectionsPerSample); + + for (const auto &sample : samples.samples) { + // Position should be in bounds + ASSERT_GE(sample.position.x, bounds.min.x); + ASSERT_LT(sample.position.x, bounds.max.x); + ASSERT_GE(sample.position.y, bounds.min.y); + ASSERT_LT(sample.position.y, bounds.max.y); + ASSERT_GE(sample.position.z, bounds.min.z); + ASSERT_LT(sample.position.z, bounds.max.z); + + // Direction should be normalized + float len = std::sqrt(sample.direction.x * sample.direction.x + + sample.direction.y * sample.direction.y + + sample.direction.z * sample.direction.z); + ASSERT_NEAR(len, 1.0f, 0.001f); + } +} + +TEST(PVSSamplingTest, HemisphereSampling) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(0.0f), Vector3(10.0f)}; + + PVSCellSamplingConfig config; + config.numSamplesPerCell = 4; + config.numDirectionsPerSample = 8; + config.useHemisphereForFloors = true; // Use hemisphere + + PVSCellSamples samples; + sampler.GenerateCellSamples(bounds, config, samples); + + ASSERT_EQ(samples.samples.size(), config.numSamplesPerCell * config.numDirectionsPerSample); + + // All directions should be in upper hemisphere (y >= 0) + for (const auto &sample : samples.samples) { + ASSERT_GE(sample.direction.y, 0.0f); + } +} + +TEST(PVSSamplingTest, DifferentStrategies) +{ + PVSSampling sampler(12345); + AABB bounds{Vector3(0.0f), Vector3(10.0f)}; + + // Test RANDOM point strategy + std::vector randomPoints; + sampler.GeneratePointsInCell(bounds, 32, PVSSamplingStrategy::RANDOM, randomPoints); + ASSERT_EQ(randomPoints.size(), 32); + + // Test HALTON point strategy + std::vector haltonPoints; + sampler.GeneratePointsInCell(bounds, 32, PVSSamplingStrategy::HALTON, haltonPoints); + ASSERT_EQ(haltonPoints.size(), 32); + + // Test RANDOM direction strategy + std::vector randomDirs; + sampler.GenerateDirections(32, PVSSamplingStrategy::RANDOM, randomDirs); + ASSERT_EQ(randomDirs.size(), 32); + + // Test FIBONACCI direction strategy + std::vector fibDirs; + sampler.GenerateDirections(32, PVSSamplingStrategy::FIBONACCI, fibDirs); + ASSERT_EQ(fibDirs.size(), 32); +} + +TEST(PVSSamplingTest, SeedDeterminism) +{ + // Two samplers with same seed should produce same results + PVSSampling sampler1(42); + PVSSampling sampler2(42); + + AABB bounds{Vector3(0.0f), Vector3(10.0f)}; + + for (int i = 0; i < 10; ++i) { + Vector3 p1 = sampler1.GenerateRandomPointInCell(bounds); + Vector3 p2 = sampler2.GenerateRandomPointInCell(bounds); + + ASSERT_FLOAT_EQ(p1.x, p2.x); + ASSERT_FLOAT_EQ(p1.y, p2.y); + ASSERT_FLOAT_EQ(p1.z, p2.z); + } +}