Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions tools/gfx-unit-test/scoped-core-debug-callback-test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#include "../render-test/slang-support.h"
Comment thread
jkwak-work marked this conversation as resolved.
#include "unit-test/slang-unit-test.h"

#include <atomic>
#include <thread>

namespace
{
void emitError(renderer_test::CoreToRHIDebugBridge& bridge, const char* message)
{
bridge.handleMessage(rhi::DebugMessageType::Error, rhi::DebugMessageSource::Layer, message);
}

renderer_test::CoreToRHIDebugBridge* getRetainedBridgeAfterStackCallbackScope()
{
auto bridge = renderer_test::createRetainedCoreToRHIDebugBridge();

renderer_test::CoreDebugCallback callback;
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(*bridge, &callback);
emitError(*bridge, "retained scope");
}
SLANG_CHECK(callback.getString() == "retained scope\n");

return bridge.Ptr();
}
} // namespace

SLANG_UNIT_TEST(scopedCoreDebugCallbackClearsBridgeOnExit)
Comment thread
jkwak-work marked this conversation as resolved.
{
renderer_test::CoreToRHIDebugBridge bridge;

{
renderer_test::CoreDebugCallback callback;
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &callback);
emitError(bridge, "inside scope");
SLANG_CHECK(callback.getString() == "inside scope\n");
}

emitError(bridge, "after scope");
SLANG_CHECK(callback.getString() == "inside scope\n");
}

emitError(bridge, "after callback");

renderer_test::CoreDebugCallback nextCallback;
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &nextCallback);
emitError(bridge, "next scope");
}
SLANG_CHECK(nextCallback.getString() == "next scope\n");
}

SLANG_UNIT_TEST(scopedCoreDebugCallbackDoesNotLeakAcrossScopes)
{
renderer_test::CoreToRHIDebugBridge bridge;
renderer_test::CoreDebugCallback firstCallback;
renderer_test::CoreDebugCallback secondCallback;

{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &firstCallback);
emitError(bridge, "first");
}

{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &secondCallback);
emitError(bridge, "second");
}

SLANG_CHECK(firstCallback.getString() == "first\n");
SLANG_CHECK(secondCallback.getString() == "second\n");
}
Comment thread
jkwak-work marked this conversation as resolved.

SLANG_UNIT_TEST(scopedCoreDebugCallbackClearsBridgeOnException)
{
renderer_test::CoreToRHIDebugBridge bridge;
renderer_test::CoreDebugCallback firstCallback;

try
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &firstCallback);
emitError(bridge, "before throw");
throw 1;
}
catch (...)
{
}

emitError(bridge, "after exception");

renderer_test::CoreDebugCallback secondCallback;
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &secondCallback);
emitError(bridge, "next iteration");
}

SLANG_CHECK(firstCallback.getString() == "before throw\n");
SLANG_CHECK(secondCallback.getString() == "next iteration\n");
}
Comment thread
jkwak-work marked this conversation as resolved.

SLANG_UNIT_TEST(scopedCoreDebugCallbackSeparatesRetainedBridgeScopes)
{
renderer_test::CoreToRHIDebugBridge* oldBridge = getRetainedBridgeAfterStackCallbackScope();
auto nextBridge = renderer_test::createRetainedCoreToRHIDebugBridge();

renderer_test::CoreDebugCallback nextCallback;
{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(*nextBridge, &nextCallback);
emitError(*oldBridge, "after stack callback");
emitError(*nextBridge, "next invocation");
}
SLANG_CHECK(nextCallback.getString() == "next invocation\n");
}

SLANG_UNIT_TEST(coreDebugBridgeHandlesConcurrentMessages)
{
static constexpr int kThreadCount = 4;
static constexpr int kMessageCount = 1024;

renderer_test::CoreToRHIDebugBridge bridge;
renderer_test::CoreDebugCallback callback;
std::atomic<bool> startWriting(false);
std::atomic<bool> keepReading(true);

std::thread readerThread(
[&]()
{
while (keepReading.load(std::memory_order_acquire))
{
callback.getString();
}
});

std::thread writerThreads[kThreadCount];
for (int threadIndex = 0; threadIndex < kThreadCount; ++threadIndex)
{
writerThreads[threadIndex] = std::thread(
[&]()
{
while (!startWriting.load(std::memory_order_acquire))
{
std::this_thread::yield();
}

for (int messageIndex = 0; messageIndex < kMessageCount; ++messageIndex)
{
emitError(bridge, "x");
}
});
}

{
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(bridge, &callback);
startWriting.store(true, std::memory_order_release);
for (int spinCount = 0; spinCount < 100000 && callback.getString().getLength() == 0;
++spinCount)
{
std::this_thread::yield();
}
SLANG_CHECK(callback.getString().getLength() > 0);
}

for (auto& writerThread : writerThreads)
{
writerThread.join();
}

keepReading.store(false, std::memory_order_release);
readerThread.join();

auto capturedLength = callback.getString().getLength();
SLANG_CHECK(capturedLength > 0);
SLANG_CHECK(capturedLength <= kThreadCount * kMessageCount * 2);
SLANG_CHECK((capturedLength % 2) == 0);
}
8 changes: 5 additions & 3 deletions tools/render-test/render-test-main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1769,8 +1769,10 @@ static SlangResult _innerMain(
}
}

