Skip to content
Open
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: 2 additions & 1 deletion gpu/vk/vk_xfer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ bool buffer_side(const NvnState& st, const Sub& s, uint64_t addr, uint64_t& size
warn(3, "copy row stride does not fit the level's blocks: skipped");
return false;
}
size = row * hb * s.layers * s.d;
// The last row ends at its last block: its trailing stride padding is not accessed by Vulkan.
size = row * ((uint64_t)hb * s.layers * s.d - 1) + (uint64_t)wb * s.bpb;
span = gpu_buffer_bind(addr, size, written);
if (!addr || !span) {
warn(3, "copy buffer is not one mapped pool span: skipped");
Expand Down
2 changes: 2 additions & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"tests/test_system_header_cache.py": ["--cc", "clang"],
"tests/test_precompile_compute_guard.py": ["--cxx", "clang++"],
"tests/test_vulkan_clear_mip.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"],
"tests/test_vulkan_copy_span.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"],
"tests/test_vulkan_copy_span_gpu_compile.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"],
"tests/test_input_background.py": ["--cc", "gcc"],
"tests/test_oot_wide_save_picture.py": ["--cc", "gcc"],
"nx2/test_import_windows.py": ["--cxx", "g++"],
Expand Down
109 changes: 109 additions & 0 deletions tests/test_vulkan_copy_span.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Run the production buffer/image copy layout against synthetic mapped pool spans."""
import argparse
import os
from pathlib import Path
import subprocess
import tempfile

ROOT = Path(__file__).resolve().parents[1]


def block(source, marker):
start = source.index(marker)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cxx", default="clang++")
parser.add_argument("--headers", type=Path, required=True)
parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_xfer.cpp")
parser.add_argument("--sanitize", action="store_true")
parser.add_argument("--gpu", action="store_true", help="opt-in bounded Vulkan upload/readback on a compatible physical device")
parser.add_argument("--gpu-device", help="case-insensitive device-name substring for the --gpu probe")
args = parser.parse_args()
args.gpu = args.gpu or bool(args.gpu_device)
source = args.source.read_text(encoding="utf-8")
fixture = r'''
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan_core.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL:%d %s\n",__LINE__,#x); std::exit(1); } } while (0)
struct Texture {};
struct NvnState { int64_t copy_stride = 0; };
struct BufferRef { VkBuffer buffer{}; uint64_t offset=0, size=0; explicit operator bool() const { return buffer != VK_NULL_HANDLE; } };
uint64_t pool_size, requested;
unsigned binds;
bool expect_written;
void warn(int, const char*) {}
BufferRef gpu_buffer_bind(uint64_t addr, uint64_t size, bool written) {
++binds; requested=size;
CHECK(written == expect_written);
return addr && size <= pool_size ? BufferRef{(VkBuffer)1, 0, size} : BufferRef{};
}
''' + block(source, "struct Sub {") + ";\n" + "\n".join(
block(source, marker) for marker in ("VkImageSubresourceLayers layers(", "VkOffset3D offset(",
"VkExtent3D extent(", "bool buffer_side(")) + r'''
void run(uint32_t w, uint32_t h, uint32_t depth, uint32_t layers_n, int bw, int bh,
uint32_t bpb, int64_t stride, uint64_t available, uint64_t expected, bool valid) {
Sub s; s.w=w; s.h=h; s.d=depth; s.layers=layers_n; s.bw=bw; s.bh=bh; s.bpb=bpb;
s.aspect=VK_IMAGE_ASPECT_COLOR_BIT; s.level=2; s.layer=3; s.x=4; s.y=8;
for (bool written : {false,true}) {
NvnState state{stride}; BufferRef span; VkBufferImageCopy copy{}; uint64_t size=0;
pool_size=available; requested=0; binds=0; expect_written=written;
const bool ok=buffer_side(state,s,0x1000,size,copy,span,written);
if(ok!=valid) std::fprintf(stderr,"copy %ux%u: requested %llu bytes, pool has %llu, expected span %llu\n",
w,h,(unsigned long long)requested,(unsigned long long)available,(unsigned long long)expected);
CHECK(ok == valid);
if (!valid) continue;
CHECK(binds==1 && requested==expected && size==expected);
CHECK(copy.bufferOffset==0 && copy.bufferRowLength==(stride ? stride/bpb*bw : ((w+bw-1)/bw)*bw));
CHECK(copy.bufferImageHeight==((h+bh-1)/bh)*bh);
CHECK(copy.imageSubresource.mipLevel==2 && copy.imageSubresource.baseArrayLayer==3 && copy.imageSubresource.layerCount==layers_n);
CHECK(copy.imageOffset.x==4 && copy.imageOffset.y==8 && copy.imageExtent.width==w && copy.imageExtent.height==h && copy.imageExtent.depth==depth);
}
}
int main() {
run(2,2,1,1,1,1,4,16,24,24,true); // no padding is accessed after the final row
run(2,1,1,1,1,1,4,16,8,8,true);
run(2,2,1,3,1,1,4,16,88,88,true); // intermediate layers retain their padding
run(2,2,3,1,1,1,4,16,88,88,true);
run(5,5,1,1,4,4,16,48,80,80,true); // partial BC edge blocks
run(2,2,1,1,1,1,4,0,16,16,true);
run(2,2,1,1,1,1,4,16,23,24,false); // genuinely truncated pool
run(2,2,1,1,1,1,4,7,32,0,false); // row too short/misaligned
std::puts("PASS: 8 copy layouts, upload/readback, exact spans and truncated pools");
}
'''
if args.gpu:
production = block(source, "struct Sub {") + ";\n" + "\n".join(
block(source, marker) for marker in ("VkImageSubresourceLayers layers(", "VkOffset3D offset(",
"VkExtent3D extent(", "bool buffer_side("))
fixture = (ROOT / "tests/vulkan_copy_span_gpu.cpp").read_text(encoding="utf-8").replace(
"// PRODUCTION_COPY_LAYOUT", production)
with tempfile.TemporaryDirectory(prefix="hookshot-copy-span-") as directory:
cpp, exe = Path(directory) / "test.cpp", Path(directory) / "test.exe"
cpp.write_text(fixture, encoding="utf-8")
flags = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all"] if args.sanitize else []
if args.gpu:
flags += ["-Wno-missing-field-initializers"]
flags += ["-static"] if os.name == "nt" and "mingw" in args.cxx.lower() else []
libraries = ["-ldl"] if args.gpu and os.name != "nt" else []
subprocess.run([args.cxx, "-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", *flags,
"-I" + str(args.headers), str(cpp), "-o", str(exe), *libraries], check=True)
command = [str(exe)]
if args.gpu_device:
command += ["--device", args.gpu_device]
subprocess.run(command, check=True, timeout=20 if args.gpu else 10)


if __name__ == "__main__":
main()
58 changes: 58 additions & 0 deletions tests/test_vulkan_copy_span_gpu_compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Compile the opt-in Vulkan hardware fixture without loading Vulkan or executing GPU work."""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import tempfile

