Merge PR #27: experimental packed and in-place DLSS output

This commit is contained in:
woutermans committed 2026-10-07 22:26:30 +02:00
commit 84e6048066
11 files changed
+1012 -22

No files matched your search

+17 -6
View File
@@ -504,6 +504,13 @@ int main(int argc, char** argv)
const bool exposureRGBA32 = std::getenv("D4R_HARNESS_EXPOSURE_RGBA32") != nullptr;
const char* replayDir = std::getenv("D4R_HARNESS_REPLAY_DIR");
const bool rgba8 = std::getenv("D4R_HARNESS_RGBA8") != nullptr;
// D4R_HARNESS_OUTPUT_RGB10A2=1: an R10G10B10A2_UNORM output texture (inputs unchanged), as some games pass
const bool rgb10 = std::getenv("D4R_HARNESS_OUTPUT_RGB10A2") != nullptr;
if (rgb10 && (rgba8 || std::getenv("D4R_HARNESS_QUALITY_SCENE") != nullptr))
{
std::fprintf(stderr, "RGB10A2 output cannot be combined with RGBA8 or the quality scene\n");
return 2;
}
if (rgba8 && (qualityScene || motionScene || jitterScene || replayDir != nullptr))
{
std::fprintf(stderr, "RGBA8 format probe requires the static synthetic scene\n");
@@ -627,7 +634,9 @@ int main(int argc, char** argv)
ID3D12Resource* exposureTexture = create_texture(1, 1,
exposureRGBA32 ? DXGI_FORMAT_R32G32B32A32_FLOAT : DXGI_FORMAT_R32_FLOAT, D3D12_RESOURCE_FLAG_NONE);
ID3D12Resource* outputTexture = create_texture(outWidth, outHeight,
rgba8 ? DXGI_FORMAT_R8G8B8A8_UNORM : DXGI_FORMAT_R16G16B16A16_FLOAT,
rgba8 ? DXGI_FORMAT_R8G8B8A8_UNORM
: rgb10 ? DXGI_FORMAT_R10G10B10A2_UNORM
: DXGI_FORMAT_R16G16B16A16_FLOAT,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
upload(colorTexture, rgba8 ? static_cast<const void*>(color8.data()) : static_cast<const void*>(color.data()),
resourceWidth * (rgba8 ? 4 : 8), srv);
@@ -636,8 +645,10 @@ int main(int argc, char** argv)
upload(exposureTexture, exposureRGBA32 ? static_cast<const void*>(exposureRGBA.data())
: static_cast<const void*>(&exposureValue),
exposureRGBA32 ? 16 : 4, srv);
upload(outputTexture, rgba8 ? static_cast<const void*>(outputInit8.data()) : static_cast<const void*>(outputInit.data()),
outWidth * (rgba8 ? 4 : 8), D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
const std::vector<uint8_t> outputInit10(rgb10 ? static_cast<size_t>(outWidth) * outHeight * 4 : 0);
upload(outputTexture, rgba8 ? static_cast<const void*>(outputInit8.data())
: rgb10 ? static_cast<const void*>(outputInit10.data()) : static_cast<const void*>(outputInit.data()),
outWidth * (rgba8 || rgb10 ? 4 : 8), D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
std::printf("synthetic inputs uploaded\n");
trace_stage("synthetic inputs uploaded");
@@ -970,7 +981,7 @@ int main(int argc, char** argv)
if (saveFrames)
{
const std::vector<uint8_t> frameOutput =
read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outWidth * (rgba8 ? 4 : 8));
read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outWidth * (rgba8 || rgb10 ? 4 : 8));
const std::string framePath = std::string(argv[2]) + ".frame" + std::to_string(frame);
if (FILE* frameFile = std::fopen(framePath.c_str(), "wb"))
{
@@ -981,7 +992,7 @@ int main(int argc, char** argv)
}
const std::vector<uint8_t> output = read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
outWidth * (rgba8 ? 4 : 8));
outWidth * (rgba8 || rgb10 ? 4 : 8));
FILE* file = std::fopen(argv[2], "wb");
if (file != nullptr)
{
@@ -1041,7 +1052,7 @@ int main(int argc, char** argv)
Sleep(frameWaitMs);
}
const std::vector<uint8_t> cycleOutput = read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
outWidth * (rgba8 ? 4 : 8));
outWidth * (rgba8 || rgb10 ? 4 : 8));
if (verifyRecreation)
{
auto& reference = recreationReference[cycle % 2];
+183 -8
View File
@@ -34,6 +34,9 @@
// D4R_SHIM_VRAM_INTEROP=1 keep inputs and output in VRAM (see "VRAM interop")
// D4R_SHIM_SPLIT_FRAME=1 with VRAM interop and the d4r vkd3d-proton patch,
// present each frame's own DLSS result (see "Split frames")
// D4R_SHIM_INPLACE=1 experimental, with VKD3D_D4R_LINEAR_TEXTURES=1 in the patched vkd3d-proton: the output
// kernel writes the game's output texture itself (no game-side output copy).
// D4R_SHIM_OUTPUT_PACKED=1 lets M's output kernel store R10G10B10A2 texels itself.
#define WIDL_EXPLICIT_AGGREGATE_RETURNS
#include <windows.h>
#include <d3d12.h>
@@ -1611,6 +1614,12 @@ struct FrameParams
bool vram = false; // inputs and output stay in VRAM (VRAM interop)
bool split = false; // this frame presents its own result (split frames)
unsigned motionDilation = 0; // actually applied, worker only
// D4R_SHIM_INPLACE: the game's linear output texture, written without a game-side copy (0 = none)
CudaDevicePtr outputInPlace = 0;
uint32_t outputInPlacePitch = 0;
// D4R_SHIM_OUTPUT_PACKED: the output kernel stores the game's R10G10B10A2 texels itself, so the game-side
// copy (or the in-place texture) takes them without the RGBA16F conversion image and blit
bool packedOutput = false;
};
struct FrameTiming
@@ -1732,6 +1741,9 @@ struct Feature
// D4R_SHIM_OUTPUT_DIRECT: this frame's result is stored by the native output kernel straight into its
// destination buffer (worker only), so the array -> buffer copy is skipped.
bool outputRedirected = false;
// The previous frame's output kernel was the native one and honoured the redirect (worker writes, game
// thread reads): only then is a frame recorded for packed output.
std::atomic<bool> packedReady{false};
bool outputDirectAllowed = false; // the forced preset's output kernel honours the redirect
// D4R_SHIM_LINEAR_INPUTS: NGX samples the interop buffers directly through pitch-linear
// texture objects (rows padded to 256 bytes), so the buffer -> array copies disappear.
@@ -2140,6 +2152,7 @@ struct VulkanInterop
ID3D12DXVKInteropDevice1* interop = nullptr;
ID3D12DXVKInteropDeviceD4R2* lifetime = nullptr;
ID3D12DXVKInteropDeviceD4R* split = nullptr; // only with the d4r vkd3d-proton patch
ID3D12DXVKInteropDeviceD4R3* linear = nullptr; // experimental: linear exportable game textures
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memory = {};
VkPhysicalDevice physical = VK_NULL_HANDLE;
@@ -2245,6 +2258,9 @@ static void init_vram_interop()
g_vk.split = nullptr;
if (FAILED(g.device->QueryInterface(__uuidof(ID3D12DXVKInteropDeviceD4R2), reinterpret_cast<void**>(&g_vk.lifetime))))
g_vk.lifetime = nullptr;
if (env_uint("D4R_SHIM_INPLACE", 0) == 0 ||
FAILED(g.device->QueryInterface(__uuidof(ID3D12DXVKInteropDeviceD4R3), reinterpret_cast<void**>(&g_vk.linear))))
g_vk.linear = nullptr;
g_vk.ready = ok;
logf("VRAM interop: %s (VkDevice %p), split frames %s", ok ? "ready" : "missing Vulkan entry points",
static_cast<void*>(g_vk.device), g_vk.split != nullptr ? "available" : "unavailable (stock vkd3d-proton)");
@@ -2470,6 +2486,52 @@ static size_t vk_texel_bytes(VkFormat format)
}
}
// D4R_SHIM_INPLACE (experimental): a game texture that vkd3d-proton created linear and exportable is imported
// into CUDA once, so DLSS reads or writes its texels without a game-side copy. Imports are kept for the
// process lifetime (ROCm does not free mapped external memory, see g_vramPool).
struct InPlaceTexture
{
CudaDevicePtr texels = 0;
size_t rowPitch = 0;
};
static std::mutex g_inPlaceMutex;
static std::vector<std::pair<uint64_t, CudaDevicePtr>> g_inPlaceImports; // VkDeviceMemory -> CUDA mapping
// Game thread.
static bool describe_in_place(ID3D12Resource* resource, InPlaceTexture& texture)
{
UINT64 memory = 0, size = 0, offset = 0, pitch = 0;
if (g_vk.linear == nullptr || FAILED(g_vk.linear->GetVulkanLinearImageInfo(resource, &memory, &size, &offset, &pitch)))
return false;
CudaDevicePtr base = 0;
{
std::lock_guard<std::mutex> lock(g_inPlaceMutex);
for (const auto& entry : g_inPlaceImports)
if (entry.first == memory)
base = entry.second;
}
if (base == 0)
{
void* external = nullptr;
const int imported = g.worker.call([&] {
return g_vk.import(g_vk.device, memory, size, &base, &external);
});
if (imported != 0 || base == 0)
{
logf("in-place: importing %llu bytes of texture memory failed (%d)", static_cast<unsigned long long>(size), imported);
return false;
}
logf("in-place: imported %llu bytes of game texture memory (offset %llu, row pitch %llu)",
static_cast<unsigned long long>(size), static_cast<unsigned long long>(offset),
static_cast<unsigned long long>(pitch));
std::lock_guard<std::mutex> lock(g_inPlaceMutex);
g_inPlaceImports.emplace_back(memory, base);
}
texture.texels = base + offset;
texture.rowPitch = static_cast<size_t>(pitch);
return true;
}
// How one input or the output moves between its D3D12 texture and a buffer.
struct VramCopy
{
@@ -3858,6 +3920,44 @@ static int synchronize_default_stream(Feature& feature, bool* querySleep = nullp
return g.cu.ctxSynchronize();
}
// A frame recorded for packed output whose output kernel then did not pack it (it was not the native one
// after all): the RGBA16F result is converted on the host, rounding as the kernel store and the game-side
// blit do. Slow, and only expected around a kernel change.
static int pack_output_on_host(Feature& feature, CudaMemcpy2D copy, size_t packedPitch, uint32_t frame)
{
const size_t width = feature.output.width, height = feature.output.height;
std::vector<uint16_t> halves(width * height * 4);
std::vector<uint32_t> packed(width * height);
const CudaDevicePtr target = copy.dstDevice;
copy.dstMemoryType = CUDA_MEMORY_HOST;
copy.dstHost = halves.data();
copy.dstPitch = width * 8;
int result = g.cu.memcpy2D(&copy);
if (result != 0)
return result;
const auto unorm = [](uint16_t half, float scale) {
float value = half_to_float(half);
value = value > 0.0f ? value : 0.0f;
value = value < 1.0f ? value : 1.0f;
return static_cast<uint32_t>(value * scale + 0.5f);
};
for (size_t texel = 0; texel < packed.size(); ++texel)
packed[texel] = unorm(halves[texel * 4], 1023.0f) | unorm(halves[texel * 4 + 1], 1023.0f) << 10 |
unorm(halves[texel * 4 + 2], 1023.0f) << 20 | unorm(halves[texel * 4 + 3], 3.0f) << 30;
CudaMemcpy2D upload = {};
upload.srcMemoryType = CUDA_MEMORY_HOST;
upload.srcHost = packed.data();
upload.srcPitch = width * 4;
upload.dstMemoryType = CUDA_MEMORY_DEVICE;
upload.dstDevice = target;
upload.dstPitch = packedPitch;
upload.WidthInBytes = width * 4;
upload.Height = height;
result = g.cu.memcpy2D(&upload);
logf("frame %u: output kernel did not pack its result; converted on the host (%d)", frame, result);
return result;
}
// VRAM interop, worker thread: copies the finished result into a free output
// slot's imported buffer and publishes it; evaluate() copies it on to the game.
static void publish_vram(Feature* feature, uint32_t frame, const FrameParams& params, FrameTiming& timing,
@@ -3873,7 +3973,7 @@ static void publish_vram(Feature* feature, uint32_t frame, const FrameParams& pa
frame_retired(feature, frame);
return;
}
const VramBuffer& buffer = feature->outputs[target].vram;
VramBuffer buffer = feature->outputs[target].vram;
const size_t rowBytes = static_cast<size_t>(feature->output.width) * 8;
CudaMemcpy2D copy = {};
copy.srcMemoryType = CUDA_MEMORY_ARRAY;
@@ -3883,7 +3983,18 @@ static void publish_vram(Feature* feature, uint32_t frame, const FrameParams& pa
copy.dstPitch = rowBytes;
copy.WidthInBytes = rowBytes;
copy.Height = feature->output.height;
int result = rowBytes * feature->output.height > buffer.bytes ? -1 : feature->outputRedirected ? 0 : copy_2d(copy);
if (params.outputInPlace != 0)
{
// no game-side copy was recorded: a result the output kernel did not redirect goes to the texture here
buffer.device = copy.dstDevice = params.outputInPlace;
copy.dstPitch = params.outputInPlacePitch;
buffer.bytes = static_cast<size_t>(params.outputInPlacePitch) * feature->output.height;
}
int result = rowBytes / (params.packedOutput ? 2 : 1) * feature->output.height > buffer.bytes ? -1
: feature->outputRedirected ? 0
: params.packedOutput ? pack_output_on_host(*feature, copy, params.outputInPlace != 0 ? copy.dstPitch
: rowBytes / 2, frame)
: copy_2d(copy);
if (result == 0)
{
const auto syncStart = timing.enabled ? ProfileClock::now() : ProfileClock::time_point{};
@@ -4100,10 +4211,20 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
(nativeOutput || feature->outputRedirected))
{
// split frames write their own slot; the staged path downloads from outputLinear
const size_t rowBytes = static_cast<size_t>(feature->output.width) * 8, bytes = rowBytes * feature->output.height;
size_t rowBytes = static_cast<size_t>(feature->output.width) * 8;
const size_t bytes = rowBytes * feature->output.height;
CudaDevicePtr target = 0;
if (params.vram && params.split && feature->outputs[frame % kOutputSlots].vram.bytes >= bytes)
if (params.packedOutput)
rowBytes = static_cast<size_t>(feature->output.width) * 4;
if (params.vram && params.split && params.outputInPlace != 0)
{
target = params.outputInPlace;
rowBytes = params.outputInPlacePitch;
}
else if (params.vram && params.split && feature->outputs[frame % kOutputSlots].vram.bytes >= bytes)
target = feature->outputs[frame % kOutputSlots].vram.device;
if (params.packedOutput)
rowBytes |= 1; // the bridge's flag for R10G10B10A2 texels (row pitches are multiples of 8)
else if (!params.vram && overlapped && ensure_linear(feature->outputLinear, feature->outputLinearBytes, bytes))
target = feature->outputLinear;
const bool redirect = nativeOutput && target != 0 &&
@@ -4138,6 +4259,7 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
g.cu.setArrayRedirect(feature->output.array, 0, 0);
feature->outputRedirected = false;
}
feature->packedReady = feature->outputRedirected;
const auto evalReturned = timing.enabled ? ProfileClock::now() : ProfileClock::time_point{};
const int eventEndResult = eventStartResult == 0 ? g.cu.eventRecord(feature->profileEnd, nullptr) : -1;
const auto syncStart = timing.enabled ? ProfileClock::now() : ProfileClock::time_point{};
@@ -4981,6 +5103,49 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
if (!ensure_vram_buffer(*feature, slot.vram[index], geometry.size()))
return NGX_FAIL_PLATFORM_ERROR;
}
// M's downsample kernel has only formatted stores, which the native store can pack (kernels/tex/tex_common.h).
p.packedOutput = feature->split && feature->outputDirectAllowed && feature->preset == 13 && vramOutput.convert &&
outputDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM && vramOutput.width % 2 == 0 &&
env_uint("D4R_SHIM_OUTPUT_DIRECT", 0) != 0 && env_uint("D4R_SHIM_OUTPUT_PACKED", 0) != 0 &&
feature->packedReady.load();
static bool packedLogged = false;
if (p.packedOutput && !packedLogged)
{
packedLogged = true;
logf("frame %u: packed output (the output kernel stores R10G10B10A2 texels; no conversion blit)", frame);
}
InPlaceTexture outputTexture;
const size_t outputTexelBytes = p.packedOutput ? 4 : 8;
if (feature->split && feature->outputDirectAllowed && (!vramOutput.convert || p.packedOutput) &&
env_uint("D4R_SHIM_OUTPUT_DIRECT", 0) != 0 &&
describe_in_place(output, outputTexture) && outputTexture.rowPitch % 8 == 0 &&
outputTexture.rowPitch / 8 <= 0xffffu &&
outputTexture.rowPitch >= static_cast<size_t>(vramOutput.width) * outputTexelBytes)
{
p.outputInPlace = outputTexture.texels;
p.outputInPlacePitch = static_cast<uint32_t>(outputTexture.rowPitch);
}
static bool outputInPlaceLogged = false;
if (p.outputInPlace != 0 && !outputInPlaceLogged)
{
outputInPlaceLogged = true;
logf("frame %u: output in place (the output kernel writes the game's %s texture; no game-side copy)", frame,
p.packedOutput ? "R10G10B10A2" : "RGBA16F");
}
static bool inPlaceLogged = false;
if (g_vk.linear != nullptr && !inPlaceLogged)
{
inPlaceLogged = true;
if (p.outputInPlace == 0)
{
UINT64 memory = 0, size = 0, offset = 0, pitch = 0;
const HRESULT linear = g_vk.linear->GetVulkanLinearImageInfo(output, &memory, &size, &offset, &pitch);
logf("in-place: output copied on the first frame (split %d, direct output allowed %d, format %d, needs conversion %d, "
"linear exportable texture %d, row pitch %llu)", static_cast<int>(feature->split), feature->outputDirectAllowed,
static_cast<int>(outputDesc.Format), vramOutput.convert, SUCCEEDED(linear),
static_cast<unsigned long long>(pitch));
}
}
if (vramInputs[0].convert && !ensure_conversion_image(*feature, feature->colorConversion,
vramInputs[0].width, vramInputs[0].height,
VK_FORMAT_R16G16B16A16_SFLOAT))
@@ -5091,14 +5256,24 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
if (p.vram && feature->split)
p.split = SUCCEEDED(g_vk.split->SplitCommandListForExternalWait(
list, reinterpret_cast<UINT64>(feature->splitSemaphore), frame));
if (!p.split)
{
p.outputInPlace = 0; // only a split frame waits for the result before the game reads its texture
p.packedOutput = false;
}
if (p.split)
{
// Runs in the second half of the list, once frame's result is in place.
const int target = static_cast<int>(frame % kOutputSlots);
transition(list, output, outputState, D3D12_RESOURCE_STATE_COPY_DEST);
if (!record_vram_output(list, feature->outputs[target].vram, vramOutput, feature->outputConversion))
logf("frame %u: BeginVkCommandBufferInterop failed for the output", frame);
transition(list, output, D3D12_RESOURCE_STATE_COPY_DEST, outputState);
if (p.outputInPlace == 0)
{
if (p.packedOutput)
vramOutput.convert = false; // the buffer already holds the texture's own texels
transition(list, output, outputState, D3D12_RESOURCE_STATE_COPY_DEST);
if (!record_vram_output(list, feature->outputs[target].vram, vramOutput, feature->outputConversion))
logf("frame %u: BeginVkCommandBufferInterop failed for the output", frame);
transition(list, output, D3D12_RESOURCE_STATE_COPY_DEST, outputState);
}
if (timing.enabled)
timing.presentedFrame = frame;
}
+10
View File
@@ -57,3 +57,13 @@ ID3D12DXVKInteropDeviceD4R2 : public ID3D12DXVKInteropDeviceD4R
};
__CRT_UUID_DECL(ID3D12DXVKInteropDeviceD4R2, 0x4f8c9462, 0x8197, 0x4dc1, 0xa5, 0x84, 0xcf, 0x44, 0xd9, 0x70, 0x6d, 0xb2)
// Experimental (VKD3D_D4R_LINEAR_TEXTURES=1 in the patched vkd3d-proton): the dedicated, exportable memory
// behind a linear-tiled texture and the layout of its only subresource.
MIDL_INTERFACE("7b1e5d0a-93c4-4e8f-b6a2-51c08f2e4d73")
ID3D12DXVKInteropDeviceD4R3 : public ID3D12DXVKInteropDeviceD4R2
{
virtual HRESULT STDMETHODCALLTYPE GetVulkanLinearImageInfo(ID3D12Resource* resource, UINT64* memory, UINT64* memorySize,
UINT64* offset, UINT64* rowPitch) = 0;
};
__CRT_UUID_DECL(ID3D12DXVKInteropDeviceD4R3, 0x7b1e5d0a, 0x93c4, 0x4e8f, 0xb6, 0xa2, 0x51, 0xc0, 0x8f, 0x2e, 0x4d, 0x73)
+4 -1
View File
@@ -9,7 +9,10 @@ inline void d4r_sleep_us(unsigned int microseconds)
using Delay = LONG(WINAPI*)(BOOLEAN, LARGE_INTEGER*);
static const auto delay = reinterpret_cast<Delay>(reinterpret_cast<void*>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtDelayExecution")));
if (microseconds == 0)
// D4R_SHIM_SPIN_POLL=1 (measurement only): every poll yields instead of sleeping, so frame timings are
// not quantised to the ~250 us a 200 us sleep takes. Costs a CPU core per waiting thread.
static const bool spin = GetEnvironmentVariableA("D4R_SHIM_SPIN_POLL", nullptr, 0) > 1;
if (microseconds == 0 || spin)
{
SwitchToThread();
return;
+479
View File
@@ -0,0 +1,479 @@
// Standalone check (no Wine, no DLSS): does a HIP texture object created on memory imported from Vulkan
// see what Vulkan writes to that memory afterwards?
//
// 1. Vulkan creates a linear-tiled RGBA16F image (or, with --buffer, a plain buffer) on exportable memory
// and uploads pattern A.
// 2. HIP imports the memory, creates a pitch-linear texture object on it and samples it in a kernel.
// 3. Vulkan uploads pattern B. HIP samples again, and also reads the memory with hipMemcpy.
// 4. HIP copies the texels out and back (a rewrite with their own bytes) and samples once more.
//
// The d4r NGX shim sees step 3's sample return pattern A for game textures while hipMemcpy returns B.
// usage: hip_vk_texture_stale_repro [--buffer] [--hip-first] [--prewrites N] [--game-usage] [WIDTH HEIGHT]
// --buffer export a VkBuffer (written with vkCmdCopyBuffer) instead of a linear VkImage
// --hip-first initialise HIP before the Vulkan object exists (default: after pattern A is uploaded)
// --prewrites N Vulkan uploads N times (ending on A) before HIP imports the memory (default 1)
// --game-usage create the image as vkd3d-proton would a render target: more usages, mutable format
#include <hip/hip_runtime.h>
#include <vulkan/vulkan.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <vector>
__global__ void sample_texture(hipTextureObject_t texture, float* out, int width, int height)
{
const int x = blockIdx.x * blockDim.x + threadIdx.x, y = blockIdx.y * blockDim.y + threadIdx.y;
if (x >= width || y >= height)
return;
const float4 texel = tex2D<float4>(texture, x + 0.5f, y + 0.5f);
float* pixel = out + (static_cast<size_t>(y) * width + x) * 4;
pixel[0] = texel.x, pixel[1] = texel.y, pixel[2] = texel.z, pixel[3] = texel.w;
}
#define VK_CHECK(call) \
do \
{ \
const VkResult vkResult_ = (call); \
if (vkResult_ != VK_SUCCESS) \
{ \
std::fprintf(stderr, "%s failed: %d\n", #call, vkResult_); \
std::exit(1); \
} \
} while (0)
#define HIP_CHECK(call) \
do \
{ \
const hipError_t hipResult_ = (call); \
if (hipResult_ != hipSuccess) \
{ \
std::fprintf(stderr, "%s failed: %s\n", #call, hipGetErrorName(hipResult_)); \
std::exit(1); \
} \
} while (0)
// Values k/256 only: exact as halves.
static uint16_t half_of(unsigned k)
{
if (k == 0)
return 0;
int exponent = 0;
while ((k >> exponent) > 1)
++exponent; // k = 1.m * 2^exponent
const unsigned mantissa = (k << (10 - exponent)) & 0x3ffu;
return static_cast<uint16_t>((exponent - 8 + 15) << 10 | mantissa);
}
// Pattern 0 ("A") and 1 ("B") differ in every texel.
static unsigned pattern_value(int pattern, int x, int y, int channel)
{
const unsigned base = (x * 7 + y * 13 + channel * 29) % 200;
return pattern == 0 ? base + 1 : 256 - base;
}
static void fill_pattern(uint16_t* texels, int width, int height, size_t rowPitch, int pattern)
{
for (int y = 0; y < height; ++y)
{
uint16_t* row = reinterpret_cast<uint16_t*>(reinterpret_cast<uint8_t*>(texels) + y * rowPitch);
for (int x = 0; x < width; ++x)
for (int channel = 0; channel < 4; ++channel)
row[x * 4 + channel] = half_of(pattern_value(pattern, x, y, channel));
}
}
// Which pattern the floats hold: 'A', 'B', or '?' with the number of texels matching each.
static char classify(const float* values, int width, int height, size_t* matchA, size_t* matchB)
{
*matchA = *matchB = 0;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x)
{
bool a = true, b = true;
for (int channel = 0; channel < 4; ++channel)
{
const float value = values[(static_cast<size_t>(y) * width + x) * 4 + channel];
a &= value == pattern_value(0, x, y, channel) / 256.0f;
b &= value == pattern_value(1, x, y, channel) / 256.0f;
}
*matchA += a, *matchB += b;
}
const size_t texels = static_cast<size_t>(width) * height;
return *matchA == texels ? 'A' : *matchB == texels ? 'B' : '?';
}
static float half_to_float(uint16_t half)
{
const int exponent = (half >> 10) & 31, mantissa = half & 0x3ff;
if (exponent == 0)
return mantissa / 1024.0f / 16384.0f;
float value = 1.0f + mantissa / 1024.0f;
for (int shift = exponent - 15; shift != 0; shift += shift < 0 ? 1 : -1)
value = shift < 0 ? value / 2 : value * 2;
return value;
}
struct Vulkan
{
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice physical = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
VkCommandPool pool = VK_NULL_HANDLE;
VkCommandBuffer cmd = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memory = {};
};
static uint32_t memory_type(const Vulkan& vk, uint32_t typeBits, VkMemoryPropertyFlags wanted)
{
for (uint32_t index = 0; index < vk.memory.memoryTypeCount; ++index)
if ((typeBits & (1u << index)) && (vk.memory.memoryTypes[index].propertyFlags & wanted) == wanted)
return index;
std::fprintf(stderr, "no memory type with flags 0x%x\n", wanted);
std::exit(1);
}
static void submit(const Vulkan& vk)
{
VK_CHECK(vkEndCommandBuffer(vk.cmd));
VkSubmitInfo info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
info.commandBufferCount = 1;
info.pCommandBuffers = &vk.cmd;
VK_CHECK(vkQueueSubmit(vk.queue, 1, &info, VK_NULL_HANDLE));
VK_CHECK(vkQueueWaitIdle(vk.queue));
}
static void begin(const Vulkan& vk)
{
VkCommandBufferBeginInfo info = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkResetCommandBuffer(vk.cmd, 0));
VK_CHECK(vkBeginCommandBuffer(vk.cmd, &info));
}
int main(int argc, char** argv)
{
bool useBuffer = false, hipFirst = false, gameUsage = false, zeroInit = false;
int prewrites = 1, forcedType = -1;
int width = 1280, height = 720, positional = 0;
for (int index = 1; index < argc; ++index)
{
if (!std::strcmp(argv[index], "--buffer"))
useBuffer = true;
else if (!std::strcmp(argv[index], "--hip-first"))
hipFirst = true;
else if (!std::strcmp(argv[index], "--zero-init"))
zeroInit = true; // allocate with VK_MEMORY_ALLOCATE_ZERO_INITIALIZE_BIT_EXT, as vkd3d-proton does
else if (!std::strcmp(argv[index], "--game-usage"))
gameUsage = true;
else if (!std::strcmp(argv[index], "--memory-type") && index + 1 < argc)
forcedType = std::atoi(argv[++index]); // Vulkan memory type index for the shared object
else if (!std::strcmp(argv[index], "--prewrites") && index + 1 < argc)
prewrites = std::atoi(argv[++index]);
else
(positional++ == 0 ? width : height) = std::atoi(argv[index]);
}
if (hipFirst)
HIP_CHECK(hipInit(0));
// --- Vulkan device ---
Vulkan vk;
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.apiVersion = VK_API_VERSION_1_2;
VkInstanceCreateInfo instanceInfo = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
instanceInfo.pApplicationInfo = &app;
VK_CHECK(vkCreateInstance(&instanceInfo, nullptr, &vk.instance));
uint32_t count = 0;
VK_CHECK(vkEnumeratePhysicalDevices(vk.instance, &count, nullptr));
std::vector<VkPhysicalDevice> devices(count);
VK_CHECK(vkEnumeratePhysicalDevices(vk.instance, &count, devices.data()));
VkPhysicalDeviceProperties properties = {};
for (VkPhysicalDevice candidate : devices)
{
vkGetPhysicalDeviceProperties(candidate, &properties);
if (properties.vendorID == 0x1002 && properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
{
vk.physical = candidate;
break;
}
}
if (vk.physical == VK_NULL_HANDLE)
{
std::fprintf(stderr, "no discrete AMD Vulkan device\n");
return 1;
}
std::printf("Vulkan device: %s (driver version 0x%x)\n", properties.deviceName, properties.driverVersion);
vkGetPhysicalDeviceMemoryProperties(vk.physical, &vk.memory);
uint32_t families = 0;
vkGetPhysicalDeviceQueueFamilyProperties(vk.physical, &families, nullptr);
std::vector<VkQueueFamilyProperties> familyProperties(families);
vkGetPhysicalDeviceQueueFamilyProperties(vk.physical, &families, familyProperties.data());
uint32_t family = 0;
while (family < families && !(familyProperties[family].queueFlags & VK_QUEUE_GRAPHICS_BIT))
++family;
const float priority = 1.0f;
VkDeviceQueueCreateInfo queueInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queueInfo.queueFamilyIndex = family;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &priority;
const char* extensions[] = {VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, "VK_EXT_zero_initialize_device_memory"};
VkPhysicalDeviceZeroInitializeDeviceMemoryFeaturesEXT zeroFeature = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_DEVICE_MEMORY_FEATURES_EXT};
zeroFeature.zeroInitializeDeviceMemory = VK_TRUE;
VkDeviceCreateInfo deviceInfo = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, zeroInit ? &zeroFeature : nullptr};
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
deviceInfo.enabledExtensionCount = zeroInit ? 2 : 1;
deviceInfo.ppEnabledExtensionNames = extensions;
VK_CHECK(vkCreateDevice(vk.physical, &deviceInfo, nullptr, &vk.device));
vkGetDeviceQueue(vk.device, family, 0, &vk.queue);
VkCommandPoolCreateInfo poolInfo = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = family;
VK_CHECK(vkCreateCommandPool(vk.device, &poolInfo, nullptr, &vk.pool));
VkCommandBufferAllocateInfo cmdInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
cmdInfo.commandPool = vk.pool;
cmdInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmdInfo.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(vk.device, &cmdInfo, &vk.cmd));
// --- the shared object: a linear image or a buffer on exportable device-local memory ---
VkExternalMemoryImageCreateInfo externalImage = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
externalImage.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
VkExternalMemoryBufferCreateInfo externalBuffer = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
externalBuffer.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
VkImage image = VK_NULL_HANDLE;
VkBuffer shared = VK_NULL_HANDLE;
VkMemoryRequirements requirements = {};
size_t rowPitch = static_cast<size_t>(width) * 8, texelOffset = 0;
VkMemoryDedicatedAllocateInfo dedicated = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
if (useBuffer)
{
rowPitch = (rowPitch + 255) & ~static_cast<size_t>(255);
VkBufferCreateInfo info = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &externalBuffer};
info.size = rowPitch * height;
info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VK_CHECK(vkCreateBuffer(vk.device, &info, nullptr, &shared));
vkGetBufferMemoryRequirements(vk.device, shared, &requirements);
dedicated.buffer = shared;
}
else
{
VkImageCreateInfo info = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, &externalImage};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = VK_FORMAT_R16G16B16A16_SFLOAT;
info.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
info.mipLevels = info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_LINEAR;
info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
if (gameUsage)
{
info.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VK_CHECK(vkCreateImage(vk.device, &info, nullptr, &image));
vkGetImageMemoryRequirements(vk.device, image, &requirements);
dedicated.image = image;
}
VkExportMemoryAllocateInfo exportInfo = {VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO, &dedicated};
exportInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
VkMemoryAllocateFlagsInfo allocateFlags = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, &exportInfo};
allocateFlags.flags = VK_MEMORY_ALLOCATE_ZERO_INITIALIZE_BIT_EXT;
VkMemoryAllocateInfo allocation = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
zeroInit ? static_cast<const void*>(&allocateFlags) : &exportInfo};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = memory_type(vk, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (forcedType >= 0)
allocation.memoryTypeIndex = static_cast<uint32_t>(forcedType);
VkDeviceMemory sharedMemory = VK_NULL_HANDLE;
VK_CHECK(vkAllocateMemory(vk.device, &allocation, nullptr, &sharedMemory));
if (useBuffer)
VK_CHECK(vkBindBufferMemory(vk.device, shared, sharedMemory, 0));
else
{
VK_CHECK(vkBindImageMemory(vk.device, image, sharedMemory, 0));
const VkImageSubresource subresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0};
VkSubresourceLayout layout = {};
vkGetImageSubresourceLayout(vk.device, image, &subresource, &layout);
rowPitch = layout.rowPitch;
texelOffset = layout.offset;
}
std::printf("shared %s: %dx%d RGBA16F, row pitch %zu, offset %zu, allocation %llu bytes, memory type %u\n",
useBuffer ? "VkBuffer" : "linear VkImage", width, height, rowPitch, texelOffset,
static_cast<unsigned long long>(requirements.size), allocation.memoryTypeIndex);
// --- host-visible staging buffer for uploads and Vulkan-side readback ---
const size_t stagingBytes = rowPitch * height;
VkBufferCreateInfo stagingInfo = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
stagingInfo.size = stagingBytes;
stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VkBuffer staging = VK_NULL_HANDLE;
VK_CHECK(vkCreateBuffer(vk.device, &stagingInfo, nullptr, &staging));
VkMemoryRequirements stagingRequirements = {};
vkGetBufferMemoryRequirements(vk.device, staging, &stagingRequirements);
VkMemoryAllocateInfo stagingAllocation = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
stagingAllocation.allocationSize = stagingRequirements.size;
stagingAllocation.memoryTypeIndex = memory_type(
vk, stagingRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
VK_CHECK(vkAllocateMemory(vk.device, &stagingAllocation, nullptr, &stagingMemory));
VK_CHECK(vkBindBufferMemory(vk.device, staging, stagingMemory, 0));
void* stagingMap = nullptr;
VK_CHECK(vkMapMemory(vk.device, stagingMemory, 0, VK_WHOLE_SIZE, 0, &stagingMap));
VkBufferImageCopy region = {};
region.bufferRowLength = static_cast<uint32_t>(rowPitch / 8);
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.imageExtent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
const VkBufferCopy bufferRegion = {0, 0, stagingBytes};
bool imageInitialised = false;
const auto vulkan_upload = [&](int pattern) {
std::memset(stagingMap, 0, stagingBytes);
fill_pattern(static_cast<uint16_t*>(stagingMap), width, height, rowPitch, pattern);
begin(vk);
if (useBuffer)
vkCmdCopyBuffer(vk.cmd, staging, shared, 1, &bufferRegion);
else
{
if (!imageInitialised)
{
VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(vk.cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &barrier);
imageInitialised = true;
}
vkCmdCopyBufferToImage(vk.cmd, staging, image, VK_IMAGE_LAYOUT_GENERAL, 1, &region);
}
submit(vk);
};
std::vector<float> floats(static_cast<size_t>(width) * height * 4);
size_t matchA = 0, matchB = 0;
const auto report = [&](const char* what, char expected, const char* source) {
const char seen = classify(floats.data(), width, height, &matchA, &matchB);
std::printf(" %-34s %-22s pattern %c (texels matching A: %zu, B: %zu) %s\n", what, source, seen, matchA, matchB,
seen == expected ? "ok" : "MISMATCH");
return seen == expected;
};
const auto halves_to_floats = [&](const uint16_t* texels) {
for (int y = 0; y < height; ++y)
{
const uint16_t* row = reinterpret_cast<const uint16_t*>(reinterpret_cast<const uint8_t*>(texels) + y * rowPitch);
for (int x = 0; x < width * 4; ++x)
floats[static_cast<size_t>(y) * width * 4 + x] = half_to_float(row[x]);
}
};
const auto vulkan_read = [&](const char* what, char expected) {
std::memset(stagingMap, 0, stagingBytes);
begin(vk);
if (useBuffer)
vkCmdCopyBuffer(vk.cmd, shared, staging, 1, &bufferRegion);
else
vkCmdCopyImageToBuffer(vk.cmd, image, VK_IMAGE_LAYOUT_GENERAL, staging, 1, &region);
submit(vk);
halves_to_floats(static_cast<const uint16_t*>(stagingMap));
return report(what, expected, "Vulkan copy");
};
for (int write = prewrites - 1; write >= 0; --write)
vulkan_upload(write & 1); // alternating, ending on A
std::printf("Vulkan uploaded %d time(s) before the import, ending on pattern A\n", prewrites);
// --- HIP: import, texture object, sampling kernel ---
if (!hipFirst)
HIP_CHECK(hipInit(0));
int fd = -1;
VkMemoryGetFdInfoKHR fdInfo = {VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR};
fdInfo.memory = sharedMemory;
fdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
const auto getMemoryFd = reinterpret_cast<PFN_vkGetMemoryFdKHR>(vkGetDeviceProcAddr(vk.device, "vkGetMemoryFdKHR"));
VK_CHECK(getMemoryFd(vk.device, &fdInfo, &fd));
hipExternalMemoryHandleDesc handle = {};
handle.type = hipExternalMemoryHandleTypeOpaqueFd;
handle.handle.fd = fd;
handle.size = requirements.size;
handle.flags = hipExternalMemoryDedicated;
hipExternalMemory_t external = nullptr;
HIP_CHECK(hipImportExternalMemory(&external, &handle));
close(fd);
hipExternalMemoryBufferDesc mapping = {};
mapping.size = requirements.size;
void* base = nullptr;
HIP_CHECK(hipExternalMemoryGetMappedBuffer(&base, external, &mapping));
uint8_t* texels = static_cast<uint8_t*>(base) + texelOffset;
std::printf("HIP imported the memory at %p\n", base);
hipResourceDesc resource = {};
resource.resType = hipResourceTypePitch2D;
resource.res.pitch2D.devPtr = texels;
resource.res.pitch2D.desc = hipCreateChannelDesc(16, 16, 16, 16, hipChannelFormatKindFloat);
resource.res.pitch2D.width = width;
resource.res.pitch2D.height = height;
resource.res.pitch2D.pitchInBytes = rowPitch;
hipTextureDesc sampler = {};
sampler.addressMode[0] = sampler.addressMode[1] = hipAddressModeClamp;
sampler.filterMode = hipFilterModePoint;
sampler.readMode = hipReadModeElementType;
sampler.normalizedCoords = 0;
hipTextureObject_t texture = nullptr;
HIP_CHECK(hipCreateTextureObject(&texture, &resource, &sampler, nullptr));
float* deviceOut = nullptr;
HIP_CHECK(hipMalloc(&deviceOut, floats.size() * sizeof(float)));
const auto hip_sample = [&](const char* what, char expected) {
HIP_CHECK(hipMemset(deviceOut, 0, floats.size() * sizeof(float)));
const dim3 block(16, 16), grid((width + 15) / 16, (height + 15) / 16);
hipLaunchKernelGGL(sample_texture, grid, block, 0, 0, texture, deviceOut, width, height);
HIP_CHECK(hipDeviceSynchronize());
HIP_CHECK(hipMemcpy(floats.data(), deviceOut, floats.size() * sizeof(float), hipMemcpyDeviceToHost));
return report(what, expected, "HIP texture sample");
};
std::vector<uint16_t> hostTexels(stagingBytes / 2);
const auto hip_memcpy = [&](const char* what, char expected) {
HIP_CHECK(hipMemcpy(hostTexels.data(), texels, stagingBytes, hipMemcpyDeviceToHost));
halves_to_floats(hostTexels.data());
return report(what, expected, "hipMemcpy to host");
};
bool ok = true;
std::printf("after import (Vulkan last wrote A):\n");
ok &= hip_sample("texture created after A", 'A');
ok &= hip_memcpy("", 'A');
vulkan_upload(1);
std::printf("Vulkan uploaded pattern B:\n");
ok &= vulkan_read("", 'B');
ok &= hip_memcpy("", 'B');
ok &= hip_sample("same texture object", 'B');
hipTextureObject_t second = nullptr;
HIP_CHECK(hipCreateTextureObject(&second, &resource, &sampler, nullptr));
std::swap(texture, second);
ok &= hip_sample("new texture object", 'B');
std::swap(texture, second);
// HIP rewrites the texels with their own bytes.
void* scratch = nullptr;
HIP_CHECK(hipMalloc(&scratch, stagingBytes));
HIP_CHECK(hipMemcpy(scratch, texels, stagingBytes, hipMemcpyDeviceToDevice));
HIP_CHECK(hipMemcpy(texels, scratch, stagingBytes, hipMemcpyDeviceToDevice));
HIP_CHECK(hipDeviceSynchronize());
std::printf("HIP copied the texels out and back:\n");
ok &= hip_sample("same texture object", 'B');
ok &= vulkan_read("", 'B');
vulkan_upload(0);
std::printf("Vulkan uploaded pattern A again:\n");
ok &= hip_memcpy("", 'A');
ok &= hip_sample("same texture object", 'A');
std::printf("%s\n", ok ? "RESULT: consistent - HIP texture sampling follows Vulkan's writes"
: "RESULT: INCONSISTENT - see MISMATCH lines");
return ok ? 0 : 3;
}
+3 -2
View File
@@ -1399,8 +1399,9 @@ static CUresult write_redirect_tail(CUsurfObject object, CUdeviceptr pointer, ui
static unsigned char ring[256][16];
static unsigned int next;
unsigned char* tail = ring[next++ % 256];
// dword 21: 0x5232 ('R2') << 16 | pitch / 8 (0 = off); dwords 22-23: pointer
const uint32_t word = pitch != 0 ? 0x52320000u | ((pitch >> 3) & 0xffffu) : 0u;
// dword 21: 0x5232 ('R2', RGBA16F texels) << 16 | pitch / 8 (0 = off); dwords 22-23: pointer.
// Row pitches are multiples of 8, so bit 0 of `pitch` selects 'R3': R10G10B10A2_UNORM texels.
const uint32_t word = pitch != 0 ? ((pitch & 1u) != 0 ? 0x52330000u : 0x52320000u) | ((pitch >> 3) & 0xffffu) : 0u;
memset(tail, 0, 16);
memcpy(tail, &word, 4);
memcpy(tail + 4, &pointer, 8);