diff --git a/CMakeLists.txt b/CMakeLists.txt index d1dddc2..452f605 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index aa16771..9dd0e7d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -315,8 +320,11 @@ Donut's wrappers elsewhere or with `-donut`. CTest runs both: `headless.` 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 diff --git a/core/donut_interop.cpp b/core/donut_interop.cpp index 774d271..6cb6b13 100644 --- a/core/donut_interop.cpp +++ b/core/donut_interop.cpp @@ -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(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(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) @@ -2394,6 +2412,7 @@ extern "C" return nullptr; donut::app::DeviceCreationParameters params; + params.messageCallback = g_NvrhiMessageCallback; params.backBufferWidth = static_cast(width); params.backBufferHeight = static_cast(height); params.vsyncEnabled = (options & AppOption_NoVsync) == 0; @@ -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; diff --git a/core/donut_interop.d.ts b/core/donut_interop.d.ts index 7cdb702..7f3cb49 100644 --- a/core/donut_interop.d.ts +++ b/core/donut_interop.d.ts @@ -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; diff --git a/core/nvrhi.ts b/core/nvrhi.ts index 97d913a..9af5f98 100644 --- a/core/nvrhi.ts +++ b/core/nvrhi.ts @@ -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. /// diff --git a/core/nvrhi_abi.linux.ts b/core/nvrhi_abi.linux.ts index fe3db7c..7ee91c6 100644 --- a/core/nvrhi_abi.linux.ts +++ b/core/nvrhi_abi.linux.ts @@ -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 { @@ -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 } diff --git a/core/nvrhi_abi.win32.ts b/core/nvrhi_abi.win32.ts index 437ffdf..765d5ed 100644 --- a/core/nvrhi_abi.win32.ts +++ b/core/nvrhi_abi.win32.ts @@ -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 { @@ -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 } diff --git a/core/nvrhi_types.ts b/core/nvrhi_types.ts index f8e1eef..973acf0 100644 --- a/core/nvrhi_types.ts +++ b/core/nvrhi_types.ts @@ -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; diff --git a/examples/nvrhi_direct.ts b/examples/nvrhi_direct.ts index 034122e..c789345 100644 --- a/examples/nvrhi_direct.ts +++ b/examples/nvrhi_direct.ts @@ -41,6 +41,31 @@ 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); @@ -48,12 +73,16 @@ namespace NvrhiDirect { 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 = app.getNvrhiDevice(); + // IDevice::getMessageCallback: the MessageLog the device was made with + check(device.getMessageCallback() == 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 @@ -166,6 +195,16 @@ namespace NvrhiDirect { } check(commandList.getDevice() == 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); @@ -270,6 +309,9 @@ namespace NvrhiDirect { } } + // the devices made from here on report to messageLog + Donut_SetNvrhiMessageCallback(messageLog); + const app = App.createHeadlessWithOptions(api, -1, options); if (app.isNull()) { console.log("Cannot initialize a graphics device with the requested parameters"); @@ -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!"); diff --git a/patches/Donut-message-callback-all-apis.patch b/patches/Donut-message-callback-all-apis.patch new file mode 100644 index 0000000..aff1a56 --- /dev/null +++ b/patches/Donut-message-callback-all-apis.patch @@ -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; diff --git a/tools/check_interop_types.py b/tools/check_interop_types.py index 80edbba..96ec851 100644 --- a/tools/check_interop_types.py +++ b/tools/check_interop_types.py @@ -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"), diff --git a/tools/check_nvrhi_slots.py b/tools/check_nvrhi_slots.py index 94b23e7..053e78d 100644 --- a/tools/check_nvrhi_slots.py +++ b/tools/check_nvrhi_slots.py @@ -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"]