static renderer_test::CoreToRHIDebugBridge debugCallback;
debugCallback.setCoreCallback(stdWriters->getDebugCallback());
auto debugCallback = renderer_test::createRetainedCoreToRHIDebugBridge();
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(
*debugCallback,
stdWriters->getDebugCallback());

// Use the profile name set on options if set
input.profile = options.profileName.getLength() ? options.profileName : input.profile;
Expand Down Expand Up @@ -1907,7 +1909,7 @@ static SlangResult _innerMain(
desc.deviceType = options.deviceType;

desc.enableValidation = options.enableDebugLayers;
desc.debugCallback = &debugCallback;
desc.debugCallback = debugCallback.Ptr();

desc.slang.lineDirectiveMode = SLANG_LINE_DIRECTIVE_MODE_NONE;
if (options.generateSPIRVDirectly)
Expand Down
86 changes: 77 additions & 9 deletions tools/render-test/slang-support.h
Original file line number Diff line number Diff line change
@@ -1,43 +1,100 @@
// slang-support.h
#pragma once

#include "core/slang-basic.h"
#include "core/slang-std-writers.h"
#include "options.h"
#include "shader-input-layout.h"
#include "slang.h"

#include <mutex>
#include <slang-rhi.h>

namespace renderer_test
{

/// Bridge from core debug callback to RHI debug callback
/// This allows core callbacks to receive messages from RHI systems
/// Bridge from core debug callback to RHI debug callback.
///
/// RHI backends may invoke debug callbacks from backend or driver threads, so
/// binding changes and forwarded messages are serialized.
/// TODO: We should replace rhi::IDebugCallback with Slang::IDebugCallback.
class CoreToRHIDebugBridge : public rhi::IDebugCallback
class CoreToRHIDebugBridge : public Slang::RefObject, public rhi::IDebugCallback
{
public:
void setCoreCallback(Slang::IDebugCallback* coreCallback) { m_coreCallback = coreCallback; }
void setCoreCallback(Slang::IDebugCallback* coreCallback)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_coreCallback = coreCallback;
}

virtual SLANG_NO_THROW void SLANG_MCALL handleMessage(
rhi::DebugMessageType type,
rhi::DebugMessageSource source,
const char* message) override
{
if (m_coreCallback)
std::lock_guard<std::mutex> lock(m_mutex);

auto coreCallback = m_coreCallback;
if (coreCallback)
{
// Convert RHI types to core types
Slang::DebugMessageType coreType = static_cast<Slang::DebugMessageType>(type);
Slang::DebugMessageSource coreSource = static_cast<Slang::DebugMessageSource>(source);
m_coreCallback->handleMessage(coreType, coreSource, message);
coreCallback->handleMessage(coreType, coreSource, message);
}
}

private:
std::mutex m_mutex;
Slang::IDebugCallback* m_coreCallback = nullptr;
Comment thread
jkwak-work marked this conversation as resolved.
};

/// Core debug callback that captures debug messages in a string buffer
/// Creates an RHI debug bridge that remains alive for process teardown.
///
/// Device descriptors store debug callbacks as raw pointers, and retained RHI
/// state may emit messages after the harness invocation that created a device.
/// Each invocation gets a distinct bridge so old emitters can only reach their
/// own cleared bridge, not the next invocation's callback.
inline Slang::RefPtr<CoreToRHIDebugBridge> createRetainedCoreToRHIDebugBridge()
{
static std::mutex* mutex = new std::mutex;
static Slang::List<Slang::RefPtr<CoreToRHIDebugBridge>>* bridges =
new Slang::List<Slang::RefPtr<CoreToRHIDebugBridge>>();

Slang::RefPtr<CoreToRHIDebugBridge> bridge = new CoreToRHIDebugBridge();
std::lock_guard<std::mutex> lock(*mutex);
bridges->add(bridge);
return bridge;
}

/// Binds an RHI debug bridge to a core callback for one active test invocation.
///
/// The bridge may be retained by RHI device state after this scope exits, but the
/// per-test core callback must not be retained. Messages that arrive while no
/// scoped callback is active are intentionally dropped by the bridge instead of
/// being written to dead callback storage.
class ScopedCoreDebugCallback
{
public:
ScopedCoreDebugCallback(CoreToRHIDebugBridge& bridge, Slang::IDebugCallback* coreCallback)
: m_bridge(bridge)
{
m_bridge.setCoreCallback(coreCallback);
}

~ScopedCoreDebugCallback() { m_bridge.setCoreCallback(nullptr); }

ScopedCoreDebugCallback(const ScopedCoreDebugCallback&) = delete;
Comment thread
jkwak-work marked this conversation as resolved.
ScopedCoreDebugCallback& operator=(const ScopedCoreDebugCallback&) = delete;

private:
CoreToRHIDebugBridge& m_bridge;
Comment thread
jkwak-work marked this conversation as resolved.
};

/// Core debug callback that captures debug messages in a string buffer.
///
/// Message capture is thread-safe so backend debug callbacks can report while
/// the test harness reads the collected messages.
class CoreDebugCallback : public Slang::IDebugCallback
{
public:
Expand All @@ -51,6 +108,7 @@ class CoreDebugCallback : public Slang::IDebugCallback
// Only capture error messages
if (type == Slang::DebugMessageType::Error)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_buf << message;
if (message[strlen(message) - 1] != '\n')
{
Expand All @@ -59,10 +117,20 @@ class CoreDebugCallback : public Slang::IDebugCallback
}
}

void clear() { m_buf.clear(); }
Slang::String getString() { return m_buf.toString(); }
void clear()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_buf.clear();
}

