433 lines
21 KiB
C++
433 lines
21 KiB
C++
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <glm/gtc/constants.hpp>
|
|
#include <destrum/ObjectModel/Component.h>
|
|
#include <destrum/ObjectModel/GameObject.h>
|
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
|
#include <destrum/Components/Physics/Collider.h>
|
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
|
#include <destrum/Graphics/Managers/Renderer2D.h>
|
|
#include <destrum/Graphics/Resources/Font.h>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include <destrum/Graphics/Frustum.h>
|
|
#include <destrum/Graphics/Material.h>
|
|
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
|
#include <destrum/Graphics/DeletionQueue.h>
|
|
#include <destrum/Graphics/IndirectDrawList.h>
|
|
|
|
#include "TestUtils.h"
|
|
|
|
using namespace test;
|
|
|
|
namespace {
|
|
void testLineRenderingManager()
|
|
{
|
|
auto& lineManager = LineRenderingManager::GetInstance();
|
|
lineManager.ClearLines();
|
|
|
|
lineManager.SubmitLine(
|
|
glm::vec3{0.0f},
|
|
glm::vec3{1.0f, 0.0f, 0.0f},
|
|
glm::vec4{1.0f, 0.0f, 0.0f, 1.0f});
|
|
lineManager.SubmitLine(Line{
|
|
.start = glm::vec3{1.0f},
|
|
.end = glm::vec3{2.0f},
|
|
.color = glm::vec4{0.0f, 1.0f, 0.0f, 1.0f},
|
|
});
|
|
|
|
check(lineManager.GetLines().size() == 2,
|
|
"line rendering manager must retain submitted lines");
|
|
|
|
lineManager.ClearLines();
|
|
check(lineManager.GetLines().empty(),
|
|
"line rendering manager must clear transient lines");
|
|
}
|
|
|
|
void testFrustumFromViewProj()
|
|
{
|
|
const auto lookingAlongPositiveZ = [](const glm::vec3& position, float range) {
|
|
const glm::mat4 view = glm::lookAt(position, position + glm::vec3{0.0f, 0.0f, 1.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
|
|
return glm::perspectiveRH_ZO(glm::radians(60.0f), 1.0f, 0.1f, range) * view;
|
|
};
|
|
|
|
const glm::mat4 viewProj = lookingAlongPositiveZ(glm::vec3{0.0f}, 20.0f);
|
|
const Frustum frustum = edge::createFrustumFromViewProj(viewProj);
|
|
|
|
check(edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, 10.0f}, 0.5f}), "a sphere in front must be inside");
|
|
check(!edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, -5.0f}, 0.5f}), "a sphere behind must be outside");
|
|
check(!edge::isInFrustum(frustum, Sphere{{0.0f, 0.0f, 30.0f}, 0.5f}), "a sphere past the far plane must be outside");
|
|
check(!edge::isInFrustum(frustum, Sphere{{15.0f, 0.0f, 10.0f}, 0.5f}), "a sphere to the side must be outside");
|
|
|
|
const auto corners = edge::calculateFrustumCorners(viewProj);
|
|
const bool cornersOnNearAndFar = std::ranges::all_of(corners, [](const glm::vec3& corner) {
|
|
return std::abs(corner.z - 0.1f) < 0.001f || std::abs(corner.z - 20.0f) < 0.01f;
|
|
});
|
|
check(cornersOnNearAndFar, "frustum corners must lie on the near and far planes");
|
|
|
|
check(edge::frustumsOverlap(frustum, lookingAlongPositiveZ(glm::vec3{0.0f, 0.0f, 5.0f}, 10.0f)),
|
|
"a frustum in front of the camera must overlap");
|
|
check(!edge::frustumsOverlap(frustum, lookingAlongPositiveZ(glm::vec3{0.0f, 0.0f, -30.0f}, 10.0f)),
|
|
"a frustum entirely behind the camera must not overlap");
|
|
}
|
|
|
|
void testRenderer2DShapes()
|
|
{
|
|
auto& r2d = Renderer2D::GetInstance();
|
|
r2d.Clear();
|
|
|
|
r2d.DrawRect({10.0f, 20.0f}, {30.0f, 40.0f}, glm::vec4{0.5f});
|
|
const Shape2D& rect = r2d.GetShapes().back();
|
|
check(rect.type == Shape2DType::Rect &&
|
|
rect.center == glm::vec2{25.0f, 40.0f} &&
|
|
rect.halfSize == glm::vec2{15.0f, 20.0f} &&
|
|
rect.textureId == NULL_IMAGE_ID &&
|
|
rect.outline == 0.0f,
|
|
"rects must be anchored at their top left corner and filled by default");
|
|
|
|
r2d.DrawEllipseOutline({50.0f, 50.0f}, {20.0f, 10.0f}, 2.0f, glm::vec4{1.0f});
|
|
const Shape2D& ellipse = r2d.GetShapes().back();
|
|
check(ellipse.type == Shape2DType::Ellipse &&
|
|
ellipse.center == glm::vec2{50.0f} &&
|
|
ellipse.halfSize == glm::vec2{20.0f, 10.0f} &&
|
|
ellipse.outline == 2.0f,
|
|
"ellipses must be centered with their radii as half size");
|
|
|
|
r2d.DrawTriangle({0.0f, 10.0f}, {5.0f, 0.0f}, {10.0f, 10.0f}, glm::vec4{1.0f});
|
|
const Shape2D& triangle = r2d.GetShapes().back();
|
|
check(triangle.type == Shape2DType::Triangle &&
|
|
triangle.center == glm::vec2{5.0f} &&
|
|
triangle.halfSize == glm::vec2{5.0f} &&
|
|
triangle.p0 == glm::vec2{-5.0f, 5.0f} &&
|
|
triangle.p1 == glm::vec2{0.0f, -5.0f} &&
|
|
triangle.p2 == glm::vec2{5.0f, 5.0f},
|
|
"triangle points must be stored relative to their bounding box center");
|
|
|
|
r2d.DrawLine({0.0f, 0.0f}, {0.0f, 10.0f}, 4.0f, glm::vec4{1.0f}, LineCap::Round);
|
|
const Shape2D& line = r2d.GetShapes().back();
|
|
check(line.type == Shape2DType::Rect &&
|
|
line.center == glm::vec2{0.0f, 5.0f} &&
|
|
close(line.halfSize.x, 7.0f) && close(line.halfSize.y, 2.0f) &&
|
|
close(line.rotation, glm::half_pi<float>()) &&
|
|
close(line.cornerRadius, 2.0f),
|
|
"round capped lines must be rotated rects extended by half their thickness");
|
|
|
|
check(r2d.GetShapes().size() == 4, "every draw call must submit exactly one shape");
|
|
r2d.Clear();
|
|
check(r2d.GetShapes().empty(), "clear must drop all submitted shapes");
|
|
}
|
|
|
|
void testFontAtlasAndLayout()
|
|
{
|
|
const std::filesystem::path fontPath =
|
|
std::filesystem::path{DESTRUM_ENGINE_ASSETS_SRC_DIR} / "fonts" / "Roboto-Medium.ttf";
|
|
const Font font = Font::LoadFromFile(fontPath, 32.0f);
|
|
|
|
const FontGlyph* a = font.GetGlyph(U'A');
|
|
const FontGlyph* space = font.GetGlyph(U' ');
|
|
check(a != nullptr && a->size.x > 0.0f && a->size.y > 0.0f && a->advance > 0.0f &&
|
|
a->uvMax.x > a->uvMin.x && a->uvMax.y > a->uvMin.y,
|
|
"printable glyphs must be rasterized into the atlas");
|
|
check(space != nullptr && space->size == glm::vec2{0.0f} && space->advance > 0.0f,
|
|
"spaces must only advance the pen");
|
|
check(font.GetGlyph(U'中') == font.GetGlyph(U'?'), "missing glyphs must fall back to '?'");
|
|
|
|
const auto& pixels = font.GetAtlasPixels();
|
|
const auto [minIt, maxIt] = std::minmax_element(pixels.begin(), pixels.end());
|
|
check(!pixels.empty() && *minIt < 128 && *maxIt > 128,
|
|
"the atlas must hold a signed distance field with the edge at 128");
|
|
|
|
const glm::vec2 one = font.MeasureText("A", 64.0f);
|
|
const glm::vec2 two = font.MeasureText("A\nAA", 64.0f);
|
|
check(close(one.x, a->advance * 2.0f) && close(one.y, font.GetLineHeight() * 2.0f),
|
|
"text must be measured at the requested size");
|
|
check(close(two.x, (a->advance * 2.0f + font.GetKerning(U'A', U'A')) * 2.0f) &&
|
|
close(two.y, one.y * 2.0f),
|
|
"multi line text must measure its widest line and every line");
|
|
|
|
std::vector<PlacedGlyph> placed;
|
|
font.Layout("A b", {100.0f, 50.0f}, 32.0f, [&](const PlacedGlyph& glyph) { placed.push_back(glyph); });
|
|
check(placed.size() == 2 && placed[0].glyph == a &&
|
|
close(placed[0].topLeft.x, 100.0f + a->offset.x) &&
|
|
close(placed[0].topLeft.y, 50.0f + font.GetAscender() + a->offset.y),
|
|
"layout must skip spaces and start on the baseline below the top left");
|
|
|
|
const std::string_view utf8 = "\xC3\xA9\xFF";
|
|
std::size_t index = 0;
|
|
check(Font::DecodeUtf8(utf8, index) == U'é' && index == 2, "utf8 must decode two byte sequences");
|
|
check(Font::DecodeUtf8(utf8, index) == 0xFFFD && index == 3, "invalid utf8 must decode to U+FFFD");
|
|
|
|
auto& r2d = Renderer2D::GetInstance();
|
|
r2d.Clear();
|
|
r2d.DrawText(font, "not uploaded", {0.0f, 0.0f}, 16.0f, glm::vec4{1.0f});
|
|
check(r2d.GetShapes().empty(), "text must not be drawn before the font atlas is uploaded");
|
|
}
|
|
|
|
void testCapsuleColliderDebugLines()
|
|
{
|
|
GameObject object{"capsule"};
|
|
object.GetTransform().SetWorldPosition({1.0f, 2.0f, 3.0f});
|
|
object.GetTransform().SetWorldScale(glm::vec3{5.0f});
|
|
auto* capsule = object.AddComponent<CapsuleCollider>(0.4f, 1.8f);
|
|
capsule->SetCenterOffset({0.0f, 0.5f, 0.0f});
|
|
|
|
auto& lines = LineRenderingManager::GetInstance();
|
|
lines.ClearLines();
|
|
capsule->DrawDebugLines(glm::vec4{1.0f});
|
|
|
|
const glm::vec3 center{1.0f, 2.5f, 3.0f};
|
|
float top = -1000.0f;
|
|
float bottom = 1000.0f;
|
|
bool inside = true;
|
|
for (const Line& line : lines.GetLines()) {
|
|
for (const glm::vec3& point : {line.start, line.end}) {
|
|
const glm::vec3 local = point - center;
|
|
top = std::max(top, local.y);
|
|
bottom = std::min(bottom, local.y);
|
|
inside = inside && glm::length(glm::vec2{local.x, local.z}) <= 0.4f + 0.001f;
|
|
}
|
|
}
|
|
lines.ClearLines();
|
|
|
|
check(inside && close(top, 0.9f) && close(bottom, -0.9f),
|
|
"capsule debug lines must match the collider shape in world units, ignoring the object scale");
|
|
}
|
|
|
|
void testPrimitiveBoxMesh()
|
|
{
|
|
const glm::vec3 size{4.0f, 1.0f, 2.0f};
|
|
const CPUMesh box = PrimitiveMeshes::MakeBox(size);
|
|
|
|
check(box.vertices.size() == 24 && box.indices.size() == 36, "a box must have 6 faces of 2 triangles");
|
|
check(box.minPos == -size * 0.5f && box.maxPos == size * 0.5f, "a box must be centered on the origin");
|
|
|
|
float maxU = 0.0f;
|
|
bool outward = true;
|
|
for (const auto& vertex : box.vertices) {
|
|
if (vertex.normal == glm::vec3{0.0f, 0.0f, 1.0f}) {
|
|
maxU = std::max(maxU, vertex.uv_x);
|
|
}
|
|
outward = outward && glm::dot(vertex.position, vertex.normal) > 0.0f;
|
|
}
|
|
check(close(maxU, size.x), "box uvs must be in meters so textures tile at the same size");
|
|
check(outward, "box normals must point away from the center");
|
|
|
|
bool counterClockwise = true;
|
|
for (std::size_t i = 0; i < box.indices.size(); i += 3) {
|
|
const auto& a = box.vertices[box.indices[i]];
|
|
const auto& b = box.vertices[box.indices[i + 1]];
|
|
const auto& c = box.vertices[box.indices[i + 2]];
|
|
const glm::vec3 faceNormal = glm::cross(b.position - a.position, c.position - a.position);
|
|
counterClockwise = counterClockwise && glm::dot(faceNormal, a.normal) > 0.0f;
|
|
}
|
|
check(counterClockwise, "box triangles must be counter clockwise seen from outside");
|
|
}
|
|
|
|
void testSimpleColorMaterial()
|
|
{
|
|
const auto material = Material::SimpleColor(
|
|
glm::vec3{0.2f, 0.4f, 0.8f},
|
|
"blue material");
|
|
|
|
check(material.baseColor == glm::vec3{0.2f, 0.4f, 0.8f},
|
|
"simple materials must preserve their color");
|
|
check(material.metallicFactor == 0.0f &&
|
|
material.roughnessFactor == 1.0f &&
|
|
material.emissiveColor == glm::vec3{0.0f},
|
|
"simple materials must use non-metallic default factors");
|
|
check(material.diffuseTexture == NULL_IMAGE_ID,
|
|
"simple materials must not require a diffuse image");
|
|
check(material.name == "blue material",
|
|
"simple materials must preserve their optional name");
|
|
}
|
|
|
|
void testPrimitiveMeshKeys()
|
|
{
|
|
const glm::vec3 size{0.35f, 0.12f, 3.0f};
|
|
const std::string key = PrimitiveMeshes::BoxKey(size);
|
|
check(PrimitiveMeshes::IsPrimitiveKey(key), "box keys must be primitive keys");
|
|
check(PrimitiveMeshes::MakeBox(size).name == key, "boxes must be named by their key");
|
|
|
|
const auto rebuilt = PrimitiveMeshes::MakeFromKey(key + "#3");
|
|
check(rebuilt.has_value() && rebuilt->maxPos == size * 0.5f,
|
|
"a box must rebuild exactly from its key, ignoring the duplicate suffix");
|
|
check(PrimitiveMeshes::StripDuplicateSuffix("BulbMaterial#12") == "BulbMaterial" &&
|
|
PrimitiveMeshes::StripDuplicateSuffix("game://a.glb#mesh") == "game://a.glb#mesh",
|
|
"only numeric duplicate suffixes must be stripped");
|
|
check(!PrimitiveMeshes::MakeFromKey("prim://box?size=1,2").has_value() &&
|
|
!PrimitiveMeshes::MakeFromKey("prim://box?size=1,-2,3").has_value() &&
|
|
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
|
|
"invalid primitive keys must not build a mesh");
|
|
}
|
|
|
|
void testPrimitiveShapes()
|
|
{
|
|
using PrimitiveMeshes::Shape;
|
|
const std::array<PrimitiveMeshes::Desc, 6> descs{{
|
|
{.shape = Shape::Box, .size = {2.0f, 0.5f, 3.0f}},
|
|
{.shape = Shape::Plane, .size = {4.0f, 1.0f, 6.0f}},
|
|
{.shape = Shape::Sphere, .radius = 0.75f},
|
|
{.shape = Shape::Cylinder, .radius = 0.5f, .height = 3.0f},
|
|
{.shape = Shape::Capsule, .radius = 0.4f, .height = 2.0f},
|
|
{.shape = Shape::Capsule, .radius = 1.0f, .height = 1.0f},
|
|
}};
|
|
|
|
for (const auto& desc : descs) {
|
|
const std::string key = PrimitiveMeshes::MakeKey(desc);
|
|
const auto parsed = PrimitiveMeshes::ParseKey(key + "#2");
|
|
check(parsed.has_value() && *parsed == desc, "every primitive must rebuild exactly from its key");
|
|
|
|
const CPUMesh mesh = PrimitiveMeshes::Make(desc);
|
|
check(mesh.name == key, "primitives must be named by their key");
|
|
check(!mesh.indices.empty() && mesh.indices.size() % 3 == 0, "primitives must be made of triangles");
|
|
|
|
bool valid = true;
|
|
for (const std::uint32_t index : mesh.indices) {
|
|
valid = valid && index < mesh.vertices.size();
|
|
}
|
|
check(valid, "primitive indices must point at their own vertices");
|
|
|
|
bool outward = true;
|
|
bool tangents = true;
|
|
for (const auto& vertex : mesh.vertices) {
|
|
outward = outward && close(glm::length(vertex.normal), 1.0f) && glm::dot(vertex.position, vertex.normal) > -0.001f;
|
|
tangents = tangents && close(glm::dot(glm::vec3{vertex.tangent}, vertex.normal), 0.0f) &&
|
|
close(std::abs(vertex.tangent.w), 1.0f);
|
|
}
|
|
check(outward, "primitive normals must be unit length and point away from the center");
|
|
check(tangents, "primitive tangents must lie in the surface with a handedness of +-1");
|
|
|
|
bool counterClockwise = true;
|
|
for (std::size_t i = 0; i < mesh.indices.size(); i += 3) {
|
|
const auto& a = mesh.vertices[mesh.indices[i]];
|
|
const auto& b = mesh.vertices[mesh.indices[i + 1]];
|
|
const auto& c = mesh.vertices[mesh.indices[i + 2]];
|
|
const glm::vec3 faceNormal = glm::cross(b.position - a.position, c.position - a.position);
|
|
counterClockwise = counterClockwise && glm::dot(faceNormal, a.normal + b.normal + c.normal) >= 0.0f;
|
|
}
|
|
check(counterClockwise, "primitive triangles must be counter clockwise seen from outside");
|
|
}
|
|
|
|
const CPUMesh sphere = PrimitiveMeshes::Make(descs[2]);
|
|
check(close(sphere.maxPos.x, 0.75f) && close(sphere.minPos.y, -0.75f), "a sphere must reach its radius");
|
|
const CPUMesh plane = PrimitiveMeshes::Make(descs[1]);
|
|
check(close(plane.maxPos.x, 2.0f) && close(plane.maxPos.z, 3.0f) && close(plane.maxPos.y, 0.0f),
|
|
"a plane must be flat on y = 0 with its size on x and z");
|
|
const CPUMesh capsule = PrimitiveMeshes::Make(descs[4]);
|
|
check(close(capsule.maxPos.y, 1.0f) && close(capsule.maxPos.x, 0.4f),
|
|
"a capsule height must be its full length, like the collider");
|
|
const CPUMesh cylinder = PrimitiveMeshes::Make(descs[3]);
|
|
check(close(cylinder.maxPos.y, 1.5f) && close(cylinder.minPos.y, -1.5f), "a cylinder must be centered on its height");
|
|
|
|
check(!PrimitiveMeshes::ParseKey("prim://sphere?radius=1&radius=2").has_value() &&
|
|
!PrimitiveMeshes::ParseKey("prim://cylinder?radius=1").has_value() &&
|
|
!PrimitiveMeshes::ParseKey("prim://plane?size=1,2,3").has_value() &&
|
|
!PrimitiveMeshes::ParseKey("prim://cone?radius=1").has_value() &&
|
|
!PrimitiveMeshes::ParseKey("prim://sphere?radius=-1").has_value() &&
|
|
!PrimitiveMeshes::ParseKey("prim://sphere").has_value(),
|
|
"malformed primitive keys must be rejected");
|
|
}
|
|
|
|
void testIndirectDrawListBatchesByMesh()
|
|
{
|
|
std::vector<GPUMesh> meshes(2);
|
|
meshes[0].numIndices = 36;
|
|
meshes[0].firstIndex = 0;
|
|
meshes[0].vertexBuffer.address = 0x1000;
|
|
meshes[1].numIndices = 6;
|
|
meshes[1].firstIndex = 36;
|
|
meshes[1].vertexBuffer.address = 0x2000;
|
|
|
|
SkinnedMesh skinned;
|
|
skinned.skinnedVertexBuffer.address = 0x3000;
|
|
|
|
const auto makeDraw = [](MeshID mesh, MaterialID material, glm::vec3 position) {
|
|
MeshDrawCommand dc;
|
|
dc.meshId = mesh;
|
|
dc.materialId = material;
|
|
dc.transformMatrix = glm::translate(glm::mat4{1.0f}, position);
|
|
dc.worldBoundingSphere = Sphere{position, 0.5f};
|
|
return dc;
|
|
};
|
|
|
|
std::vector<MeshDrawCommand> draws{
|
|
makeDraw(0, 1, {0.0f, 0.0f, 5.0f}),
|
|
makeDraw(1, 2, {1.0f, 0.0f, 5.0f}),
|
|
makeDraw(0, 3, {-1.0f, 0.0f, 5.0f}),
|
|
makeDraw(0, 4, {0.0f, 1.0f, 5.0f}),
|
|
makeDraw(1, 2, {0.0f, 0.0f, -50.0f}),
|
|
};
|
|
draws[2].skinnedMesh = &skinned;
|
|
draws[3].castShadow = false;
|
|
const std::vector<std::size_t> sorted{0, 2, 3, 1, 4};
|
|
|
|
const glm::mat4 view = glm::lookAt(glm::vec3{0.0f}, glm::vec3{0.0f, 0.0f, 1.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
|
|
const Frustum frustum = edge::createFrustumFromViewProj(
|
|
glm::perspectiveRH_ZO(glm::radians(90.0f), 1.0f, 0.1f, 20.0f) * view);
|
|
|
|
IndirectDrawList list;
|
|
list.begin(0);
|
|
const auto camera = list.addView(meshes, draws, sorted, frustum, IndirectDrawList::Filter::All);
|
|
const auto shadow = list.addView(meshes, draws, sorted, frustum, IndirectDrawList::Filter::ShadowCasters);
|
|
|
|
const auto& commands = list.getCommands();
|
|
const auto& instances = list.getInstances();
|
|
|
|
check(camera.firstCommand == 0 && camera.commandCount == 2 && camera.instanceCount == 4,
|
|
"a view must merge draws of the same mesh into one command and cull the rest");
|
|
check(commands[0].indexCount == 36 && commands[0].firstIndex == 0 && commands[0].instanceCount == 3 &&
|
|
commands[0].firstInstance == 0,
|
|
"the first batch must cover the three visible draws of mesh 0");
|
|
check(commands[1].indexCount == 6 && commands[1].firstIndex == 36 && commands[1].instanceCount == 1 &&
|
|
commands[1].firstInstance == 3,
|
|
"the second batch must start after the first batch's instances");
|
|
check(instances[0].materialId == 1 && instances[1].materialId == 3 && instances[2].materialId == 4,
|
|
"materials must be per instance so they don't split batches");
|
|
check(instances[0].vertexBuffer == 0x1000 && instances[1].vertexBuffer == 0x3000,
|
|
"skinned draws must point at their own skinned vertex buffer");
|
|
|
|
check(shadow.firstCommand == 2 && shadow.commandCount == 2 && shadow.instanceCount == 3,
|
|
"the shadow view must skip non-casters and get its own command range");
|
|
check(commands[2].instanceCount == 2 && commands[2].firstInstance == 4,
|
|
"shadow instances must follow the camera view's instances");
|
|
}
|
|
|
|
void testDeletionQueueWaitsForFrame()
|
|
{
|
|
DeletionQueue queue;
|
|
std::vector<int> deleted;
|
|
queue.push(3, [&] { deleted.push_back(1); });
|
|
queue.push(3, [&] { deleted.push_back(2); });
|
|
queue.push(5, [&] { deleted.push_back(3); });
|
|
|
|
queue.flush(2);
|
|
check(deleted.empty(), "resources retired in frame 3 must survive until frame 3 completes");
|
|
|
|
queue.flush(3);
|
|
check(deleted == std::vector<int>{1, 2}, "deleters must run in retirement order once their frame completes");
|
|
check(queue.size() == 1, "later frames must stay queued");
|
|
|
|
queue.flushAll();
|
|
check(deleted.size() == 3 && queue.empty(), "flushAll must run every pending deleter");
|
|
}
|
|
}
|
|
|
|
void RunGraphicsTests()
|
|
{
|
|
testLineRenderingManager();
|
|
testFrustumFromViewProj();
|
|
testRenderer2DShapes();
|
|
testFontAtlasAndLayout();
|
|
testCapsuleColliderDebugLines();
|
|
testPrimitiveBoxMesh();
|
|
testSimpleColorMaterial();
|
|
testPrimitiveMeshKeys();
|
|
testPrimitiveShapes();
|
|
testDeletionQueueWaitsForFrame();
|
|
testIndirectDrawListBatchesByMesh();
|
|
}
|