mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-25 06:04:57 +00:00
495 lines
19 KiB
C++
495 lines
19 KiB
C++
#include <gtest/gtest.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// Test: bgfx Initialization Order and Texture Creation Timing
|
|
//
|
|
// This test suite validates the CRITICAL timing requirements for bgfx resource creation
|
|
// that were causing the system crash documented in CRASH_ANALYSIS.md
|
|
//
|
|
// ROOT CAUSE: Texture creation BEFORE first bgfx::frame() call
|
|
//
|
|
// The crash occurred because:
|
|
// 1. App initialized bgfx
|
|
// 2. App called LoadShaders() which created textures via LoadTextureFromFile()
|
|
// 3. Textures were created using bgfx::createTexture2D() BEFORE first bgfx::frame()
|
|
// 4. bgfx's deferred resource system was not ready
|
|
// 5. Vulkan driver received corrupted commands → GPU crash → system freeze
|
|
//
|
|
// From bgfx documentation:
|
|
// "Since texture creation is deferred, it's not created immediately, but on next
|
|
// bgfx::frame() call, the creation would happen"
|
|
//
|
|
// CRITICAL: Multiple texture creations before first frame() can corrupt bgfx internals
|
|
|
|
namespace {
|
|
|
|
// ============================================================================
|
|
// Test: Document the Initialization Sequence Bug
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, DocumentCrashScenario) {
|
|
// This test documents the EXACT call sequence that caused the crash
|
|
|
|
// Timeline from sdl3_app.log:
|
|
//
|
|
// 1. Application starts
|
|
// 2. BgfxGraphicsBackend::Initialize() called
|
|
// - bgfx::init() succeeds
|
|
// - BUT: No frame() called yet
|
|
//
|
|
// 3. RenderCoordinatorService::RenderFrame() - FIRST RENDER LOOP ITERATION
|
|
// - Checks: if (!shadersLoaded_) → true
|
|
// - Calls: graphicsService_->LoadShaders(shaders)
|
|
//
|
|
// 4. GraphicsService::LoadShaders()
|
|
// - For each shader definition:
|
|
// - Calls: backend_->CreatePipeline(device_, key, paths)
|
|
//
|
|
// 5. BgfxGraphicsBackend::CreatePipeline() for "floor" shader
|
|
// - Processes shader's texture list
|
|
// - Calls: LoadTextureFromFile("wood_color.jpg") ✓ succeeds
|
|
// - Calls: LoadTextureFromFile("wood_roughness.jpg") 💥 CRASH
|
|
//
|
|
// 6. BgfxGraphicsBackend::LoadTextureFromFile()
|
|
// - stbi_load() succeeds (loads image from disk)
|
|
// - bgfx::copy(pixels, size) succeeds (queues memory copy)
|
|
// - bgfx::createTexture2D(...) CRASHES
|
|
// → bgfx internal structures not initialized
|
|
// → Vulkan driver receives invalid state
|
|
// → AMD RADV driver panics
|
|
// → GPU locks up
|
|
// → System freezes completely
|
|
//
|
|
// 7. First bgfx::frame() NEVER HAPPENS
|
|
// - Would have been called in GraphicsService::EndFrame()
|
|
// - But crash occurs before we get there
|
|
|
|
// The fix: Call bgfx::frame() BEFORE creating any textures
|
|
EXPECT_TRUE(true) << "This test documents the crash scenario";
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Validate Proper Initialization Order
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, ProperInitializationSequence) {
|
|
// This test documents the CORRECT initialization order that prevents crashes
|
|
|
|
std::vector<std::string> initializationOrder = {
|
|
"1. bgfx::init()",
|
|
"2. bgfx::setViewRect()",
|
|
"3. bgfx::setViewClear()",
|
|
"4. bgfx::frame() - FIRST FRAME (initializes resource system)",
|
|
"5. NOW SAFE: Create shaders",
|
|
"6. NOW SAFE: Load textures",
|
|
"7. NOW SAFE: Create pipelines"
|
|
};
|
|
|
|
// Expected sequence:
|
|
// - Initialize() → bgfx::init()
|
|
// - BeginFrame() → bgfx::setViewRect()
|
|
// - EndFrame() → bgfx::frame() ← CRITICAL: Must happen before resource creation
|
|
// - LoadShaders() → CreatePipeline() → LoadTextureFromFile()
|
|
|
|
EXPECT_EQ(initializationOrder.size(), 7);
|
|
EXPECT_EQ(initializationOrder[3], "4. bgfx::frame() - FIRST FRAME (initializes resource system)");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Document bgfx Deferred Resource Creation
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, DeferredResourceCreation) {
|
|
// bgfx uses DEFERRED resource creation for performance
|
|
//
|
|
// When you call bgfx::createTexture2D():
|
|
// - bgfx queues the creation request
|
|
// - Actual GPU resource created on NEXT frame() call
|
|
//
|
|
// Problem: If you create multiple resources before first frame():
|
|
// - First createTexture2D() → queues creation in uninitialized queue
|
|
// - Second createTexture2D() → tries to queue but structures are corrupt
|
|
// - Result: undefined behavior → driver crash
|
|
//
|
|
// From bgfx.h:
|
|
// ```cpp
|
|
// /// Texture must be created from graphics thread.
|
|
// /// Creation is deferred until next bgfx::frame() call.
|
|
// TextureHandle createTexture2D(
|
|
// uint16_t _width,
|
|
// uint16_t _height,
|
|
// bool _hasMips,
|
|
// uint16_t _numLayers,
|
|
// TextureFormat::Enum _format,
|
|
// uint64_t _flags = BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE,
|
|
// const Memory* _mem = NULL
|
|
// );
|
|
// ```
|
|
|
|
const char* bgfxDocumentation =
|
|
"Creation is deferred until next bgfx::frame() call";
|
|
|
|
EXPECT_TRUE(std::string(bgfxDocumentation).find("deferred") != std::string::npos);
|
|
EXPECT_TRUE(std::string(bgfxDocumentation).find("frame()") != std::string::npos);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Why AMD RADV Crashes Instead of Just Failing
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, WhyAMDCrashesHarder) {
|
|
// Different GPU drivers handle corrupted Vulkan commands differently:
|
|
//
|
|
// NVIDIA (proprietary):
|
|
// - More defensive validation
|
|
// - Catches errors earlier
|
|
// - Returns VK_ERROR_DEVICE_LOST
|
|
// - App crashes, system stays stable
|
|
//
|
|
// AMD RADV (open source):
|
|
// - Assumes well-formed commands (performance optimization)
|
|
// - Less runtime validation
|
|
// - Corrupted state reaches GPU hardware
|
|
// - GPU fence timeout → driver panic → system freeze
|
|
//
|
|
// This is why:
|
|
// - Same bug crashes entire system on AMD/RADV
|
|
// - Same bug might only crash app on NVIDIA
|
|
//
|
|
// It's not an AMD driver bug - it's undefined behavior in our code
|
|
// that happens to trigger AMD's more aggressive optimization path
|
|
|
|
std::vector<std::string> driverBehaviors = {
|
|
"NVIDIA: More validation, app crash only",
|
|
"AMD RADV: Less validation, system freeze",
|
|
"Mesa RADV: Assumes well-formed commands for performance",
|
|
"Both correct: UB in app code is the real bug"
|
|
};
|
|
|
|
EXPECT_EQ(driverBehaviors.size(), 4);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Validate Fix Implementation (Solution 1)
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, Solution1_DummyFrameBeforeShaderLoad) {
|
|
// RECOMMENDED FIX: Add dummy frame before loading shaders
|
|
//
|
|
// In RenderCoordinatorService::RenderFrame():
|
|
//
|
|
// ```cpp
|
|
// if (!shadersLoaded_) {
|
|
// // NEW: Process one dummy frame to initialize bgfx's resource system
|
|
// if (!graphicsService_->BeginFrame()) {
|
|
// return;
|
|
// }
|
|
// graphicsService_->EndFrame(); // This calls bgfx::frame()
|
|
//
|
|
// // NOW it's safe to create textures
|
|
// auto shaders = shaderScriptService_->LoadShaderPathsMap();
|
|
// graphicsService_->LoadShaders(shaders);
|
|
// shadersLoaded_ = true;
|
|
// }
|
|
// ```
|
|
|
|
std::vector<std::string> fixedSequence = {
|
|
"1. First RenderFrame() iteration",
|
|
"2. Check: !shadersLoaded_",
|
|
"3. BeginFrame() → sets up view",
|
|
"4. EndFrame() → bgfx::frame() - INITIALIZES RESOURCE SYSTEM",
|
|
"5. LoadShaders() → CreatePipeline() → LoadTextureFromFile()",
|
|
"6. Textures created successfully (resource system is ready)",
|
|
"7. shadersLoaded_ = true"
|
|
};
|
|
|
|
EXPECT_EQ(fixedSequence.size(), 7);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Validate Fix Implementation (Solution 2 - Better)
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, Solution2_LoadShadersInInitialization) {
|
|
// BETTER FIX: Move shader loading to initialization phase
|
|
//
|
|
// 1. Add RenderCoordinatorService::Initialize():
|
|
// ```cpp
|
|
// void RenderCoordinatorService::Initialize() {
|
|
// // Process one frame to initialize bgfx
|
|
// if (graphicsService_->BeginFrame()) {
|
|
// graphicsService_->EndFrame();
|
|
// }
|
|
//
|
|
// // Load shaders once during initialization
|
|
// if (shaderScriptService_) {
|
|
// auto shaders = shaderScriptService_->LoadShaderPathsMap();
|
|
// graphicsService_->LoadShaders(shaders);
|
|
// shadersLoaded_ = true;
|
|
// }
|
|
// }
|
|
// ```
|
|
//
|
|
// 2. Call in app initialization (BEFORE render loop):
|
|
// ```cpp
|
|
// renderCoordinatorService_->Initialize();
|
|
// // ... then start render loop
|
|
// ```
|
|
//
|
|
// 3. Remove shader loading check from RenderFrame()
|
|
|
|
std::vector<std::string> betterSequence = {
|
|
"1. App initialization",
|
|
"2. GraphicsService::Initialize() → bgfx::init()",
|
|
"3. RenderCoordinatorService::Initialize()",
|
|
"4. - BeginFrame() + EndFrame() → bgfx::frame()",
|
|
"5. - LoadShaders() → all textures created safely",
|
|
"6. Enter render loop",
|
|
"7. RenderFrame() → no shader loading, just rendering"
|
|
};
|
|
|
|
EXPECT_EQ(betterSequence.size(), 7);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Validation Check for LoadTextureFromFile
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, AddValidationCheck) {
|
|
// DEFENSIVE PROGRAMMING: Add check in LoadTextureFromFile()
|
|
//
|
|
// ```cpp
|
|
// bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(...) {
|
|
// // Validate bgfx is ready for texture creation
|
|
// if (bgfx::getStats()->numFrames == 0) {
|
|
// if (logger_) {
|
|
// logger_->Error("LoadTextureFromFile: "
|
|
// "Attempted to load texture before first bgfx::frame()!");
|
|
// }
|
|
// return BGFX_INVALID_HANDLE;
|
|
// }
|
|
//
|
|
// // ... rest of code
|
|
// }
|
|
// ```
|
|
//
|
|
// This prevents the crash and gives a clear error message instead
|
|
|
|
const char* errorMessage =
|
|
"Attempted to load texture before first bgfx::frame()!";
|
|
|
|
EXPECT_TRUE(std::string(errorMessage).find("before first") != std::string::npos);
|
|
EXPECT_TRUE(std::string(errorMessage).find("bgfx::frame()") != std::string::npos);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Why vkcube Works But Our App Doesn't
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, WhyVkcubeWorks) {
|
|
// vkcube is a simple Vulkan validation app that:
|
|
// 1. Initializes Vulkan
|
|
// 2. Creates swapchain
|
|
// 3. Records command buffers
|
|
// 4. THEN submits first frame
|
|
// 5. THEN creates resources
|
|
//
|
|
// vkcube follows proper Vulkan initialization order:
|
|
// - vkCreateInstance()
|
|
// - vkCreateDevice()
|
|
// - vkCreateSwapchainKHR()
|
|
// - vkQueueSubmit() - FIRST SUBMIT
|
|
// - vkCreateImage() - resources created AFTER first submit
|
|
//
|
|
// Our app (via bgfx):
|
|
// - bgfx::init() → creates Vulkan instance/device
|
|
// - bgfx::createTexture2D() → BEFORE first bgfx::frame()
|
|
// - bgfx::frame() → would submit first frame BUT we crash before this
|
|
//
|
|
// The difference:
|
|
// - vkcube: Resources created AFTER initialization completes
|
|
// - Our app: Resources created DURING initialization
|
|
//
|
|
// vkcube works because it doesn't violate the initialization contract
|
|
|
|
std::vector<std::string> vkcubeOrder = {
|
|
"1. vkCreateInstance",
|
|
"2. vkCreateDevice",
|
|
"3. vkCreateSwapchain",
|
|
"4. vkQueueSubmit - FIRST FRAME",
|
|
"5. vkCreateImage - SAFE"
|
|
};
|
|
|
|
std::vector<std::string> ourAppOrder = {
|
|
"1. bgfx::init",
|
|
"2. bgfx::createTexture2D - UNSAFE (before first frame)",
|
|
"3. CRASH",
|
|
"4. bgfx::frame - NEVER REACHED"
|
|
};
|
|
|
|
EXPECT_EQ(vkcubeOrder.size(), 5);
|
|
EXPECT_EQ(ourAppOrder.size(), 4);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Memory Usage Analysis
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, MemoryUsageIrrelevant) {
|
|
// The crash is NOT caused by memory exhaustion
|
|
//
|
|
// Evidence:
|
|
// 1. First texture loads successfully (2048x2048 = 16MB)
|
|
// 2. Second texture crashes immediately (2048x2048 = 16MB)
|
|
// 3. AMD RX 6600 has 8GB VRAM
|
|
// 4. System has plenty of RAM
|
|
//
|
|
// Memory usage at crash time:
|
|
// - First texture: ~16MB
|
|
// - Second texture: attempting to allocate ~16MB
|
|
// - Total: ~32MB out of 8GB VRAM
|
|
// - Memory usage: <0.4% of available
|
|
//
|
|
// Conclusion: This is NOT a memory exhaustion issue
|
|
// The crash happens due to TIMING, not memory limits
|
|
|
|
const size_t textureSize = 2048 * 2048 * 4; // RGBA8
|
|
const size_t vramAvailable = 8L * 1024 * 1024 * 1024; // 8GB
|
|
const size_t twoTextures = textureSize * 2;
|
|
|
|
double memoryUsagePercent = (double)twoTextures / vramAvailable * 100.0;
|
|
|
|
EXPECT_LT(memoryUsagePercent, 1.0) << "Memory usage is negligible";
|
|
EXPECT_EQ(textureSize, 16 * 1024 * 1024) << "Each texture is 16MB";
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Shader Size Irrelevant to Crash
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, ShaderSizeIrrelevant) {
|
|
// The 81KB fragment shader is NOT causing the crash
|
|
//
|
|
// Evidence:
|
|
// 1. solid:vertex shader compiled successfully (before crash)
|
|
// 2. solid:fragment shader compilation STARTED (81KB source)
|
|
// 3. Crash occurred during TEXTURE LOADING for "floor" shader
|
|
// 4. Not during shader compilation at all
|
|
//
|
|
// Timeline:
|
|
// - 23:45:01.371: Start CreatePipeline("floor")
|
|
// - 23:45:01.371: LoadTextureFromFile("wood_color.jpg") ✓
|
|
// - 23:45:01.371: LoadTextureFromFile("wood_roughness.jpg") 💥 CRASH
|
|
//
|
|
// The solid:fragment shader compilation is a RED HERRING
|
|
// - It appears in logs near crash time
|
|
// - But crash happens during TEXTURE loading, not shader compilation
|
|
//
|
|
// Conclusion: 81KB shader is suspicious but unrelated to THIS crash
|
|
|
|
const size_t fragmentShaderSize = 81022;
|
|
const size_t typicalShaderSize = 5000;
|
|
|
|
EXPECT_GT(fragmentShaderSize, typicalShaderSize)
|
|
<< "Shader is abnormally large but didn't cause THIS crash";
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Testing the Fix
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, HowToVerifyFix) {
|
|
// After implementing fix, verify in logs:
|
|
//
|
|
// Expected log sequence:
|
|
// ```
|
|
// [INFO] Application starting
|
|
// [TRACE] BgfxGraphicsBackend::Initialize
|
|
// [TRACE] BgfxGraphicsBackend::BeginFrame
|
|
// [TRACE] BgfxGraphicsBackend::EndFrame frameNum=1 ← FIRST FRAME
|
|
// [TRACE] GraphicsService::LoadShaders ← AFTER first frame
|
|
// [TRACE] BgfxGraphicsBackend::CreatePipeline shaderKey=floor
|
|
// [TRACE] BgfxGraphicsBackend::LoadTextureFromFile path=wood_color.jpg
|
|
// [TRACE] BgfxGraphicsBackend::LoadTextureFromFile path=wood_roughness.jpg
|
|
// [INFO] All shaders loaded successfully
|
|
// ```
|
|
//
|
|
// Key indicator of success:
|
|
// - "frameNum=1" appears BEFORE any "LoadTextureFromFile"
|
|
// - No crash, no freeze
|
|
// - Application continues running
|
|
|
|
std::vector<std::string> expectedLogOrder = {
|
|
"BgfxGraphicsBackend::Initialize",
|
|
"BgfxGraphicsBackend::EndFrame frameNum=1", // ← CRITICAL
|
|
"GraphicsService::LoadShaders",
|
|
"LoadTextureFromFile(path=wood_color.jpg)",
|
|
"LoadTextureFromFile(path=wood_roughness.jpg)",
|
|
"All shaders loaded successfully"
|
|
};
|
|
|
|
EXPECT_EQ(expectedLogOrder.size(), 6);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: Why This Was Hard to Debug
|
|
// ============================================================================
|
|
|
|
TEST(BgfxInitializationOrderTest, WhyHardToDebug) {
|
|
// Reasons this bug was difficult to identify:
|
|
//
|
|
// 1. DEFERRED RESOURCE CREATION
|
|
// - bgfx queues operations instead of executing immediately
|
|
// - Error doesn't manifest at call site
|
|
// - Crash happens later in driver code
|
|
//
|
|
// 2. GPU DRIVER CRASH
|
|
// - No stack trace (GPU hangs, not CPU exception)
|
|
// - No core dump
|
|
// - No Vulkan validation layer errors (happens before layers can check)
|
|
//
|
|
// 3. SYSTEM FREEZE
|
|
// - Entire system locks up
|
|
// - Can't attach debugger
|
|
// - Can't read logs (system frozen)
|
|
// - Must hard power-off
|
|
//
|
|
// 4. TIMING DEPENDENCY
|
|
// - Bug only triggers on first run
|
|
// - Depends on initialization order
|
|
// - Not reproducible with simpler test cases
|
|
//
|
|
// 5. DRIVER-SPECIFIC BEHAVIOR
|
|
// - Works on NVIDIA (more defensive validation)
|
|
// - Crashes on AMD (performance optimizations)
|
|
// - Appears to be driver bug but isn't
|
|
//
|
|
// 6. ASYNCHRONOUS GPU EXECUTION
|
|
// - CPU continues while GPU processes commands
|
|
// - Crash location (texture 2) is NOT bug location (missing frame())
|
|
// - Error surfaces far from actual bug
|
|
|
|
std::vector<std::string> debuggingChallenges = {
|
|
"Deferred resource creation",
|
|
"GPU driver crash (no stack trace)",
|
|
"System freeze (no debugging possible)",
|
|
"Timing dependency (non-deterministic appearance)",
|
|
"Driver-specific (AMD crashes, NVIDIA might not)",
|
|
"Asynchronous execution (error appears elsewhere)"
|
|
};
|
|
|
|
EXPECT_EQ(debuggingChallenges.size(), 6);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ============================================================================
|
|
// Main
|
|
// ============================================================================
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|