ROOT = Path(__file__).resolve().parents[1]


def block(source, marker):
start = source.index(marker)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cxx", default="clang++")
parser.add_argument("--headers", type=Path)
parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_xfer.cpp")
args = parser.parse_args()
compiler = shutil.which(args.cxx) or (args.cxx if Path(args.cxx).is_file() else None)
if not compiler:
print(f"SKIP: C++ compiler unavailable: {args.cxx}")
return 0
if not args.headers or not (args.headers / "vulkan/vulkan_core.h").is_file():
print("SKIP: Vulkan headers unavailable")
return 0
source = args.source.read_text(encoding="utf-8")
production = block(source, "struct Sub {") + ";\n" + "\n".join(
block(source, marker) for marker in (
"VkImageSubresourceLayers layers(", "VkOffset3D offset(",
"VkExtent3D extent(", "bool buffer_side("
)
)
fixture = (ROOT / "tests/vulkan_copy_span_gpu.cpp").read_text(encoding="utf-8").replace(
"// PRODUCTION_COPY_LAYOUT", production
)
with tempfile.TemporaryDirectory(prefix="hookshot-copy-span-gpu-compile-") as directory:
cpp, exe = Path(directory) / "fixture.cpp", Path(directory) / "fixture.exe"
cpp.write_text(fixture, encoding="utf-8")
flags = ["-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers"]
if os.name == "nt" and "mingw" in compiler.lower():
flags.append("-static")
libraries = ["-ldl"] if os.name != "nt" else []
subprocess.run([compiler, *flags, "-I" + str(args.headers), str(cpp), "-o", str(exe), *libraries],
check=True, timeout=120)
print("PASS: Vulkan hardware fixture compiled; no Vulkan loader or GPU work executed")


if __name__ == "__main__":
main()
Loading
Loading