Slang::String getString()
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_buf.toString();
}

private:
std::mutex m_mutex;
Slang::StringBuilder m_buf;
};

Expand Down
9 changes: 6 additions & 3 deletions tools/slang-test/slang-test-main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,6 @@ static SlangResult runUnitTestModule(
return SLANG_FAIL;

renderer_test::CoreDebugCallback coreDebugCallback;
renderer_test::CoreToRHIDebugBridge rhiDebugBridge;
rhiDebugBridge.setCoreCallback(&coreDebugCallback);

UnitTestContext unitTestContext;
unitTestContext.slangGlobalSession = context->getSession();
Expand All @@ -5568,7 +5566,7 @@ static SlangResult runUnitTestModule(
context->options.enabledApis & _getAvailableRenderApiFlags(context);
unitTestContext.enableDebugLayers = context->options.enableDebugLayers;
unitTestContext.executableDirectory = context->exeDirectoryPath.getBuffer();
unitTestContext.debugCallback = &rhiDebugBridge;
unitTestContext.debugCallback = nullptr;

auto testCount = testModule->getTestCount();

Expand Down Expand Up @@ -5687,6 +5685,11 @@ static SlangResult runUnitTestModule(

// Clear any previous debug messages
coreDebugCallback.clear();
auto rhiDebugBridge = renderer_test::createRetainedCoreToRHIDebugBridge();
unitTestContext.debugCallback = rhiDebugBridge.Ptr();
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(
*rhiDebugBridge,
&coreDebugCallback);

try
{
Expand Down
8 changes: 5 additions & 3 deletions tools/test-server/test-server-main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,10 @@ SlangResult TestServer::_executeUnitTest(const JSONRPCCall& call)

TestReporter testReporter;
renderer_test::CoreDebugCallback coreDebugCallback;
renderer_test::CoreToRHIDebugBridge rhiDebugCallback;
rhiDebugCallback.setCoreCallback(&coreDebugCallback);
auto rhiDebugCallback = renderer_test::createRetainedCoreToRHIDebugBridge();
renderer_test::ScopedCoreDebugCallback scopedDebugCallback(
*rhiDebugCallback,
&coreDebugCallback);

testModule->setTestReporter(&testReporter);

Expand All @@ -561,7 +563,7 @@ SlangResult TestServer::_executeUnitTest(const JSONRPCCall& call)
unitTestContext.enabledApis = RenderApiFlags(args.enabledApis);
unitTestContext.executableDirectory = m_exeDirectory.getBuffer();
unitTestContext.enableDebugLayers = args.enableDebugLayers;
unitTestContext.debugCallback = &rhiDebugCallback;
unitTestContext.debugCallback = rhiDebugCallback.Ptr();

auto testCount = testModule->getTestCount();
SLANG_ASSERT(testIndex >= 0 && testIndex < testCount);
Expand Down
Loading