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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ set(DONUT_PATCHES
# clamps the rest to the near or far plane, Vulkan clips it away). environment_map uses both.
"${CMAKE_CURRENT_SOURCE_DIR}/patches/Donut-planar-shadow-map-clear.patch"
"${CMAKE_CURRENT_SOURCE_DIR}/patches/Donut-planar-shadow-map-whole-scene-depth-range.patch"
# DeviceCreationParameters::messageCallback, NVRHI's IMessageCallback, on Vulkan and D3D11 too
# (D3D12 has it): the application's, as a tslang class implementing it (nvrhi_direct).
"${CMAKE_CURRENT_SOURCE_DIR}/patches/Donut-message-callback-all-apis.patch"
)

# The same for Donut's GLFW submodule (external/Donut/thirdparty/glfw), which is a repository of
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ if (commandList) {
on the C++ library), so `nvrhi.layoutMatches()` compares the tuples' sizes with the C++ ones. A
struct passed by value is the ABIs' difference: MSVC passes a pointer to a copy, the Itanium ABI
the fields in registers (`clearTextureFloat`'s `TextureSubresourceSet`, see the two files).
- **Callbacks.** `nvrhi.IMessageCallback` goes the other way: a `@cppvtable` class extending it
whose `message()` has a body is an object NVRHI calls, through the vtable tslang makes for it.
Pass it to `Donut_SetNvrhiMessageCallback` before creating the app, and keep it in a global
while the device lives. `Donut_LogNvrhiMessage` passes a message on to Donut's logging.
`nvrhi_direct` does this.
- **Platform.** `nvrhi.supported()` says whether the build has one of the two ABIs (MSVC x64,
Android's clang on x86-64 or AArch64) and their layouts: code that also runs elsewhere keeps
another path.
Expand All @@ -315,8 +320,11 @@ Donut's wrappers elsewhere or with `-donut`. CTest runs both: `headless.<api>` a
3. copies the three into a readback buffer, the last copy with hand-made barriers;
4. clears a texture, read back through Donut.

It checks what it reads, and on the way the graphics API, the native objects and queues, reference
counting, debug names, the descs NVRHI keeps and the struct layouts.
It checks what it reads. On the way it also checks:
- the graphics API, the native objects and queues;
- reference counting, debug names, the descs NVRHI keeps and the struct layouts;
- NVRHI's messages, which go through an `IMessageCallback` implemented in tslang. With `-debug`,
it provokes a validation error and checks that NVRHI called the callback.

## Memory model

Expand Down
20 changes: 20 additions & 0 deletions core/donut_interop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2377,6 +2377,24 @@ extern "C"
AppOption_DescriptorBuffers = 1024, // with Vulkan, descriptor sets in buffers (Donut_HasDescriptorBuffers)
};

// The nvrhi::IMessageCallback the devices made after Donut_SetNvrhiMessageCallback get: null for
// Donut's (DefaultMessageCallback, which logs).
static nvrhi::IMessageCallback* g_NvrhiMessageCallback = nullptr;

// Sets it: an object of a tslang class implementing nvrhi::IMessageCallback (core/nvrhi.ts),
// which the caller keeps alive for as long as the devices live; null for Donut's again.
void Donut_SetNvrhiMessageCallback(void* callback)
{
g_NvrhiMessageCallback = static_cast<nvrhi::IMessageCallback*>(callback);
}

// What Donut does with an NVRHI message - logs it, and a Fatal one ends the program - for a
// callback that passes messages on.
void Donut_LogNvrhiMessage(int severity, const char* text)
{
donut::app::DefaultMessageCallback::GetInstance().message(static_cast<nvrhi::MessageSeverity>(severity), text);
}

// Creates the device and window for graphicsApi (an nvrhi::GraphicsAPI value), with the
// AppOptions bits in options. Returns null on failure.
App* Donut_CreateAppWithOptions(int graphicsApi, const char* title, int width, int height, int options)
Expand All @@ -2394,6 +2412,7 @@ extern "C"
return nullptr;

donut::app::DeviceCreationParameters params;
params.messageCallback = g_NvrhiMessageCallback;
params.backBufferWidth = static_cast<uint32_t>(width);
params.backBufferHeight = static_cast<uint32_t>(height);
params.vsyncEnabled = (options & AppOption_NoVsync) == 0;
Expand Down Expand Up @@ -3010,6 +3029,7 @@ extern "C"
return nullptr;

donut::app::DeviceCreationParameters params;
params.messageCallback = g_NvrhiMessageCallback;
params.adapterIndex = adapterIndex;
params.enableRayTracingExtensions = (options & AppOption_RayTracing) != 0;
params.enableComputeQueue = (options & AppOption_ComputeQueue) != 0;
Expand Down
8 changes: 8 additions & 0 deletions core/donut_interop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,14 @@ declare function Donut_GetNvrhiDevice(app: AppHandle): Opaque;
// convention and std::string are MSVC x64's (core/nvrhi_abi.win32.ts) or those of Android's clang
// and libc++ on x86-64 or AArch64 (core/nvrhi_abi.linux.ts); 0 otherwise.
declare function Donut_CanCallNvrhiDirectly(): int;
// The nvrhi::IMessageCallback the devices created after this get: an object of a tslang class
// implementing nvrhi.IMessageCallback (core/nvrhi.ts), which the caller keeps alive - in a global -
// for as long as the devices live, as C++ holds it where the collector doesn't look; null for
// Donut's, which logs the messages.
declare function Donut_SetNvrhiMessageCallback(callback: Opaque | null): void;
// What Donut does with an NVRHI message (an nvrhi.MESSAGE_SEVERITY_* value): logs it, and a Fatal
// one ends the program. For a callback that passes messages on.
declare function Donut_LogNvrhiMessage(severity: int, text: string): void;
// sizeof an NVRHI struct core/nvrhi.ts lays out as a tuple (named as in NVRHI), or -1: its layout
// depends on the C++ build (std::string's on the CRT), so nvrhi.layoutMatches() compares the two.
declare function Donut_GetNvrhiStructSize(name: string): int;
Expand Down
6 changes: 6 additions & 0 deletions core/nvrhi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
//
// Objects returned with a reference (createBuffer, createTexture, createCommandList,
// createComputePipeline) are released with Release().
//
// nvrhi.IMessageCallback is implemented here, not called: a @cppvtable class extending it whose
// message() has a body is one NVRHI calls (examples/nvrhi_direct.ts' MessageLog). Hand it to
// Donut_SetNvrhiMessageCallback before creating the app, and keep it in a global while the device
// lives: NVRHI holds a pointer the collector doesn't see. The text is NVRHI's, valid during the
// call only.

/// <reference path="donut_interop.d.ts" />

Expand Down
11 changes: 10 additions & 1 deletion core/nvrhi_abi.linux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ export namespace nvrhi {
commandList.clearTextureFloat(texture, mips, arraySlices, Ref(colorValue));
}

// nvrhi::IMessageCallback, which the application implements: extend it with a @cppvtable class
// whose message() has a body (see core/nvrhi.ts)
@cppvtable
export class IMessageCallback {
_completeDestructor(): void; // 0
_deletingDestructor(): void; // 1
message(severity: u8, text: string): void; // 2
}

// nvrhi::IResource
@cppvtable
export class IResource {
Expand Down Expand Up @@ -303,7 +312,7 @@ export namespace nvrhi {
_queryCoopVecTrainingFormatSupport(): void; // 69
_getCoopVecMatrixSize(): void; // 70
getNativeQueue(objectType: u32, queue: u8): NativeObject; // 71
_getMessageCallback(): void; // 72
getMessageCallback(): IMessageCallback; // 72
_isAftermathEnabled(): void; // 73
_getAftermathCrashDumpHelper(): void; // 74
}
Expand Down
10 changes: 9 additions & 1 deletion core/nvrhi_abi.win32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ export namespace nvrhi {
commandList.clearTextureFloat(texture, Ref(subresourcesValue), Ref(colorValue));
}

// nvrhi::IMessageCallback, which the application implements: extend it with a @cppvtable class
// whose message() has a body (see core/nvrhi.ts)
@cppvtable
export class IMessageCallback {
_destructor(): void; // 0
message(severity: u8, text: string): void; // 1
}

// nvrhi::IResource
@cppvtable
export class IResource {
Expand Down Expand Up @@ -291,7 +299,7 @@ export namespace nvrhi {
_queryCoopVecTrainingFormatSupport(): void; // 68
_getCoopVecMatrixSize(): void; // 69
getNativeQueue(objectType: u32, queue: u8): NativeObject; // 70
_getMessageCallback(): void; // 71
getMessageCallback(): IMessageCallback; // 71
_isAftermathEnabled(): void; // 72
_getAftermathCrashDumpHelper(): void; // 73
}
Expand Down
6 changes: 6 additions & 0 deletions core/nvrhi_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export namespace nvrhi {
bindingCount: index, indirectParams: Opaque | null
];

// nvrhi::MessageSeverity
export const MESSAGE_SEVERITY_INFO: u8 = 0;
export const MESSAGE_SEVERITY_WARNING: u8 = 1;
export const MESSAGE_SEVERITY_ERROR: u8 = 2;
export const MESSAGE_SEVERITY_FATAL: u8 = 3;

// nvrhi::GraphicsAPI
export const GRAPHICS_API_D3D11: u8 = 0;
export const GRAPHICS_API_D3D12: u8 = 1;
Expand Down
47 changes: 45 additions & 2 deletions examples/nvrhi_direct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,48 @@ namespace NvrhiDirect {
return value >= expected - 1 && value <= expected + 1;
}

// NVRHI's messages, through an nvrhi::IMessageCallback implemented here: a @cppvtable class
// whose message() has a body, which NVRHI calls through the vtable tslang makes for it. Each
// goes on to Donut's handling (it logs them, and CTest fails on an error it logs), except the
// one the test provokes.
const PROVOKED_MESSAGE = "Cannot close a command list before it is opened";

@cppvtable
class MessageLog extends nvrhi.IMessageCallback {
expecting = false;
provoked = 0;

message(severity: u8, text: string): void {
if (this.expecting && severity == nvrhi.MESSAGE_SEVERITY_ERROR && text == PROVOKED_MESSAGE) {
this.provoked++;
return;
}

Donut_LogNvrhiMessage(severity, text);
}
}

// A global: NVRHI keeps a pointer to it where the collector doesn't look, for as long as the
// device lives.
let messageLog = new MessageLog();

function renderTargetDesc(): nvrhi.TextureDesc {
let desc = nvrhi.textureDesc(TEXTURE_SIZE, TEXTURE_SIZE, Format.RGBA8_UNORM, "Cleared texture",
nvrhi.RESOURCE_STATE_RENDER_TARGET);
desc.isRenderTarget = true;
return desc;
}

function runTest(app: App, api: GraphicsAPI): void {
function runTest(app: App, api: GraphicsAPI, validation: boolean): void {
// the tuples NVRHI reads as its structs: the same sizes as the C++ build's
check(nvrhi.layoutMatches(), "the tuples have the C++ structs' sizes");

const device = <nvrhi.IDevice>app.getNvrhiDevice();

// IDevice::getMessageCallback: the MessageLog the device was made with
check(<index><Opaque>device.getMessageCallback() == <index><Opaque>messageLog,
"the device's IMessageCallback is the one implemented here");

// IDevice::getGraphicsAPI; IResource::getNativeObject and IDevice::getNativeQueue (an
// nvrhi::Object returned by value); IDevice::queryFeatureSupport, which says whether the
// device has a compute queue (Donut's headless device has one on Vulkan, not on D3D12), as
Expand Down Expand Up @@ -166,6 +195,16 @@ namespace NvrhiDirect {
}
check(<index><Opaque>commandList.getDevice() == <index><Opaque>device, "ICommandList::getDevice");

// IMessageCallback::message, called by NVRHI: its validation layer (-debug) reports closing
// a command list never opened as an error, and does nothing else (without it, the backend
// would close a list it has none of)
if (validation) {
messageLog.expecting = true;
commandList.close();
messageLog.expecting = false;
check(messageLog.provoked == 1, "NVRHI calls the IMessageCallback implemented here");
}

let data: u32[] = [];
for (let i = 0; i < COUNT; i++) {
data.push(i * 3 + 1);
Expand Down Expand Up @@ -270,6 +309,9 @@ namespace NvrhiDirect {
}
}

// the devices made from here on report to messageLog
Donut_SetNvrhiMessageCallback(<Opaque>messageLog);

const app = App.createHeadlessWithOptions(api, -1, options);
if (app.isNull()) {
console.log("Cannot initialize a graphics device with the requested parameters");
Expand All @@ -278,8 +320,9 @@ namespace NvrhiDirect {

console.log(`Using ${Donut_GraphicsAPIToString(api)} API with ${app.getRendererString()}.`);

runTest(app, api);
runTest(app, api, options == AppOptions.DebugRuntime);
app.destroy();
Donut_SetNvrhiMessageCallback(null);

if (failures > 0) {
console.log("Test FAILED!");
Expand Down
24 changes: 24 additions & 0 deletions patches/Donut-message-callback-all-apis.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/src/app/dx11/DeviceManager_DX11.cpp b/src/app/dx11/DeviceManager_DX11.cpp
--- a/src/app/dx11/DeviceManager_DX11.cpp
+++ b/src/app/dx11/DeviceManager_DX11.cpp
@@ -270,7 +270,7 @@
#endif

nvrhi::d3d11::DeviceDesc deviceDesc;
- deviceDesc.messageCallback = &DefaultMessageCallback::GetInstance();
+ deviceDesc.messageCallback = m_DeviceParams.messageCallback ? m_DeviceParams.messageCallback : &DefaultMessageCallback::GetInstance();
deviceDesc.context = m_ImmediateContext;
#if DONUT_WITH_AFTERMATH
deviceDesc.aftermathEnabled = m_DeviceParams.enableAftermath;
diff --git a/src/app/vulkan/DeviceManager_VK.cpp b/src/app/vulkan/DeviceManager_VK.cpp
--- a/src/app/vulkan/DeviceManager_VK.cpp
+++ b/src/app/vulkan/DeviceManager_VK.cpp
@@ -1275,7 +1275,7 @@
auto vecDeviceExt = stringSetToVector(enabledExtensions.device);

nvrhi::vulkan::DeviceDesc deviceDesc;
- deviceDesc.errorCB = &DefaultMessageCallback::GetInstance();
+ deviceDesc.errorCB = m_DeviceParams.messageCallback ? m_DeviceParams.messageCallback : &DefaultMessageCallback::GetInstance();
deviceDesc.instance = m_VulkanInstance;
deviceDesc.physicalDevice = m_VulkanPhysicalDevice;
deviceDesc.device = m_VulkanDevice;
2 changes: 2 additions & 0 deletions tools/check_interop_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@
("Donut_CopyBufferDeviceAddress", "dst"),
# The app's nvrhi::IDevice, handed to modules that use NVRHI directly.
("Donut_GetNvrhiDevice", "*"),
# An nvrhi::IMessageCallback implemented in tslang (core/nvrhi.ts), handed to NVRHI.
("Donut_SetNvrhiMessageCallback", "callback"),
("Donut_InitPagedImageSet", "device"),
("Donut_CopyObjModelMeshIndices", "dst"),
("Donut_CopyObjModelMeshVertices", "dst"),
Expand Down
4 changes: 2 additions & 2 deletions tools/check_nvrhi_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
}

# the classes in the `nvrhi` namespace, named as NVRHI's interfaces
CLASSES = ["IResource", "IBuffer", "ITexture", "IShader", "IBindingLayout", "IBindingSet", "IComputePipeline",
"ICommandList", "IDevice"]
CLASSES = ["IMessageCallback", "IResource", "IBuffer", "ITexture", "IShader", "IBindingLayout", "IBindingSet",
"IComputePipeline", "ICommandList", "IDevice"]

# the Android targets the Itanium layouts are taken for (the same, but both are checked)
ANDROID_TARGETS = ["x86_64-linux-android29", "aarch64-linux-android29"]
Expand Down