From 1f6c3e2e70456ca6b03f1c0ac7dfc78aa63b1ad6 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 13 Oct 2026 22:38:38 +0100 Subject: [PATCH] Textures are read back through NVRHI's staging textures nvrhi.entireTexture() (nvrhi::TextureSlice's defaults), createStagingTexture(), mapStagingTexture() (the mapped rows and their pitch), copyTexture() between textures and copyTextureToStaging(). nvrhi_direct copies its drawn texture into another texture, that into a staging texture it maps, and checks the pixels read row by row (256 bytes apart on D3D12) instead of reading the texture through Donut; it checks IStagingTexture::getDesc too. layoutMatches() compares TextureSlice. The two copyTexture overloads take their textures through `| null` locals: a class argument fits no overload until a tslang release has TypeScriptCompiler#577 (TSLANG_ISSUES.md #19). Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++-- core/donut_interop.cpp | 2 ++ core/nvrhi.ts | 47 ++++++++++++++++++++++++++++++++++++++- examples/nvrhi_direct.ts | 48 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8a01ce0..22c3eed 100644 --- a/README.md +++ b/README.md @@ -331,8 +331,10 @@ Donut's wrappers elsewhere or with `-donut`. CTest runs both: `headless.` a depth test and no culling, a viewport and its scissor rectangle); 5. draws an indexed quad into its top right quarter from a vertex buffer (an input layout of position and texture coordinates), sampling a 2 x 2 texture uploaded with `writeTexture` through - a point sampler, scaled by a constant buffer: one texel a pixel. The texture is read back through - Donut, its bottom right quarter as cleared. + a point sampler, scaled by a constant buffer: one texel a pixel. Its bottom right quarter is as + cleared; +6. copies the texture into another, and that into a staging texture (two of `copyTexture`'s + overloads), which it maps to read the pixels back, row by row (`mapStagingTexture`'s row pitch). It checks what it reads. On the way it also checks: - the graphics API, the native objects and queues; diff --git a/core/donut_interop.cpp b/core/donut_interop.cpp index f250e00..9f19a0f 100644 --- a/core/donut_interop.cpp +++ b/core/donut_interop.cpp @@ -3328,6 +3328,8 @@ extern "C" return int(sizeof(nvrhi::SamplerDesc)); if (n == "VertexAttributeDesc") return int(sizeof(nvrhi::VertexAttributeDesc)); + if (n == "TextureSlice") + return int(sizeof(nvrhi::TextureSlice)); return -1; } diff --git a/core/nvrhi.ts b/core/nvrhi.ts index fc8e118..c48fb1f 100644 --- a/core/nvrhi.ts +++ b/core/nvrhi.ts @@ -59,7 +59,8 @@ export namespace nvrhi { && sizeof() == Donut_GetNvrhiStructSize("GraphicsState") && sizeof() == Donut_GetNvrhiStructSize("DrawArguments") && sizeof() == Donut_GetNvrhiStructSize("SamplerDesc") - && sizeof() == Donut_GetNvrhiStructSize("VertexAttributeDesc"); + && sizeof() == Donut_GetNvrhiStructSize("VertexAttributeDesc") + && sizeof() == Donut_GetNvrhiStructSize("TextureSlice"); } // A BufferDesc as nvrhi::BufferDesc's defaults make it, with byteSize, a debug name (see @@ -113,6 +114,50 @@ export namespace nvrhi { return handle.ptr_ ? handle.ptr_ : null; } + // nvrhi::TextureSlice's defaults: the whole of mip level 0 of array slice 0 (its -1 sizes are the + // texture's, as TextureSlice::resolve makes them) + export function entireTexture(): TextureSlice { + return [0, 0, 0, 0xffffffff, 0xffffffff, 0xffffffff, 0, 0]; + } + + // IDevice::createStagingTexture: a texture of desc's size and format that the CPU maps with + // cpuAccess (CPU_ACCESS_READ, CPU_ACCESS_WRITE); null on failure. Release() it. + export function createStagingTexture(device: IDevice, desc: TextureDesc, cpuAccess: u8): IStagingTexture | null { + let descValue = desc; + const handle = device.createStagingTexture(Ref(descValue), cpuAccess); + return handle.ptr_ ? handle.ptr_ : null; + } + + // IDevice::mapStagingTexture: the slice's first byte, its rows rowPitch bytes apart; null on + // failure. unmapStagingTexture it. + export function mapStagingTexture(device: IDevice, texture: IStagingTexture, slice: TextureSlice, cpuAccess: u8, + rowPitch: Ref): Opaque | null { + let sliceValue = slice; + return device.mapStagingTexture(texture, Ref(sliceValue), cpuAccess, rowPitch); + } + + // ICommandList::copyTexture between two textures + export function copyTexture(commandList: ICommandList, dest: ITexture, destSlice: TextureSlice, + src: ITexture, srcSlice: TextureSlice): void { + // (passed as the overload's `| null`: a class fits no overload, TSLANG_ISSUES.md #19) + const destArg: ITexture | null = dest; + const srcArg: ITexture | null = src; + let destSliceValue = destSlice; + let srcSliceValue = srcSlice; + commandList.copyTexture(destArg, Ref(destSliceValue), srcArg, Ref(srcSliceValue)); + } + + // ICommandList::copyTexture of a texture into a staging texture, to read it back + export function copyTextureToStaging(commandList: ICommandList, dest: IStagingTexture, destSlice: TextureSlice, + src: ITexture, srcSlice: TextureSlice): void { + // (as in copyTexture: TSLANG_ISSUES.md #19) + const destArg: IStagingTexture | null = dest; + const srcArg: ITexture | null = src; + let destSliceValue = destSlice; + let srcSliceValue = srcSlice; + commandList.copyTexture(destArg, Ref(destSliceValue), srcArg, Ref(srcSliceValue)); + } + // A ShaderDesc as nvrhi::ShaderDesc's defaults make it, with the shader's stage (SHADER_TYPE_*), // a debug name and its entry point's name (see stdString). export function shaderDesc(shaderType: u16, debugName: string, entryName: string): ShaderDesc { diff --git a/examples/nvrhi_direct.ts b/examples/nvrhi_direct.ts index 0726289..18a930b 100644 --- a/examples/nvrhi_direct.ts +++ b/examples/nvrhi_direct.ts @@ -23,7 +23,9 @@ namespace NvrhiDirect { // framebuffer over the texture, vertex and pixel shaders, the render state with no depth test // and no culling, a viewport), and into its top right quarter an indexed quad from a vertex // buffer (an input layout), sampling a 2 x 2 texture (writeTexture, a sampler) scaled by a - // constant buffer; read back through Donut, the bottom right quarter as cleared. + // constant buffer, the bottom right quarter as cleared; + // - copies the texture into another (copyTexture), and that into a staging texture, mapped to + // read it back. // It checks what it reads, and on the way the device's graphics API, its native objects, // reference counting, debug names, the desc NVRHI keeps and the layouts of the structs it passes. @@ -182,6 +184,22 @@ namespace NvrhiDirect { check(nvrhi.getNativeObject(texture, isD3D12 ? nvrhi.OBJECT_TYPE_D3D12_RESOURCE : nvrhi.OBJECT_TYPE_VK_IMAGE) != null, "the texture has its native resource"); + // the texture's copy, and a staging texture the copy is read back through: + // IDevice::createStagingTexture, IStagingTexture::getDesc + const copied = nvrhi.createTexture(device, nvrhi.textureDesc(TEXTURE_SIZE, TEXTURE_SIZE, Format.RGBA8_UNORM, + "Copied texture", nvrhi.RESOURCE_STATE_COPY_DEST)); + let stagingDesc = nvrhi.textureDesc(TEXTURE_SIZE, TEXTURE_SIZE, Format.RGBA8_UNORM, "Staging texture", + nvrhi.RESOURCE_STATE_COPY_DEST); + const staging = nvrhi.createStagingTexture(device, stagingDesc, nvrhi.CPU_ACCESS_READ); + if (!copied || !staging) { + check(false, "IDevice::createTexture and createStagingTexture"); + return; + } + const keptStagingDesc = Deref(staging.getDesc()); + check(keptStagingDesc.width == TEXTURE_SIZE && keptStagingDesc.height == TEXTURE_SIZE + && keptStagingDesc.format == Format.RGBA8_UNORM + && nvrhi.stdStringIs(keptStagingDesc.debugName, "Staging texture"), "IStagingTexture::getDesc"); + // The compute pipeline: IDevice::createShader of the bytecode ShaderMake compiled (DXIL, // SPIR-V), IShader::getDesc, getBytecode; its binding layout and set, // IDevice::createComputePipeline @@ -382,6 +400,11 @@ namespace NvrhiDirect { nvrhi.setGraphicsState(commandList, nvrhi.graphicsState(texturedPipeline, framebuffer, [TEXTURE_SIZE / 2, TEXTURE_SIZE, 0, TEXTURE_SIZE / 2, 0, 1], texturedSet, vertexBuffer, indexBuffer, Format.R16_UINT)); nvrhi.drawIndexed(commandList, 6); + + // ICommandList::copyTexture, two of its overloads: the texture into its copy, the copy into + // the staging texture + nvrhi.copyTexture(commandList, copied, nvrhi.entireTexture(), texture, nvrhi.entireTexture()); + nvrhi.copyTextureToStaging(commandList, staging, nvrhi.entireTexture(), copied, nvrhi.entireTexture()); commandList.endMarker(); commandList.close(); @@ -412,11 +435,26 @@ namespace NvrhiDirect { check(clearMismatches == 0, `ICommandList::clearBufferUInt (${clearMismatches} of ${COUNT} differ)`); check(computeMismatches == 0, `the compute dispatch (${computeMismatches} of ${COUNT} differ)`); - // the texture, read back through Donut (a staging texture) + // the texture's copy, read back through the staging texture: IDevice::mapStagingTexture (its + // rows rowPitch bytes apart: 256 on D3D12), unmapStagingTexture let pixels: u8[] = []; pixels.length = TEXTURE_SIZE * TEXTURE_SIZE * 4; - if (app.readTextureLevel(texture, 0, Ref(pixels[0]), pixels.length) == 0) { - check(false, "the texture's readback"); + let rowPitch: index = 0; + const mappedPixels = nvrhi.mapStagingTexture(device, staging, nvrhi.entireTexture(), nvrhi.CPU_ACCESS_READ, Ref(rowPitch)); + const readBack = mappedPixels != null && rowPitch >= TEXTURE_SIZE * 4; + if (mappedPixels) { + if (readBack) { + const rows = >mappedPixels; + for (let y = 0; y < TEXTURE_SIZE; y++) { + for (let i = 0; i < TEXTURE_SIZE * 4; i++) { + pixels[y * TEXTURE_SIZE * 4 + i] = Deref(rows[y * rowPitch + i]); + } + } + } + device.unmapStagingTexture(staging); + } + if (!readBack) { + check(false, `IDevice::mapStagingTexture (row pitch ${rowPitch})`); } else { // the left half drawn green; the top right quarter the texels, a pixel each, green // halved; the bottom right quarter as cleared @@ -472,6 +510,8 @@ namespace NvrhiDirect { bindingLayout.Release(); shader.Release(); texture.Release(); + copied.Release(); + staging.Release(); input.Release(); output.Release(); cleared.Release();