diff --git a/README.md b/README.md index 3e32216..67f2faf 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,9 @@ of 16 bytes for the pixel shader) before each of two draws, a color for each hal indirectly (`drawIndirect`), from arguments the command list writes into a buffer: from offset 16 into one half, and from offset 0, no instance, into the other, which stays as cleared. A sixth dispatches the compute pipeline indirectly (`dispatchIndirect`) the same way: from offset 12 into -one output buffer, and from offset 0, no group, into another, which stays as cleared. +one output buffer, and from offset 0, no group, into another, which stays as cleared. A seventh +uploads a texture through a staging texture the CPU writes (`mapStagingTexture` for writing, +`copyTexture`'s third overload) and reads it back, pixel for pixel. It checks what it reads. On the way it also checks: - the graphics API, the native objects and queues; diff --git a/core/nvrhi.ts b/core/nvrhi.ts index 7d52b4b..adc1b82 100644 --- a/core/nvrhi.ts +++ b/core/nvrhi.ts @@ -158,6 +158,17 @@ export namespace nvrhi { commandList.copyTexture(destArg, Ref(destSliceValue), srcArg, Ref(srcSliceValue)); } + // ICommandList::copyTexture of a staging texture into a texture, to upload it + export function copyTextureFromStaging(commandList: ICommandList, dest: ITexture, destSlice: TextureSlice, + src: IStagingTexture, srcSlice: TextureSlice): void { + // (as in copyTexture: TSLANG_ISSUES.md #19) + const destArg: ITexture | null = dest; + const srcArg: IStagingTexture | 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 17bd2d6..74cc767 100644 --- a/examples/nvrhi_direct.ts +++ b/examples/nvrhi_direct.ts @@ -31,7 +31,8 @@ namespace NvrhiDirect { // transparent color drawn over a cleared texture, alpha blended and added under a write mask; // and another, push constants (testPushConstants): a color set before each of two draws; and // another, indirect draws (testDrawIndirect): two draws' arguments from a buffer; and another, - // indirect dispatches (testDispatchIndirect) of the compute pipeline above. + // indirect dispatches (testDispatchIndirect) of the compute pipeline above; and another, an + // upload through a staging texture the CPU writes (testStagingUpload). // 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. @@ -579,6 +580,77 @@ namespace NvrhiDirect { skipped.Release(); } + // The upload staging texture's pixel (x, y): a different color for each + function uploadedPixel(x: int, y: int): u8[] { + return [(40 + 50 * x), (40 + 50 * y), (220 - 20 * x - 30 * y), 255]; + } + + // An upload through a staging texture, with a command list of its own: a staging texture the + // CPU writes (IDevice::createStagingTexture with CPU_ACCESS_WRITE, mapStagingTexture for writing, + // its rows rowPitch bytes apart), ICommandList::copyTexture from it into a texture (the third + // overload), and that read back through a staging texture: the same pixels. + function testStagingUpload(device: nvrhi.IDevice): void { + const upload = nvrhi.createStagingTexture(device, nvrhi.textureDesc(TEXTURE_SIZE, TEXTURE_SIZE, + Format.RGBA8_UNORM, "Upload texture", nvrhi.RESOURCE_STATE_COPY_SOURCE), nvrhi.CPU_ACCESS_WRITE); + const uploaded = nvrhi.createTexture(device, nvrhi.textureDesc(TEXTURE_SIZE, TEXTURE_SIZE, + Format.RGBA8_UNORM, "Uploaded", nvrhi.RESOURCE_STATE_COPY_DEST)); + const readback = nvrhi.createStagingTexture(device, renderTargetDesc(), nvrhi.CPU_ACCESS_READ); + const commandList = nvrhi.createCommandList(device, nvrhi.commandListParameters()); + if (!upload || !uploaded || !readback || !commandList) { + check(false, "the staging upload test's textures and command list"); + return; + } + + let rowPitch: index = 0; + const mapped = nvrhi.mapStagingTexture(device, upload, nvrhi.entireTexture(), nvrhi.CPU_ACCESS_WRITE, Ref(rowPitch)); + if (!mapped || rowPitch < TEXTURE_SIZE * 4) { + check(false, `IDevice::mapStagingTexture for writing (row pitch ${rowPitch})`); + if (mapped) { + device.unmapStagingTexture(upload); + } + return; + } + const rows = >mapped; + for (let y = 0; y < TEXTURE_SIZE; y++) { + for (let x = 0; x < TEXTURE_SIZE; x++) { + const pixel = uploadedPixel(x, y); + for (let c = 0; c < 4; c++) { + Deref(rows[y * rowPitch + 4 * x + c]) = pixel[c]; + } + } + } + device.unmapStagingTexture(upload); + + commandList.open(); + nvrhi.copyTextureFromStaging(commandList, uploaded, nvrhi.entireTexture(), upload, nvrhi.entireTexture()); + nvrhi.copyTextureToStaging(commandList, readback, nvrhi.entireTexture(), uploaded, nvrhi.entireTexture()); + commandList.close(); + nvrhi.executeCommandList(device, commandList); + check(device.waitForIdle(), "IDevice::waitForIdle after the staging upload test"); + + const pixels = readStaging(device, readback); + if (pixels.length != 0) { + let mismatches = 0; + for (let y = 0; y < TEXTURE_SIZE; y++) { + for (let x = 0; x < TEXTURE_SIZE; x++) { + const i = 4 * (y * TEXTURE_SIZE + x); + const pixel = uploadedPixel(x, y); + if (pixels[i] != pixel[0] || pixels[i + 1] != pixel[1] || pixels[i + 2] != pixel[2] || pixels[i + 3] != pixel[3]) { + mismatches++; + } + } + } + const last = 4 * (TEXTURE_SIZE * TEXTURE_SIZE - 1); + console.log(`Uploaded (${pixels[0]}, ${pixels[1]}, ${pixels[2]}), ..., (${pixels[last]}, ${pixels[last + 1]}, ${pixels[last + 2]})`); + check(mismatches == 0, `ICommandList::copyTexture from a staging texture the CPU wrote (${mismatches} of ${TEXTURE_SIZE * TEXTURE_SIZE} pixels differ)`); + } + + commandList.Release(); + readback.Release(); + uploaded.Release(); + upload.Release(); + } + 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"); @@ -952,6 +1024,7 @@ namespace NvrhiDirect { testPushConstants(app, device, isD3D12); testDrawIndirect(app, device, isD3D12); testDispatchIndirect(device, pipeline, bindingLayout, input, data); + testStagingUpload(device); // IResource::Release on what the create functions returned with a reference commandList.Release();