From 6a816dbeafa9d66e58dc54420cc33de7123383d0 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Thu, 2 Jul 2026 15:12:25 +0000 Subject: [PATCH 01/12] seam: backend-neutral IRenderList behind Viewport#getRenderList (zero GL behavior change) --- .../me/cortex/voxy/client/core/gl/GlBuffer.java | 5 ++++- .../voxy/client/core/rendering/IRenderList.java | 14 ++++++++++++++ .../voxy/client/core/rendering/Viewport.java | 2 +- .../HierarchicalOcclusionTraverser.java | 6 +++--- 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/IRenderList.java diff --git a/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java b/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java index 7a44e0e49..bf10aa1ef 100644 --- a/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java +++ b/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java @@ -10,11 +10,14 @@ import static org.lwjgl.opengl.GL15.glDeleteBuffers; import static org.lwjgl.opengl.GL45C.*; -public class GlBuffer extends TrackedObject { +public class GlBuffer extends TrackedObject implements me.cortex.voxy.client.core.rendering.IRenderList { public final int id; private final long size; private final int flags; + @Override public int glId() { return this.id; } + @Override public long sizeBytes() { return this.size; } + private static int COUNT; private static long TOTAL_SIZE; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/IRenderList.java b/src/main/java/me/cortex/voxy/client/core/rendering/IRenderList.java new file mode 100644 index 000000000..9da525a82 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/IRenderList.java @@ -0,0 +1,14 @@ +package me.cortex.voxy.client.core.rendering; + +/** + * Backend-neutral handle to the hierarchical traversal's render list output. + * The GL path implements this directly on {@link me.cortex.voxy.client.core.gl.GlBuffer}; + * the Vulkan path implements it on a VK-allocated, GL-imported shared buffer so the + * (phase-1, still-GL) traversal compute can keep writing it with zero changes. + */ +public interface IRenderList { + /** GL buffer name usable with glBindBufferBase from the traversal compute pass. */ + int glId(); + /** Size of the buffer in bytes. */ + long sizeBytes(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java b/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java index 8637fcabe..4935c337b 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java @@ -124,5 +124,5 @@ public A update() { return (A) this; } - public abstract GlBuffer getRenderList(); + public abstract IRenderList getRenderList(); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 188a936cd..9a807fe7b 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -212,7 +212,7 @@ private void uploadUniform(Viewport viewport) { setFrustum(viewport, ptr); ptr += 4*4*6; - MemoryUtil.memPutInt(ptr, (int) (viewport.getRenderList().size()/4-1)); ptr += 4; + MemoryUtil.memPutInt(ptr, (int) (viewport.getRenderList().sizeBytes()/4-1)); ptr += 4; //VisibilityId MemoryUtil.memPutInt(ptr, this.nodeCleaner.visibilityId); ptr += 4; @@ -237,7 +237,7 @@ private void bindings(Viewport viewport) { //Bind the hiz buffer glBindTextureUnit(0, viewport.hiZBuffer.getHizTextureId()); glBindSampler(0, this.hizSampler); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, RENDER_QUEUE_BINDING, viewport.getRenderList().id); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, RENDER_QUEUE_BINDING, viewport.getRenderList().glId()); } public void doTraversal(Viewport viewport) { @@ -257,7 +257,7 @@ public void doTraversal(Viewport viewport) { } //Clear the render output counter - nglClearNamedBufferSubData(viewport.getRenderList().id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0); + nglClearNamedBufferSubData(viewport.getRenderList().glId(), GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0); //Traverse this.traverseInternal(); From 8564723aee21cb1c954307005d245eaa9b2c7b64 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Thu, 2 Jul 2026 15:12:26 +0000 Subject: [PATCH 02/12] vk: private-device Vulkan context, GL-interop primitives (external memory/semaphores), shaderc bridge --- build.gradle | 12 ++ .../voxy/client/core/vk/ShadercCompiler.java | 49 ++++++ .../voxy/client/core/vk/SharedBuffer.java | 92 ++++++++++ .../voxy/client/core/vk/SharedImage.java | 103 +++++++++++ .../voxy/client/core/vk/SharedSemaphore.java | 59 +++++++ .../me/cortex/voxy/client/core/vk/VkUtil.java | 13 ++ .../voxy/client/core/vk/VulkanBackend.java | 59 +++++++ .../voxy/client/core/vk/VulkanContext.java | 162 ++++++++++++++++++ 8 files changed, 549 insertions(+) create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/ShadercCompiler.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkUtil.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java diff --git a/build.gradle b/build.gradle index 860b70103..9f4a9f07d 100644 --- a/build.gradle +++ b/build.gradle @@ -361,6 +361,18 @@ jar { } project.ext.lwjglVersion = "3.4.1" + +// --- Voxy Vulkan backend deps (optional at runtime; probed via capability detection) --- +dependencies { + include(implementation("org.lwjgl:lwjgl-vulkan:${project.ext.lwjglVersion}")) + include(implementation("org.lwjgl:lwjgl-shaderc:${project.ext.lwjglVersion}")) + for (n in ["natives-windows", "natives-linux", "natives-macos", "natives-macos-arm64"]) { + include(runtimeOnly("org.lwjgl:lwjgl-shaderc:${project.ext.lwjglVersion}:${n}")) + } + // MoltenVK translation layer path on macOS is provided by lwjgl-vulkan natives there: + include(runtimeOnly("org.lwjgl:lwjgl-vulkan:${project.ext.lwjglVersion}:natives-macos")) + include(runtimeOnly("org.lwjgl:lwjgl-vulkan:${project.ext.lwjglVersion}:natives-macos-arm64")) +} project.ext.lwjglNatives = "natives-windows" repositories { diff --git a/src/main/java/me/cortex/voxy/client/core/vk/ShadercCompiler.java b/src/main/java/me/cortex/voxy/client/core/vk/ShadercCompiler.java new file mode 100644 index 000000000..00449ca60 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/ShadercCompiler.java @@ -0,0 +1,49 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.gl.shader.ShaderType; + +import java.nio.ByteBuffer; + +import static org.lwjgl.util.shaderc.Shaderc.*; + +/** + * Runtime GLSL -> SPIR-V so Voxy's shaders stay single-source. Compiles the + * SAME .glsl assets the GL path uses, with VOXY_VULKAN defined and vulkan + * semantics enabled (auto bind/set mapping for the existing layout(binding=N) + * declarations). Requires the lwjgl-shaderc natives added in build.gradle. + */ +public final class ShadercCompiler { + public static ByteBuffer compile(String source, ShaderType type, String name) { + long compiler = shaderc_compiler_initialize(); + long options = shaderc_compile_options_initialize(); + try { + shaderc_compile_options_set_target_env(options, shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + shaderc_compile_options_set_auto_bind_uniforms(options, true); + shaderc_compile_options_set_auto_map_locations(options, true); + shaderc_compile_options_add_macro_definition(options, "VOXY_VULKAN", "1"); + int kind = switch (type) { + case VERTEX -> shaderc_vertex_shader; + case FRAGMENT -> shaderc_fragment_shader; + case COMPUTE -> shaderc_compute_shader; + default -> throw new IllegalArgumentException("Unsupported stage for VK: " + type); + }; + long result = shaderc_compile_into_spv(compiler, source, kind, name, "main", options); + try { + if (shaderc_result_get_compilation_status(result) != shaderc_compilation_status_success) { + throw new IllegalStateException("SPIR-V compile failed for " + name + ":\n" + + shaderc_result_get_error_message(result)); + } + ByteBuffer spv = shaderc_result_get_bytes(result); + ByteBuffer copy = ByteBuffer.allocateDirect(spv.remaining()); + copy.put(spv).flip(); + return copy; + } finally { + shaderc_result_release(result); + } + } finally { + shaderc_compile_options_release(options); + shaderc_compiler_release(compiler); + } + } + private ShadercCompiler() {} +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java new file mode 100644 index 000000000..2064ac32d --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java @@ -0,0 +1,92 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.rendering.IRenderList; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.Platform; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.opengl.EXTMemoryObject.*; +import static org.lwjgl.opengl.EXTMemoryObjectFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; +import static org.lwjgl.opengl.EXTMemoryObjectFD.glImportMemoryFdEXT; +import static org.lwjgl.opengl.EXTMemoryObjectWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; +import static org.lwjgl.opengl.EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT; +import static org.lwjgl.opengl.GL45C.*; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRExternalMemoryFd.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalMemoryFd.vkGetMemoryFdKHR; +import static org.lwjgl.vulkan.KHRExternalMemoryWin32.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalMemoryWin32.vkGetMemoryWin32HandleKHR; +import static org.lwjgl.vulkan.VK10.*; +import org.lwjgl.vulkan.*; + +/** + * A buffer whose memory is allocated by Vulkan with an exportable handle and + * imported into the current GL context. Both APIs address the same storage: + * GL compute passes (hierarchical traversal, command generation) write it, + * the VK graphics queue consumes it. Implements IRenderList so it can sit + * directly behind Viewport#getRenderList(). + */ +public final class SharedBuffer implements IRenderList { + public final long vkBuffer; + public final long vkMemory; + private final int glMemoryObject; + public final int glBufferId; + private final long size; + private final VulkanContext ctx; + + public SharedBuffer(VulkanContext ctx, long size, int vkUsage) { + this.ctx = ctx; + this.size = size; + boolean win = Platform.get() == Platform.WINDOWS; + int vkHandleType = win ? VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR + : VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; + try (MemoryStack stack = stackPush()) { + var extBuf = VkExternalMemoryBufferCreateInfo.calloc(stack).sType$Default().handleTypes(vkHandleType); + var bci = VkBufferCreateInfo.calloc(stack).sType$Default().pNext(extBuf) + .size(size).usage(vkUsage).sharingMode(VK_SHARING_MODE_EXCLUSIVE); + var pBuf = stack.mallocLong(1); + check(vkCreateBuffer(ctx.device, bci, null, pBuf), "vkCreateBuffer(shared)"); + this.vkBuffer = pBuf.get(0); + + var req = VkMemoryRequirements.calloc(stack); + vkGetBufferMemoryRequirements(ctx.device, this.vkBuffer, req); + var export = VkExportMemoryAllocateInfo.calloc(stack).sType$Default().handleTypes(vkHandleType); + var dedicated = VkMemoryDedicatedAllocateInfo.calloc(stack).sType$Default() + .pNext(export).buffer(this.vkBuffer); + var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default().pNext(dedicated) + .allocationSize(req.size()) + .memoryTypeIndex(ctx.findMemoryType(req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); + var pMem = stack.mallocLong(1); + check(vkAllocateMemory(ctx.device, mai, null, pMem), "vkAllocateMemory(shared)"); + this.vkMemory = pMem.get(0); + check(vkBindBufferMemory(ctx.device, this.vkBuffer, this.vkMemory, 0), "vkBindBufferMemory"); + + this.glMemoryObject = glCreateMemoryObjectsEXT(); + if (win) { + var ghi = VkMemoryGetWin32HandleInfoKHR.calloc(stack).sType$Default() + .memory(this.vkMemory).handleType(vkHandleType); + var pHandle = stack.mallocPointer(1); + check(vkGetMemoryWin32HandleKHR(ctx.device, ghi, pHandle), "vkGetMemoryWin32HandleKHR"); + glImportMemoryWin32HandleEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); + } else { + var gfi = VkMemoryGetFdInfoKHR.calloc(stack).sType$Default() + .memory(this.vkMemory).handleType(vkHandleType); + var pFd = stack.mallocInt(1); + check(vkGetMemoryFdKHR(ctx.device, gfi, pFd), "vkGetMemoryFdKHR"); + glImportMemoryFdEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); + } + this.glBufferId = glCreateBuffers(); + glNamedBufferStorageMemEXT(this.glBufferId, size, this.glMemoryObject, 0); + } + } + + @Override public int glId() { return this.glBufferId; } + @Override public long sizeBytes() { return this.size; } + + public void free() { + glDeleteBuffers(this.glBufferId); + glDeleteMemoryObjectsEXT(this.glMemoryObject); + vkDestroyBuffer(this.ctx.device, this.vkBuffer, null); + vkFreeMemory(this.ctx.device, this.vkMemory, null); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java new file mode 100644 index 000000000..17faac40c --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java @@ -0,0 +1,103 @@ +package me.cortex.voxy.client.core.vk; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.Platform; +import org.lwjgl.vulkan.*; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.opengl.EXTMemoryObject.*; +import static org.lwjgl.opengl.EXTMemoryObjectFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; +import static org.lwjgl.opengl.EXTMemoryObjectFD.glImportMemoryFdEXT; +import static org.lwjgl.opengl.EXTMemoryObjectWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; +import static org.lwjgl.opengl.EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT; +import static org.lwjgl.opengl.GL45C.*; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRExternalMemoryFd.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalMemoryFd.vkGetMemoryFdKHR; +import static org.lwjgl.vulkan.KHRExternalMemoryWin32.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalMemoryWin32.vkGetMemoryWin32HandleKHR; +import static org.lwjgl.vulkan.VK10.*; + +/** + * A 2D render target allocated by VK with exportable memory, visible in GL as a + * texture: VK renders LOD color/depth into it; GL composites it into MC's + * framebuffer. Recreated on resolution change. + */ +public final class SharedImage { + public final long vkImage; + public final long vkMemory; + public final long vkView; + public final int glMemoryObject; + public final int glTexture; + public final int width, height; + public final int vkFormat; + private final VulkanContext ctx; + + public SharedImage(VulkanContext ctx, int width, int height, int vkFormat, int glInternalFormat, int vkUsage, int aspect) { + this.ctx = ctx; this.width = width; this.height = height; this.vkFormat = vkFormat; + boolean win = Platform.get() == Platform.WINDOWS; + int handleType = win ? VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR + : VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; + try (MemoryStack stack = stackPush()) { + var ext = VkExternalMemoryImageCreateInfo.calloc(stack).sType$Default().handleTypes(handleType); + var ici = VkImageCreateInfo.calloc(stack).sType$Default().pNext(ext) + .imageType(VK_IMAGE_TYPE_2D) + .format(vkFormat) + .extent(e -> e.width(width).height(height).depth(1)) + .mipLevels(1).arrayLayers(1) + .samples(VK_SAMPLE_COUNT_1_BIT) + .tiling(VK_IMAGE_TILING_OPTIMAL) + .usage(vkUsage) + .sharingMode(VK_SHARING_MODE_EXCLUSIVE) + .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED); + var pImg = stack.mallocLong(1); + check(vkCreateImage(ctx.device, ici, null, pImg), "vkCreateImage(shared)"); + this.vkImage = pImg.get(0); + + var req = VkMemoryRequirements.calloc(stack); + vkGetImageMemoryRequirements(ctx.device, this.vkImage, req); + var export = VkExportMemoryAllocateInfo.calloc(stack).sType$Default().handleTypes(handleType); + var dedicated = VkMemoryDedicatedAllocateInfo.calloc(stack).sType$Default().pNext(export).image(this.vkImage); + var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default().pNext(dedicated) + .allocationSize(req.size()) + .memoryTypeIndex(ctx.findMemoryType(req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); + var pMem = stack.mallocLong(1); + check(vkAllocateMemory(ctx.device, mai, null, pMem), "vkAllocateMemory(sharedImage)"); + this.vkMemory = pMem.get(0); + check(vkBindImageMemory(ctx.device, this.vkImage, this.vkMemory, 0), "vkBindImageMemory"); + + var vci = VkImageViewCreateInfo.calloc(stack).sType$Default() + .image(this.vkImage).viewType(VK_IMAGE_VIEW_TYPE_2D).format(vkFormat); + vci.subresourceRange().aspectMask(aspect).levelCount(1).layerCount(1); + var pView = stack.mallocLong(1); + check(vkCreateImageView(ctx.device, vci, null, pView), "vkCreateImageView(shared)"); + this.vkView = pView.get(0); + + this.glMemoryObject = glCreateMemoryObjectsEXT(); + if (win) { + var ghi = VkMemoryGetWin32HandleInfoKHR.calloc(stack).sType$Default() + .memory(this.vkMemory).handleType(handleType); + var pHandle = stack.mallocPointer(1); + check(vkGetMemoryWin32HandleKHR(ctx.device, ghi, pHandle), "vkGetMemoryWin32HandleKHR(img)"); + glImportMemoryWin32HandleEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); + } else { + var gfi = VkMemoryGetFdInfoKHR.calloc(stack).sType$Default() + .memory(this.vkMemory).handleType(handleType); + var pFd = stack.mallocInt(1); + check(vkGetMemoryFdKHR(ctx.device, gfi, pFd), "vkGetMemoryFdKHR(img)"); + glImportMemoryFdEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); + } + this.glTexture = glCreateTextures(GL_TEXTURE_2D); + glTextureParameteri(this.glTexture, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT); + glTextureStorageMem2DEXT(this.glTexture, 1, glInternalFormat, width, height, this.glMemoryObject, 0); + } + } + + public void free() { + glDeleteTextures(this.glTexture); + glDeleteMemoryObjectsEXT(this.glMemoryObject); + vkDestroyImageView(this.ctx.device, this.vkView, null); + vkDestroyImage(this.ctx.device, this.vkImage, null); + vkFreeMemory(this.ctx.device, this.vkMemory, null); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java new file mode 100644 index 000000000..227fe527c --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java @@ -0,0 +1,59 @@ +package me.cortex.voxy.client.core.vk; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.Platform; +import org.lwjgl.vulkan.*; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.opengl.EXTSemaphore.glDeleteSemaphoresEXT; +import static org.lwjgl.opengl.EXTSemaphore.glGenSemaphoresEXT; +import static org.lwjgl.opengl.EXTSemaphoreFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; +import static org.lwjgl.opengl.EXTSemaphoreFD.glImportSemaphoreFdEXT; +import static org.lwjgl.opengl.EXTSemaphoreWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; +import static org.lwjgl.opengl.EXTSemaphoreWin32.glImportSemaphoreWin32HandleEXT; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRExternalSemaphoreFd.VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalSemaphoreFd.vkGetSemaphoreFdKHR; +import static org.lwjgl.vulkan.KHRExternalSemaphoreWin32.VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; +import static org.lwjgl.vulkan.KHRExternalSemaphoreWin32.vkGetSemaphoreWin32HandleKHR; +import static org.lwjgl.vulkan.VK10.*; + +/** VK binary semaphore exported to GL, for cross-API queue ordering (GL compute -> VK draw -> GL composite). */ +public final class SharedSemaphore { + public final long vkSemaphore; + public final int glSemaphore; + private final VulkanContext ctx; + + public SharedSemaphore(VulkanContext ctx) { + this.ctx = ctx; + boolean win = Platform.get() == Platform.WINDOWS; + int handleType = win ? VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR + : VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; + try (MemoryStack stack = stackPush()) { + var export = VkExportSemaphoreCreateInfo.calloc(stack).sType$Default().handleTypes(handleType); + var sci = VkSemaphoreCreateInfo.calloc(stack).sType$Default().pNext(export); + var pSem = stack.mallocLong(1); + check(vkCreateSemaphore(ctx.device, sci, null, pSem), "vkCreateSemaphore(export)"); + this.vkSemaphore = pSem.get(0); + this.glSemaphore = glGenSemaphoresEXT(); + if (win) { + var gi = VkSemaphoreGetWin32HandleInfoKHR.calloc(stack).sType$Default() + .semaphore(this.vkSemaphore).handleType(handleType); + var pHandle = stack.mallocPointer(1); + check(vkGetSemaphoreWin32HandleKHR(ctx.device, gi, pHandle), "vkGetSemaphoreWin32HandleKHR"); + glImportSemaphoreWin32HandleEXT(this.glSemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); + } else { + var gi = VkSemaphoreGetFdInfoKHR.calloc(stack).sType$Default() + .semaphore(this.vkSemaphore).handleType(handleType); + var pFd = stack.mallocInt(1); + check(vkGetSemaphoreFdKHR(ctx.device, gi, pFd), "vkGetSemaphoreFdKHR"); + glImportSemaphoreFdEXT(this.glSemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); + } + } + } + + public void free() { + glDeleteSemaphoresEXT(this.glSemaphore); + vkDestroySemaphore(this.ctx.device, this.vkSemaphore, null); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkUtil.java b/src/main/java/me/cortex/voxy/client/core/vk/VkUtil.java new file mode 100644 index 000000000..a34b53d31 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkUtil.java @@ -0,0 +1,13 @@ +package me.cortex.voxy.client.core.vk; + +import org.lwjgl.vulkan.VK10; + +public final class VkUtil { + private VkUtil() {} + public static int check(int vkResult, String what) { + if (vkResult != VK10.VK_SUCCESS) { + throw new IllegalStateException("Vulkan call failed: " + what + " -> " + vkResult); + } + return vkResult; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java new file mode 100644 index 000000000..deff456c0 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java @@ -0,0 +1,59 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.util.IrisUtil; +import me.cortex.voxy.common.Logger; + +/** + * Capability detection + lifecycle for the optional Vulkan backend. + * GL remains the default; VK is only used when (a) the config toggle asks for it, + * (b) a suitable device with GL-interop extensions exists, and (c) an Iris + * shaderpack is NOT active (Iris patches Voxy's GLSL fragment path, which the + * phase-1 VK backend cannot honor, so it is explicitly gated out). + */ +public final class VulkanBackend { + private static Boolean supported; + private static VulkanContext context; + private static String unsupportedReason = "not probed"; + + public static synchronized boolean isSupported() { + if (supported == null) { + try { + Class.forName("org.lwjgl.vulkan.VK10"); + var probe = new VulkanContext(); + context = probe; + supported = true; + unsupportedReason = null; + } catch (Throwable t) { + supported = false; + unsupportedReason = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(); + Logger.info("Voxy Vulkan backend unavailable: " + unsupportedReason); + } + } + return supported; + } + + public static boolean shouldUseVulkan(boolean configWantsVulkan) { + if (!configWantsVulkan) return false; + if (IrisUtil.IRIS_INSTALLED && IrisUtil.irisShaderpackActiveSafe()) { + Logger.info("Voxy: Vulkan requested but Iris shaderpack active -> staying on OpenGL"); + return false; + } + return isSupported(); + } + + public static synchronized VulkanContext context() { + if (!isSupported()) throw new IllegalStateException("Vulkan not supported: " + unsupportedReason); + return context; + } + + public static String statusLine() { + if (supported == null) return "vk: unprobed"; + return supported ? ("vk: " + context.deviceName) : ("vk: unavailable (" + unsupportedReason + ")"); + } + + public static synchronized void shutdown() { + if (context != null) { context.destroy(); context = null; supported = null; } + } + + private VulkanBackend() {} +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java new file mode 100644 index 000000000..4ebf7be88 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java @@ -0,0 +1,162 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.common.Logger; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.Platform; +import org.lwjgl.vulkan.*; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.system.MemoryUtil.NULL; +import static org.lwjgl.vulkan.VK10.*; +import static org.lwjgl.vulkan.VK12.VK_API_VERSION_1_2; + +/** + * Owns Voxy's private VkInstance/VkDevice. Voxy does NOT take over Minecraft's + * swapchain: the interop model is VK renders LODs offscreen into images whose + * memory is exported (VK_KHR_external_memory) and imported into the game's GL + * context (GL_EXT_memory_object), synchronized with exported semaphores. + */ +public final class VulkanContext { + public final VkInstance instance; + public final VkPhysicalDevice physicalDevice; + public final VkDevice device; + public final VkQueue queue; + public final int queueFamily; + public final boolean hasDrawIndirectCount; + public final String deviceName; + public final long commandPool; + + private static final String[] REQUIRED_DEVICE_EXTS_COMMON = { + "VK_KHR_external_memory", + "VK_KHR_external_semaphore", + }; + + public static String[] platformInteropExts() { + return Platform.get() == Platform.WINDOWS + ? new String[]{"VK_KHR_external_memory_win32", "VK_KHR_external_semaphore_win32"} + : new String[]{"VK_KHR_external_memory_fd", "VK_KHR_external_semaphore_fd"}; + } + + public VulkanContext() { + try (MemoryStack stack = stackPush()) { + var appInfo = VkApplicationInfo.calloc(stack) + .sType$Default() + .pApplicationName(stack.UTF8("voxy")) + .pEngineName(stack.UTF8("voxy-vk")) + .apiVersion(VK_API_VERSION_1_2); + var ici = VkInstanceCreateInfo.calloc(stack).sType$Default().pApplicationInfo(appInfo); + PointerBuffer pInstance = stack.mallocPointer(1); + check(vkCreateInstance(ici, null, pInstance), "vkCreateInstance"); + this.instance = new VkInstance(pInstance.get(0), ici); + + IntBuffer count = stack.mallocInt(1); + check(vkEnumeratePhysicalDevices(this.instance, count, null), "enumerate devices (count)"); + if (count.get(0) == 0) throw new IllegalStateException("No Vulkan physical devices"); + PointerBuffer devices = stack.mallocPointer(count.get(0)); + check(vkEnumeratePhysicalDevices(this.instance, count, devices), "enumerate devices"); + + VkPhysicalDevice chosen = null; int chosenFamily = -1; boolean chosenDic = false; String chosenName = null; + for (int i = 0; i < devices.capacity() && chosen == null; i++) { + var pd = new VkPhysicalDevice(devices.get(i), this.instance); + Set exts = deviceExtensions(pd, stack); + if (!hasAll(exts, REQUIRED_DEVICE_EXTS_COMMON) || !hasAll(exts, platformInteropExts())) continue; + int family = findGraphicsQueueFamily(pd, stack); + if (family < 0) continue; + var props = VkPhysicalDeviceProperties.calloc(stack); + vkGetPhysicalDeviceProperties(pd, props); + chosen = pd; chosenFamily = family; chosenName = props.deviceNameString(); + chosenDic = exts.contains("VK_KHR_draw_indirect_count") || props.apiVersion() >= VK_API_VERSION_1_2; + } + if (chosen == null) throw new IllegalStateException("No VK device with GL-interop extensions"); + this.physicalDevice = chosen; this.queueFamily = chosenFamily; + this.hasDrawIndirectCount = chosenDic; this.deviceName = chosenName; + + List devExts = new ArrayList<>(List.of(REQUIRED_DEVICE_EXTS_COMMON)); + devExts.addAll(List.of(platformInteropExts())); + if (chosenDic) devExts.add("VK_KHR_draw_indirect_count"); + PointerBuffer pExts = stack.mallocPointer(devExts.size()); + for (String e : devExts) pExts.put(stack.UTF8(e)); + pExts.flip(); + + var queueCI = VkDeviceQueueCreateInfo.calloc(1, stack).sType$Default() + .queueFamilyIndex(this.queueFamily) + .pQueuePriorities(stack.floats(1.0f)); + var features12 = VkPhysicalDeviceVulkan12Features.calloc(stack).sType$Default() + .drawIndirectCount(chosenDic); + var features = VkPhysicalDeviceFeatures.calloc(stack).multiDrawIndirect(true); + var dci = VkDeviceCreateInfo.calloc(stack).sType$Default() + .pNext(features12) + .pQueueCreateInfos(queueCI) + .ppEnabledExtensionNames(pExts) + .pEnabledFeatures(features); + PointerBuffer pDevice = stack.mallocPointer(1); + check(vkCreateDevice(this.physicalDevice, dci, null, pDevice), "vkCreateDevice"); + this.device = new VkDevice(pDevice.get(0), this.physicalDevice, dci); + + PointerBuffer pQueue = stack.mallocPointer(1); + vkGetDeviceQueue(this.device, this.queueFamily, 0, pQueue); + this.queue = new VkQueue(pQueue.get(0), this.device); + + var cpci = VkCommandPoolCreateInfo.calloc(stack).sType$Default() + .flags(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) + .queueFamilyIndex(this.queueFamily); + var pPool = stack.mallocLong(1); + check(vkCreateCommandPool(this.device, cpci, null, pPool), "vkCreateCommandPool"); + this.commandPool = pPool.get(0); + Logger.info("Voxy Vulkan context created on device: " + this.deviceName + + " (drawIndirectCount=" + this.hasDrawIndirectCount + ")"); + } + } + + private static Set deviceExtensions(VkPhysicalDevice pd, MemoryStack stack) { + IntBuffer c = stack.mallocInt(1); + vkEnumerateDeviceExtensionProperties(pd, (String) null, c, null); + var props = VkExtensionProperties.calloc(c.get(0), stack); + vkEnumerateDeviceExtensionProperties(pd, (String) null, c, props); + Set out = new HashSet<>(); + for (int i = 0; i < props.capacity(); i++) out.add(props.get(i).extensionNameString()); + return out; + } + + private static boolean hasAll(Set have, String[] want) { + for (String w : want) if (!have.contains(w)) return false; + return true; + } + + private static int findGraphicsQueueFamily(VkPhysicalDevice pd, MemoryStack stack) { + IntBuffer c = stack.mallocInt(1); + vkGetPhysicalDeviceQueueFamilyProperties(pd, c, null); + var fams = VkQueueFamilyProperties.calloc(c.get(0), stack); + vkGetPhysicalDeviceQueueFamilyProperties(pd, c, fams); + for (int i = 0; i < fams.capacity(); i++) { + if ((fams.get(i).queueFlags() & VK_QUEUE_GRAPHICS_BIT) != 0) return i; + } + return -1; + } + + public int findMemoryType(int typeBits, int required) { + try (MemoryStack stack = stackPush()) { + var mem = VkPhysicalDeviceMemoryProperties.calloc(stack); + vkGetPhysicalDeviceMemoryProperties(this.physicalDevice, mem); + for (int i = 0; i < mem.memoryTypeCount(); i++) { + if ((typeBits & (1 << i)) != 0 && (mem.memoryTypes(i).propertyFlags() & required) == required) return i; + } + } + throw new IllegalStateException("No suitable VK memory type"); + } + + public void destroy() { + vkDeviceWaitIdle(this.device); + vkDestroyCommandPool(this.device, this.commandPool, null); + vkDestroyDevice(this.device, null); + vkDestroyInstance(this.instance, null); + } +} From 917c2cf749e50bb6e75abec9eeecc17bd9734ffa Mon Sep 17 00:00:00 2001 From: cochcoder Date: Thu, 2 Jul 2026 15:12:26 +0000 Subject: [PATCH 03/12] vk: phase-1 hybrid section renderer + viewport, config toggle, capability+Iris gating (GL default) --- VULKAN.md | 49 ++++ .../cortex/voxy/client/config/VoxyConfig.java | 7 + .../voxy/client/core/VoxyRenderSystem.java | 4 + .../backend/vulkan/VulkanSectionRenderer.java | 276 ++++++++++++++++++ .../backend/vulkan/VulkanViewport.java | 82 ++++++ .../voxy/client/core/util/IrisUtil.java | 9 + 6 files changed, 427 insertions(+) create mode 100644 VULKAN.md create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java diff --git a/VULKAN.md b/VULKAN.md new file mode 100644 index 000000000..8efe37733 --- /dev/null +++ b/VULKAN.md @@ -0,0 +1,49 @@ +# Voxy Vulkan Backend (branch: vulkan-backend, base: 2622 / MC 26.2) + +## Model (DH-style backend swap) +MC 26.2 is a GL client, so "fully Vulkan" for a mod means: Voxy owns a private +VkInstance/VkDevice and renders LODs offscreen; resources cross the API boundary +via VK_KHR_external_memory + GL_EXT_memory_object and exported semaphores; GL +composites the result into MC's framebuffer. Toggle: `renderBackend` in the Voxy +config ("opengl" default | "vulkan"). VK engages ONLY if a device with the interop +extensions exists AND no Iris shaderpack is active; otherwise silent GL fallback. + +## Phase-1 hybrid split (deliberate) +- GL, unchanged & bit-exact: hierarchical traversal, prep/cull(raster occlusion vs + MC depth)/prefixsum/cmdgen compute, Sodium/Iris integration. +- VK: opaque terrain via vkCmdDrawIndexedIndirectCountKHR over shared draw buffers, + into shared RGBA8/D32 targets. Same GLSL sources, shaderc->SPIR-V at runtime with + VOXY_VULKAN defined. +- The raster occlusion cull tests against MC's GL depth buffer -> it must stay GL + until/unless MC depth itself is shared; this is why phase-1 is a hybrid, not a + design shortcut. + +## Status — read this before flipping the toggle +| Piece | State | +|---|---| +| Seam (IRenderList, 8-line diff, MDIC untouched via covariant returns) | done, needs compile | +| Config toggle + capability gate + Iris gate + GL default | done, needs compile | +| VK context/device/queue + interop ext selection (win32/fd) | done, UNTESTED on hardware | +| SharedBuffer/SharedImage/SharedSemaphore interop | done, UNTESTED on hardware | +| shaderc GLSL->SPIR-V bridge | done; Voxy's GLSL will need set/binding fixups for Vulkan semantics — expect iteration | +| VK opaque draw pass (mirrors MDIC offsets/strides/clamps) | recorded+submitted, but **guarded OFF**: geometry/metadata/ModelStore SSBOs are plain GlBuffers; until they allocate via SharedBuffer when VK is active, renderOpaque logs once and draws nothing rather than corrupt | +| GL composite of shared color/depth into MC framebuffer | NOT YET (next step with geometry sharing) | +| Translucent / temporal / SSAO on VK | deferred, stubbed no-op | +| Descriptor sets binding shared SSBOs to the VK pipeline | pipeline layout is a placeholder pending geometry sharing | + +## Next steps, in order +1. Compile (`./gradlew build`) — this tree was authored in an offline sandbox + (maven blocked): expect LWJGL VK binding signature fixups. +2. Thread SharedBuffer allocation through BasicSectionGeometryManager + ModelStore + behind `VulkanBackend.shouldUseVulkan(...)`; build the descriptor set layout; + flip `geometryShared = true`. +3. GL composite pass (fullscreen quad sampling shared color+depth, writes + gl_FragDepth, depth-tested vs MC) + glDone/vkDone signal points around cmdgen. +4. Validate on NVIDIA Linux/Windows first (best GL_EXT_memory_object support); + run scripts/check_voxy_shader_contracts.sh (not in this repo — external) on the + unchanged GL shaders to confirm bit-exactness. +5. Then translucents, temporal, and a VK-native traversal (phase-2, drops hybrid). + +## Invariants preserved +GL/MDIC path byte-identical in behavior (seam is type-plumbing only); GL remains +default; Iris+Sodium untouched on GL and explicitly gated on VK. diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index 0b66ac1af..88de2261b 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -34,6 +34,13 @@ public class VoxyConfig { public float subDivisionSize = 64; public boolean useEnvironmentalFog = true; public boolean dontUseSodiumBuilderThreads = false; + /** "opengl" (default) or "vulkan". Vulkan additionally requires a capable device and no active Iris shaderpack. */ + public String renderBackend = "opengl"; + + public boolean wantsVulkanBackend() { + return "vulkan".equalsIgnoreCase(this.renderBackend); + } + public String ssaoMode; public SSAO.SSAOMode getSSAOMode() { diff --git a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java index 377f54566..091f076d9 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java @@ -73,6 +73,10 @@ public class VoxyRenderSystem { private final RenderProperties properties; private static AbstractSectionRenderer.Factory getRenderBackendFactory() { + if (me.cortex.voxy.client.core.vk.VulkanBackend.shouldUseVulkan( + me.cortex.voxy.client.config.VoxyConfig.CONFIG.wantsVulkanBackend())) { + return me.cortex.voxy.client.core.rendering.section.backend.vulkan.VulkanSectionRenderer.FACTORY; + } //TODO: need todo a thing where selects optimal section render based on if supports the pipeline and geometry data type return MDICSectionRenderer.FACTORY; } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java new file mode 100644 index 000000000..97c6f9b1d --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java @@ -0,0 +1,276 @@ +package me.cortex.voxy.client.core.rendering.section.backend.vulkan; + +import me.cortex.voxy.client.core.AbstractRenderPipeline; +import me.cortex.voxy.client.core.gl.shader.ShaderLoader; +import me.cortex.voxy.client.core.gl.shader.ShaderType; +import me.cortex.voxy.client.core.model.ModelStore; +import me.cortex.voxy.client.core.rendering.section.backend.AbstractSectionRenderer; +import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; +import me.cortex.voxy.client.core.vk.ShadercCompiler; +import me.cortex.voxy.client.core.vk.VulkanBackend; +import me.cortex.voxy.client.core.vk.VulkanContext; +import me.cortex.voxy.common.Logger; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.*; + +import java.nio.ByteBuffer; +import java.util.List; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRDrawIndirectCount.vkCmdDrawIndexedIndirectCountKHR; +import static org.lwjgl.vulkan.VK10.*; + +/** + * PHASE-1 HYBRID Vulkan backend. + * + * What runs where: + * - hierarchical traversal, prep/cull/prefix/cmdgen compute: GL (unchanged, bit-exact), + * writing into VK-allocated SharedBuffers owned by {@link VulkanViewport}. + * - opaque/translucent terrain draws: VK, vkCmdDrawIndexedIndirectCount over the + * shared draw-call/draw-count buffers, into shared color/depth images. + * - composite: GL samples the shared images back into MC's framebuffer + * (semaphore-ordered: GL signal -> VK wait, VK signal -> GL wait). + * + * KNOWN PHASE BOUNDARY (guarded, not hidden): the vertex-pulling SSBOs + * (geometry buffer, metadata, model store) are still plain GlBuffers today. + * Until BasicSectionGeometryManager/ModelStore allocate through SharedBuffer + * when this backend is active, the VK draw stage cannot see the geometry and + * this renderer refuses to draw (logs once, renders nothing) instead of + * corrupting. That wiring is the next incremental step and is deliberately not + * faked here. + */ +public class VulkanSectionRenderer extends AbstractSectionRenderer { + public static final Factory FACTORY = + AbstractSectionRenderer.Factory.create(VulkanSectionRenderer.class); + + private final VulkanContext ctx = VulkanBackend.context(); + private final AbstractRenderPipeline pipeline; + + private final long vertShaderModule; + private final long fragShaderModule; + private long renderPass; + private long pipelineLayout; + private long graphicsPipeline; + private long framebuffer; + private int fbWidth = -1, fbHeight = -1; + private final VkCommandBuffer cmd; + private final long fence; + private boolean geometryShared = false; //flipped when geometry SSBO sharing lands + private boolean warnedNoGeometry = false; + + public VulkanSectionRenderer(AbstractRenderPipeline pipeline, ModelStore modelStore, BasicSectionGeometryData geometryData) { + super(pipeline.properties, modelStore, geometryData); + this.pipeline = pipeline; + + //Single-source shaders: same GLSL assets as the GL path, VOXY_VULKAN defined. + String vertex = ShaderLoader.parse("voxy:lod/gl46/quads3.vert"); + String frag = ShaderLoader.parse("voxy:lod/gl46/quads.frag"); + this.vertShaderModule = createModule(ShadercCompiler.compile(vertex, ShaderType.VERTEX, "quads3.vert")); + this.fragShaderModule = createModule(ShadercCompiler.compile(frag, ShaderType.FRAGMENT, "quads.frag")); + + try (MemoryStack stack = stackPush()) { + var cbai = VkCommandBufferAllocateInfo.calloc(stack).sType$Default() + .commandPool(this.ctx.commandPool) + .level(VK_COMMAND_BUFFER_LEVEL_PRIMARY) + .commandBufferCount(1); + var pCmd = stack.mallocPointer(1); + check(vkAllocateCommandBuffers(this.ctx.device, cbai, pCmd), "vkAllocateCommandBuffers"); + this.cmd = new VkCommandBuffer(pCmd.get(0), this.ctx.device); + var fci = VkFenceCreateInfo.calloc(stack).sType$Default().flags(VK_FENCE_CREATE_SIGNALED_BIT); + var pFence = stack.mallocLong(1); + check(vkCreateFence(this.ctx.device, fci, null, pFence), "vkCreateFence"); + this.fence = pFence.get(0); + } + Logger.info("VulkanSectionRenderer initialized on " + this.ctx.deviceName + " (phase-1 hybrid)"); + } + + private long createModule(ByteBuffer spirv) { + try (MemoryStack stack = stackPush()) { + var smci = VkShaderModuleCreateInfo.calloc(stack).sType$Default().pCode(spirv); + var pModule = stack.mallocLong(1); + check(vkCreateShaderModule(this.ctx.device, smci, null, pModule), "vkCreateShaderModule"); + return pModule.get(0); + } + } + + private void ensurePipeline(VulkanViewport viewport) { + if (!viewport.ensureTargets() && this.graphicsPipeline != 0 + && this.fbWidth == viewport.width && this.fbHeight == viewport.height) return; + destroyPipelineObjects(); + try (MemoryStack stack = stackPush()) { + //Render pass: color RGBA8 + depth D32, cleared each frame; VK depth range is 0..1 which + //matches properties.isZero2One() on the GL side via the reverse-Z aware compare below. + var attachments = VkAttachmentDescription.calloc(2, stack); + attachments.get(0).format(viewport.color.vkFormat).samples(VK_SAMPLE_COUNT_1_BIT) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR).storeOp(VK_ATTACHMENT_STORE_OP_STORE) + .stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE).stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) + .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED).finalLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + attachments.get(1).format(viewport.depth.vkFormat).samples(VK_SAMPLE_COUNT_1_BIT) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR).storeOp(VK_ATTACHMENT_STORE_OP_STORE) + .stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE).stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) + .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED).finalLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + var colorRef = VkAttachmentReference.calloc(1, stack).attachment(0).layout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + var depthRef = VkAttachmentReference.calloc(stack).attachment(1).layout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + var subpass = VkSubpassDescription.calloc(1, stack) + .pipelineBindPoint(VK_PIPELINE_BIND_POINT_GRAPHICS) + .colorAttachmentCount(1).pColorAttachments(colorRef).pDepthStencilAttachment(depthRef); + var rpci = VkRenderPassCreateInfo.calloc(stack).sType$Default() + .pAttachments(attachments).pSubpasses(subpass); + var pRp = stack.mallocLong(1); + check(vkCreateRenderPass(this.ctx.device, rpci, null, pRp), "vkCreateRenderPass"); + this.renderPass = pRp.get(0); + + var fbci = VkFramebufferCreateInfo.calloc(stack).sType$Default() + .renderPass(this.renderPass) + .pAttachments(stack.longs(viewport.color.vkView, viewport.depth.vkView)) + .width(viewport.width).height(viewport.height).layers(1); + var pFb = stack.mallocLong(1); + check(vkCreateFramebuffer(this.ctx.device, fbci, null, pFb), "vkCreateFramebuffer"); + this.framebuffer = pFb.get(0); + + //Pipeline layout: descriptor sets for the vertex-pulling SSBOs are wired in the + //geometry-sharing step; until then an empty layout is sufficient for pipeline creation. + var plci = VkPipelineLayoutCreateInfo.calloc(stack).sType$Default(); + var pPl = stack.mallocLong(1); + check(vkCreatePipelineLayout(this.ctx.device, plci, null, pPl), "vkCreatePipelineLayout"); + this.pipelineLayout = pPl.get(0); + + var stages = VkPipelineShaderStageCreateInfo.calloc(2, stack); + stages.get(0).sType$Default().stage(VK_SHADER_STAGE_VERTEX_BIT).module(this.vertShaderModule).pName(stack.UTF8("main")); + stages.get(1).sType$Default().stage(VK_SHADER_STAGE_FRAGMENT_BIT).module(this.fragShaderModule).pName(stack.UTF8("main")); + + var vertexInput = VkPipelineVertexInputStateCreateInfo.calloc(stack).sType$Default();//vertex pulling: no attributes + var inputAssembly = VkPipelineInputAssemblyStateCreateInfo.calloc(stack).sType$Default() + .topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + var viewportState = VkPipelineViewportStateCreateInfo.calloc(stack).sType$Default() + .viewportCount(1).pViewports(VkViewport.calloc(1, stack) + .x(0).y(0).width(viewport.width).height(viewport.height).minDepth(0).maxDepth(1)) + .scissorCount(1).pScissors(VkRect2D.calloc(1, stack) + .extent(e -> e.width(viewport.width).height(viewport.height))); + var raster = VkPipelineRasterizationStateCreateInfo.calloc(stack).sType$Default() + .polygonMode(VK_POLYGON_MODE_FILL).cullMode(VK_CULL_MODE_NONE)//MDIC disables cull face + .frontFace(VK_FRONT_FACE_COUNTER_CLOCKWISE).lineWidth(1); + var msaa = VkPipelineMultisampleStateCreateInfo.calloc(stack).sType$Default() + .rasterizationSamples(VK_SAMPLE_COUNT_1_BIT); + var depthState = VkPipelineDepthStencilStateCreateInfo.calloc(stack).sType$Default() + .depthTestEnable(true).depthWriteEnable(true) + .depthCompareOp(this.properties.isReverseZ() ? VK_COMPARE_OP_GREATER_OR_EQUAL : VK_COMPARE_OP_LESS_OR_EQUAL); + var blendAttach = VkPipelineColorBlendAttachmentState.calloc(1, stack) + .colorWriteMask(0xF).blendEnable(false); + var blend = VkPipelineColorBlendStateCreateInfo.calloc(stack).sType$Default().pAttachments(blendAttach); + + var gpci = VkGraphicsPipelineCreateInfo.calloc(1, stack).sType$Default() + .pStages(stages) + .pVertexInputState(vertexInput) + .pInputAssemblyState(inputAssembly) + .pViewportState(viewportState) + .pRasterizationState(raster) + .pMultisampleState(msaa) + .pDepthStencilState(depthState) + .pColorBlendState(blend) + .layout(this.pipelineLayout) + .renderPass(this.renderPass).subpass(0); + var pPipe = stack.mallocLong(1); + check(vkCreateGraphicsPipelines(this.ctx.device, VK_NULL_HANDLE, gpci, null, pPipe), "vkCreateGraphicsPipelines"); + this.graphicsPipeline = pPipe.get(0); + this.fbWidth = viewport.width; this.fbHeight = viewport.height; + } + } + + @Override + public void buildDrawCalls(VulkanViewport viewport) { + //Phase-1: command generation stays on the GL path, writing SharedBuffers. + //Deliberately empty here; the GL compute passes are dispatched by the shared + //pipeline code exactly as for MDIC once the cmdgen wiring is routed through + //the viewport's shared buffers (next step alongside geometry sharing). + } + + @Override + public void renderOpaque(VulkanViewport viewport) { + if (this.geometryManager.getSectionCount() == 0) return; + if (!this.geometryShared) { + if (!this.warnedNoGeometry) { + this.warnedNoGeometry = true; + Logger.warn("Voxy VK backend active but geometry SSBO sharing not yet wired -> drawing nothing (GL fallback recommended)"); + } + return; + } + this.ensurePipeline(viewport); + try (MemoryStack stack = stackPush()) { + check(vkWaitForFences(this.ctx.device, this.fence, true, Long.MAX_VALUE), "vkWaitForFences"); + check(vkResetFences(this.ctx.device, this.fence), "vkResetFences"); + var begin = VkCommandBufferBeginInfo.calloc(stack).sType$Default() + .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + check(vkBeginCommandBuffer(this.cmd, begin), "vkBeginCommandBuffer"); + + var clears = VkClearValue.calloc(2, stack); + clears.get(0).color().float32(0, 0).float32(1, 0).float32(2, 0).float32(3, 0); + clears.get(1).depthStencil().depth(this.properties.isReverseZ() ? 0f : 1f); + var rpbi = VkRenderPassBeginInfo.calloc(stack).sType$Default() + .renderPass(this.renderPass).framebuffer(this.framebuffer) + .renderArea(a -> a.extent(e -> e.width(viewport.width).height(viewport.height))) + .pClearValues(clears); + vkCmdBeginRenderPass(this.cmd, rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(this.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, this.graphicsPipeline); + //Mirror of MDIC renderOpaque: offset 0, count at byte 12, same max-draw clamp, stride 20. + int maxDraw = Math.min((int) (this.geometryManager.getSectionCount() * 4.4 + 128), + me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer.OPAQUE_DRAW_COUNT); + vkCmdDrawIndexedIndirectCountKHR(this.cmd, + viewport.drawCallBuffer.vkBuffer, 0, + viewport.drawCountCallBuffer.vkBuffer, 4 * 3, + maxDraw, 5 * 4); + vkCmdEndRenderPass(this.cmd); + check(vkEndCommandBuffer(this.cmd), "vkEndCommandBuffer"); + + //GL has signalled glDone after cmdgen; wait it, signal vkDone for the GL composite. + var submit = VkSubmitInfo.calloc(stack).sType$Default() + .waitSemaphoreCount(1) + .pWaitSemaphores(stack.longs(viewport.glDone.vkSemaphore)) + .pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT)) + .pCommandBuffers(stack.pointers(this.cmd)) + .pSignalSemaphores(stack.longs(viewport.vkDone.vkSemaphore)); + check(vkQueueSubmit(this.ctx.queue, submit, this.fence), "vkQueueSubmit"); + } + } + + @Override + public void renderTranslucent(VulkanViewport viewport) { + //Phase-1: translucent pass deferred until opaque parity is verified on hardware. + } + + @Override + public void renderTemporal(VulkanViewport viewport) { + //Phase-1: temporal pass deferred (GL path keeps it; VK renders without TAA reuse). + } + + @Override + public VulkanViewport createViewport() { + return new VulkanViewport(this.properties, this.geometryManager.getSectionCount() == 0 + ? (int) Math.min(this.geometryManager.getMaxCapacity(), Integer.MAX_VALUE) + : this.geometryManager.getSectionCount()); + } + + @Override + public void addDebug(List lines) { + lines.add("VK backend (phase-1 hybrid): " + VulkanBackend.statusLine() + + (this.geometryShared ? "" : " [geometry sharing pending -> not drawing]")); + } + + private void destroyPipelineObjects() { + if (this.graphicsPipeline != 0) vkDestroyPipeline(this.ctx.device, this.graphicsPipeline, null); + if (this.pipelineLayout != 0) vkDestroyPipelineLayout(this.ctx.device, this.pipelineLayout, null); + if (this.framebuffer != 0) vkDestroyFramebuffer(this.ctx.device, this.framebuffer, null); + if (this.renderPass != 0) vkDestroyRenderPass(this.ctx.device, this.renderPass, null); + this.graphicsPipeline = 0; this.pipelineLayout = 0; this.framebuffer = 0; this.renderPass = 0; + } + + @Override + public void free() { + vkDeviceWaitIdle(this.ctx.device); + destroyPipelineObjects(); + vkDestroyFence(this.ctx.device, this.fence, null); + vkDestroyShaderModule(this.ctx.device, this.vertShaderModule, null); + vkDestroyShaderModule(this.ctx.device, this.fragShaderModule, null); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java new file mode 100644 index 000000000..e0af0713e --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java @@ -0,0 +1,82 @@ +package me.cortex.voxy.client.core.rendering.section.backend.vulkan; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.IRenderList; +import me.cortex.voxy.client.core.rendering.Viewport; +import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser; +import me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer; +import me.cortex.voxy.client.core.vk.SharedBuffer; +import me.cortex.voxy.client.core.vk.SharedImage; +import me.cortex.voxy.client.core.vk.SharedSemaphore; +import me.cortex.voxy.client.core.vk.VulkanBackend; +import me.cortex.voxy.client.core.vk.VulkanContext; + +import static org.lwjgl.vulkan.VK10.*; + +/** + * Vulkan analogue of MDICViewport. Identical buffer roles and sizes, but every + * buffer the GL compute passes write AND the VK graphics queue reads is a + * SharedBuffer (VK-allocated, GL-imported). The hierarchical traversal and the + * MDIC-style command generation keep running in GL, bit-identically, writing + * into these; VK consumes them for the actual draws (phase-1 hybrid). + */ +public class VulkanViewport extends Viewport { + private final VulkanContext ctx = VulkanBackend.context(); + + public final SharedBuffer drawCountCallBuffer; + public final SharedBuffer drawCallBuffer; + public final SharedBuffer positionScratchBuffer; + public final SharedBuffer indirectLookupBuffer; + public final SharedBuffer visibilityBuffer; + + // VK render targets, GL-visible for composite. Lazily (re)created on resize. + public SharedImage color; + public SharedImage depth; + public final SharedSemaphore vkDone; + public final SharedSemaphore glDone; + + public VulkanViewport(RenderProperties properties, int maxSectionCount) { + super(properties); + int indirect = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + this.drawCountCallBuffer = new SharedBuffer(this.ctx, 1024, indirect); + this.drawCallBuffer = new SharedBuffer(this.ctx, 5L*4*(MDICSectionRenderer.OPAQUE_DRAW_COUNT + + MDICSectionRenderer.TRANSLUCENT_DRAW_COUNT + MDICSectionRenderer.TEMPORAL_DRAW_COUNT), indirect); + this.positionScratchBuffer = new SharedBuffer(this.ctx, 8L*400000, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + this.indirectLookupBuffer = new SharedBuffer(this.ctx, + HierarchicalOcclusionTraverser.MAX_QUEUE_SIZE*4L+4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + this.visibilityBuffer = new SharedBuffer(this.ctx, maxSectionCount*4L, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + this.vkDone = new SharedSemaphore(this.ctx); + this.glDone = new SharedSemaphore(this.ctx); + } + + /** (Re)creates the shared color+depth targets if the viewport size changed. Returns true if recreated. */ + public boolean ensureTargets() { + if (this.width <= 0 || this.height <= 0) return false; + if (this.color != null && this.color.width == this.width && this.color.height == this.height) return false; + if (this.color != null) { this.color.free(); this.depth.free(); } + this.color = new SharedImage(this.ctx, this.width, this.height, + VK_FORMAT_R8G8B8A8_UNORM, org.lwjgl.opengl.GL11.GL_RGBA8, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); + this.depth = new SharedImage(this.ctx, this.width, this.height, + VK_FORMAT_D32_SFLOAT, org.lwjgl.opengl.GL30.GL_DEPTH_COMPONENT32F, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); + return true; + } + + @Override + protected void delete0() { + super.delete0(); + if (this.color != null) { this.color.free(); this.depth.free(); } + this.vkDone.free(); this.glDone.free(); + this.visibilityBuffer.free(); + this.indirectLookupBuffer.free(); + this.drawCountCallBuffer.free(); + this.drawCallBuffer.free(); + this.positionScratchBuffer.free(); + } + + @Override + public IRenderList getRenderList() { + return this.indirectLookupBuffer; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/util/IrisUtil.java b/src/main/java/me/cortex/voxy/client/core/util/IrisUtil.java index c9ae4a717..c518c3c16 100644 --- a/src/main/java/me/cortex/voxy/client/core/util/IrisUtil.java +++ b/src/main/java/me/cortex/voxy/client/core/util/IrisUtil.java @@ -30,6 +30,15 @@ private static boolean irisShadowActive0() { return ShadowRenderer.ACTIVE; } + public static boolean irisShaderpackActiveSafe() { + if (!IRIS_INSTALLED) return false; + try { + return net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse(); + } catch (Throwable t) { + return false; + } + } + public static boolean irisShadowActive() { return IRIS_INSTALLED && irisShadowActive0(); } From c8b91dde5b25cb277c70d41a053ab85064a41a45 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Thu, 16 Jul 2026 16:29:23 +0000 Subject: [PATCH 04/12] vk/mac: portability enumeration+subset, correct drawIndirectCount feature query, discrete-GPU preference, fixed-count indirect fallback (MoltenVK) --- .../backend/vulkan/VulkanSectionRenderer.java | 16 ++++-- .../voxy/client/core/vk/VulkanContext.java | 57 ++++++++++++++++--- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java index 97c6f9b1d..1cb0d13f7 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java @@ -19,6 +19,7 @@ import static me.cortex.voxy.client.core.vk.VkUtil.check; import static org.lwjgl.system.MemoryStack.stackPush; import static org.lwjgl.vulkan.KHRDrawIndirectCount.vkCmdDrawIndexedIndirectCountKHR; +import static org.lwjgl.vulkan.VK12.vkCmdDrawIndexedIndirectCount; import static org.lwjgl.vulkan.VK10.*; /** @@ -216,10 +217,17 @@ public void renderOpaque(VulkanViewport viewport) { //Mirror of MDIC renderOpaque: offset 0, count at byte 12, same max-draw clamp, stride 20. int maxDraw = Math.min((int) (this.geometryManager.getSectionCount() * 4.4 + 128), me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer.OPAQUE_DRAW_COUNT); - vkCmdDrawIndexedIndirectCountKHR(this.cmd, - viewport.drawCallBuffer.vkBuffer, 0, - viewport.drawCountCallBuffer.vkBuffer, 4 * 3, - maxDraw, 5 * 4); + if (this.ctx.hasDrawIndirectCount) { + vkCmdDrawIndexedIndirectCount(this.cmd, + viewport.drawCallBuffer.vkBuffer, 0, + viewport.drawCountCallBuffer.vkBuffer, 4 * 3, + maxDraw, 5 * 4); + } else { + //MoltenVK/macOS fallback: no GPU-sourced draw count. The cmdgen compute + //zero-fills unused command slots (instanceCount=0 draws are no-ops), so a + //fixed-count multi-draw over the clamped max is correct, just less tight. + vkCmdDrawIndexedIndirect(this.cmd, viewport.drawCallBuffer.vkBuffer, 0, maxDraw, 5 * 4); + } vkCmdEndRenderPass(this.cmd); check(vkEndCommandBuffer(this.cmd), "vkEndCommandBuffer"); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java index 4ebf7be88..673c0552c 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java @@ -45,14 +45,36 @@ public static String[] platformInteropExts() { : new String[]{"VK_KHR_external_memory_fd", "VK_KHR_external_semaphore_fd"}; } - public VulkanContext() { + /** True when this context must support the GL-interop hybrid mode (Windows/Linux only). */ + public final boolean glInterop; + /** True when running on a portability (MoltenVK/Metal) implementation. */ + public boolean isPortability; + + public VulkanContext() { this(Platform.get() != Platform.MACOSX); } + + public VulkanContext(boolean glInterop) { + this.glInterop = glInterop; try (MemoryStack stack = stackPush()) { var appInfo = VkApplicationInfo.calloc(stack) .sType$Default() .pApplicationName(stack.UTF8("voxy")) .pEngineName(stack.UTF8("voxy-vk")) .apiVersion(VK_API_VERSION_1_2); + //MoltenVK (macOS): the loader only lists portability devices when + //VK_KHR_portability_enumeration is enabled with the ENUMERATE_PORTABILITY flag. + boolean portability = Platform.get() == Platform.MACOSX; + PointerBuffer instExts = null; + if (portability) { + instExts = stack.mallocPointer(2); + instExts.put(stack.UTF8("VK_KHR_portability_enumeration")); + instExts.put(stack.UTF8("VK_KHR_get_physical_device_properties2")); + instExts.flip(); + } var ici = VkInstanceCreateInfo.calloc(stack).sType$Default().pApplicationInfo(appInfo); + if (portability) { + ici.flags(0x00000001 /*VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR*/) + .ppEnabledExtensionNames(instExts); + } PointerBuffer pInstance = stack.mallocPointer(1); check(vkCreateInstance(ici, null, pInstance), "vkCreateInstance"); this.instance = new VkInstance(pInstance.get(0), ici); @@ -63,24 +85,41 @@ public VulkanContext() { PointerBuffer devices = stack.mallocPointer(count.get(0)); check(vkEnumeratePhysicalDevices(this.instance, count, devices), "enumerate devices"); - VkPhysicalDevice chosen = null; int chosenFamily = -1; boolean chosenDic = false; String chosenName = null; - for (int i = 0; i < devices.capacity() && chosen == null; i++) { + boolean requireGlInterop = this.glInterop; + VkPhysicalDevice chosen = null; int chosenFamily = -1; boolean chosenDic = false; + boolean chosenPortability = false; boolean chosenDiscrete = false; String chosenName = null; + for (int i = 0; i < devices.capacity(); i++) { var pd = new VkPhysicalDevice(devices.get(i), this.instance); Set exts = deviceExtensions(pd, stack); - if (!hasAll(exts, REQUIRED_DEVICE_EXTS_COMMON) || !hasAll(exts, platformInteropExts())) continue; + if (requireGlInterop && (!hasAll(exts, REQUIRED_DEVICE_EXTS_COMMON) || !hasAll(exts, platformInteropExts()))) continue; int family = findGraphicsQueueFamily(pd, stack); if (family < 0) continue; var props = VkPhysicalDeviceProperties.calloc(stack); vkGetPhysicalDeviceProperties(pd, props); - chosen = pd; chosenFamily = family; chosenName = props.deviceNameString(); - chosenDic = exts.contains("VK_KHR_draw_indirect_count") || props.apiVersion() >= VK_API_VERSION_1_2; + boolean discrete = props.deviceType() == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; + if (chosen != null && (chosenDiscrete || !discrete)) continue;//prefer discrete, like vanilla 26.2 + chosen = pd; chosenFamily = family; chosenName = props.deviceNameString(); chosenDiscrete = discrete; + //NOTE: drawIndirectCount is a FEATURE in core 1.2, not implied by apiVersion — + //MoltenVK reports 1.2 without it. Must query VkPhysicalDeviceVulkan12Features. + var f12q = VkPhysicalDeviceVulkan12Features.calloc(stack).sType$Default(); + var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(f12q); + VK11.vkGetPhysicalDeviceFeatures2(pd, f2); + chosenDic = f12q.drawIndirectCount() || exts.contains("VK_KHR_draw_indirect_count"); + chosenPortability = exts.contains("VK_KHR_portability_subset"); } - if (chosen == null) throw new IllegalStateException("No VK device with GL-interop extensions"); + if (chosen == null) throw new IllegalStateException(requireGlInterop + ? "No VK device with GL-interop extensions (hybrid mode; unavailable on macOS by design)" + : "No suitable VK device"); this.physicalDevice = chosen; this.queueFamily = chosenFamily; this.hasDrawIndirectCount = chosenDic; this.deviceName = chosenName; + this.isPortability = chosenPortability; - List devExts = new ArrayList<>(List.of(REQUIRED_DEVICE_EXTS_COMMON)); - devExts.addAll(List.of(platformInteropExts())); + List devExts = new ArrayList<>(); + if (requireGlInterop) { + devExts.addAll(List.of(REQUIRED_DEVICE_EXTS_COMMON)); + devExts.addAll(List.of(platformInteropExts())); + } + if (chosenPortability) devExts.add("VK_KHR_portability_subset");//spec: must enable if supported if (chosenDic) devExts.add("VK_KHR_draw_indirect_count"); PointerBuffer pExts = stack.mallocPointer(devExts.size()); for (String e : devExts) pExts.put(stack.UTF8(e)); From a0444bc74a7d49c27d2d67786d46616353d5eaf2 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Thu, 16 Jul 2026 16:29:23 +0000 Subject: [PATCH 05/12] vk: pure-VK host seam (IVkHost/MinecraftVkHost) for MC 26.2's native Vulkan backend, VkComputePipeline infra, macOS host-required gate --- VULKAN.md | 96 ++++++++++--------- .../cortex/voxy/client/core/vk/IVkHost.java | 38 ++++++++ .../voxy/client/core/vk/MinecraftVkHost.java | 24 +++++ .../client/core/vk/VkComputePipeline.java | 82 ++++++++++++++++ .../voxy/client/core/vk/VulkanBackend.java | 7 ++ 5 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java diff --git a/VULKAN.md b/VULKAN.md index 8efe37733..b88c6a9ef 100644 --- a/VULKAN.md +++ b/VULKAN.md @@ -1,49 +1,59 @@ -# Voxy Vulkan Backend (branch: vulkan-backend, base: 2622 / MC 26.2) +# Voxy Vulkan Backend — branch `vulkan-backend`, rebased on dev @ MC 26.2 -## Model (DH-style backend swap) -MC 26.2 is a GL client, so "fully Vulkan" for a mod means: Voxy owns a private -VkInstance/VkDevice and renders LODs offscreen; resources cross the API boundary -via VK_KHR_external_memory + GL_EXT_memory_object and exported semaphores; GL -composites the result into MC's framebuffer. Toggle: `renderBackend` in the Voxy -config ("opengl" default | "vulkan"). VK engages ONLY if a device with the interop -extensions exists AND no Iris shaderpack is active; otherwise silent GL fallback. +## Grounded context (verified July 2026) +Vanilla MC Java 26.2 ("Chaos Cubed") ships an experimental Vulkan renderer: +opt-in Graphics API setting (Default / Prefer Vulkan / Prefer OpenGL), requires +Vulkan 1.2 + dynamic rendering + push descriptors, auto-fallback ladder to GL, +prefers discrete GPUs, and runs on macOS through MoltenVK officially. Iris is +GL-only (its VK successor "Aperture" is in development); Sodium is GL-only. +Therefore gating Iris/Sodium OFF on the VK path is correct, not a limitation. -## Phase-1 hybrid split (deliberate) -- GL, unchanged & bit-exact: hierarchical traversal, prep/cull(raster occlusion vs - MC depth)/prefixsum/cmdgen compute, Sodium/Iris integration. -- VK: opaque terrain via vkCmdDrawIndexedIndirectCountKHR over shared draw buffers, - into shared RGBA8/D32 targets. Same GLSL sources, shaderc->SPIR-V at runtime with - VOXY_VULKAN defined. -- The raster occlusion cull tests against MC's GL depth buffer -> it must stay GL - until/unless MC depth itself is shared; this is why phase-1 is a hybrid, not a - design shortcut. +## Two VK modes +1. HYBRID (Windows/Linux, MC on OpenGL): GL keeps traversal/culling/cmdgen + compute bit-exact; buffers are VK-allocated + GL-imported + (VK_KHR_external_memory <-> GL_EXT_memory_object); VK does the draws; + GL composites. IMPLEMENTED (guarded off until geometry-SSBO sharing lands). + IMPOSSIBLE ON macOS: MoltenVK has no external_memory_fd and Apple GL is 4.1. +2. PURE-VK / HOST MODE (all OSes incl. macOS, MC on "Prefer Vulkan"): Voxy + adopts Minecraft's own VkDevice/queue/frame via the IVkHost seam and records + LOD passes into MC's frame; no OpenGL anywhere. This is THE Mac path and the + end-state on every platform. ARCHITECTURE LANDED; adapter mixin pending. -## Status — read this before flipping the toggle -| Piece | State | +## macOS blockers — status after this commit set +| Blocker | Status | |---|---| -| Seam (IRenderList, 8-line diff, MDIC untouched via covariant returns) | done, needs compile | -| Config toggle + capability gate + Iris gate + GL default | done, needs compile | -| VK context/device/queue + interop ext selection (win32/fd) | done, UNTESTED on hardware | -| SharedBuffer/SharedImage/SharedSemaphore interop | done, UNTESTED on hardware | -| shaderc GLSL->SPIR-V bridge | done; Voxy's GLSL will need set/binding fixups for Vulkan semantics — expect iteration | -| VK opaque draw pass (mirrors MDIC offsets/strides/clamps) | recorded+submitted, but **guarded OFF**: geometry/metadata/ModelStore SSBOs are plain GlBuffers; until they allocate via SharedBuffer when VK is active, renderOpaque logs once and draws nothing rather than corrupt | -| GL composite of shared color/depth into MC framebuffer | NOT YET (next step with geometry sharing) | -| Translucent / temporal / SSAO on VK | deferred, stubbed no-op | -| Descriptor sets binding shared SSBOs to the VK pipeline | pipeline layout is a placeholder pending geometry sharing | +| VK_KHR_portability_enumeration missing at instance creation (MoltenVK loader hides devices without it) | FIXED | +| VK_KHR_portability_subset must be enabled when exposed | FIXED | +| drawIndirectCount incorrectly inferred from apiVersion (MoltenVK reports 1.2 WITHOUT this feature) | FIXED — proper VkPhysicalDeviceVulkan12Features query | +| Hard dependency on draw-indirect-count | FIXED — fixed-count vkCmdDrawIndexedIndirect fallback (cmdgen zero-fills unused slots; instanceCount=0 draws are no-ops) | +| GL-interop unavailable on Metal | BY DESIGN — pure-VK host mode is the Mac path; gate refuses hybrid on macOS with a clear log | +| shaderc natives | already bundled (macos + macos-arm64) | +| Residual risk | MoltenVK SSBO/vertex-pulling perf & any portability-subset gaps in Voxy's shaders — only measurable on Apple hardware | -## Next steps, in order -1. Compile (`./gradlew build`) — this tree was authored in an offline sandbox - (maven blocked): expect LWJGL VK binding signature fixups. -2. Thread SharedBuffer allocation through BasicSectionGeometryManager + ModelStore - behind `VulkanBackend.shouldUseVulkan(...)`; build the descriptor set layout; - flip `geometryShared = true`. -3. GL composite pass (fullscreen quad sampling shared color+depth, writes - gl_FragDepth, depth-tested vs MC) + glDone/vkDone signal points around cmdgen. -4. Validate on NVIDIA Linux/Windows first (best GL_EXT_memory_object support); - run scripts/check_voxy_shader_contracts.sh (not in this repo — external) on the - unchanged GL shaders to confirm bit-exactness. -5. Then translucents, temporal, and a VK-native traversal (phase-2, drops hybrid). +## What is REAL in code vs what REMAINS +Done (unverified — this sandbox cannot compile [maven blocked] or render [no GPU]): +seam/IRenderList; config toggle + capability + Iris + macOS gates (GL default); +private-device VK context w/ portability + discrete-GPU preference; interop +primitives; shaderc bridge; VK opaque pipeline + indirect draws + fallback; +IVkHost + MinecraftVkHost registry; VkComputePipeline for the compute ports. -## Invariants preserved -GL/MDIC path byte-identical in behavior (seam is type-plumbing only); GL remains -default; Iris+Sodium untouched on GL and explicitly gated on VK. +Remaining for "complete, identical-experience" VK (in order): +1. `./gradlew build` fixups (authored offline against LWJGL 3.4.1 VK bindings). +2. Blaze3D-VK adapter mixin implementing IVkHost — REQUIRES the real 26.2 + mappings/jar; targets deliberately not guessed. Note MC's VK renderer runs on + a dedicated render thread: the adapter must hand Voxy a command buffer at a + defined sync point, this is the trickiest integration detail. +3. IDeviceBuffer abstraction under GlBuffer so geometry/metadata/ModelStore + allocate VkBuffers natively in host mode (kills the last GL dependency). +4. Compute ports via VkComputePipeline (prep/cull/prefix/cmdgen, then the + hierarchical traversal); occlusion vs MC's VK depth via IVkHost views. + Switch graphics pipeline to dynamic rendering (host requires the ext anyway). +5. Translucents, temporal, SSAO parity; then perf work (persistent descriptor + sets, device-generated-commands/metal ICBs where available). +6. Validation matrix: NVIDIA+AMD Windows/Linux (hybrid + host), Apple Silicon + (host only). Parity = pixel-compare vs GL on same seed/camera; perf = frame + times at 64/128/256 render distance. + +## Invariants +GL/MDIC untouched and default; VK strictly behind config + capability gates; +Iris/Sodium gated out on VK, untouched on GL. diff --git a/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java b/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java new file mode 100644 index 000000000..b0fe82fe4 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java @@ -0,0 +1,38 @@ +package me.cortex.voxy.client.core.vk; + +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkDevice; +import org.lwjgl.vulkan.VkInstance; +import org.lwjgl.vulkan.VkPhysicalDevice; +import org.lwjgl.vulkan.VkQueue; + +/** + * The PURE-VK integration seam. When Minecraft 26.2+ itself runs on its + * experimental Vulkan backend ("Prefer Vulkan" Graphics API setting), Voxy must + * not create a second device: it adopts the game's device and records into the + * game's frame. An adapter implements this against Blaze3D's Vulkan internals. + * + * This is also THE macOS path: MoltenVK has no VK_KHR_external_memory_fd, so + * the GL-interop hybrid can never run there — on Mac, Voxy-on-Vulkan requires + * MC-on-Vulkan (which vanilla 26.2 officially supports via MoltenVK). + */ +public interface IVkHost { + VkInstance instance(); + VkPhysicalDevice physicalDevice(); + VkDevice device(); + VkQueue graphicsQueue(); + int graphicsQueueFamily(); + + /** Command buffer currently recording for this frame's world rendering, at the LOD injection point. */ + VkCommandBuffer frameCommandBuffer(); + + /** The game's depth attachment view for the current frame (for occlusion tests + depth-correct LOD compositing). */ + long frameDepthImageView(); + /** The game's color attachment view for the current frame. */ + long frameColorImageView(); + int frameWidth(); + int frameHeight(); + /** Formats of the above, VkFormat values, needed for pipeline rendering-info. */ + int frameColorFormat(); + int frameDepthFormat(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java new file mode 100644 index 000000000..d03c38be9 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java @@ -0,0 +1,24 @@ +package me.cortex.voxy.client.core.vk; + +/** + * Adapter registry for the vanilla 26.2 Blaze3D Vulkan backend. + * + * STATUS: intentionally an unwired registration point. Binding this requires + * mixing into MC 26.2's obfuscated Blaze3D-Vulkan classes (device holder, frame + * command-buffer, swapchain attachments); those mixin targets must be authored + * against the actual 26.2 mappings in a dev environment with the game jar — + * guessing class/method names here would produce fiction, not integration. + * The mixin, once written, calls {@link #register(IVkHost)} during render init + * and {@link #clear()} on backend teardown/API switch. + */ +public final class MinecraftVkHost { + private static volatile IVkHost host; + + public static void register(IVkHost h) { host = h; } + public static void clear() { host = null; } + /** Non-null only when MC itself is presenting through Vulkan and the adapter mixin is active. */ + public static IVkHost get() { return host; } + public static boolean isMinecraftOnVulkan() { return host != null; } + + private MinecraftVkHost() {} +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java b/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java new file mode 100644 index 000000000..2d08c2bc2 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java @@ -0,0 +1,82 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.gl.shader.ShaderType; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.*; + +import java.nio.ByteBuffer; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +/** + * Compute pipeline for the pure-VK path: prep/cull/prefixsum/cmdgen and, + * phase-3, the hierarchical traversal itself — the same GLSL compute sources + * as GL, compiled via shaderc. Descriptors: one set of N storage buffers + + * one UBO at binding 0, mirroring the GL binding-base layout so the shared + * shader sources keep a single binding scheme under VOXY_VULKAN. + */ +public final class VkComputePipeline { + private final VulkanContext ctx; + public final long descriptorSetLayout; + public final long pipelineLayout; + public final long pipeline; + private final long shaderModule; + + public VkComputePipeline(VulkanContext ctx, String glslSource, String name, int uboCount, int ssboCount) { + this.ctx = ctx; + ByteBuffer spv = ShadercCompiler.compile(glslSource, ShaderType.COMPUTE, name); + try (MemoryStack stack = stackPush()) { + var smci = VkShaderModuleCreateInfo.calloc(stack).sType$Default().pCode(spv); + var pMod = stack.mallocLong(1); + check(vkCreateShaderModule(ctx.device, smci, null, pMod), "vkCreateShaderModule(" + name + ")"); + this.shaderModule = pMod.get(0); + + var bindings = VkDescriptorSetLayoutBinding.calloc(uboCount + ssboCount, stack); + for (int i = 0; i < uboCount; i++) { + bindings.get(i).binding(i).descriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) + .descriptorCount(1).stageFlags(VK_SHADER_STAGE_COMPUTE_BIT); + } + for (int i = 0; i < ssboCount; i++) { + bindings.get(uboCount + i).binding(uboCount + i) + .descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + .descriptorCount(1).stageFlags(VK_SHADER_STAGE_COMPUTE_BIT); + } + var dslci = VkDescriptorSetLayoutCreateInfo.calloc(stack).sType$Default().pBindings(bindings); + var pDsl = stack.mallocLong(1); + check(vkCreateDescriptorSetLayout(ctx.device, dslci, null, pDsl), "vkCreateDescriptorSetLayout(" + name + ")"); + this.descriptorSetLayout = pDsl.get(0); + + var plci = VkPipelineLayoutCreateInfo.calloc(stack).sType$Default() + .pSetLayouts(stack.longs(this.descriptorSetLayout)); + var pPl = stack.mallocLong(1); + check(vkCreatePipelineLayout(ctx.device, plci, null, pPl), "vkCreatePipelineLayout(" + name + ")"); + this.pipelineLayout = pPl.get(0); + + var cpci = VkComputePipelineCreateInfo.calloc(1, stack).sType$Default() + .layout(this.pipelineLayout); + cpci.stage().sType$Default().stage(VK_SHADER_STAGE_COMPUTE_BIT) + .module(this.shaderModule).pName(stack.UTF8("main")); + var pPipe = stack.mallocLong(1); + check(vkCreateComputePipelines(ctx.device, VK_NULL_HANDLE, cpci, null, pPipe), "vkCreateComputePipelines(" + name + ")"); + this.pipeline = pPipe.get(0); + } + } + + public void bindAndDispatch(VkCommandBuffer cmd, long descriptorSet, int gx, int gy, int gz) { + try (MemoryStack stack = stackPush()) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, this.pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, this.pipelineLayout, + 0, stack.longs(descriptorSet), null); + vkCmdDispatch(cmd, gx, gy, gz); + } + } + + public void free() { + vkDestroyPipeline(this.ctx.device, this.pipeline, null); + vkDestroyPipelineLayout(this.ctx.device, this.pipelineLayout, null); + vkDestroyDescriptorSetLayout(this.ctx.device, this.descriptorSetLayout, null); + vkDestroyShaderModule(this.ctx.device, this.shaderModule, null); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java index deff456c0..ed2332a24 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java @@ -38,6 +38,13 @@ public static boolean shouldUseVulkan(boolean configWantsVulkan) { Logger.info("Voxy: Vulkan requested but Iris shaderpack active -> staying on OpenGL"); return false; } + if (org.lwjgl.system.Platform.get() == org.lwjgl.system.Platform.MACOSX + && !MinecraftVkHost.isMinecraftOnVulkan()) { + //macOS has no GL-interop (MoltenVK lacks external_memory_fd; Apple GL is 4.1), + //so the only viable VK path is riding Minecraft's own 26.2 Vulkan backend. + Logger.info("Voxy: Vulkan on macOS requires Minecraft's Graphics API set to 'Prefer Vulkan' -> staying on OpenGL"); + return false; + } return isSupported(); } From ba42faff7f29dc914b51b04f8b3eed4d0ce7c8d9 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Sun, 19 Jul 2026 14:12:38 -0400 Subject: [PATCH 06/12] vk: pure-Vulkan render core adopting MC's own device, backend-neutral seams for GL/VK parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voxy now follows MC's own graphics API: when MC 26.2 runs on its Vulkan backend, Voxy adopts the game's VkDevice/queue via a Blaze3D-VK adapter mixin and records all LOD rendering into MC's frame command buffer from a Sodium hook (TAIL of drawChunkLayer/OPAQUE). When MC is on OpenGL, Voxy uses its existing MDIC backend unchanged. No user-facing toggle; no GL fallback either way (no GL context exists under MC-on-Vulkan). Shared CPU pipeline (NodeManager, mesh gen, model bakery, render-distance tracker, viewport math, all GLSL sources via #ifdef VOXY_VULKAN guards) is unmodified between backends; backend-specific glue lives behind seams (IDeviceBuffer, AbstractUpload/DownloadStream, IModelStore, IBasicGeometryData, INodeGpuOps, INodeCleaner, IAtlasTextureReader). Pure-VK render core: compositor (depth setup + alpha composite with env fog), terrain renderer (indexed-indirect-count with MoltenVK fixed-count fallback + visibility-tracked budget), HiZ pyramid (subgroup-reduced), hierarchical traversal, SSAO, depth-bound culling, node cleaner, geometry/model stores, frame context (VkEvent frame retirement + deferred destruction), upload/download streams. MoltenVK: drawIndirectCount feature query + fixed-count fallback, mutable-format D32S8 depth-only sampling, macOS natives for lwjgl-lmdb/zstd, keep osx-arm64 RocksDB native. CmpLog: opt-in (-Dvoxy.cmplog=) A/B parity logging for GL/VK comparison; no-op when unset. Drops the phase-1 GL-interop hybrid renderer (VulkanSectionRenderer/Viewport, SharedBuffer/Image/Semaphore, VkComputePipeline) — superseded by the pure-VK host path. --- VULKAN.md | 161 ++++-- build.gradle | 11 +- .../me/cortex/voxy/client/VoxyClient.java | 65 ++- .../cortex/voxy/client/config/VoxyConfig.java | 11 +- .../client/core/AbstractRenderPipeline.java | 7 +- .../client/core/IrisVoxyRenderPipeline.java | 6 +- .../voxy/client/core/VoxyRenderSystem.java | 71 ++- .../cortex/voxy/client/core/gl/GlBuffer.java | 2 +- .../client/core/gl/shader/PrintfInjector.java | 4 +- .../voxy/client/core/model/IModelStore.java | 27 + .../core/model/ModelBakerySubsystem.java | 9 +- .../voxy/client/core/model/ModelFactory.java | 45 +- .../voxy/client/core/model/ModelStore.java | 39 +- .../model/bakery/GlAtlasTextureReader.java | 35 ++ .../model/bakery/IAtlasTextureReader.java | 35 ++ .../bakery/SoftwareModelTextureBakery.java | 28 +- .../voxy/client/core/rendering/Viewport.java | 22 +- .../rendering/bounding/BoundRenderer.java | 10 +- .../rendering/bounding/ChunkBoundStore.java | 8 +- .../bounding/ColumnStreamedBoundStore.java | 6 +- .../rendering/bounding/ExactBoundStore.java | 6 +- .../core/rendering/bounding/IBoundStore.java | 5 +- .../bounding/StreamedBoundStore.java | 25 +- .../hierachical/AsyncNodeManager.java | 84 +-- .../rendering/hierachical/DebugRenderer.java | 8 +- .../rendering/hierachical/GlNodeGpuOps.java | 85 +++ .../HierarchicalOcclusionTraverser.java | 19 +- .../rendering/hierachical/INodeCleaner.java | 13 + .../rendering/hierachical/INodeGpuOps.java | 22 + .../rendering/hierachical/NodeCleaner.java | 22 +- .../rendering/hierachical/NodeManager.java | 6 +- .../core/rendering/post/FullscreenBlit.java | 2 +- .../backend/AbstractSectionRenderer.java | 11 +- .../backend/mdic/MDICSectionRenderer.java | 19 +- .../backend/vulkan/VulkanSectionRenderer.java | 284 ---------- .../backend/vulkan/VulkanViewport.java | 82 --- .../geometry/BasicSectionGeometryData.java | 12 +- .../geometry/BasicSectionGeometryManager.java | 6 +- .../section/geometry/IBasicGeometryData.java | 24 + .../util/AbstractDownloadStream.java | 61 +++ .../rendering/util/AbstractUploadStream.java | 87 +++ .../core/rendering/util/BufferArena.java | 4 +- .../core/rendering/util/DownloadStream.java | 38 +- .../core/rendering/util/IDeviceBuffer.java | 12 + .../rendering/util/SharedIndexBuffer.java | 30 +- .../core/rendering/util/UploadStream.java | 41 +- .../voxy/client/core/util/GPUTiming.java | 4 +- .../cortex/voxy/client/core/vk/IVkHost.java | 10 - .../voxy/client/core/vk/MinecraftVkHost.java | 31 +- .../core/vk/MinecraftVkHostAdapter.java | 38 ++ .../voxy/client/core/vk/SharedBuffer.java | 92 ---- .../voxy/client/core/vk/SharedImage.java | 103 ---- .../voxy/client/core/vk/SharedSemaphore.java | 59 -- .../client/core/vk/VkAtlasTextureReader.java | 79 +++ .../cortex/voxy/client/core/vk/VkBuffer.java | 124 +++++ .../me/cortex/voxy/client/core/vk/VkCmd.java | 42 ++ .../client/core/vk/VkComputePipeline.java | 82 --- .../voxy/client/core/vk/VkDownloadStream.java | 162 ++++++ .../voxy/client/core/vk/VkFrameCtx.java | 264 +++++++++ .../cortex/voxy/client/core/vk/VkImage2D.java | 195 +++++++ .../voxy/client/core/vk/VkShaderPipeline.java | 315 +++++++++++ .../voxy/client/core/vk/VkShaderSource.java | 74 +++ .../voxy/client/core/vk/VkUploadStream.java | 164 ++++++ .../voxy/client/core/vk/VulkanBackend.java | 65 +-- .../voxy/client/core/vk/VulkanContext.java | 260 ++++----- .../core/vk/render/VkBoundRenderer.java | 165 ++++++ .../client/core/vk/render/VkCompositor.java | 299 +++++++++++ .../client/core/vk/render/VkFrameHost.java | 89 ++++ .../voxy/client/core/vk/render/VkHiZ.java | 189 +++++++ .../client/core/vk/render/VkModelStore.java | 134 +++++ .../client/core/vk/render/VkNodeCleaner.java | 168 ++++++ .../client/core/vk/render/VkNodeGpuOps.java | 104 ++++ .../client/core/vk/render/VkRenderCore.java | 340 ++++++++++++ .../voxy/client/core/vk/render/VkSSAO.java | 189 +++++++ .../core/vk/render/VkSectionGeometryData.java | 94 ++++ .../core/vk/render/VkTerrainRenderer.java | 504 ++++++++++++++++++ .../client/core/vk/render/VkTraversal.java | 287 ++++++++++ .../client/core/vk/render/VkViewport.java | 116 ++++ .../sodium/MixinDefaultChunkRenderer.java | 9 + .../MixinFallbackVisibleChunkCollector.java | 4 +- .../sodium/MixinRenderSectionManager.java | 4 +- .../sodium/MixinVisibleChunkCollector.java | 4 +- .../vk/AccessorVulkanCommandEncoder.java | 16 + .../mixin/vk/MixinSodiumOpaqueVkFrame.java | 51 ++ .../client/mixin/vk/MixinVulkanDevice.java | 39 ++ .../java/me/cortex/voxy/common/CmpLog.java | 80 +++ .../voxy/shaders/chunkoutline/outline.vsh | 16 +- .../voxy/shaders/hiz/vk/hiz_reduce.comp | 49 ++ .../voxy/shaders/hiz/vk/hiz_subgroup.comp | 121 +++++ .../voxy/shaders/lod/gl46/cull/raster.vert | 12 +- .../assets/voxy/shaders/lod/gl46/prep.comp | 6 + .../assets/voxy/shaders/lod/gl46/quads.frag | 5 + .../assets/voxy/shaders/lod/gl46/quads3.vert | 16 +- .../cleaner/batch_visibility_set.comp | 4 + .../cleaner/result_transformer.comp | 4 + .../voxy/shaders/lod/hierarchical/queue.glsl | 4 + .../post/blit_texture_depth_cutout.frag | 14 + .../assets/voxy/shaders/post/fullscreen2.vert | 7 +- .../shaders/post/setup_stencil_depth.frag | 4 + .../assets/voxy/shaders/post/ssao.comp | 21 + .../shaders/util/prefixsum/inital3_vk.comp | 79 +++ .../assets/voxy/shaders/util/scatter.comp | 4 + src/main/resources/client.voxy.mixins.json | 5 +- 103 files changed, 5765 insertions(+), 1264 deletions(-) create mode 100644 src/main/java/me/cortex/voxy/client/core/model/IModelStore.java create mode 100644 src/main/java/me/cortex/voxy/client/core/model/bakery/GlAtlasTextureReader.java create mode 100644 src/main/java/me/cortex/voxy/client/core/model/bakery/IAtlasTextureReader.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/hierachical/GlNodeGpuOps.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeCleaner.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeGpuOps.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IBasicGeometryData.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractDownloadStream.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractUploadStream.java create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/util/IDeviceBuffer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHostAdapter.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkAtlasTextureReader.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkBuffer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkCmd.java delete mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkDownloadStream.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkFrameCtx.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkShaderPipeline.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkShaderSource.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/VkUploadStream.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkBoundRenderer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkCompositor.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkHiZ.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkModelStore.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeCleaner.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeGpuOps.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkSectionGeometryData.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java create mode 100644 src/main/java/me/cortex/voxy/client/core/vk/render/VkViewport.java create mode 100644 src/main/java/me/cortex/voxy/client/mixin/vk/AccessorVulkanCommandEncoder.java create mode 100644 src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java create mode 100644 src/main/java/me/cortex/voxy/client/mixin/vk/MixinVulkanDevice.java create mode 100644 src/main/java/me/cortex/voxy/common/CmpLog.java create mode 100644 src/main/resources/assets/voxy/shaders/hiz/vk/hiz_reduce.comp create mode 100644 src/main/resources/assets/voxy/shaders/hiz/vk/hiz_subgroup.comp create mode 100644 src/main/resources/assets/voxy/shaders/util/prefixsum/inital3_vk.comp diff --git a/VULKAN.md b/VULKAN.md index b88c6a9ef..aa1ab5ea6 100644 --- a/VULKAN.md +++ b/VULKAN.md @@ -1,59 +1,106 @@ -# Voxy Vulkan Backend — branch `vulkan-backend`, rebased on dev @ MC 26.2 - -## Grounded context (verified July 2026) -Vanilla MC Java 26.2 ("Chaos Cubed") ships an experimental Vulkan renderer: -opt-in Graphics API setting (Default / Prefer Vulkan / Prefer OpenGL), requires -Vulkan 1.2 + dynamic rendering + push descriptors, auto-fallback ladder to GL, -prefers discrete GPUs, and runs on macOS through MoltenVK officially. Iris is -GL-only (its VK successor "Aperture" is in development); Sodium is GL-only. -Therefore gating Iris/Sodium OFF on the VK path is correct, not a limitation. - -## Two VK modes -1. HYBRID (Windows/Linux, MC on OpenGL): GL keeps traversal/culling/cmdgen - compute bit-exact; buffers are VK-allocated + GL-imported - (VK_KHR_external_memory <-> GL_EXT_memory_object); VK does the draws; - GL composites. IMPLEMENTED (guarded off until geometry-SSBO sharing lands). - IMPOSSIBLE ON macOS: MoltenVK has no external_memory_fd and Apple GL is 4.1. -2. PURE-VK / HOST MODE (all OSes incl. macOS, MC on "Prefer Vulkan"): Voxy - adopts Minecraft's own VkDevice/queue/frame via the IVkHost seam and records - LOD passes into MC's frame; no OpenGL anywhere. This is THE Mac path and the - end-state on every platform. ARCHITECTURE LANDED; adapter mixin pending. - -## macOS blockers — status after this commit set -| Blocker | Status | -|---|---| -| VK_KHR_portability_enumeration missing at instance creation (MoltenVK loader hides devices without it) | FIXED | -| VK_KHR_portability_subset must be enabled when exposed | FIXED | -| drawIndirectCount incorrectly inferred from apiVersion (MoltenVK reports 1.2 WITHOUT this feature) | FIXED — proper VkPhysicalDeviceVulkan12Features query | -| Hard dependency on draw-indirect-count | FIXED — fixed-count vkCmdDrawIndexedIndirect fallback (cmdgen zero-fills unused slots; instanceCount=0 draws are no-ops) | -| GL-interop unavailable on Metal | BY DESIGN — pure-VK host mode is the Mac path; gate refuses hybrid on macOS with a clear log | -| shaderc natives | already bundled (macos + macos-arm64) | -| Residual risk | MoltenVK SSBO/vertex-pulling perf & any portability-subset gaps in Voxy's shaders — only measurable on Apple hardware | - -## What is REAL in code vs what REMAINS -Done (unverified — this sandbox cannot compile [maven blocked] or render [no GPU]): -seam/IRenderList; config toggle + capability + Iris + macOS gates (GL default); -private-device VK context w/ portability + discrete-GPU preference; interop -primitives; shaderc bridge; VK opaque pipeline + indirect draws + fallback; -IVkHost + MinecraftVkHost registry; VkComputePipeline for the compute ports. - -Remaining for "complete, identical-experience" VK (in order): -1. `./gradlew build` fixups (authored offline against LWJGL 3.4.1 VK bindings). -2. Blaze3D-VK adapter mixin implementing IVkHost — REQUIRES the real 26.2 - mappings/jar; targets deliberately not guessed. Note MC's VK renderer runs on - a dedicated render thread: the adapter must hand Voxy a command buffer at a - defined sync point, this is the trickiest integration detail. -3. IDeviceBuffer abstraction under GlBuffer so geometry/metadata/ModelStore - allocate VkBuffers natively in host mode (kills the last GL dependency). -4. Compute ports via VkComputePipeline (prep/cull/prefix/cmdgen, then the - hierarchical traversal); occlusion vs MC's VK depth via IVkHost views. - Switch graphics pipeline to dynamic rendering (host requires the ext anyway). -5. Translucents, temporal, SSAO parity; then perf work (persistent descriptor - sets, device-generated-commands/metal ICBs where available). -6. Validation matrix: NVIDIA+AMD Windows/Linux (hybrid + host), Apple Silicon - (host only). Parity = pixel-compare vs GL on same seed/camera; perf = frame - times at 64/128/256 render distance. +# Voxy Vulkan Backend + +Voxy follows Minecraft's own graphics API: when MC 26.2 runs on its Vulkan +backend, Voxy renders through Vulkan; when MC is on OpenGL, Voxy uses its +OpenGL (MDIC) backend. There is no user-facing toggle and no fallback either +way — a GL context cannot exist in a MC-on-Vulkan process, so "falling back to +OpenGL" is impossible, and forcing a cross-API split is incoherent with the +identical-experience goal. + +## Architecture + +When MC is on Vulkan, Voxy adopts MC's own `VkDevice`/queue via the Blaze3D +adapter mixin (`MixinVulkanDevice` registers a `MinecraftVkHostAdapter` at +device init) and records all of its GPU work into MC's frame command buffer +from `MixinSodiumOpaqueVkFrame` (TAIL of `SodiumWorldRenderer.drawChunkLayer` +for the OPAQUE group) — right after Sodium's opaque terrain, render pass +closed, frame command buffer recording. Per frame: + +1. **SETUP** (`VkCompositor`): clear Voxy's offscreen colour + D32S8 + depth-stencil (stencil=1); fullscreen pass copies MC's depth in + (projection-transformed) writing stencil=0 where vanilla terrain exists. +2. **Opaque LOD terrain**: `vkCmdDrawIndexedIndirectCount`, stencil==1 test + (draw calls generated last frame — same latency model as GL). +3. **HiZ pyramid** (`VkHiZ`, R32F mips, conservative REDUCTION) + + `AsyncNodeManager` sync (`VkNodeGpuOps` scatter/multi-memcpy computes) + + `VkNodeCleaner` + hierarchical traversal (`VkTraversal`: 12 flip-flop indirect + dispatches, HiZ-tested, request readback via `VkDownloadStream`). +4. **Draw-call build** (`VkTerrainRenderer`): prep -> raster box cull + (depth-only, early-fragment-test visibility writes) -> cmdgen (indirect + dispatch) -> translucency prefix-sort + build. +5. Temporal + translucent draws. +6. **COMPOSITE**: alpha-blend into MC's colour/depth attachments (dynamic + rendering, LOAD/STORE), fragment emits vanilla-space depth + env fog (fog + params sourced from MC's `CameraRenderState.fogData`). +7. Streams tick (staging recycled on VkEvent frame retirement), model bakery + tick (atlas mips via `vkCmdCopyBufferToImage`), render-distance tracking. + +Shared with GL (zero duplication): `NodeManager`/`AsyncNodeManager`, mesh +generation, model bakery CPU pipeline, render-distance tracker, viewport +math, and all shader sources — the GLSL is single-source with +`#ifdef VOXY_VULKAN` guards (push constants replace default-block uniforms, +sampler bindings remapped to the unified VK namespace, +`gl_VertexID`/`gl_InstanceID`/`gl_BaseInstance` aliased, u16 cube indices +replace u8). Seams: `IDeviceBuffer`, `AbstractUploadStream`/ +`AbstractDownloadStream` (backend-settable singletons), `INodeGpuOps`, +`INodeCleaner`, `IBasicGeometryData`, `IModelStore`, `IAtlasTextureReader`. + +## MoltenVK / macOS + +- `drawIndirectCount` is a 1.2 *feature*, not implied by `apiVersion` — + MoltenVK reports 1.2 without it. `VulkanContext.hasDrawIndirectCount` is + queried and `VkTerrainRenderer.renderTerrain` branches on it: the + fixed-count fallback issues `vkCmdDrawIndexedIndirect` with a clamped + `maxDrawCount` and zeroes the three `drawCallBuffer` slices (opaque / + temporal / translucent) before cmdgen so stale trailing slots never read as + ghost draws. +- The fixed-count budget tracks the last-read real per-pass draw count (async + readback via `VkDownloadStream`, no stall), so looking at the sky actually + reduces encoded Metal draws (MoltenVK emulates multi-draw-indirect as one + Metal draw per slot — a view-independent cap encoded ~sectionCount*6.4 + no-op draws per frame and hid all culling wins). +- `VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT` + a `VkImageFormatListCreateInfo` is + used on the D32S8 offscreen depth so a depth-only aspect view can alias the + packed image (without it MoltenVK may synthesise a separate staging texture + for depth-only sampling). +- macOS natives for `lwjgl-lmdb` and `lwjgl-zstd` must be bundled or section + storage throws `UnsatisfiedLinkError` on save and no geometry is persisted + (the bound-renderer AABBs leak through as the only visible geometry). ## Invariants -GL/MDIC untouched and default; VK strictly behind config + capability gates; -Iris/Sodium gated out on VK, untouched on GL. + +- GL/MDIC byte-identical when MC is on GL (seam refactors are lazy-init only). +- VK strictly follows MC's own API; never falls back to GL under + MC-on-Vulkan; gates no mod off as "GL-only". +- No GL classloads can occur on the VK path — `VoxyClient.initVoxyClient` + branches on `MinecraftVkHost.isMinecraftOnVulkan()` before the first GL + touch (`Capabilities`'s `` runs `GL.getCapabilities()` and throws + with no GL context). The streams, the shared index buffer, and the atlas + readback are lazily backend-selected, so their GL implementations never + classload on the VK path. +- `VkRenderCore.shutdown()` is idempotent and device-alive-gated + (`MinecraftVkHost.get() != null` skips GPU teardown if MC already tore its + device down on full-game exit). CPU stop (node/gen thread joins, callback + detach, world `releaseRef`) always runs. `modelService.shutdown()` owns the + `VkModelStore` lifetime (single `vkDestroySampler`) and runs after + `frameCtx.waitIdleRetireAll()`. + +## Feature parity + +VK visual output matches GL: depth-space transform in setup/composite, +stencil mask correctness, fog ramp, lightmap sampling (vertex-stage), model +atlas mips, SSAO (`VkSSAO`), depth-bound culling (`VkBoundRenderer`), and +view-bob tracking (the frame hook feeds Sodium's bobbed `ChunkRenderMatrices` ++ camera offset into `renderFrame`). + +No-op by design on the VK path: FREX integration, shader printf debugging, +GPU-timing markers (all GL-debug-only paths). + +## A/B comparison logging + +`-Dvoxy.cmplog=` makes both backends emit the same per-frame semantic +quantities (section/geometry counts, traversal request counts) as +tab-separated records via `CmpLog`. For a stationary camera the values +converge to identical integers when the VK translation is faithful, so a +script can flag any divergence without the rounding noise of a pixel diff. +Every method is a no-op when the property is unset. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9f4a9f07d..8768e4384 100644 --- a/build.gradle +++ b/build.gradle @@ -326,7 +326,10 @@ tasks.register('makeExcludedRocksDB', Zip) { exclude { def file = it.name if (file.endsWith(".jnilib")) { - return true + //Keep macOS arm64 native so RocksDB works on Apple Silicon (VK/MoltenVK + // target); drop x86_64. Without this, world load crashes on M-series Macs + // with "librocksdbjni-osx-arm64.jnilib was not found inside JAR". + return !file.contains("osx-arm64") } if (!file.endsWith(".so")) { return false @@ -392,6 +395,12 @@ dependencies { include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-windows") include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-linux") include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-linux") + //macOS natives: required for the MoltenVK/Vulkan path so Voxy's section + // storage (zstd compression + lmdb index) can load on Apple Silicon. + include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-macos") + include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-macos") + include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-macos-arm64") + include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-macos-arm64") if (INCLUDE_OTHER_ARCHS) { runtimeOnly "org.lwjgl:lwjgl:$lwjglVersion:natives-linux-arm64" include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-linux-arm64") diff --git a/src/main/java/me/cortex/voxy/client/VoxyClient.java b/src/main/java/me/cortex/voxy/client/VoxyClient.java index c0029a7d1..ae96ebdaa 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyClient.java +++ b/src/main/java/me/cortex/voxy/client/VoxyClient.java @@ -2,6 +2,8 @@ import me.cortex.voxy.client.core.gl.Capabilities; import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; +import me.cortex.voxy.client.core.vk.MinecraftVkHost; +import me.cortex.voxy.client.core.vk.VulkanBackend; import me.cortex.voxy.common.Logger; import me.cortex.voxy.commonImpl.VoxyCommon; import net.fabricmc.api.ClientModInitializer; @@ -21,6 +23,16 @@ public class VoxyClient implements ClientModInitializer { private static final HashSet FREX = new HashSet<>(); private static FileLock EXCLUSIVE_LOCK; public static void initVoxyClient() { + //When MC itself presents through its 26.2 Vulkan backend there is no GL + // context on the render thread. Nothing GL-backed may be touched here — + // above all not Capabilities, whose calls GL.getCapabilities() + // and throws. Everything below this branch classloads or queries GL and + // is only valid when MC is on OpenGL. + if (MinecraftVkHost.isMinecraftOnVulkan()) { + initVoxyClientVulkan(); + return; + } + Capabilities.init();//Ensure clinit is called if (Capabilities.INSTANCE.hasBrokenDepthSampler) { @@ -33,25 +45,14 @@ public static void initVoxyClient() { } if (systemSupported && System.getProperty("voxy.exclusiveLock", "false").equalsIgnoreCase("true")) { - //Try acquire the lock file - var vf = Minecraft.getInstance().gameDirectory.toPath().resolve(".voxy"); - if (!vf.toFile().isDirectory()) { - vf.toFile().mkdir(); - } - try { - FileOutputStream fis = new FileOutputStream(vf.resolve("voxy.lock").toFile()); - EXCLUSIVE_LOCK = fis.getChannel().lock(0, Long.MAX_VALUE, false); - } catch (NonWritableChannelException | IOException e) { - //If some error write to log and unsupport - Logger.error("Failed to acquire exclusive voxy lock file, mod will be disabled"); + if (!acquireExclusiveLock()) { systemSupported = false; } - } if (systemSupported) { - SharedIndexBuffer.INSTANCE.id(); + SharedIndexBuffer.INSTANCE().id(); VoxyCommon.setInstanceFactory(VoxyClientInstance::new); @@ -62,6 +63,44 @@ public static void initVoxyClient() { } } + //Vulkan init: MC is presenting through its own Vulkan backend, so Voxy adopts + // that device (VulkanBackend) and never runs the GL capability probes. This + // method — and everything it reaches — must stay free of any GL classload. + // If VK cannot be adopted (host adapter not registered, missing bindings) + // Voxy disables itself: falling back to GL is impossible (no GL context). + private static void initVoxyClientVulkan() { + if (!VulkanBackend.shouldUseVulkan()) { + Logger.error("Voxy is unsupported on your system. Minecraft is on Vulkan but the Voxy Vulkan backend could not be used (" + VulkanBackend.statusLine() + ")"); + return; + } + + if (System.getProperty("voxy.exclusiveLock", "false").equalsIgnoreCase("true")) { + if (!acquireExclusiveLock()) { + return; + } + } + + VoxyCommon.setInstanceFactory(VoxyClientInstance::new); + Logger.info("Voxy initialised on the Vulkan backend (" + VulkanBackend.statusLine() + ")"); + } + + //Acquire the cross-process exclusive lock file. Backend-agnostic (pure file + // IO, no GPU work). Returns false and logs on failure so callers can disable Voxy. + private static boolean acquireExclusiveLock() { + var vf = Minecraft.getInstance().gameDirectory.toPath().resolve(".voxy"); + if (!vf.toFile().isDirectory()) { + vf.toFile().mkdir(); + } + try { + FileOutputStream fis = new FileOutputStream(vf.resolve("voxy.lock").toFile()); + EXCLUSIVE_LOCK = fis.getChannel().lock(0, Long.MAX_VALUE, false); + return true; + } catch (NonWritableChannelException | IOException e) { + Logger.error("Failed to acquire exclusive voxy lock file, mod will be disabled"); + return false; + } + } + @Override public void onInitializeClient() { DebugEntries.init(); diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index 88de2261b..ae4a70937 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -34,11 +34,14 @@ public class VoxyConfig { public float subDivisionSize = 64; public boolean useEnvironmentalFog = true; public boolean dontUseSodiumBuilderThreads = false; - /** "opengl" (default) or "vulkan". Vulkan additionally requires a capable device and no active Iris shaderpack. */ - public String renderBackend = "opengl"; - + //Voxy's rendering API is not user-selectable: it follows whichever graphics + // API MC itself is running on. When MC resolves to Vulkan, Voxy renders + // through Vulkan (host mode, adopting MC's own device); otherwise OpenGL. + // A manual override is deliberately absent: Voxy cannot run OpenGL while MC + // runs Vulkan (no GL context exists in that case), and forcing a cross-API + // split is incoherent with the goal of an identical experience. public boolean wantsVulkanBackend() { - return "vulkan".equalsIgnoreCase(this.renderBackend); + return me.cortex.voxy.client.core.vk.MinecraftVkHost.isMinecraftOnVulkan(); } public String ssaoMode; diff --git a/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java b/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java index 986472928..2a6ffd0db 100644 --- a/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java +++ b/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java @@ -12,8 +12,9 @@ import me.cortex.voxy.client.core.rendering.post.FullscreenBlit; import me.cortex.voxy.client.core.rendering.section.backend.AbstractSectionRenderer; import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.util.GPUTiming; +import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.util.TrackedObject; import org.joml.Matrix4f; import org.lwjgl.opengl.GL30; @@ -76,6 +77,7 @@ public abstract class AbstractRenderPipeline extends TrackedObject { } protected AbstractRenderPipeline(RenderProperties properties, AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier, boolean deferTranslucency) { + CmpLog.backend = "opengl"; this.properties = properties; this.frexStillHasWork = frexSupplier; this.nodeManager = nodeManager; @@ -208,7 +210,7 @@ protected void innerPrimaryWork(Viewport viewport, int depthBuffer) { TimingStatistics.D.start(); //Tick download stream - DownloadStream.INSTANCE.tick(); + AbstractDownloadStream.INSTANCE().tick(); TimingStatistics.D.stop(); this.nodeManager.tick(this.traversal.getNodeBuffer(), this.nodeCleaner); @@ -221,6 +223,7 @@ protected void innerPrimaryWork(Viewport viewport, int depthBuffer) { glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT | GL_PIXEL_BUFFER_BARRIER_BIT); + this.nodeManager.logCompareStats(); TimingStatistics.F.start(); this.traversal.doTraversal(viewport); TimingStatistics.F.stop(); diff --git a/src/main/java/me/cortex/voxy/client/core/IrisVoxyRenderPipeline.java b/src/main/java/me/cortex/voxy/client/core/IrisVoxyRenderPipeline.java index 6360c70c6..b5975dcb1 100644 --- a/src/main/java/me/cortex/voxy/client/core/IrisVoxyRenderPipeline.java +++ b/src/main/java/me/cortex/voxy/client/core/IrisVoxyRenderPipeline.java @@ -9,7 +9,7 @@ import me.cortex.voxy.client.core.rendering.post.FullscreenBlit; import me.cortex.voxy.client.core.rendering.section.backend.AbstractSectionRenderer; import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.client.iris.IrisVoxyRenderPipelineData; import net.irisshaders.iris.shaderpack.materialmap.WorldRenderingSettings; import org.joml.Matrix4f; @@ -110,9 +110,9 @@ public void preSetup(Viewport viewport) { super.preSetup(viewport); if (this.shaderUniforms != null) { //Update the uniforms - long ptr = UploadStream.INSTANCE.uploadTo(this.shaderUniforms); + long ptr = AbstractUploadStream.INSTANCE().uploadTo(this.shaderUniforms); this.data.getUniforms().updater().accept(ptr); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } } diff --git a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java index 091f076d9..ef96b6dfc 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java @@ -23,9 +23,9 @@ import me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer; import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.client.core.util.GPUTiming; import me.cortex.voxy.client.core.util.IrisUtil; import me.cortex.voxy.common.Logger; @@ -54,6 +54,10 @@ public class VoxyRenderSystem { private final WorldEngine worldIn; + //Non-null exactly when MC runs on Vulkan: the whole render path is the + // pure-VK core and every GL member below stays null. + public final @Nullable me.cortex.voxy.client.core.vk.render.VkRenderCore vkCore; + private final ModelBakerySubsystem modelService; private final RenderGenerationService renderGen; private final IGeometryData geometryData; @@ -64,7 +68,7 @@ public class VoxyRenderSystem { private final RenderDistanceTracker renderDistanceTracker; private final BoundRenderer boundOutlineRenderer; - public final StreamedBoundStore visbleSectionStream = new StreamedBoundStore(); + public StreamedBoundStore visbleSectionStream;//Sodium mixin fed; backend-neutral (GL creates here, VK core supplies its own) private @Nullable ColumnStreamedBoundStore columnStreamedBoundStore;//Only used when FREX is enabled private final ViewportSelector viewportSelector; @@ -73,10 +77,6 @@ public class VoxyRenderSystem { private final RenderProperties properties; private static AbstractSectionRenderer.Factory getRenderBackendFactory() { - if (me.cortex.voxy.client.core.vk.VulkanBackend.shouldUseVulkan( - me.cortex.voxy.client.config.VoxyConfig.CONFIG.wantsVulkanBackend())) { - return me.cortex.voxy.client.core.rendering.section.backend.vulkan.VulkanSectionRenderer.FACTORY; - } //TODO: need todo a thing where selects optimal section render based on if supports the pipeline and geometry data type return MDICSectionRenderer.FACTORY; } @@ -84,6 +84,28 @@ private static AbstractSectionRenderer.Factory getRen public VoxyRenderSystem(WorldEngine world, ServiceManager sm) { //Keep the world loaded, NOTE: this is done FIRST, to keep and ensure that even if the rest of loading takes more // than timeout, we keep the world acquired + //When MC itself renders through Vulkan there is no GL context at all; the + // entire renderer is the VkRenderCore and nothing below may run. + if (me.cortex.voxy.client.core.vk.VulkanBackend.shouldUseVulkan()) { + this.worldIn = world; + this.vkCore = new me.cortex.voxy.client.core.vk.render.VkRenderCore(world, sm); + this.visbleSectionStream = this.vkCore.getVisibleSectionStream();//Sodium visibility mixins feed it on VK too + this.modelService = null; + this.renderGen = null; + this.geometryData = null; + this.nodeManager = null; + this.nodeCleaner = null; + this.traversal = null; + this.renderDistanceTracker = null; + this.boundOutlineRenderer = null; + this.viewportSelector = null; + this.pipeline = null; + this.properties = null; + return; + } + this.vkCore = null; + this.visbleSectionStream = new StreamedBoundStore(GlBuffer::new); + //OpenGL path (unchanged) world.acquireRef(); Logger.info("Creating Voxy render system"); @@ -116,7 +138,7 @@ public VoxyRenderSystem(WorldEngine world, ServiceManager sm) { this.geometryData = new BasicSectionGeometryData(1<<20, RenderResourceReuse.getOrCreateGeometryBuffer()); - this.nodeManager = new AsyncNodeManager(1 << 21, this.geometryData, this.renderGen); + this.nodeManager = new AsyncNodeManager(1 << 21, this.geometryData, this.renderGen, new me.cortex.voxy.client.core.rendering.hierachical.GlNodeGpuOps()); this.nodeCleaner = new NodeCleaner(this.nodeManager); this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner, this.renderGen); @@ -178,7 +200,16 @@ public VoxyRenderSystem(WorldEngine world, ServiceManager sm) { } + //True when MC — and therefore Voxy — is on the Vulkan backend. On VK Voxy + // renders through its own frame hook (MixinSodiumOpaqueVkFrame), so the + // GL/Sodium-interop hooks stay inert (Sodium 0.9.1 also renders through MC's + // Vulkan device, so its texture views are VulkanGpuTextureView). + public boolean isVulkanBackend() { + return this.vkCore != null; + } + public Viewport setupViewport(Matrix4fc vanillaProjection, Matrix4fc modelView, FogParameters fogParameters, int width, int height, double cameraX, double cameraY, double cameraZ) { + if (this.vkCore != null) return null;//VK path renders via its own hook var viewport = this.getViewport(); if (viewport == null) { return null; @@ -232,6 +263,7 @@ public Viewport setupViewport(Matrix4fc vanillaProjection, Matrix4fc modelVie public void renderOpaque(Viewport viewport, int sourceDepthTexture, int sourceColourTexture) { + if (this.vkCore != null) return;//VK path renders via its own hook if (viewport == null) { return; } @@ -310,7 +342,7 @@ public void renderOpaque(Viewport viewport, int sourceDepthTexture, int sourc //As much dynamic runtime stuff here { //Tick upload stream (this is ok to do here as upload ticking is just memory management) - UploadStream.INSTANCE.tick(); + AbstractUploadStream.INSTANCE().tick(); while (this.renderDistanceTracker.setCenterAndProcess(viewport.cameraX, viewport.cameraZ) && VoxyClient.isFrexActive());//While FF is active, run until everything is processed TimingStatistics.H.start(); @@ -455,7 +487,7 @@ private static Matrix4f computeProjectionMat(Matrix4fc base) { ).mulLocal(makeProjectionMatrix(nearVoxy, 16*3000)); }*/ - private static Matrix4f computeProjectionMat(RenderProperties properties, Matrix4fc base) { + public static Matrix4f computeProjectionMat(RenderProperties properties, Matrix4fc base) { //this jank is to capture the extra crap they inject like viewbobbing var rawMCProj = Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState.projectionMatrix; @@ -492,7 +524,7 @@ private boolean frexStillHasWork() { return false; } //If frex is running we must tick everything to ensure correctness - UploadStream.INSTANCE.tick(); + AbstractUploadStream.INSTANCE().tick(); //Done here as is allows less gl state resetup this.modelService.tick(100_000_000); GL11.glFinish(); @@ -500,10 +532,15 @@ private boolean frexStillHasWork() { } public void setRenderDistance(float renderDistance) { + if (this.vkCore != null) { + this.vkCore.setRenderDistance(renderDistance); + return; + } this.renderDistanceTracker.setRenderDistance((int) Math.ceil(renderDistance+1));//the +1 is to cover the outer ring of chunks when rendering a circle } public Viewport getViewport() { + if (this.vkCore != null) return null; if (IrisUtil.irisShadowActive()) { return null; } @@ -511,6 +548,10 @@ public Viewport getViewport() { } public void addDebugInfo(List debug) { + if (this.vkCore != null) { + this.vkCore.addDebugInfo(debug); + return; + } debug.add("Buf/Tex [#/Mb]: [" + GlBuffer.getCount() + "/" + (GlBuffer.getTotalSize()/1_000_000) + "],[" + GlTexture.getCount() + "/" + (GlTexture.getEstimatedTotalSize()/1_000_000)+"]"); { this.modelService.addDebugData(debug); @@ -529,8 +570,12 @@ public void addDebugInfo(List debug) { } public void shutdown() { + if (this.vkCore != null) { + this.vkCore.shutdown(); + return; + } Logger.info("Flushing download stream"); - DownloadStream.INSTANCE.flushWaitClear(); + AbstractDownloadStream.INSTANCE().flushWaitClear(); Logger.info("Shutting down rendering"); try { //Cleanup callbacks @@ -564,7 +609,7 @@ public void shutdown() { Logger.info("Flushing download stream"); - DownloadStream.INSTANCE.flushWaitClear(); + AbstractDownloadStream.INSTANCE().flushWaitClear(); //Release hold on the world this.worldIn.releaseRef(); diff --git a/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java b/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java index bf10aa1ef..c1d8f54b8 100644 --- a/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java +++ b/src/main/java/me/cortex/voxy/client/core/gl/GlBuffer.java @@ -10,7 +10,7 @@ import static org.lwjgl.opengl.GL15.glDeleteBuffers; import static org.lwjgl.opengl.GL45C.*; -public class GlBuffer extends TrackedObject implements me.cortex.voxy.client.core.rendering.IRenderList { +public class GlBuffer extends TrackedObject implements me.cortex.voxy.client.core.rendering.IRenderList, me.cortex.voxy.client.core.rendering.util.IDeviceBuffer { public final int id; private final long size; private final int flags; diff --git a/src/main/java/me/cortex/voxy/client/core/gl/shader/PrintfInjector.java b/src/main/java/me/cortex/voxy/client/core/gl/shader/PrintfInjector.java index ac9b0fa5b..22c07a220 100644 --- a/src/main/java/me/cortex/voxy/client/core/gl/shader/PrintfInjector.java +++ b/src/main/java/me/cortex/voxy/client/core/gl/shader/PrintfInjector.java @@ -1,7 +1,7 @@ package me.cortex.voxy.client.core.gl.shader; import me.cortex.voxy.client.core.gl.GlBuffer; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import org.lwjgl.system.MemoryUtil; import java.util.ArrayList; @@ -226,7 +226,7 @@ private void processResult(long ptr, long size) { } public void download() { - DownloadStream.INSTANCE.download(this.textBuffer, this::processResult); + AbstractDownloadStream.INSTANCE().download(this.textBuffer, this::processResult); nglClearNamedBufferSubData(this.textBuffer.id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0); } diff --git a/src/main/java/me/cortex/voxy/client/core/model/IModelStore.java b/src/main/java/me/cortex/voxy/client/core/model/IModelStore.java new file mode 100644 index 000000000..5d9eae5ae --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/model/IModelStore.java @@ -0,0 +1,27 @@ +package me.cortex.voxy.client.core.model; + +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.common.util.MemoryBuffer; + +//Backend-neutral model store: the block-model data buffer, the biome/colour +// buffer, and the baked model texture atlas. Implemented by ModelStore (GL) +// and VkModelStore (pure Vulkan) so ModelFactory's CPU-side baking pipeline is +// shared verbatim; only the atlas texture upload differs per API. +public interface IModelStore { + IDeviceBuffer modelBufferHandle(); + + IDeviceBuffer colourBufferHandle(); + + /** Called once before a batch of texture uploads (GL: unpack-state reset; VK: layout transition). */ + void beginTextureUploads(); + + //Upload one baked model's mip chain into its atlas slot. Layout of texture: + // LAYERS consecutive RGBA8 mip images of a (MODEL_TEXTURE_SIZE*3 x + // MODEL_TEXTURE_SIZE*2) tile, tightly packed. + void uploadModelTexture(int modelId, MemoryBuffer texture); + + /** Called once after a batch of texture uploads (VK: transition back to sampled). */ + void endTextureUploads(); + + void free(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/model/ModelBakerySubsystem.java b/src/main/java/me/cortex/voxy/client/core/model/ModelBakerySubsystem.java index 8cb588d34..5f5ddb3b8 100644 --- a/src/main/java/me/cortex/voxy/client/core/model/ModelBakerySubsystem.java +++ b/src/main/java/me/cortex/voxy/client/core/model/ModelBakerySubsystem.java @@ -13,7 +13,7 @@ public class ModelBakerySubsystem { //Redo to just make it request the block faces with the async texture download stream which // basicly solves all the render stutter due to the baking - private final ModelStore storage = new ModelStore(); + private final IModelStore storage; public final ModelFactory factory; private final Mapper mapper; @@ -21,6 +21,11 @@ public class ModelBakerySubsystem { private volatile boolean isRunning = true; private volatile Throwable processingThreadException; public ModelBakerySubsystem(Mapper mapper) { + this(mapper, new ModelStore()); + } + + public ModelBakerySubsystem(Mapper mapper, IModelStore store) { + this.storage = store; this.mapper = mapper; this.factory = new ModelFactory(mapper, this.storage); this.processingThread = new Thread(()->{//TODO replace this with something good/integrate it into the async processor so that we just have less threads overall @@ -89,7 +94,7 @@ public void addDebugData(List debug) { debug.add(String.format("IF/MC: %03d, %04d", this.factory.getInflightCount(), this.factory.getBakedCount()));//Model bake queue/in flight/model baked count } - public ModelStore getStore() { + public IModelStore getStore() { return this.storage; } diff --git a/src/main/java/me/cortex/voxy/client/core/model/ModelFactory.java b/src/main/java/me/cortex/voxy/client/core/model/ModelFactory.java index 68167e3a0..ff7f1a829 100644 --- a/src/main/java/me/cortex/voxy/client/core/model/ModelFactory.java +++ b/src/main/java/me/cortex/voxy/client/core/model/ModelFactory.java @@ -8,7 +8,7 @@ import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.gl.GlTexture; import me.cortex.voxy.client.core.model.bakery.SoftwareModelTextureBakery; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.util.Pair; @@ -122,7 +122,7 @@ public ModelEntry(ColourDepthTextureData[] textures, int fluidBlockStateId, int private static final ObjectSet LOGGED_SELF_CULLING_WARNING = new ObjectOpenHashSet<>(); private final Mapper mapper; - private final ModelStore storage; + private final IModelStore storage; private final ConcurrentLinkedDeque bakeQueue = new ConcurrentLinkedDeque<>(); @@ -132,7 +132,7 @@ public ModelEntry(ColourDepthTextureData[] textures, int fluidBlockStateId, int //TODO: NOTE!!! is it worth even uploading as a 16x16 texture, since automatic lod selection... doing 8x8 textures might be perfectly ok!!! // this _quarters_ the memory requirements for the texture atlas!!! WHICH IS HUGE saving - public ModelFactory(Mapper mapper, ModelStore storage) { + public ModelFactory(Mapper mapper, IModelStore storage) { this.mapper = mapper; this.storage = storage; this.bakery2 = new SoftwareModelTextureBakery(); @@ -325,20 +325,18 @@ public void processUploads() { var upload = this.uploadResults.poll(); if (upload==null) return; - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + this.storage.beginTextureUploads(); do { upload.upload(this.storage); upload.free(); upload = this.uploadResults.poll(); } while (upload != null); - UploadStream.INSTANCE.commit(); + this.storage.endTextureUploads(); + AbstractUploadStream.INSTANCE().commit(); } private interface ResultUploader { - void upload(ModelStore store); + void upload(IModelStore store); void free(); } @@ -351,27 +349,16 @@ private static final class ModelBakeResultUpload implements ResultUploader { public int biomeUploadIndex = -1; public @Nullable MemoryBuffer biomeUpload; - public void upload(ModelStore store) {//Uploads and resets for reuse - this.upload(store.modelBuffer, store.modelColourBuffer, store.textures); - } - - public void upload(GlBuffer modelBuffer, GlBuffer colourBuffer, GlTexture atlas) {//Uploads and resets for reuse - this.model.cpyTo(UploadStream.INSTANCE.upload(modelBuffer, (long) this.modelId * MODEL_SIZE, MODEL_SIZE)); + public void upload(IModelStore store) {//Uploads and resets for reuse + this.model.cpyTo(AbstractUploadStream.INSTANCE().upload(store.modelBufferHandle(), (long) this.modelId * MODEL_SIZE, MODEL_SIZE)); if (this.biomeUploadIndex != -1) { - this.biomeUpload.cpyTo(UploadStream.INSTANCE.upload(colourBuffer, this.biomeUploadIndex * 4L, this.biomeUpload.size)); + this.biomeUpload.cpyTo(AbstractUploadStream.INSTANCE().upload(store.colourBufferHandle(), this.biomeUploadIndex * 4L, this.biomeUpload.size)); this.biomeUploadIndex = -1; this.biomeUpload.free(); this.biomeUpload = null; } - int X = (this.modelId&0xFF) * MODEL_TEXTURE_SIZE*3; - int Y = ((this.modelId>>8)&0xFF) * MODEL_TEXTURE_SIZE*2; - - long cAddr = this.texture.address; - for (int lvl = 0; lvl < LAYERS; lvl++) { - nglTextureSubImage2D(atlas.id, lvl, X >> lvl, Y >> lvl, (MODEL_TEXTURE_SIZE*3) >> lvl, (MODEL_TEXTURE_SIZE*2) >> lvl, GL_RGBA, GL_UNSIGNED_BYTE, cAddr); - cAddr += (MODEL_TEXTURE_SIZE*MODEL_TEXTURE_SIZE*3*2*4)>>(lvl<<1); - } + store.uploadModelTexture(this.modelId, this.texture); this.modelId = -1; } @@ -710,18 +697,18 @@ private BiomeUploadResult(int biomes, int models) { this.modelBiomeIndexPairs = new MemoryBuffer(models*8); } - public void upload(ModelStore store) { - this.upload(store.modelBuffer, store.modelColourBuffer); + public void upload(IModelStore store) { + this.upload(store.modelBufferHandle(), store.colourBufferHandle()); } - public void upload(GlBuffer modelBuffer, GlBuffer modelColourBuffer) { - this.biomeColourBuffer.cpyTo(UploadStream.INSTANCE.upload(modelColourBuffer, 0, this.biomeColourBuffer.size)); + public void upload(me.cortex.voxy.client.core.rendering.util.IDeviceBuffer modelBuffer, me.cortex.voxy.client.core.rendering.util.IDeviceBuffer modelColourBuffer) { + this.biomeColourBuffer.cpyTo(AbstractUploadStream.INSTANCE().upload(modelColourBuffer, 0, this.biomeColourBuffer.size)); //TODO: optimize this to like a compute scatter update or something long ptr = this.modelBiomeIndexPairs.address; for (long offset = 0; offset < this.modelBiomeIndexPairs.size; offset += 8) { long v = MemoryUtil.memGetLong(ptr);ptr += 8; - MemoryUtil.memPutInt(UploadStream.INSTANCE.upload(modelBuffer, (MODEL_SIZE*(v&((1L<<32)-1)))+ 4*6 + 4, 4), (int) (v>>>32)); + MemoryUtil.memPutInt(AbstractUploadStream.INSTANCE().upload(modelBuffer, (MODEL_SIZE*(v&((1L<<32)-1)))+ 4*6 + 4, 4), (int) (v>>>32)); } this.biomeColourBuffer.free(); diff --git a/src/main/java/me/cortex/voxy/client/core/model/ModelStore.java b/src/main/java/me/cortex/voxy/client/core/model/ModelStore.java index b2381291e..118a4e19a 100644 --- a/src/main/java/me/cortex/voxy/client/core/model/ModelStore.java +++ b/src/main/java/me/cortex/voxy/client/core/model/ModelStore.java @@ -19,7 +19,7 @@ import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; import static org.lwjgl.opengl.GL45.glBindTextureUnit; -public class ModelStore { +public class ModelStore implements IModelStore { public static final int MODEL_SIZE = 64; final GlBuffer modelBuffer; final GlBuffer modelColourBuffer; @@ -43,6 +43,7 @@ public ModelStore() { } + @Override public void free() { this.modelBuffer.free(); this.modelColourBuffer.free(); @@ -51,6 +52,42 @@ public void free() { } + @Override + public me.cortex.voxy.client.core.rendering.util.IDeviceBuffer modelBufferHandle() { + return this.modelBuffer; + } + + @Override + public me.cortex.voxy.client.core.rendering.util.IDeviceBuffer colourBufferHandle() { + return this.modelColourBuffer; + } + + @Override + public void beginTextureUploads() { + org.lwjgl.opengl.GL11.glPixelStorei(org.lwjgl.opengl.GL11.GL_UNPACK_ROW_LENGTH, 0); + org.lwjgl.opengl.GL11.glPixelStorei(org.lwjgl.opengl.GL11.GL_UNPACK_SKIP_PIXELS, 0); + org.lwjgl.opengl.GL11.glPixelStorei(org.lwjgl.opengl.GL11.GL_UNPACK_SKIP_ROWS, 0); + org.lwjgl.opengl.GL11.glPixelStorei(org.lwjgl.opengl.GL11.GL_UNPACK_ALIGNMENT, 4); + } + + @Override + public void uploadModelTexture(int modelId, me.cortex.voxy.common.util.MemoryBuffer texture) { + final int TS = ModelFactory.MODEL_TEXTURE_SIZE; + int X = (modelId&0xFF) * TS*3; + int Y = ((modelId>>8)&0xFF) * TS*2; + long cAddr = texture.address; + for (int lvl = 0; lvl < ModelFactory.LAYERS; lvl++) { + org.lwjgl.opengl.ARBDirectStateAccess.nglTextureSubImage2D(this.textures.id, lvl, X >> lvl, Y >> lvl, + (TS*3) >> lvl, (TS*2) >> lvl, + org.lwjgl.opengl.GL11.GL_RGBA, org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE, cAddr); + cAddr += (TS*TS*3*2*4)>>(lvl<<1); + } + } + + @Override + public void endTextureUploads() { + } + public void bind(int modelBindingIndex, int colourBindingIndex, int textureBindingIndex) { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, modelBindingIndex, this.modelBuffer.id); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, colourBindingIndex, this.modelColourBuffer.id); diff --git a/src/main/java/me/cortex/voxy/client/core/model/bakery/GlAtlasTextureReader.java b/src/main/java/me/cortex/voxy/client/core/model/bakery/GlAtlasTextureReader.java new file mode 100644 index 000000000..70f58d019 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/model/bakery/GlAtlasTextureReader.java @@ -0,0 +1,35 @@ +package me.cortex.voxy.client.core.model.bakery; + +import com.mojang.blaze3d.opengl.GlTexture; +import com.mojang.blaze3d.textures.GpuTexture; + +import static org.lwjgl.opengl.ARBDirectStateAccess.glGetTextureImage; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL11C.GL_RGBA; +import static org.lwjgl.opengl.GL12.GL_PACK_IMAGE_HEIGHT; +import static org.lwjgl.opengl.GL15C.glBindBuffer; +import static org.lwjgl.opengl.GL21.GL_PIXEL_PACK_BUFFER; +import static org.lwjgl.opengl.GL30C.GL_FRAMEBUFFER; +import static org.lwjgl.opengl.GL30C.glBindFramebuffer; + +//OpenGL block-atlas readback: glGetTextureImage of the atlas' mip 0. This is +// the original in-line path from SoftwareModelTextureBakery, unchanged; loaded +// only on the GL backend (see IAtlasTextureReader). +final class GlAtlasTextureReader extends IAtlasTextureReader { + @Override + public int[] read(GpuTexture tex, int width, int height) { + //Just do it ourselves as doing it with b3d has some issues, (doing it ourselves is also just much much much shorter) + var texture = new int[width * height]; + glFlush(); + glFinish(); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + glPixelStorei(GL_PACK_ROW_LENGTH, width); + glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glGetTextureImage(((GlTexture) tex).glId(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture); + return texture; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/model/bakery/IAtlasTextureReader.java b/src/main/java/me/cortex/voxy/client/core/model/bakery/IAtlasTextureReader.java new file mode 100644 index 000000000..e8a12d665 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/model/bakery/IAtlasTextureReader.java @@ -0,0 +1,35 @@ +package me.cortex.voxy.client.core.model.bakery; + +import com.mojang.blaze3d.textures.GpuTexture; + +//Backend-neutral readback of MC's stitched block atlas into a CPU int[] +// (RGBA8, one int per texel, byte order R,G,B,A — identical for GL +// RGBA/UNSIGNED_BYTE and VK_FORMAT_R8G8B8A8_UNORM). The software model-texture +// rasterizer samples this array on worker threads. +// +//The GL default lazily loads GlAtlasTextureReader on first use (there is always +// a GL context by then). When MC is on Vulkan, VkRenderCore installs the VK +// implementation via setInstance BEFORE the model bakery is constructed, so the +// GL class — and any GL classload — never happens on the VK path. Mirrors the +// AbstractUploadStream/AbstractDownloadStream seam pattern. +public abstract class IAtlasTextureReader { + /** Reads mip 0 of the given RGBA8 atlas into a fresh {@code int[width*height]}. */ + public abstract int[] read(GpuTexture atlas, int width, int height); + + private static IAtlasTextureReader INSTANCE; + + public static IAtlasTextureReader INSTANCE() { + var i = INSTANCE; + if (i == null) i = INSTANCE = new GlAtlasTextureReader(); + return i; + } + + public static void setInstance(IAtlasTextureReader instance) { + if (INSTANCE != null) throw new IllegalStateException("Atlas texture reader already initialized"); + INSTANCE = instance; + } + + public static void clearInstance() { + INSTANCE = null; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/model/bakery/SoftwareModelTextureBakery.java b/src/main/java/me/cortex/voxy/client/core/model/bakery/SoftwareModelTextureBakery.java index 61cc30fe5..59c7cf0cd 100644 --- a/src/main/java/me/cortex/voxy/client/core/model/bakery/SoftwareModelTextureBakery.java +++ b/src/main/java/me/cortex/voxy/client/core/model/bakery/SoftwareModelTextureBakery.java @@ -1,7 +1,6 @@ package me.cortex.voxy.client.core.model.bakery; import com.mojang.blaze3d.GpuFormat; -import com.mojang.blaze3d.opengl.GlTexture; import com.mojang.blaze3d.vertex.PoseStack; import me.cortex.voxy.client.core.model.ModelFactory; import me.cortex.voxy.common.util.UnsafeUtil; @@ -34,15 +33,6 @@ import java.util.ArrayList; import java.util.List; -import static org.lwjgl.opengl.ARBDirectStateAccess.glGetTextureImage; -import static org.lwjgl.opengl.GL11.*; -import static org.lwjgl.opengl.GL11C.GL_RGBA; -import static org.lwjgl.opengl.GL12.GL_PACK_IMAGE_HEIGHT; -import static org.lwjgl.opengl.GL15C.glBindBuffer; -import static org.lwjgl.opengl.GL21.GL_PIXEL_PACK_BUFFER; -import static org.lwjgl.opengl.GL30C.GL_FRAMEBUFFER; -import static org.lwjgl.opengl.GL30C.glBindFramebuffer; - public class SoftwareModelTextureBakery { //Note: the first bit of metadata is if alpha discard is enabled private static final Matrix4f[] VIEWS = new Matrix4f[6]; @@ -67,19 +57,11 @@ public void setupTexture() { int width = tex.getWidth(targetMipLevel); int height = tex.getHeight(targetMipLevel); - //Just do it ourselves as doing it with b3d has some issues, (doing it ourselves is also just much much much shorter) - var texture = new int[width * height]; - - glFlush(); - glFinish(); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - glPixelStorei(GL_PACK_ROW_LENGTH, width); - glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0); - glPixelStorei(GL_PACK_SKIP_ROWS, 0); - glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glGetTextureImage(((GlTexture) tex).glId(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture); + //Read MC's atlas back to the CPU through the active backend (GL + // glGetTextureImage or VK vkCmdCopyImageToBuffer). This class is shared + // and must stay GL-free so it can load when MC is on Vulkan — the + // readback lives behind the IAtlasTextureReader seam. + var texture = IAtlasTextureReader.INSTANCE().read(tex, width, height); this.rasterizer.setSamplerTexture(texture, width, height); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java b/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java index 4935c337b..c4e7e4bd4 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/Viewport.java @@ -12,8 +12,9 @@ public abstract class Viewport > { //public final HiZBuffer2 hiZBuffer = new HiZBuffer2(); + //Null on backends that do not use the GL helpers (pure Vulkan) public final HiZBuffer hiZBuffer; - public final DepthFramebuffer depthBoundingBuffer = new DepthFramebuffer(); + public final DepthFramebuffer depthBoundingBuffer; private static final Field planesField; static { @@ -54,7 +55,18 @@ protected Viewport(RenderProperties properties) { this.frustumPlanes = planes; this.properties = properties; - this.hiZBuffer = new HiZBuffer(properties); + if (this.useGlViewportHelpers()) { + this.hiZBuffer = new HiZBuffer(properties); + this.depthBoundingBuffer = new DepthFramebuffer(); + } else { + this.hiZBuffer = null; + this.depthBoundingBuffer = null; + } + } + + /** Overridden false by the pure-Vulkan viewport: no GL HiZ / depth-bound helpers. */ + protected boolean useGlViewportHelpers() { + return true; } public final void delete() { @@ -62,8 +74,8 @@ public final void delete() { } protected void delete0() { - this.hiZBuffer.free(); - this.depthBoundingBuffer.free(); + if (this.hiZBuffer != null) this.hiZBuffer.free(); + if (this.depthBoundingBuffer != null) this.depthBoundingBuffer.free(); } public A setVanillaProjection(Matrix4fc projection) { @@ -117,7 +129,7 @@ public A update() { (float) (this.cameraY-(sy<<5)), (float) (this.cameraZ-(sz<<5))); - if (this.depthBoundingBuffer.resize(this.width, this.height)) { + if (this.depthBoundingBuffer != null && this.depthBoundingBuffer.resize(this.width, this.height)) { this.depthBoundingBuffer.clear(this.properties.inverseClearDepth()); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/BoundRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/BoundRenderer.java index f1933a41b..d76f1dac2 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/BoundRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/BoundRenderer.java @@ -10,7 +10,7 @@ import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.rendering.Viewport; import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import net.minecraft.client.Minecraft; import org.joml.Matrix4f; import org.joml.Vector3f; @@ -63,7 +63,7 @@ public void render(Viewport viewport, IBoundStore store) { viewport.depthBoundingBuffer.clear(this.properties.inverseClearDepth()); return; } - ((AutoBindingShader)this.rasterShader).ssbo(1, store.getBuffer()); + ((AutoBindingShader)this.rasterShader).ssbo(1, (GlBuffer) store.getBuffer()); final float renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance()*16;//In blocks this.renderInner(viewport, renderDistance, count); store.postRender(viewport); @@ -75,7 +75,7 @@ private void renderInner(Viewport viewport, float renderDistanceBlocks, int c return; } - long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 128); + long ptr = AbstractUploadStream.INSTANCE().upload(this.uniformBuffer, 0, 128); long matPtr = ptr; ptr += 4*4*4; {//This is recomputed to be in chunk section space not worldsection @@ -96,7 +96,7 @@ private void renderInner(Viewport viewport, float renderDistanceBlocks, int c viewport.MVP.translate(negInnerBlock.negate(), new Matrix4f()).getToAddress(matPtr); MemoryUtil.memPutFloat(ptr, renderDistanceBlocks); ptr += 4; } - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); { @@ -113,7 +113,7 @@ private void renderInner(Viewport viewport, float renderDistanceBlocks, int c glBindVertexArray(GlVertexArray.STATIC_VAO); viewport.depthBoundingBuffer.bind(); this.rasterShader.bind(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BB_BYTE.id()); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BB_BYTE().id()); if (this.pipeline != null) this.pipeline.bindUniforms();//shader TAA //Batch the draws into groups of size 32 diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ChunkBoundStore.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ChunkBoundStore.java index 6d16b077a..620290e16 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ChunkBoundStore.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ChunkBoundStore.java @@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.Viewport; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.common.Logger; import org.lwjgl.system.MemoryUtil; @@ -52,7 +52,7 @@ public void postRender(Viewport viewport) { if (!this.addQueue.isEmpty()) { this.addQueue.forEach(this::_addPos);//TODO: REPLACE WITH SCATTER COMPUTE this.addQueue.clear(); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } } @@ -98,7 +98,7 @@ private void _addPos(long pos) { private void ensureSize1() { if (this.chunk2idx.size() < this.idx2chunk.length) return; //Commit any copies, ensures is synced to new buffer - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); int size = (int) (this.idx2chunk.length*1.5); Logger.info("Resizing chunk position buffer to: " + size); @@ -113,7 +113,7 @@ private void ensureSize1() { } private void put(int idx, long pos) { - long ptr2 = UploadStream.INSTANCE.upload(this.chunkPosBuffer, 8L*idx, 8); + long ptr2 = AbstractUploadStream.INSTANCE().upload(this.chunkPosBuffer, 8L*idx, 8); //Need to do it in 2 parts because ivec2 is 2 parts putPos(ptr2, pos); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ColumnStreamedBoundStore.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ColumnStreamedBoundStore.java index 4a85daf5b..271c62187 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ColumnStreamedBoundStore.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ColumnStreamedBoundStore.java @@ -2,7 +2,7 @@ import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.Viewport; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.common.util.MemoryBuffer; import net.caffeinemc.mods.sodium.client.render.chunk.map.ChunkTrackerHolder; import net.minecraft.client.Minecraft; @@ -24,9 +24,9 @@ public ColumnStreamedBoundStore() { public void preRender(Viewport viewport) { final float renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance()*16;//In blocks { - long addr = UploadStream.INSTANCE.uploadTo(this.chunkPosBuffer); + long addr = AbstractUploadStream.INSTANCE().uploadTo(this.chunkPosBuffer); this.count = findEmitBoundingChunks(viewport, renderDistance, (int) (this.chunkPosBuffer.size() / 8), addr); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } if (this.count<0) { this.chunkPosBuffer.free();//Destroy old diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ExactBoundStore.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ExactBoundStore.java index 4d40b3965..7be5fa9af 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ExactBoundStore.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/ExactBoundStore.java @@ -2,7 +2,7 @@ import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.Viewport; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.common.util.MemoryBuffer; import net.minecraft.client.Minecraft; import net.minecraft.core.SectionPos; @@ -22,9 +22,9 @@ public ExactBoundStore() { public void preRender(Viewport viewport) { final float renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance()*16;//In blocks { - long addr = UploadStream.INSTANCE.uploadTo(this.chunkPosBuffer); + long addr = AbstractUploadStream.INSTANCE().uploadTo(this.chunkPosBuffer); this.count = findEmitBoundingChunks(viewport, renderDistance, (int) (this.chunkPosBuffer.size() / 8), addr); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } if (this.count<0) { this.chunkPosBuffer.free();//Destroy old diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/IBoundStore.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/IBoundStore.java index dc1ea8ecb..d2ab1f270 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/IBoundStore.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/IBoundStore.java @@ -1,12 +1,13 @@ package me.cortex.voxy.client.core.rendering.bounding; -import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.Viewport; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; import me.cortex.voxy.commonImpl.VoxyCommon; import net.minecraft.core.SectionPos; public interface IBoundStore { - GlBuffer getBuffer(); + //Backend-neutral; the GL-only stores narrow the return to GlBuffer covariantly + IDeviceBuffer getBuffer(); int getCount(); default void preRender(Viewport viewport) {}; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/StreamedBoundStore.java b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/StreamedBoundStore.java index b9d7d0ce9..ade586ac8 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/bounding/StreamedBoundStore.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/bounding/StreamedBoundStore.java @@ -1,34 +1,41 @@ package me.cortex.voxy.client.core.rendering.bounding; -import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.Viewport; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; import me.cortex.voxy.common.util.UnsafeUtil; import java.util.Arrays; +import java.util.function.LongFunction; //This is a render subsystem, its very simple in what it does // it renders an AABB around loaded chunks, thats it +//Backend-neutral: the device buffer comes from the injected allocator +// (GlBuffer::new on the GL path, VkBuffer on the VK path) so the Sodium +// visibility mixins can feed this store on both backends. public final class StreamedBoundStore implements IBoundStore { private static final int INIT_MAX_CHUNK_COUNT = 1<<12; - private GlBuffer chunkPosBuffer = new GlBuffer(INIT_MAX_CHUNK_COUNT*8);//Stored as ivec2 + private final LongFunction allocator; + private IDeviceBuffer chunkPosBuffer;//Stored as ivec2 private int count;//NOTE: count here is in INTS, not LONGS (i.e. 1 pos is 2 ints) private boolean didChange = false; private int[] visibleSections = new int[INIT_MAX_CHUNK_COUNT*2]; - public StreamedBoundStore() { + public StreamedBoundStore(LongFunction allocator) { + this.allocator = allocator; + this.chunkPosBuffer = allocator.apply(INIT_MAX_CHUNK_COUNT*8); } //Bind and render, changing as little gl state as possible so that the caller may configure how it wants to render @Override public void preRender(Viewport viewport) { if (this.count == 0 || !this.didChange) return; - if (this.count*4L>this.chunkPosBuffer.size()) { + if (this.count*4L>this.chunkPosBuffer.sizeBytes()) { this.chunkPosBuffer.free(); - this.chunkPosBuffer = new GlBuffer(((long) Math.ceil(this.count*1.25))*4); + this.chunkPosBuffer = this.allocator.apply(((long) Math.ceil(this.count*1.25))*4); } - long addr = UploadStream.INSTANCE.upload(this.chunkPosBuffer, 0, this.count*4); + long addr = AbstractUploadStream.INSTANCE().upload(this.chunkPosBuffer, 0, this.count*4); UnsafeUtil.memcpy(this.visibleSections, this.count, addr); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); this.didChange = false; } @@ -51,7 +58,7 @@ public void free() { } @Override - public GlBuffer getBuffer() { + public IDeviceBuffer getBuffer() { return this.chunkPosBuffer; } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java index 383dd178d..3c8bd0e41 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java @@ -14,8 +14,10 @@ import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; import me.cortex.voxy.client.core.rendering.section.geometry.BasicAsyncGeometryManager; import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; +import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.AllocationArena; import me.cortex.voxy.common.util.MemoryBuffer; @@ -86,13 +88,14 @@ public class AsyncNodeManager { private boolean needsWaitForSync = false; - public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGenerationService renderService) { + public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGenerationService renderService, INodeGpuOps gpuOps) { + this.gpuOps = gpuOps; //Note the current implmentation of ISectionWatcher is threadsafe //Note: geometry data is the data store/source, not the management, it is just a raw store of data // it MUST ONLY be accessed on the render thread // AsyncNodeManager will use an AsyncGeometryManager as the manager for the data store, and sync the results on the render thread this.geometryData = geometryData; - this.geometryCapacity = ((BasicSectionGeometryData)geometryData).getGeometryCapacityBytes(); + this.geometryCapacity = ((IBasicGeometryData)geometryData).getGeometryCapacityBytes(); this.maxNodeCount = maxNodeCount; @@ -115,7 +118,7 @@ public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGene }); this.thread.setName("Async Node Manager"); - this.geometryManager = new BasicAsyncGeometryManager(((BasicSectionGeometryData)geometryData).getMaxSectionCount(), this.geometryCapacity); + this.geometryManager = new BasicAsyncGeometryManager(((IBasicGeometryData)geometryData).getMaxSectionCount(), this.geometryCapacity); this.router = new SectionUpdateRouter(); this.router.setCallbacks(pos->{//On initial render gen, try get from geometry cache @@ -177,19 +180,7 @@ private SyncResults getMakeResultObject() { return resultSet; } - private final Shader scatterWrite = Shader.make() - .define("INPUT_BUFFER_BINDING", 0) - .define("OUTPUT_BUFFER1_BINDING", 1) - .define("OUTPUT_BUFFER2_BINDING", 2) - .add(ShaderType.COMPUTE, "voxy:util/scatter.comp") - .compile(); - - private final Shader multiMemcpy = Shader.make() - .define("INPUT_HEADER_BUFFER_BINDING", 0) - .define("INPUT_DATA_BUFFER_BINDING", 1) - .define("OUTPUT_BUFFER_BINDING", 2) - .add(ShaderType.COMPUTE, "voxy:util/memcpy.comp") - .compile(); + private final INodeGpuOps gpuOps; private void run() { if (this.workCounter.get() <= 0) { @@ -511,7 +502,7 @@ private void run() { private IntConsumer tlnAddCallback; private IntConsumer tlnRemoveCallback; //Render thread synchronization - public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass nodeBuffer here??, do something else thats better + public void tick(me.cortex.voxy.client.core.rendering.util.IDeviceBuffer nodeBuffer, INodeCleaner cleaner) {//TODO: dont pass nodeBuffer here??, do something else thats better if (this.uncaughtException != null) { throw new RuntimeException(this.uncaughtException);//Propagate internal exception } @@ -535,37 +526,17 @@ public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass no } {//Update basic geometry data - var store = (BasicSectionGeometryData)this.geometryData; + var store = (IBasicGeometryData)this.geometryData; store.setSectionCount(results.geometrySectionCount); var upload = results.geometryUpload; if (!upload.dataUploadPoints.isEmpty()) { - ((BasicSectionGeometryData)this.geometryData).ensureAccessable(upload.maxElementAccess); + store.ensureAccessable(upload.maxElementAccess); TimingStatistics.A.start(); - int copies = upload.dataUploadPoints.size(); - int upCopies = UploadStream.alignUpAlloc(copies*16); int scratchSize = (int) upload.arena.getSize() * 8; - int upScratchSize = UploadStream.alignUpAlloc(scratchSize); - long ptr = UploadStream.INSTANCE.rawUploadAddress(upScratchSize + upCopies); - UnsafeUtil.memcpy(upload.scratchHeaderBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr, copies * 16L); - UnsafeUtil.memcpy(upload.scratchDataBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr + upCopies, scratchSize); - UploadStream.INSTANCE.commit();//Commit the buffer - - this.multiMemcpy.bind(); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, UploadStream.INSTANCE.getRawBufferId(), ptr, upCopies); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, UploadStream.INSTANCE.getRawBufferId(), ptr+upCopies, upScratchSize); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((BasicSectionGeometryData) this.geometryData).getGeometryBuffer().id); - - if (copies > 500) { - Logger.warn("Large amount of copies, lag will probably happen: " + copies); - } - - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - glDispatchCompute(copies, 1, 1);//Execute the copies - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - + this.gpuOps.multiMemcpy(upload.scratchHeaderBuffer.address, copies, upload.scratchDataBuffer.address, scratchSize, store); TimingStatistics.A.stop(); } } @@ -573,20 +544,7 @@ public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass no TimingStatistics.B.start(); if (!results.scatterWriteLocationMap.isEmpty()) {//Scatter write int count = results.scatterWriteLocationMap.size();//Number of writes, not chunks or uvec4 count - int chunks = (count+3)/4; - int streamSize = chunks*80;//80 bytes per chunk, it is guaranteed the buffer is big enough - long ptr = UploadStream.INSTANCE.rawUploadAddress(streamSize);//Internally implicitly aligned alloc - MemoryUtil.memCopy(results.scatterWriteBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr, streamSize); - UploadStream.INSTANCE.commit();//Commit the buffer - - this.scatterWrite.bind(); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, UploadStream.INSTANCE.getRawBufferId(), ptr, UploadStream.alignUpAlloc(streamSize)); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeBuffer.id); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((BasicSectionGeometryData) this.geometryData).getMetadataBuffer().id); - glUniform1ui(0, count); - glMemoryBarrier(GL_UNIFORM_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT); - glDispatchCompute((count+127)/128, 1, 1); - glMemoryBarrier(GL_UNIFORM_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT); + this.gpuOps.scatterWrite(results.scatterWriteBuffer.address, count, nodeBuffer, (IBasicGeometryData)this.geometryData); } TimingStatistics.B.stop(); @@ -780,11 +738,23 @@ public void stop() { result.scatterWriteBuffer.free(); } - this.scatterWrite.free(); - this.multiMemcpy.free(); + this.gpuOps.free(); this.geometryCache.free(); } + //Emit the per-frame "same scene?" gate metrics for A/B log comparison. The + // OpenGL and Vulkan paths log byte-identical values by construction — if + // they don't match, the two captures weren't the same viewpoint/world state + // and any downstream (GPU-computed) comparison is moot. Also advances + // CmpLog's frame counter (call once per rendered frame). + public void logCompareStats() { + if (!CmpLog.ENABLED) return; + CmpLog.nextFrame(); + CmpLog.rec("nodeManager", "sectionCount", this.geometryData.getSectionCount()); + CmpLog.rec("nodeManager", "usedGeometry", this.getUsedGeometryCapacity()); + CmpLog.rec("nodeManager", "geometryCapacity", this.getGeometryCapacity()); + } + public void addDebug(List debug) { debug.add("UC/GC,#N: " + (this.getUsedGeometryCapacity()/(1<<20))+"/"+(this.getGeometryCapacity()/(1<<20)) + "," + (this.geometryData.getSectionCount())); //debug.add("GUQ/NRC: " + this.geometryUpdateQueue.size()+"/"+this.removeBatchQueue.size()); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/DebugRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/DebugRenderer.java index 6954b882d..83ea2c766 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/DebugRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/DebugRenderer.java @@ -6,7 +6,7 @@ import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.rendering.Viewport; import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import net.minecraft.util.Mth; import org.joml.Matrix4f; import org.joml.Vector3f; @@ -37,7 +37,7 @@ public class DebugRenderer { private final GlBuffer drawBuffer = new GlBuffer(1024).zero(); private void uploadUniform(Viewport viewport) { - long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 1024); + long ptr = AbstractUploadStream.INSTANCE().upload(this.uniformBuffer, 0, 1024); int sx = Mth.floor(viewport.cameraX)>>5; int sy = Mth.floor(viewport.cameraY)>>5; int sz = Mth.floor(viewport.cameraZ)>>5; @@ -57,7 +57,7 @@ private void uploadUniform(Viewport viewport) { public void render(Viewport viewport, GlBuffer nodeData, GlBuffer nodeList) { this.uploadUniform(viewport); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); this.setupShader.bind(); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, this.drawBuffer.id); @@ -69,7 +69,7 @@ public void render(Viewport viewport, GlBuffer nodeData, GlBuffer nodeList) { this.debugShader.bind(); glBindVertexArray(GlVertexArray.STATIC_VAO); glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.drawBuffer.id); - GL15.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE.id()); + GL15.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE().id()); glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeData.id); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, nodeList.id); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/GlNodeGpuOps.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/GlNodeGpuOps.java new file mode 100644 index 000000000..e0c1b84f2 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/GlNodeGpuOps.java @@ -0,0 +1,85 @@ +package me.cortex.voxy.client.core.rendering.hierachical; + +import me.cortex.voxy.client.core.gl.GlBuffer; +import me.cortex.voxy.client.core.gl.shader.Shader; +import me.cortex.voxy.client.core.gl.shader.ShaderType; +import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.UnsafeUtil; +import org.lwjgl.system.MemoryUtil; + +import static org.lwjgl.opengl.ARBUniformBufferObject.glBindBufferBase; +import static org.lwjgl.opengl.GL30C.glBindBufferRange; +import static org.lwjgl.opengl.GL30C.glUniform1ui; +import static org.lwjgl.opengl.GL42C.GL_UNIFORM_BARRIER_BIT; +import static org.lwjgl.opengl.GL42C.glMemoryBarrier; +import static org.lwjgl.opengl.GL43C.*; + +//OpenGL implementation of the node-manager GPU ops (moved verbatim from +// AsyncNodeManager). +public class GlNodeGpuOps implements INodeGpuOps { + private final Shader scatterWrite = Shader.make() + .define("INPUT_BUFFER_BINDING", 0) + .define("OUTPUT_BUFFER1_BINDING", 1) + .define("OUTPUT_BUFFER2_BINDING", 2) + .add(ShaderType.COMPUTE, "voxy:util/scatter.comp") + .compile(); + + private final Shader multiMemcpy = Shader.make() + .define("INPUT_HEADER_BUFFER_BINDING", 0) + .define("INPUT_DATA_BUFFER_BINDING", 1) + .define("OUTPUT_BUFFER_BINDING", 2) + .add(ShaderType.COMPUTE, "voxy:util/memcpy.comp") + .compile(); + + @Override + public void multiMemcpy(long headerPtr, int copies, long dataPtr, int dataSize, IBasicGeometryData geometry) { + var stream = AbstractUploadStream.INSTANCE(); + int upCopies = stream.alignUpAlloc(copies * 16); + int upScratchSize = stream.alignUpAlloc(dataSize); + long ptr = stream.rawUploadAddress(upScratchSize + upCopies); + UnsafeUtil.memcpy(headerPtr, stream.getBaseAddress() + ptr, copies * 16L); + UnsafeUtil.memcpy(dataPtr, stream.getBaseAddress() + ptr + upCopies, dataSize); + stream.commit();//Commit the buffer + + this.multiMemcpy.bind(); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, stream.getRawBufferId(), ptr, upCopies); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, stream.getRawBufferId(), ptr + upCopies, upScratchSize); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((GlBuffer) geometry.geometryBufferHandle()).id); + + if (copies > 500) { + Logger.warn("Large amount of copies, lag will probably happen: " + copies); + } + + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + glDispatchCompute(copies, 1, 1);//Execute the copies + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + @Override + public void scatterWrite(long chunksPtr, int count, IDeviceBuffer nodeBuffer, IBasicGeometryData geometry) { + var stream = AbstractUploadStream.INSTANCE(); + int chunks = (count + 3) / 4; + int streamSize = chunks * 80;//80 bytes per chunk, it is guaranteed the buffer is big enough + long ptr = stream.rawUploadAddress(streamSize);//Internally implicitly aligned alloc + MemoryUtil.memCopy(chunksPtr, stream.getBaseAddress() + ptr, streamSize); + stream.commit();//Commit the buffer + + this.scatterWrite.bind(); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, stream.getRawBufferId(), ptr, stream.alignUpAlloc(streamSize)); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ((GlBuffer) nodeBuffer).id); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((GlBuffer) geometry.metadataBufferHandle()).id); + glUniform1ui(0, count); + glMemoryBarrier(GL_UNIFORM_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); + glDispatchCompute((count + 127) / 128, 1, 1); + glMemoryBarrier(GL_UNIFORM_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); + } + + @Override + public void free() { + this.scatterWrite.free(); + this.multiMemcpy.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 9a807fe7b..2abe26b7f 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -11,9 +11,10 @@ import me.cortex.voxy.client.core.gl.shader.ShaderType; import me.cortex.voxy.client.core.rendering.Viewport; import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; @@ -193,7 +194,7 @@ private static void setFrustum(Viewport viewport, long ptr) { } private void uploadUniform(Viewport viewport) { - long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 1024); + long ptr = AbstractUploadStream.INSTANCE().upload(this.uniformBuffer, 0, 1024); viewport.MVP.getToAddress(ptr); ptr += 4*4*4; @@ -242,7 +243,7 @@ private void bindings(Viewport viewport) { public void doTraversal(Viewport viewport) { this.uploadUniform(viewport); - //UploadStream.INSTANCE.commit(); //Done inside traversal + //AbstractUploadStream.INSTANCE().commit(); //Done inside traversal this.traversal.bind(); this.bindings(viewport); @@ -265,7 +266,7 @@ public void doTraversal(Viewport viewport) { this.downloadResetRequestQueue(); if (RenderStatistics.enabled) { - DownloadStream.INSTANCE.download(this.statisticsBuffer, down->{ + AbstractDownloadStream.INSTANCE().download(this.statisticsBuffer, down->{ for (int i = 0; i < MAX_ITERATIONS; i++) { RenderStatistics.hierarchicalTraversalCounts[i] = MemoryUtil.memGetInt(down.address+i*4L); } @@ -300,7 +301,7 @@ private void traverseInternal() { glClearNamedBufferSubData(this.queueMetaBuffer.id, GL_RGBA32UI, 0, 16, GL_RGBA, GL_UNSIGNED_INT, new int[]{firstDispatchSize,1,1,initialQueueSize}); */ {//TODO:FIXME: THIS IS BULLSHIT BY INTEL need to fix the clearing - long ptr = UploadStream.INSTANCE.upload(this.queueMetaBuffer, 0, 16*MAX_ITERATIONS); + long ptr = AbstractUploadStream.INSTANCE().upload(this.queueMetaBuffer, 0, 16*MAX_ITERATIONS); MemoryUtil.memPutInt(ptr + 0, firstDispatchSize); MemoryUtil.memPutInt(ptr + 4, 1); MemoryUtil.memPutInt(ptr + 8, 1); @@ -311,7 +312,7 @@ private void traverseInternal() { MemoryUtil.memPutInt(ptr + (i*16)+ 8, 1); MemoryUtil.memPutInt(ptr + (i*16)+12, 0); } - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } //Execute first iteration @@ -349,7 +350,7 @@ private void traverseInternal() { private void downloadResetRequestQueue() { glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - DownloadStream.INSTANCE.download(this.requestBuffer, this::forwardDownloadResult); + AbstractDownloadStream.INSTANCE().download(this.requestBuffer, this::forwardDownloadResult); nglClearNamedBufferSubData(this.requestBuffer.id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0); } @@ -371,6 +372,8 @@ private void forwardDownloadResult(long ptr, long size) { //if (count > REQUEST_QUEUE_SIZE) { // Logger.warn("Count larger than 'maxRequestCount', overflow captured. Overflowed by " + (count-REQUEST_QUEUE_SIZE)); //} + CmpLog.rec("traversal", "requestCount", count); + CmpLog.rec("traversal", "topNodeCount", this.topNodeCount); if (count != 0) { var buffer = new MemoryBuffer(count*8L+8).cpyFrom(ptr-8); //Write back the exact count into the new memory buffer (not the download stream buffer) diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeCleaner.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeCleaner.java new file mode 100644 index 000000000..11178637b --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeCleaner.java @@ -0,0 +1,13 @@ +package me.cortex.voxy.client.core.rendering.hierachical; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; + +//Backend-neutral contract of the GPU node cleaner (GL NodeCleaner / VkNodeCleaner). +public interface INodeCleaner { + void tick(IDeviceBuffer nodeDataBuffer); + + void updateIds(IntOpenHashSet collection); + + void free(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeGpuOps.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeGpuOps.java new file mode 100644 index 000000000..5dfad930f --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/INodeGpuOps.java @@ -0,0 +1,22 @@ +package me.cortex.voxy.client.core.rendering.hierachical; + +import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; + +//The two GPU operations AsyncNodeManager needs each render-thread sync, +// abstracted per backend (GlNodeGpuOps / VkNodeGpuOps): +// +// - multiMemcpy: stream (header,data) scratch to the GPU and scatter-copy the +// described ranges into the geometry buffer (voxy:util/memcpy.comp); +// - scatterWrite: stream packed (location,value) chunks and scatter them into +// the node buffer + section metadata buffer (voxy:util/scatter.comp). +// +//Source pointers are CPU addresses owned by the caller; implementations stage +// them through their upload stream and dispatch compute. +public interface INodeGpuOps { + void multiMemcpy(long headerPtr, int copies, long dataPtr, int dataSize, IBasicGeometryData geometry); + + void scatterWrite(long chunksPtr, int count, IDeviceBuffer nodeBuffer, IBasicGeometryData geometry); + + void free(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java index 0664bcad0..24b1c8be2 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java @@ -5,9 +5,9 @@ import me.cortex.voxy.client.core.gl.shader.AutoBindingShader; import me.cortex.voxy.client.core.gl.shader.Shader; import me.cortex.voxy.client.core.gl.shader.ShaderType; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import org.lwjgl.opengl.ARBDirectStateAccess; import org.lwjgl.system.MemoryUtil; @@ -23,7 +23,7 @@ //TODO : USE THIS IN HierarchicalOcclusionTraverser instead of other shit -public class NodeCleaner { +public class NodeCleaner implements INodeCleaner { //TODO: use batch_visibility_set to clear visibility data when nodes are removed!! (TODO: nodeManager will need to forward info to this) @@ -101,7 +101,9 @@ public void free(int id) { } - public void tick(GlBuffer nodeDataBuffer) { + @Override + public void tick(me.cortex.voxy.client.core.rendering.util.IDeviceBuffer nodeDataBufferIn) { + GlBuffer nodeDataBuffer = (GlBuffer) nodeDataBufferIn; this.visibilityId++; if (this.shouldCleanGeometry()) { this.outputBuffer.fill(this.nodeManager.maxNodeCount - 2);//TODO: maybe dont set to zero?? @@ -126,7 +128,7 @@ public void tick(GlBuffer nodeDataBuffer) { glDispatchCompute(1, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - DownloadStream.INSTANCE.download(this.outputBuffer, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT, + AbstractDownloadStream.INSTANCE().download(this.outputBuffer, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT, buffer -> this.nodeManager.submitRemoveBatch(buffer.copy())//Copy into buffer and emit to node manager ); } @@ -143,20 +145,21 @@ private boolean shouldCleanGeometry() { } } + @Override public void updateIds(IntOpenHashSet collection) { if (!collection.isEmpty()) { int count = collection.size(); - long addr = UploadStream.INSTANCE.rawUploadAddress(count*4);//Internally does upsizing alignement + long addr = AbstractUploadStream.INSTANCE().rawUploadAddress(count*4);//Internally does upsizing alignement - long ptr = UploadStream.INSTANCE.getBaseAddress() + addr; + long ptr = AbstractUploadStream.INSTANCE().getBaseAddress() + addr; var iter = collection.iterator(); while (iter.hasNext()) { MemoryUtil.memPutInt(ptr, iter.nextInt()); ptr+=4; } - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); this.batchClear.bind(); - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, UploadStream.INSTANCE.getRawBufferId(), addr, UploadStream.alignUpAlloc(count*4)); + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, AbstractUploadStream.INSTANCE().getRawBufferId(), addr, AbstractUploadStream.INSTANCE().alignUpAlloc(count*4)); glUniform1ui(0, count); glUniform1ui(1, this.visibilityId); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); @@ -181,6 +184,7 @@ private void dumpDebugData() { int a = 0; } + @Override public void free() { this.sorter.free(); this.visibilityBuffer.free(); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java index 0c91b2b29..02891c5c6 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java @@ -10,7 +10,7 @@ import me.cortex.voxy.client.core.rendering.ISectionWatcher; import me.cortex.voxy.client.core.rendering.building.BuiltSection; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryManager; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.client.core.util.ExpandingObjectAllocationList; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.MemoryBuffer; @@ -1354,13 +1354,13 @@ private void clearGeometryInternal(long pos, int nodeId) { } //================================================================================================================== - public boolean writeChanges(GlBuffer nodeBuffer) { + public boolean writeChanges(me.cortex.voxy.client.core.rendering.util.IDeviceBuffer nodeBuffer) { //TODO: use like compute based copy system or something // since microcopies are bad if (this.nodeUpdates.isEmpty()) { return false; } - this.nodeUpdates.forEach((int i) -> this.nodeData.writeNode(UploadStream.INSTANCE.upload(nodeBuffer, i*16L, 16L), i)); + this.nodeUpdates.forEach((int i) -> this.nodeData.writeNode(AbstractUploadStream.INSTANCE().upload(nodeBuffer, i*16L, 16L), i)); this.nodeUpdates.clear(); return true; } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/post/FullscreenBlit.java b/src/main/java/me/cortex/voxy/client/core/rendering/post/FullscreenBlit.java index 1166123ed..8fc5b4378 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/post/FullscreenBlit.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/post/FullscreenBlit.java @@ -45,7 +45,7 @@ public void bind() { public void blit() { glBindVertexArray(EMPTY_VAO); this.shader.bind(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE.id()); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE().id()); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, 0); glBindVertexArray(0); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/AbstractSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/AbstractSectionRenderer.java index 60e1c2a27..ccc8ab379 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/AbstractSectionRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/AbstractSectionRenderer.java @@ -5,6 +5,7 @@ import me.cortex.voxy.client.core.RenderProperties; import me.cortex.voxy.client.core.gl.shader.Shader; import me.cortex.voxy.client.core.gl.shader.ShaderType; +import me.cortex.voxy.client.core.model.IModelStore; import me.cortex.voxy.client.core.model.ModelStore; import me.cortex.voxy.client.core.rendering.Viewport; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; @@ -17,11 +18,11 @@ //Takes in mesh ids from the hierachical traversal and may perform more culling then renders it public abstract class AbstractSectionRenderer , J extends IGeometryData> { public interface FactoryConstructor, GEODATA extends IGeometryData> { - AbstractSectionRenderer create(AbstractRenderPipeline pipeline, ModelStore modelStore, GEODATA geometryData); + AbstractSectionRenderer create(AbstractRenderPipeline pipeline, IModelStore modelStore, GEODATA geometryData); } public record Factory, GEODATA extends IGeometryData>(Class> clz, FactoryConstructor constructor) { - public AbstractSectionRenderer create(AbstractRenderPipeline pipeline, ModelStore store, IGeometryData geometryData) { + public AbstractSectionRenderer create(AbstractRenderPipeline pipeline, IModelStore store, IGeometryData geometryData) { return this.constructor.create(pipeline, store, (GEODATA) geometryData); } @@ -33,7 +34,7 @@ public static , GEODATA2 extends IGeometry } var constructor = constructors[0]; var params = constructor.getParameterTypes(); - if (params.length != 3 || params[0] != AbstractRenderPipeline.class || params[1] != ModelStore.class || !IGeometryData.class.isAssignableFrom(params[2])) { + if (params.length != 3 || params[0] != AbstractRenderPipeline.class || params[1] != IModelStore.class || !IGeometryData.class.isAssignableFrom(params[2])) { Logger.error("Render backend " + clz.getCanonicalName() + " had invalid constructor"); return null; } @@ -49,9 +50,9 @@ public static , GEODATA2 extends IGeometry protected final J geometryManager; - protected final ModelStore modelStore; + protected final IModelStore modelStore; protected final RenderProperties properties; - protected AbstractSectionRenderer(RenderProperties properties, ModelStore modelStore, J geometryManager) { + protected AbstractSectionRenderer(RenderProperties properties, IModelStore modelStore, J geometryManager) { this.properties = properties; this.geometryManager = geometryManager; this.modelStore = modelStore; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/mdic/MDICSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/mdic/MDICSectionRenderer.java index 8a18fedfd..b6c9f30ab 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/mdic/MDICSectionRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/mdic/MDICSectionRenderer.java @@ -10,13 +10,14 @@ import me.cortex.voxy.client.core.gl.shader.Shader; import me.cortex.voxy.client.core.gl.shader.ShaderLoader; import me.cortex.voxy.client.core.gl.shader.ShaderType; +import me.cortex.voxy.client.core.model.IModelStore; import me.cortex.voxy.client.core.model.ModelStore; import me.cortex.voxy.client.core.rendering.section.backend.AbstractSectionRenderer; import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.rendering.util.LightMapHelper; import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.client.core.util.GPUTiming; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.world.WorldEngine; @@ -95,7 +96,7 @@ public class MDICSectionRenderer extends AbstractSectionRenderer{ + AbstractDownloadStream.INSTANCE().download(this.statisticsBuffer, down->{ final int LAYERS = WorldEngine.MAX_LOD_LAYER+1; for (int i = 0; i < LAYERS; i++) { RenderStatistics.visibleSections[i] = MemoryUtil.memGetInt(down.address+i*4L); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java deleted file mode 100644 index 1cb0d13f7..000000000 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanSectionRenderer.java +++ /dev/null @@ -1,284 +0,0 @@ -package me.cortex.voxy.client.core.rendering.section.backend.vulkan; - -import me.cortex.voxy.client.core.AbstractRenderPipeline; -import me.cortex.voxy.client.core.gl.shader.ShaderLoader; -import me.cortex.voxy.client.core.gl.shader.ShaderType; -import me.cortex.voxy.client.core.model.ModelStore; -import me.cortex.voxy.client.core.rendering.section.backend.AbstractSectionRenderer; -import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData; -import me.cortex.voxy.client.core.vk.ShadercCompiler; -import me.cortex.voxy.client.core.vk.VulkanBackend; -import me.cortex.voxy.client.core.vk.VulkanContext; -import me.cortex.voxy.common.Logger; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.vulkan.*; - -import java.nio.ByteBuffer; -import java.util.List; - -import static me.cortex.voxy.client.core.vk.VkUtil.check; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.vulkan.KHRDrawIndirectCount.vkCmdDrawIndexedIndirectCountKHR; -import static org.lwjgl.vulkan.VK12.vkCmdDrawIndexedIndirectCount; -import static org.lwjgl.vulkan.VK10.*; - -/** - * PHASE-1 HYBRID Vulkan backend. - * - * What runs where: - * - hierarchical traversal, prep/cull/prefix/cmdgen compute: GL (unchanged, bit-exact), - * writing into VK-allocated SharedBuffers owned by {@link VulkanViewport}. - * - opaque/translucent terrain draws: VK, vkCmdDrawIndexedIndirectCount over the - * shared draw-call/draw-count buffers, into shared color/depth images. - * - composite: GL samples the shared images back into MC's framebuffer - * (semaphore-ordered: GL signal -> VK wait, VK signal -> GL wait). - * - * KNOWN PHASE BOUNDARY (guarded, not hidden): the vertex-pulling SSBOs - * (geometry buffer, metadata, model store) are still plain GlBuffers today. - * Until BasicSectionGeometryManager/ModelStore allocate through SharedBuffer - * when this backend is active, the VK draw stage cannot see the geometry and - * this renderer refuses to draw (logs once, renders nothing) instead of - * corrupting. That wiring is the next incremental step and is deliberately not - * faked here. - */ -public class VulkanSectionRenderer extends AbstractSectionRenderer { - public static final Factory FACTORY = - AbstractSectionRenderer.Factory.create(VulkanSectionRenderer.class); - - private final VulkanContext ctx = VulkanBackend.context(); - private final AbstractRenderPipeline pipeline; - - private final long vertShaderModule; - private final long fragShaderModule; - private long renderPass; - private long pipelineLayout; - private long graphicsPipeline; - private long framebuffer; - private int fbWidth = -1, fbHeight = -1; - private final VkCommandBuffer cmd; - private final long fence; - private boolean geometryShared = false; //flipped when geometry SSBO sharing lands - private boolean warnedNoGeometry = false; - - public VulkanSectionRenderer(AbstractRenderPipeline pipeline, ModelStore modelStore, BasicSectionGeometryData geometryData) { - super(pipeline.properties, modelStore, geometryData); - this.pipeline = pipeline; - - //Single-source shaders: same GLSL assets as the GL path, VOXY_VULKAN defined. - String vertex = ShaderLoader.parse("voxy:lod/gl46/quads3.vert"); - String frag = ShaderLoader.parse("voxy:lod/gl46/quads.frag"); - this.vertShaderModule = createModule(ShadercCompiler.compile(vertex, ShaderType.VERTEX, "quads3.vert")); - this.fragShaderModule = createModule(ShadercCompiler.compile(frag, ShaderType.FRAGMENT, "quads.frag")); - - try (MemoryStack stack = stackPush()) { - var cbai = VkCommandBufferAllocateInfo.calloc(stack).sType$Default() - .commandPool(this.ctx.commandPool) - .level(VK_COMMAND_BUFFER_LEVEL_PRIMARY) - .commandBufferCount(1); - var pCmd = stack.mallocPointer(1); - check(vkAllocateCommandBuffers(this.ctx.device, cbai, pCmd), "vkAllocateCommandBuffers"); - this.cmd = new VkCommandBuffer(pCmd.get(0), this.ctx.device); - var fci = VkFenceCreateInfo.calloc(stack).sType$Default().flags(VK_FENCE_CREATE_SIGNALED_BIT); - var pFence = stack.mallocLong(1); - check(vkCreateFence(this.ctx.device, fci, null, pFence), "vkCreateFence"); - this.fence = pFence.get(0); - } - Logger.info("VulkanSectionRenderer initialized on " + this.ctx.deviceName + " (phase-1 hybrid)"); - } - - private long createModule(ByteBuffer spirv) { - try (MemoryStack stack = stackPush()) { - var smci = VkShaderModuleCreateInfo.calloc(stack).sType$Default().pCode(spirv); - var pModule = stack.mallocLong(1); - check(vkCreateShaderModule(this.ctx.device, smci, null, pModule), "vkCreateShaderModule"); - return pModule.get(0); - } - } - - private void ensurePipeline(VulkanViewport viewport) { - if (!viewport.ensureTargets() && this.graphicsPipeline != 0 - && this.fbWidth == viewport.width && this.fbHeight == viewport.height) return; - destroyPipelineObjects(); - try (MemoryStack stack = stackPush()) { - //Render pass: color RGBA8 + depth D32, cleared each frame; VK depth range is 0..1 which - //matches properties.isZero2One() on the GL side via the reverse-Z aware compare below. - var attachments = VkAttachmentDescription.calloc(2, stack); - attachments.get(0).format(viewport.color.vkFormat).samples(VK_SAMPLE_COUNT_1_BIT) - .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR).storeOp(VK_ATTACHMENT_STORE_OP_STORE) - .stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE).stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) - .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED).finalLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - attachments.get(1).format(viewport.depth.vkFormat).samples(VK_SAMPLE_COUNT_1_BIT) - .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR).storeOp(VK_ATTACHMENT_STORE_OP_STORE) - .stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE).stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) - .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED).finalLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - var colorRef = VkAttachmentReference.calloc(1, stack).attachment(0).layout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - var depthRef = VkAttachmentReference.calloc(stack).attachment(1).layout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); - var subpass = VkSubpassDescription.calloc(1, stack) - .pipelineBindPoint(VK_PIPELINE_BIND_POINT_GRAPHICS) - .colorAttachmentCount(1).pColorAttachments(colorRef).pDepthStencilAttachment(depthRef); - var rpci = VkRenderPassCreateInfo.calloc(stack).sType$Default() - .pAttachments(attachments).pSubpasses(subpass); - var pRp = stack.mallocLong(1); - check(vkCreateRenderPass(this.ctx.device, rpci, null, pRp), "vkCreateRenderPass"); - this.renderPass = pRp.get(0); - - var fbci = VkFramebufferCreateInfo.calloc(stack).sType$Default() - .renderPass(this.renderPass) - .pAttachments(stack.longs(viewport.color.vkView, viewport.depth.vkView)) - .width(viewport.width).height(viewport.height).layers(1); - var pFb = stack.mallocLong(1); - check(vkCreateFramebuffer(this.ctx.device, fbci, null, pFb), "vkCreateFramebuffer"); - this.framebuffer = pFb.get(0); - - //Pipeline layout: descriptor sets for the vertex-pulling SSBOs are wired in the - //geometry-sharing step; until then an empty layout is sufficient for pipeline creation. - var plci = VkPipelineLayoutCreateInfo.calloc(stack).sType$Default(); - var pPl = stack.mallocLong(1); - check(vkCreatePipelineLayout(this.ctx.device, plci, null, pPl), "vkCreatePipelineLayout"); - this.pipelineLayout = pPl.get(0); - - var stages = VkPipelineShaderStageCreateInfo.calloc(2, stack); - stages.get(0).sType$Default().stage(VK_SHADER_STAGE_VERTEX_BIT).module(this.vertShaderModule).pName(stack.UTF8("main")); - stages.get(1).sType$Default().stage(VK_SHADER_STAGE_FRAGMENT_BIT).module(this.fragShaderModule).pName(stack.UTF8("main")); - - var vertexInput = VkPipelineVertexInputStateCreateInfo.calloc(stack).sType$Default();//vertex pulling: no attributes - var inputAssembly = VkPipelineInputAssemblyStateCreateInfo.calloc(stack).sType$Default() - .topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); - var viewportState = VkPipelineViewportStateCreateInfo.calloc(stack).sType$Default() - .viewportCount(1).pViewports(VkViewport.calloc(1, stack) - .x(0).y(0).width(viewport.width).height(viewport.height).minDepth(0).maxDepth(1)) - .scissorCount(1).pScissors(VkRect2D.calloc(1, stack) - .extent(e -> e.width(viewport.width).height(viewport.height))); - var raster = VkPipelineRasterizationStateCreateInfo.calloc(stack).sType$Default() - .polygonMode(VK_POLYGON_MODE_FILL).cullMode(VK_CULL_MODE_NONE)//MDIC disables cull face - .frontFace(VK_FRONT_FACE_COUNTER_CLOCKWISE).lineWidth(1); - var msaa = VkPipelineMultisampleStateCreateInfo.calloc(stack).sType$Default() - .rasterizationSamples(VK_SAMPLE_COUNT_1_BIT); - var depthState = VkPipelineDepthStencilStateCreateInfo.calloc(stack).sType$Default() - .depthTestEnable(true).depthWriteEnable(true) - .depthCompareOp(this.properties.isReverseZ() ? VK_COMPARE_OP_GREATER_OR_EQUAL : VK_COMPARE_OP_LESS_OR_EQUAL); - var blendAttach = VkPipelineColorBlendAttachmentState.calloc(1, stack) - .colorWriteMask(0xF).blendEnable(false); - var blend = VkPipelineColorBlendStateCreateInfo.calloc(stack).sType$Default().pAttachments(blendAttach); - - var gpci = VkGraphicsPipelineCreateInfo.calloc(1, stack).sType$Default() - .pStages(stages) - .pVertexInputState(vertexInput) - .pInputAssemblyState(inputAssembly) - .pViewportState(viewportState) - .pRasterizationState(raster) - .pMultisampleState(msaa) - .pDepthStencilState(depthState) - .pColorBlendState(blend) - .layout(this.pipelineLayout) - .renderPass(this.renderPass).subpass(0); - var pPipe = stack.mallocLong(1); - check(vkCreateGraphicsPipelines(this.ctx.device, VK_NULL_HANDLE, gpci, null, pPipe), "vkCreateGraphicsPipelines"); - this.graphicsPipeline = pPipe.get(0); - this.fbWidth = viewport.width; this.fbHeight = viewport.height; - } - } - - @Override - public void buildDrawCalls(VulkanViewport viewport) { - //Phase-1: command generation stays on the GL path, writing SharedBuffers. - //Deliberately empty here; the GL compute passes are dispatched by the shared - //pipeline code exactly as for MDIC once the cmdgen wiring is routed through - //the viewport's shared buffers (next step alongside geometry sharing). - } - - @Override - public void renderOpaque(VulkanViewport viewport) { - if (this.geometryManager.getSectionCount() == 0) return; - if (!this.geometryShared) { - if (!this.warnedNoGeometry) { - this.warnedNoGeometry = true; - Logger.warn("Voxy VK backend active but geometry SSBO sharing not yet wired -> drawing nothing (GL fallback recommended)"); - } - return; - } - this.ensurePipeline(viewport); - try (MemoryStack stack = stackPush()) { - check(vkWaitForFences(this.ctx.device, this.fence, true, Long.MAX_VALUE), "vkWaitForFences"); - check(vkResetFences(this.ctx.device, this.fence), "vkResetFences"); - var begin = VkCommandBufferBeginInfo.calloc(stack).sType$Default() - .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - check(vkBeginCommandBuffer(this.cmd, begin), "vkBeginCommandBuffer"); - - var clears = VkClearValue.calloc(2, stack); - clears.get(0).color().float32(0, 0).float32(1, 0).float32(2, 0).float32(3, 0); - clears.get(1).depthStencil().depth(this.properties.isReverseZ() ? 0f : 1f); - var rpbi = VkRenderPassBeginInfo.calloc(stack).sType$Default() - .renderPass(this.renderPass).framebuffer(this.framebuffer) - .renderArea(a -> a.extent(e -> e.width(viewport.width).height(viewport.height))) - .pClearValues(clears); - vkCmdBeginRenderPass(this.cmd, rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(this.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, this.graphicsPipeline); - //Mirror of MDIC renderOpaque: offset 0, count at byte 12, same max-draw clamp, stride 20. - int maxDraw = Math.min((int) (this.geometryManager.getSectionCount() * 4.4 + 128), - me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer.OPAQUE_DRAW_COUNT); - if (this.ctx.hasDrawIndirectCount) { - vkCmdDrawIndexedIndirectCount(this.cmd, - viewport.drawCallBuffer.vkBuffer, 0, - viewport.drawCountCallBuffer.vkBuffer, 4 * 3, - maxDraw, 5 * 4); - } else { - //MoltenVK/macOS fallback: no GPU-sourced draw count. The cmdgen compute - //zero-fills unused command slots (instanceCount=0 draws are no-ops), so a - //fixed-count multi-draw over the clamped max is correct, just less tight. - vkCmdDrawIndexedIndirect(this.cmd, viewport.drawCallBuffer.vkBuffer, 0, maxDraw, 5 * 4); - } - vkCmdEndRenderPass(this.cmd); - check(vkEndCommandBuffer(this.cmd), "vkEndCommandBuffer"); - - //GL has signalled glDone after cmdgen; wait it, signal vkDone for the GL composite. - var submit = VkSubmitInfo.calloc(stack).sType$Default() - .waitSemaphoreCount(1) - .pWaitSemaphores(stack.longs(viewport.glDone.vkSemaphore)) - .pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT)) - .pCommandBuffers(stack.pointers(this.cmd)) - .pSignalSemaphores(stack.longs(viewport.vkDone.vkSemaphore)); - check(vkQueueSubmit(this.ctx.queue, submit, this.fence), "vkQueueSubmit"); - } - } - - @Override - public void renderTranslucent(VulkanViewport viewport) { - //Phase-1: translucent pass deferred until opaque parity is verified on hardware. - } - - @Override - public void renderTemporal(VulkanViewport viewport) { - //Phase-1: temporal pass deferred (GL path keeps it; VK renders without TAA reuse). - } - - @Override - public VulkanViewport createViewport() { - return new VulkanViewport(this.properties, this.geometryManager.getSectionCount() == 0 - ? (int) Math.min(this.geometryManager.getMaxCapacity(), Integer.MAX_VALUE) - : this.geometryManager.getSectionCount()); - } - - @Override - public void addDebug(List lines) { - lines.add("VK backend (phase-1 hybrid): " + VulkanBackend.statusLine() - + (this.geometryShared ? "" : " [geometry sharing pending -> not drawing]")); - } - - private void destroyPipelineObjects() { - if (this.graphicsPipeline != 0) vkDestroyPipeline(this.ctx.device, this.graphicsPipeline, null); - if (this.pipelineLayout != 0) vkDestroyPipelineLayout(this.ctx.device, this.pipelineLayout, null); - if (this.framebuffer != 0) vkDestroyFramebuffer(this.ctx.device, this.framebuffer, null); - if (this.renderPass != 0) vkDestroyRenderPass(this.ctx.device, this.renderPass, null); - this.graphicsPipeline = 0; this.pipelineLayout = 0; this.framebuffer = 0; this.renderPass = 0; - } - - @Override - public void free() { - vkDeviceWaitIdle(this.ctx.device); - destroyPipelineObjects(); - vkDestroyFence(this.ctx.device, this.fence, null); - vkDestroyShaderModule(this.ctx.device, this.vertShaderModule, null); - vkDestroyShaderModule(this.ctx.device, this.fragShaderModule, null); - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java deleted file mode 100644 index e0af0713e..000000000 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/backend/vulkan/VulkanViewport.java +++ /dev/null @@ -1,82 +0,0 @@ -package me.cortex.voxy.client.core.rendering.section.backend.vulkan; - -import me.cortex.voxy.client.core.RenderProperties; -import me.cortex.voxy.client.core.rendering.IRenderList; -import me.cortex.voxy.client.core.rendering.Viewport; -import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser; -import me.cortex.voxy.client.core.rendering.section.backend.mdic.MDICSectionRenderer; -import me.cortex.voxy.client.core.vk.SharedBuffer; -import me.cortex.voxy.client.core.vk.SharedImage; -import me.cortex.voxy.client.core.vk.SharedSemaphore; -import me.cortex.voxy.client.core.vk.VulkanBackend; -import me.cortex.voxy.client.core.vk.VulkanContext; - -import static org.lwjgl.vulkan.VK10.*; - -/** - * Vulkan analogue of MDICViewport. Identical buffer roles and sizes, but every - * buffer the GL compute passes write AND the VK graphics queue reads is a - * SharedBuffer (VK-allocated, GL-imported). The hierarchical traversal and the - * MDIC-style command generation keep running in GL, bit-identically, writing - * into these; VK consumes them for the actual draws (phase-1 hybrid). - */ -public class VulkanViewport extends Viewport { - private final VulkanContext ctx = VulkanBackend.context(); - - public final SharedBuffer drawCountCallBuffer; - public final SharedBuffer drawCallBuffer; - public final SharedBuffer positionScratchBuffer; - public final SharedBuffer indirectLookupBuffer; - public final SharedBuffer visibilityBuffer; - - // VK render targets, GL-visible for composite. Lazily (re)created on resize. - public SharedImage color; - public SharedImage depth; - public final SharedSemaphore vkDone; - public final SharedSemaphore glDone; - - public VulkanViewport(RenderProperties properties, int maxSectionCount) { - super(properties); - int indirect = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - this.drawCountCallBuffer = new SharedBuffer(this.ctx, 1024, indirect); - this.drawCallBuffer = new SharedBuffer(this.ctx, 5L*4*(MDICSectionRenderer.OPAQUE_DRAW_COUNT - + MDICSectionRenderer.TRANSLUCENT_DRAW_COUNT + MDICSectionRenderer.TEMPORAL_DRAW_COUNT), indirect); - this.positionScratchBuffer = new SharedBuffer(this.ctx, 8L*400000, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - this.indirectLookupBuffer = new SharedBuffer(this.ctx, - HierarchicalOcclusionTraverser.MAX_QUEUE_SIZE*4L+4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - this.visibilityBuffer = new SharedBuffer(this.ctx, maxSectionCount*4L, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - this.vkDone = new SharedSemaphore(this.ctx); - this.glDone = new SharedSemaphore(this.ctx); - } - - /** (Re)creates the shared color+depth targets if the viewport size changed. Returns true if recreated. */ - public boolean ensureTargets() { - if (this.width <= 0 || this.height <= 0) return false; - if (this.color != null && this.color.width == this.width && this.color.height == this.height) return false; - if (this.color != null) { this.color.free(); this.depth.free(); } - this.color = new SharedImage(this.ctx, this.width, this.height, - VK_FORMAT_R8G8B8A8_UNORM, org.lwjgl.opengl.GL11.GL_RGBA8, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); - this.depth = new SharedImage(this.ctx, this.width, this.height, - VK_FORMAT_D32_SFLOAT, org.lwjgl.opengl.GL30.GL_DEPTH_COMPONENT32F, - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); - return true; - } - - @Override - protected void delete0() { - super.delete0(); - if (this.color != null) { this.color.free(); this.depth.free(); } - this.vkDone.free(); this.glDone.free(); - this.visibilityBuffer.free(); - this.indirectLookupBuffer.free(); - this.drawCountCallBuffer.free(); - this.drawCallBuffer.free(); - this.positionScratchBuffer.free(); - } - - @Override - public IRenderList getRenderList() { - return this.indirectLookupBuffer; - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java index 39eeab76c..a516c1ec0 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryData.java @@ -11,7 +11,7 @@ import static org.lwjgl.opengl.GL15C.GL_ARRAY_BUFFER; import static org.lwjgl.opengl.GL15C.glBindBuffer; -public class BasicSectionGeometryData implements IGeometryData { +public class BasicSectionGeometryData implements IBasicGeometryData { public static final int SECTION_METADATA_SIZE = 32; private final GlBuffer sectionMetadataBuffer; private final GlBuffer geometryBuffer; @@ -98,6 +98,16 @@ public GlBuffer getGeometryBuffer() { return this.geometryBuffer; } + @Override + public me.cortex.voxy.client.core.rendering.util.IDeviceBuffer geometryBufferHandle() { + return this.geometryBuffer; + } + + @Override + public me.cortex.voxy.client.core.rendering.util.IDeviceBuffer metadataBufferHandle() { + return this.sectionMetadataBuffer; + } + public GlBuffer getMetadataBuffer() { return this.sectionMetadataBuffer; } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java index 76e421bc9..454369986 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/BasicSectionGeometryManager.java @@ -5,7 +5,7 @@ import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.rendering.building.BuiltSection; import me.cortex.voxy.client.core.rendering.util.BufferArena; -import me.cortex.voxy.client.core.rendering.util.UploadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; import me.cortex.voxy.common.util.HierarchicalBitSet; import org.lwjgl.system.MemoryUtil; @@ -109,7 +109,7 @@ public void tick() { if (!this.invalidatedSectionIds.isEmpty()) { this.invalidatedSectionIds.forEach((int id)-> { var meta = this.sectionMetadata.get(id); - long ptr = UploadStream.INSTANCE.upload(this.sectionMetadataBuffer, (long) id *SECTION_METADATA_SIZE, SECTION_METADATA_SIZE); + long ptr = AbstractUploadStream.INSTANCE().upload(this.sectionMetadataBuffer, (long) id *SECTION_METADATA_SIZE, SECTION_METADATA_SIZE); if (meta == null) {//We need to clear the gpu side buffer MemoryUtil.memSet(ptr, 0, SECTION_METADATA_SIZE); } else { @@ -117,7 +117,7 @@ public void tick() { } }); this.invalidatedSectionIds.clear(); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IBasicGeometryData.java b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IBasicGeometryData.java new file mode 100644 index 000000000..f9fa96998 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/section/geometry/IBasicGeometryData.java @@ -0,0 +1,24 @@ +package me.cortex.voxy.client.core.rendering.section.geometry; + +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; + +//Backend-neutral view of the "basic" geometry data store (a big quad buffer + +// per-section metadata). Implemented by BasicSectionGeometryData (GL) and +// VkSectionGeometryData (pure Vulkan); AsyncNodeManager and the section +// renderers traffic in this so the CPU-side management is shared. +public interface IBasicGeometryData extends IGeometryData { + int SECTION_METADATA_SIZE = BasicSectionGeometryData.SECTION_METADATA_SIZE; + + IDeviceBuffer geometryBufferHandle(); + + IDeviceBuffer metadataBufferHandle(); + + /** Ensure element indices up to maxElementAccess are backed (sparse GL); no-op elsewhere. */ + void ensureAccessable(int maxElementAccess); + + void setSectionCount(int count); + + int getMaxSectionCount(); + + long getGeometryCapacityBytes(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractDownloadStream.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractDownloadStream.java new file mode 100644 index 000000000..8ed6bc1f7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractDownloadStream.java @@ -0,0 +1,61 @@ +package me.cortex.voxy.client.core.rendering.util; + +import me.cortex.voxy.common.util.MemoryBuffer; + +import java.util.function.Consumer; + +//Backend-neutral GPU->CPU readback API; see AbstractUploadStream for the +// frame/tick model. Callbacks fire on the render thread from tick() once the +// GPU work that produced the data has provably completed. +public abstract class AbstractDownloadStream { + public interface DownloadResultConsumer { + void consume(long ptr, long size); + } + + public void download(IDeviceBuffer buffer, DownloadResultConsumer resultConsumer) { + this.download(buffer, 0, buffer.sizeBytes(), resultConsumer); + } + + public void download(IDeviceBuffer buffer, Consumer resultConsumer) { + this.download(buffer, 0, buffer.sizeBytes(), resultConsumer); + } + + public void download(IDeviceBuffer buffer, long downloadOffset, long size, Consumer consumer) { + this.download(buffer, downloadOffset, size, (ptr, size2) -> consumer.accept(MemoryBuffer.createUntrackedUnfreeableRawFrom(ptr, size))); + } + + public abstract void download(IDeviceBuffer buffer, long downloadOffset, long size, DownloadResultConsumer resultConsumer); + + public abstract void commit(); + + public abstract void tick(); + + /** Force-completes and discards all pending downloads (shutdown paths). */ + public abstract void waitDiscard(); + + /** Force-completes all pending downloads, firing callbacks (shutdown flush). */ + public abstract void flushWaitClear(); + + public abstract void free(); + + //Backend-selected global download stream; see AbstractUploadStream.INSTANCE() for + // why the holder lives on the abstract class (GL classload safety on Vulkan). + private static AbstractDownloadStream INSTANCE; + + public static AbstractDownloadStream INSTANCE() { + var instance = INSTANCE; + if (instance == null) { + instance = INSTANCE = new DownloadStream(1 << 25);//32 mb download buffer + } + return instance; + } + + public static void setInstance(AbstractDownloadStream instance) { + if (INSTANCE != null) throw new IllegalStateException("Download stream already initialized"); + INSTANCE = instance; + } + + public static void clearInstance() { + INSTANCE = null; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractUploadStream.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractUploadStream.java new file mode 100644 index 000000000..d13d7ee86 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/AbstractUploadStream.java @@ -0,0 +1,87 @@ +package me.cortex.voxy.client.core.rendering.util; + +import me.cortex.voxy.common.util.MemoryBuffer; + +//Backend-neutral streaming-upload API. The contract (shared by the GL and VK +// implementations, and relied on by NodeManager/geometry/model code): +// +// - upload(IDeviceBuffer, long, long) returns a CPU-writable pointer into a +// persistently-mapped staging area; the data is copied into the target buffer +// at commit(). +// - rawUploadAddress(int) allocates staging space without a copy target; +// callers bind the staging buffer region directly as an SSBO (scatter / +// multi-memcpy paths) via backend-specific code. +// - tick() retires frames whose GPU work completed, recycling staging space. +// Must be called once per frame on the render thread. +public abstract class AbstractUploadStream { + public void upload(IDeviceBuffer buffer, long destOffset, MemoryBuffer data) { + data.cpyTo(this.upload(buffer, destOffset, data.size)); + } + + public long uploadTo(IDeviceBuffer buffer) { + return this.upload(buffer, 0, buffer.sizeBytes()); + } + + public abstract long upload(IDeviceBuffer buffer, long destOffset, long size); + + public long rawUpload(int size) { + return this.getBaseAddress() + this.rawUploadAddress(size); + } + + public abstract long rawUploadAddress(int size); + + public abstract void commit(); + + public abstract void tick(); + + /** CPU base address of the persistently-mapped staging buffer. */ + public abstract long getBaseAddress(); + + /** GL name of the staging buffer; only valid on the OpenGL backend. */ + public int getRawBufferId() { + throw new UnsupportedOperationException("Staging buffer has no GL id on this backend"); + } + + //Allocation-block alignment of this stream's staging arena. + public abstract int baseAlignment(); + + public final int alignUpAlloc(int val) { + int a = this.baseAlignment(); + return ((val + a - 1) / a) * a; + } + + public abstract void free(); + + //The render-thread upload stream, backend-selected. The VK path installs its + // implementation via setInstance BEFORE any render code runs; on the GL path + // first access lazily creates the GL UploadStream exactly as the old eager + // static did (there is always a GL context by then). Living HERE (not on + // UploadStream) matters: loading UploadStream runs GL capability queries, + // which must never happen when MC is on Vulkan. + private static AbstractUploadStream INSTANCE; + + public static AbstractUploadStream INSTANCE() { + var instance = INSTANCE; + if (instance == null) { + instance = INSTANCE = new UploadStream(1 << 26);//64 mb upload buffer + } + return instance; + } + + public static void setInstance(AbstractUploadStream instance) { + if (INSTANCE != null) throw new IllegalStateException("Upload stream already initialized"); + INSTANCE = instance; + } + + public static void clearInstance() { + INSTANCE = null; + } + + public static long alignUp(long val, long alignment) { + return ((val + alignment - 1) / alignment) * alignment; + } + + public static int alignUp(int val, int alignment) { + return ((val + alignment - 1) / alignment) * alignment; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/BufferArena.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/BufferArena.java index a8c77e296..562df246e 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/util/BufferArena.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/BufferArena.java @@ -43,7 +43,7 @@ public long upload(MemoryBuffer buffer) { if (addr == -1) { return -1; } - long uploadPtr = UploadStream.INSTANCE.upload(this.buffer, addr * this.elementSize, buffer.size); + long uploadPtr = AbstractUploadStream.INSTANCE().upload(this.buffer, addr * this.elementSize, buffer.size); UnsafeUtil.memcpy(buffer.address, uploadPtr, buffer.size); this.used += size; return addr; @@ -73,6 +73,6 @@ public long getUsedBytes() { public void downloadRemove(long allocation, Consumer consumer) { int size = this.allocationMap.free(allocation); this.used -= size; - DownloadStream.INSTANCE.download(this.buffer, allocation*this.elementSize, (long) size *this.elementSize, consumer); + AbstractDownloadStream.INSTANCE().download(this.buffer, allocation*this.elementSize, (long) size *this.elementSize, consumer); } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/DownloadStream.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/DownloadStream.java index 060cd03e3..9a6c1f4b7 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/util/DownloadStream.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/DownloadStream.java @@ -21,11 +21,7 @@ import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT; import static org.lwjgl.opengl.GL45.glCopyNamedBufferSubData; -public class DownloadStream { - public interface DownloadResultConsumer { - void consume(long ptr, long size); - } - +public class DownloadStream extends AbstractDownloadStream { private final AllocationArena allocationArena = new AllocationArena(); private final GlPersistentMappedBuffer downloadBuffer; @@ -42,29 +38,15 @@ public DownloadStream(long size) { private long caddr = -1; private long offset = 0; - //Pulls the entire buffer from the gpu - public void download(GlBuffer buffer, DownloadResultConsumer resultConsumer) { - this.download(buffer, 0, buffer.size(), resultConsumer); - } - - public void download(GlBuffer buffer, Consumer resultConsumer) { - this.download(buffer, 0, buffer.size(), resultConsumer); - } - - public void download(GlBuffer buffer, long downloadOffset, long size, Consumer consumer) { - this.download(buffer, downloadOffset, size, (ptr,size2)-> { - consumer.accept(MemoryBuffer.createUntrackedUnfreeableRawFrom(ptr, size)); - }); - } - - public void download(GlBuffer buffer, long downloadOffset, long size, DownloadResultConsumer resultConsumer) { + @Override + public void download(IDeviceBuffer buffer, long downloadOffset, long size, DownloadResultConsumer resultConsumer) { if (size > Integer.MAX_VALUE) { throw new IllegalArgumentException(); } if (size <= 0) { throw new IllegalArgumentException(); } - if (downloadOffset+size > buffer.size()) { + if (downloadOffset+size > buffer.sizeBytes()) { throw new IllegalArgumentException(); } @@ -96,13 +78,14 @@ public void download(GlBuffer buffer, long downloadOffset, long size, DownloadRe throw new IllegalStateException(); } - this.downloadList.add(new DownloadData(buffer, addr, downloadOffset, size, resultConsumer)); + this.downloadList.add(new DownloadData((GlBuffer) buffer, addr, downloadOffset, size, resultConsumer)); //TODO: maybe not auto-commit this.commit(); } + @Override public void commit() { if (this.downloadList.isEmpty()) { return; @@ -120,6 +103,7 @@ public void commit() { this.offset = 0; } + @Override public void tick() { this.commit(); if (!this.thisFrameAllocations.isEmpty()) { @@ -149,6 +133,7 @@ public void tick() { } //Synchonize force flushes everything + @Override public void waitDiscard() { glFinish(); var fence = new GlFence(); @@ -164,6 +149,7 @@ public void waitDiscard() { } } + @Override public void flushWaitClear() { glFinish(); this.tick(); @@ -184,6 +170,8 @@ private record DownloadFrame(GlFence fence, LongArrayList allocations, ArrayList private record DownloadData(GlBuffer target, long downloadStreamOffset, long targetOffset, long size, DownloadResultConsumer resultConsumer) {} - // Global download stream - public static final DownloadStream INSTANCE = new DownloadStream(1<<25);//32 mb download buffer + @Override + public void free() { + this.downloadBuffer.free(); + } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/IDeviceBuffer.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/IDeviceBuffer.java new file mode 100644 index 000000000..9924262cd --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/IDeviceBuffer.java @@ -0,0 +1,12 @@ +package me.cortex.voxy.client.core.rendering.util; + +//Backend-neutral handle to a GPU buffer. GlBuffer implements this on the +// OpenGL path; VkBuffer on the pure-Vulkan (host) path. The shared CPU-side +// logic (node manager, geometry managers, upload/download streams) traffics +// exclusively in this type so it runs unmodified on both backends; backend- +// specific code casts to its concrete type. +public interface IDeviceBuffer { + long sizeBytes(); + + void free(); +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/SharedIndexBuffer.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/SharedIndexBuffer.java index 503e1b09a..3d174be1d 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/util/SharedIndexBuffer.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/SharedIndexBuffer.java @@ -8,9 +8,21 @@ //Has a base index buffer of 16380 quads, and also a 1 cube byte index buffer at the end public class SharedIndexBuffer { public static final int CUBE_INDEX_OFFSET = (1<<16)*6*2; - public static final SharedIndexBuffer INSTANCE = new SharedIndexBuffer(); - public static final SharedIndexBuffer INSTANCE_BYTE = new SharedIndexBuffer(true); - public static final SharedIndexBuffer INSTANCE_BB_BYTE = new SharedIndexBuffer(true, true); + //Lazy: creating these does GL work, and this class must never touch the GPU + // before a context exists (nor load at all when MC is on Vulkan). + private static SharedIndexBuffer INSTANCE, INSTANCE_BYTE, INSTANCE_BB_BYTE; + public static SharedIndexBuffer INSTANCE() { + var i = INSTANCE; if (i == null) i = INSTANCE = new SharedIndexBuffer(); + return i; + } + public static SharedIndexBuffer INSTANCE_BYTE() { + var i = INSTANCE_BYTE; if (i == null) i = INSTANCE_BYTE = new SharedIndexBuffer(true); + return i; + } + public static SharedIndexBuffer INSTANCE_BB_BYTE() { + var i = INSTANCE_BB_BYTE; if (i == null) i = INSTANCE_BB_BYTE = new SharedIndexBuffer(true, true); + return i; + } private final GlBuffer indexBuffer; @@ -19,13 +31,13 @@ public SharedIndexBuffer() { var quadIndexBuff = generateQuadIndicesShort(16380); var cubeBuff = generateCubeIndexBuffer(); - long ptr = UploadStream.INSTANCE.upload(this.indexBuffer, 0, this.indexBuffer.size()); + long ptr = AbstractUploadStream.INSTANCE().upload(this.indexBuffer, 0, this.indexBuffer.size()); quadIndexBuff.cpyTo(ptr); cubeBuff.cpyTo((1<<16)*2*6 + ptr); quadIndexBuff.free(); cubeBuff.free(); - UploadStream.INSTANCE.commit(); + AbstractUploadStream.INSTANCE().commit(); } private SharedIndexBuffer(boolean type2) { @@ -33,7 +45,7 @@ private SharedIndexBuffer(boolean type2) { var quadIndexBuff = generateQuadIndicesByte(63); var cubeBuff = generateCubeIndexBuffer(); - long ptr = UploadStream.INSTANCE.upload(this.indexBuffer, 0, this.indexBuffer.size()); + long ptr = AbstractUploadStream.INSTANCE().upload(this.indexBuffer, 0, this.indexBuffer.size()); quadIndexBuff.cpyTo(ptr); cubeBuff.cpyTo((1<<8)*6 + ptr); @@ -45,12 +57,12 @@ private SharedIndexBuffer(boolean type2, boolean type3) { this.indexBuffer = new GlBuffer(6*2*3*(256/8)); var cubeBuff = generateByteCubesIndexBuffer(256/8); - cubeBuff.cpyTo(UploadStream.INSTANCE.upload(this.indexBuffer, 0, this.indexBuffer.size())); - UploadStream.INSTANCE.commit(); + cubeBuff.cpyTo(AbstractUploadStream.INSTANCE().upload(this.indexBuffer, 0, this.indexBuffer.size())); + AbstractUploadStream.INSTANCE().commit(); cubeBuff.free(); } - private static MemoryBuffer generateCubeIndexBuffer() { + public static MemoryBuffer generateCubeIndexBuffer() { var buffer = new MemoryBuffer(6*2*3); long ptr = buffer.address; MemoryUtil.memSet(ptr, 0, 6*2*3 ); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java b/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java index 29ef21731..961382008 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java @@ -22,7 +22,8 @@ import static org.lwjgl.opengl.GL44.GL_MAP_COHERENT_BIT; import static org.lwjgl.opengl.GL45C.glFlushMappedNamedBufferRange; -public class UploadStream { +public class UploadStream extends AbstractUploadStream { + //NOTE: referencing Capabilities does GL queries; this class must only be loaded on the GL backend. public static final int BASE_ALLOCATION_ALIGNEMENT = Math.max(Capabilities.INSTANCE.ssboBindingAlignment, 16); private final AllocationArena allocationArena = new AllocationArena(); @@ -41,26 +42,22 @@ public UploadStream(long size) { private long caddr = -1; private long offset = 0; - public void upload(GlBuffer buffer, long destOffset, MemoryBuffer data) {//Note: does not free data, nor does it commit - data.cpyTo(this.upload(buffer, destOffset, data.size)); - } - - public long uploadTo(GlBuffer buffer) { - return this.upload(buffer, 0, buffer.size()); - } - public long upload(GlBuffer buffer, long destOffset, long size) { + @Override + public long upload(IDeviceBuffer buffer, long destOffset, long size) { long addr = this.rawUploadAddress((int) size); - this.uploadList.add(new UploadData(buffer, addr, destOffset, size)); + this.uploadList.add(new UploadData((GlBuffer) buffer, addr, destOffset, size)); return this.uploadBuffer.addr() + addr; } + @Override public long rawUpload(int size) { return this.uploadBuffer.addr() + this.rawUploadAddress(size); } + @Override public long rawUploadAddress(int size) { if (size < 0) { throw new IllegalStateException("Negative size"); @@ -111,6 +108,7 @@ public long rawUploadAddress(int size) { return addr; } + @Override public void commit() { if ((!USE_COHERENT)&&this.caddr != -1) { //Flush this allocation @@ -134,6 +132,7 @@ public void commit() { this.offset = 0; } + @Override public void tick() { this.tick(true); } @@ -160,28 +159,26 @@ private void tick(boolean commit) { } } + @Override public long getBaseAddress() { return this.uploadBuffer.addr(); } + @Override public int getRawBufferId() { return this.uploadBuffer.id; } + @Override + public void free() { + this.uploadBuffer.free(); + } + private record UploadFrame(GlFence fence, LongArrayList allocations) {} private record UploadData(GlBuffer target, long uploadOffset, long targetOffset, long size) {} - //A upload instance instead of passing one around by reference - // MUST ONLY BE USED ON THE RENDER THREAD - public static final UploadStream INSTANCE = new UploadStream(1<<26);//64 mb upload buffer - - public static long alignUp(long val, long alignment) { - return ((val+alignment-1)/alignment)*alignment; - } - public static int alignUp(int val, int alignment) { - return ((val+alignment-1)/alignment)*alignment; - } - public static int alignUpAlloc(int val) { - return ((val+BASE_ALLOCATION_ALIGNEMENT-1)/BASE_ALLOCATION_ALIGNEMENT)*BASE_ALLOCATION_ALIGNEMENT; + @Override + public int baseAlignment() { + return BASE_ALLOCATION_ALIGNEMENT; } } diff --git a/src/main/java/me/cortex/voxy/client/core/util/GPUTiming.java b/src/main/java/me/cortex/voxy/client/core/util/GPUTiming.java index 22a16c728..d7059727f 100644 --- a/src/main/java/me/cortex/voxy/client/core/util/GPUTiming.java +++ b/src/main/java/me/cortex/voxy/client/core/util/GPUTiming.java @@ -1,5 +1,7 @@ package me.cortex.voxy.client.core.util; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; + import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; import me.cortex.voxy.common.util.TrackedObject; @@ -194,7 +196,7 @@ public void capture(int metadata) { public void download(TimingDataConsumer consumer) { var meta = Arrays.copyOf(this.metadata, this.index); this.index = 0; - //DownloadStream.INSTANCE.download(this.store, buffer->consumer.accept(meta, buffer)); + //AbstractDownloadStream.INSTANCE().download(this.store, buffer->consumer.accept(meta, buffer)); } @Override diff --git a/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java b/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java index b0fe82fe4..58ff4818d 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/IVkHost.java @@ -25,14 +25,4 @@ public interface IVkHost { /** Command buffer currently recording for this frame's world rendering, at the LOD injection point. */ VkCommandBuffer frameCommandBuffer(); - - /** The game's depth attachment view for the current frame (for occlusion tests + depth-correct LOD compositing). */ - long frameDepthImageView(); - /** The game's color attachment view for the current frame. */ - long frameColorImageView(); - int frameWidth(); - int frameHeight(); - /** Formats of the above, VkFormat values, needed for pipeline rendering-info. */ - int frameColorFormat(); - int frameDepthFormat(); } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java index d03c38be9..f2c14cd4b 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHost.java @@ -3,13 +3,10 @@ /** * Adapter registry for the vanilla 26.2 Blaze3D Vulkan backend. * - * STATUS: intentionally an unwired registration point. Binding this requires - * mixing into MC 26.2's obfuscated Blaze3D-Vulkan classes (device holder, frame - * command-buffer, swapchain attachments); those mixin targets must be authored - * against the actual 26.2 mappings in a dev environment with the game jar — - * guessing class/method names here would produce fiction, not integration. - * The mixin, once written, calls {@link #register(IVkHost)} during render init - * and {@link #clear()} on backend teardown/API switch. + * The Blaze3D-VK adapter mixin ({@link MixinVulkanDevice}) calls {@link #register} + * during render init and {@link #clear} on backend teardown. The mixin target + * classes must be authored against the real 26.2 mappings in a dev environment + * with the game jar, so are kept in the client mixin package. */ public final class MinecraftVkHost { private static volatile IVkHost host; @@ -18,7 +15,25 @@ public final class MinecraftVkHost { public static void clear() { host = null; } /** Non-null only when MC itself is presenting through Vulkan and the adapter mixin is active. */ public static IVkHost get() { return host; } - public static boolean isMinecraftOnVulkan() { return host != null; } + + //Whether MC is currently rendering through its own Vulkan backend. + //Reads the live Blaze3D device so it reflects the Graphics API setting AFTER + // MC's own capability fallback ladder has run, not merely the preference. + public static boolean isMinecraftOnVulkan() { + if (host != null) return true;//explicitly registered host is authoritative + return detectActiveVulkan(); + } + + private static boolean detectActiveVulkan() { + try { + var device = com.mojang.blaze3d.systems.RenderSystem.tryGetDevice(); + if (device == null) return false; + String backend = device.getDeviceInfo().backendName(); + return backend != null && backend.toLowerCase(java.util.Locale.ROOT).contains("vulkan"); + } catch (Throwable t) { + return false; + } + } private MinecraftVkHost() {} } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHostAdapter.java b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHostAdapter.java new file mode 100644 index 000000000..791ff44a5 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/MinecraftVkHostAdapter.java @@ -0,0 +1,38 @@ +package me.cortex.voxy.client.core.vk; + +import com.mojang.blaze3d.vulkan.VulkanDevice; +import me.cortex.voxy.client.mixin.vk.AccessorVulkanCommandEncoder; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkDevice; +import org.lwjgl.vulkan.VkInstance; +import org.lwjgl.vulkan.VkPhysicalDevice; +import org.lwjgl.vulkan.VkQueue; + +//IVkHost backed by MC 26.2's live Blaze3D Vulkan device. The device-level +// handles (instance/physical device/device/queue+family) are pulled directly +// from MC's VulkanDevice and are stable for the device lifetime. The per-frame +// command buffer is resolved live from MC's persistent command encoder; the +// world colour/depth attachments are passed straight to the render core each +// frame from the Sodium hook's output target, so the adapter holds no per-frame +// state. +public final class MinecraftVkHostAdapter implements IVkHost { + private final VulkanDevice device; + + public MinecraftVkHostAdapter(VulkanDevice device) { + this.device = device; + } + + @Override public VkInstance instance() { return this.device.instance().vkInstance(); } + @Override public VkPhysicalDevice physicalDevice() { return this.device.vkDevice().getPhysicalDevice(); } + @Override public VkDevice device() { return this.device.vkDevice(); } + @Override public VkQueue graphicsQueue() { return this.device.graphicsQueue().vkQueue(); } + @Override public int graphicsQueueFamily() { return this.device.graphicsQueue().queueFamilyIndex(); } + + @Override + public VkCommandBuffer frameCommandBuffer() { + //MC's persistent per-frame encoder; the command buffer it is currently + // recording into (null outside a render pass) + var encoder = (AccessorVulkanCommandEncoder) (Object) this.device.createCommandEncoder(); + return encoder.voxy$currentCommandBuffer(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java deleted file mode 100644 index 2064ac32d..000000000 --- a/src/main/java/me/cortex/voxy/client/core/vk/SharedBuffer.java +++ /dev/null @@ -1,92 +0,0 @@ -package me.cortex.voxy.client.core.vk; - -import me.cortex.voxy.client.core.rendering.IRenderList; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.Platform; - -import static me.cortex.voxy.client.core.vk.VkUtil.check; -import static org.lwjgl.opengl.EXTMemoryObject.*; -import static org.lwjgl.opengl.EXTMemoryObjectFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; -import static org.lwjgl.opengl.EXTMemoryObjectFD.glImportMemoryFdEXT; -import static org.lwjgl.opengl.EXTMemoryObjectWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; -import static org.lwjgl.opengl.EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT; -import static org.lwjgl.opengl.GL45C.*; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.vulkan.KHRExternalMemoryFd.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalMemoryFd.vkGetMemoryFdKHR; -import static org.lwjgl.vulkan.KHRExternalMemoryWin32.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalMemoryWin32.vkGetMemoryWin32HandleKHR; -import static org.lwjgl.vulkan.VK10.*; -import org.lwjgl.vulkan.*; - -/** - * A buffer whose memory is allocated by Vulkan with an exportable handle and - * imported into the current GL context. Both APIs address the same storage: - * GL compute passes (hierarchical traversal, command generation) write it, - * the VK graphics queue consumes it. Implements IRenderList so it can sit - * directly behind Viewport#getRenderList(). - */ -public final class SharedBuffer implements IRenderList { - public final long vkBuffer; - public final long vkMemory; - private final int glMemoryObject; - public final int glBufferId; - private final long size; - private final VulkanContext ctx; - - public SharedBuffer(VulkanContext ctx, long size, int vkUsage) { - this.ctx = ctx; - this.size = size; - boolean win = Platform.get() == Platform.WINDOWS; - int vkHandleType = win ? VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR - : VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; - try (MemoryStack stack = stackPush()) { - var extBuf = VkExternalMemoryBufferCreateInfo.calloc(stack).sType$Default().handleTypes(vkHandleType); - var bci = VkBufferCreateInfo.calloc(stack).sType$Default().pNext(extBuf) - .size(size).usage(vkUsage).sharingMode(VK_SHARING_MODE_EXCLUSIVE); - var pBuf = stack.mallocLong(1); - check(vkCreateBuffer(ctx.device, bci, null, pBuf), "vkCreateBuffer(shared)"); - this.vkBuffer = pBuf.get(0); - - var req = VkMemoryRequirements.calloc(stack); - vkGetBufferMemoryRequirements(ctx.device, this.vkBuffer, req); - var export = VkExportMemoryAllocateInfo.calloc(stack).sType$Default().handleTypes(vkHandleType); - var dedicated = VkMemoryDedicatedAllocateInfo.calloc(stack).sType$Default() - .pNext(export).buffer(this.vkBuffer); - var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default().pNext(dedicated) - .allocationSize(req.size()) - .memoryTypeIndex(ctx.findMemoryType(req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); - var pMem = stack.mallocLong(1); - check(vkAllocateMemory(ctx.device, mai, null, pMem), "vkAllocateMemory(shared)"); - this.vkMemory = pMem.get(0); - check(vkBindBufferMemory(ctx.device, this.vkBuffer, this.vkMemory, 0), "vkBindBufferMemory"); - - this.glMemoryObject = glCreateMemoryObjectsEXT(); - if (win) { - var ghi = VkMemoryGetWin32HandleInfoKHR.calloc(stack).sType$Default() - .memory(this.vkMemory).handleType(vkHandleType); - var pHandle = stack.mallocPointer(1); - check(vkGetMemoryWin32HandleKHR(ctx.device, ghi, pHandle), "vkGetMemoryWin32HandleKHR"); - glImportMemoryWin32HandleEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); - } else { - var gfi = VkMemoryGetFdInfoKHR.calloc(stack).sType$Default() - .memory(this.vkMemory).handleType(vkHandleType); - var pFd = stack.mallocInt(1); - check(vkGetMemoryFdKHR(ctx.device, gfi, pFd), "vkGetMemoryFdKHR"); - glImportMemoryFdEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); - } - this.glBufferId = glCreateBuffers(); - glNamedBufferStorageMemEXT(this.glBufferId, size, this.glMemoryObject, 0); - } - } - - @Override public int glId() { return this.glBufferId; } - @Override public long sizeBytes() { return this.size; } - - public void free() { - glDeleteBuffers(this.glBufferId); - glDeleteMemoryObjectsEXT(this.glMemoryObject); - vkDestroyBuffer(this.ctx.device, this.vkBuffer, null); - vkFreeMemory(this.ctx.device, this.vkMemory, null); - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java deleted file mode 100644 index 17faac40c..000000000 --- a/src/main/java/me/cortex/voxy/client/core/vk/SharedImage.java +++ /dev/null @@ -1,103 +0,0 @@ -package me.cortex.voxy.client.core.vk; - -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.Platform; -import org.lwjgl.vulkan.*; - -import static me.cortex.voxy.client.core.vk.VkUtil.check; -import static org.lwjgl.opengl.EXTMemoryObject.*; -import static org.lwjgl.opengl.EXTMemoryObjectFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; -import static org.lwjgl.opengl.EXTMemoryObjectFD.glImportMemoryFdEXT; -import static org.lwjgl.opengl.EXTMemoryObjectWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; -import static org.lwjgl.opengl.EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT; -import static org.lwjgl.opengl.GL45C.*; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.vulkan.KHRExternalMemoryFd.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalMemoryFd.vkGetMemoryFdKHR; -import static org.lwjgl.vulkan.KHRExternalMemoryWin32.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalMemoryWin32.vkGetMemoryWin32HandleKHR; -import static org.lwjgl.vulkan.VK10.*; - -/** - * A 2D render target allocated by VK with exportable memory, visible in GL as a - * texture: VK renders LOD color/depth into it; GL composites it into MC's - * framebuffer. Recreated on resolution change. - */ -public final class SharedImage { - public final long vkImage; - public final long vkMemory; - public final long vkView; - public final int glMemoryObject; - public final int glTexture; - public final int width, height; - public final int vkFormat; - private final VulkanContext ctx; - - public SharedImage(VulkanContext ctx, int width, int height, int vkFormat, int glInternalFormat, int vkUsage, int aspect) { - this.ctx = ctx; this.width = width; this.height = height; this.vkFormat = vkFormat; - boolean win = Platform.get() == Platform.WINDOWS; - int handleType = win ? VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR - : VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; - try (MemoryStack stack = stackPush()) { - var ext = VkExternalMemoryImageCreateInfo.calloc(stack).sType$Default().handleTypes(handleType); - var ici = VkImageCreateInfo.calloc(stack).sType$Default().pNext(ext) - .imageType(VK_IMAGE_TYPE_2D) - .format(vkFormat) - .extent(e -> e.width(width).height(height).depth(1)) - .mipLevels(1).arrayLayers(1) - .samples(VK_SAMPLE_COUNT_1_BIT) - .tiling(VK_IMAGE_TILING_OPTIMAL) - .usage(vkUsage) - .sharingMode(VK_SHARING_MODE_EXCLUSIVE) - .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED); - var pImg = stack.mallocLong(1); - check(vkCreateImage(ctx.device, ici, null, pImg), "vkCreateImage(shared)"); - this.vkImage = pImg.get(0); - - var req = VkMemoryRequirements.calloc(stack); - vkGetImageMemoryRequirements(ctx.device, this.vkImage, req); - var export = VkExportMemoryAllocateInfo.calloc(stack).sType$Default().handleTypes(handleType); - var dedicated = VkMemoryDedicatedAllocateInfo.calloc(stack).sType$Default().pNext(export).image(this.vkImage); - var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default().pNext(dedicated) - .allocationSize(req.size()) - .memoryTypeIndex(ctx.findMemoryType(req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); - var pMem = stack.mallocLong(1); - check(vkAllocateMemory(ctx.device, mai, null, pMem), "vkAllocateMemory(sharedImage)"); - this.vkMemory = pMem.get(0); - check(vkBindImageMemory(ctx.device, this.vkImage, this.vkMemory, 0), "vkBindImageMemory"); - - var vci = VkImageViewCreateInfo.calloc(stack).sType$Default() - .image(this.vkImage).viewType(VK_IMAGE_VIEW_TYPE_2D).format(vkFormat); - vci.subresourceRange().aspectMask(aspect).levelCount(1).layerCount(1); - var pView = stack.mallocLong(1); - check(vkCreateImageView(ctx.device, vci, null, pView), "vkCreateImageView(shared)"); - this.vkView = pView.get(0); - - this.glMemoryObject = glCreateMemoryObjectsEXT(); - if (win) { - var ghi = VkMemoryGetWin32HandleInfoKHR.calloc(stack).sType$Default() - .memory(this.vkMemory).handleType(handleType); - var pHandle = stack.mallocPointer(1); - check(vkGetMemoryWin32HandleKHR(ctx.device, ghi, pHandle), "vkGetMemoryWin32HandleKHR(img)"); - glImportMemoryWin32HandleEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); - } else { - var gfi = VkMemoryGetFdInfoKHR.calloc(stack).sType$Default() - .memory(this.vkMemory).handleType(handleType); - var pFd = stack.mallocInt(1); - check(vkGetMemoryFdKHR(ctx.device, gfi, pFd), "vkGetMemoryFdKHR(img)"); - glImportMemoryFdEXT(this.glMemoryObject, req.size(), GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); - } - this.glTexture = glCreateTextures(GL_TEXTURE_2D); - glTextureParameteri(this.glTexture, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT); - glTextureStorageMem2DEXT(this.glTexture, 1, glInternalFormat, width, height, this.glMemoryObject, 0); - } - } - - public void free() { - glDeleteTextures(this.glTexture); - glDeleteMemoryObjectsEXT(this.glMemoryObject); - vkDestroyImageView(this.ctx.device, this.vkView, null); - vkDestroyImage(this.ctx.device, this.vkImage, null); - vkFreeMemory(this.ctx.device, this.vkMemory, null); - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java b/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java deleted file mode 100644 index 227fe527c..000000000 --- a/src/main/java/me/cortex/voxy/client/core/vk/SharedSemaphore.java +++ /dev/null @@ -1,59 +0,0 @@ -package me.cortex.voxy.client.core.vk; - -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.Platform; -import org.lwjgl.vulkan.*; - -import static me.cortex.voxy.client.core.vk.VkUtil.check; -import static org.lwjgl.opengl.EXTSemaphore.glDeleteSemaphoresEXT; -import static org.lwjgl.opengl.EXTSemaphore.glGenSemaphoresEXT; -import static org.lwjgl.opengl.EXTSemaphoreFD.GL_HANDLE_TYPE_OPAQUE_FD_EXT; -import static org.lwjgl.opengl.EXTSemaphoreFD.glImportSemaphoreFdEXT; -import static org.lwjgl.opengl.EXTSemaphoreWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; -import static org.lwjgl.opengl.EXTSemaphoreWin32.glImportSemaphoreWin32HandleEXT; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.vulkan.KHRExternalSemaphoreFd.VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalSemaphoreFd.vkGetSemaphoreFdKHR; -import static org.lwjgl.vulkan.KHRExternalSemaphoreWin32.VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR; -import static org.lwjgl.vulkan.KHRExternalSemaphoreWin32.vkGetSemaphoreWin32HandleKHR; -import static org.lwjgl.vulkan.VK10.*; - -/** VK binary semaphore exported to GL, for cross-API queue ordering (GL compute -> VK draw -> GL composite). */ -public final class SharedSemaphore { - public final long vkSemaphore; - public final int glSemaphore; - private final VulkanContext ctx; - - public SharedSemaphore(VulkanContext ctx) { - this.ctx = ctx; - boolean win = Platform.get() == Platform.WINDOWS; - int handleType = win ? VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT_KHR - : VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT_KHR; - try (MemoryStack stack = stackPush()) { - var export = VkExportSemaphoreCreateInfo.calloc(stack).sType$Default().handleTypes(handleType); - var sci = VkSemaphoreCreateInfo.calloc(stack).sType$Default().pNext(export); - var pSem = stack.mallocLong(1); - check(vkCreateSemaphore(ctx.device, sci, null, pSem), "vkCreateSemaphore(export)"); - this.vkSemaphore = pSem.get(0); - this.glSemaphore = glGenSemaphoresEXT(); - if (win) { - var gi = VkSemaphoreGetWin32HandleInfoKHR.calloc(stack).sType$Default() - .semaphore(this.vkSemaphore).handleType(handleType); - var pHandle = stack.mallocPointer(1); - check(vkGetSemaphoreWin32HandleKHR(ctx.device, gi, pHandle), "vkGetSemaphoreWin32HandleKHR"); - glImportSemaphoreWin32HandleEXT(this.glSemaphore, GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, pHandle.get(0)); - } else { - var gi = VkSemaphoreGetFdInfoKHR.calloc(stack).sType$Default() - .semaphore(this.vkSemaphore).handleType(handleType); - var pFd = stack.mallocInt(1); - check(vkGetSemaphoreFdKHR(ctx.device, gi, pFd), "vkGetSemaphoreFdKHR"); - glImportSemaphoreFdEXT(this.glSemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, pFd.get(0)); - } - } - } - - public void free() { - glDeleteSemaphoresEXT(this.glSemaphore); - vkDestroySemaphore(this.ctx.device, this.vkSemaphore, null); - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkAtlasTextureReader.java b/src/main/java/me/cortex/voxy/client/core/vk/VkAtlasTextureReader.java new file mode 100644 index 000000000..1e33403c8 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkAtlasTextureReader.java @@ -0,0 +1,79 @@ +package me.cortex.voxy.client.core.vk; + +import com.mojang.blaze3d.textures.GpuTexture; +import com.mojang.blaze3d.vulkan.VulkanGpuTexture; +import me.cortex.voxy.client.core.model.bakery.IAtlasTextureReader; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkBufferImageCopy; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkImageMemoryBarrier; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Vulkan block-atlas readback: copies MC's stitched atlas image (mip 0) into a +// host-visible staging buffer via vkCmdCopyImageToBuffer and reads it into an +// int[] in the same RGBA8 byte order (R,G,B,A) the GL path produced. The atlas +// is VK_FORMAT_R8G8B8A8_UNORM, so the raw texel bytes match GL RGBA/UNSIGNED_BYTE +// one-for-one and the software rasterizer sees identical data. +// +//Runs once at model-bakery construction, outside any frame, through the frame +// ctx's immediate command buffer (submitted + waited synchronously). Assumes +// MC creates the atlas with TRANSFER_SRC usage and keeps it in +// SHADER_READ_ONLY_OPTIMAL between frames (validation layers flag both). +public final class VkAtlasTextureReader extends IAtlasTextureReader { + private final VkFrameCtx frameCtx; + + public VkAtlasTextureReader(VkFrameCtx frameCtx) { + this.frameCtx = frameCtx; + } + + @Override + public int[] read(GpuTexture atlas, int width, int height) { + long image = ((VulkanGpuTexture) atlas).vkImage(); + long size = (long) width * height * 4; + var staging = new VkBuffer(this.frameCtx, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, true); + try { + var cmd = this.frameCtx.cmd(); + //MC keeps the sampled atlas in SHADER_READ_ONLY_OPTIMAL; move mip 0 to + // TRANSFER_SRC for the copy, then restore it so MC's sampling is unaffected. + transition(cmd, image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + try (MemoryStack stack = stackPush()) { + var region = VkBufferImageCopy.calloc(1, stack) + .bufferOffset(0).bufferRowLength(0).bufferImageHeight(0); + region.imageSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT) + .mipLevel(0).baseArrayLayer(0).layerCount(1); + region.imageOffset().set(0, 0, 0); + region.imageExtent().set(width, height, 1); + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging.buffer, region); + } + transition(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + this.frameCtx.flushImmediate(); + + var out = new int[width * height]; + long ptr = staging.map(); + MemoryUtil.memIntBuffer(ptr, out.length).get(out); + return out; + } finally { + //Deferred destroy; vkFreeMemory implicitly unmaps the staging map above. + staging.free(); + } + } + + private static void transition(VkCommandBuffer cmd, long image, int oldLayout, int newLayout) { + try (MemoryStack stack = stackPush()) { + var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() + .srcAccessMask(VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT) + .dstAccessMask(VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT) + .oldLayout(oldLayout).newLayout(newLayout) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED).dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .image(image); + imb.subresourceRange().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT) + .baseMipLevel(0).levelCount(VK_REMAINING_MIP_LEVELS) + .baseArrayLayer(0).layerCount(VK_REMAINING_ARRAY_LAYERS); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + 0, null, null, imb); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkBuffer.java b/src/main/java/me/cortex/voxy/client/core/vk/VkBuffer.java new file mode 100644 index 000000000..0482016ff --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkBuffer.java @@ -0,0 +1,124 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.rendering.IRenderList; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.common.util.TrackedObject; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkBufferCreateInfo; +import org.lwjgl.vulkan.VkMemoryAllocateInfo; +import org.lwjgl.vulkan.VkMemoryRequirements; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.system.MemoryUtil.NULL; +import static org.lwjgl.vulkan.VK10.*; + +//Device buffer on the pure-Vulkan path; the VK analogue of GlBuffer. All Voxy +// buffers get a superset of usage flags (storage/indirect/index/transfer) so a +// single class covers every role the GL path used raw buffer ids for. Memory is +// a dedicated device-local allocation (Voxy has few, large, long-lived buffers). +// +//Freeing is DEFERRED through VkFrameCtx — a buffer may still be referenced by +// command buffers in flight when free() is called. +public class VkBuffer extends TrackedObject implements IDeviceBuffer, IRenderList { + public static final int USAGE_DEFAULT = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT + | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT + | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + | VK_BUFFER_USAGE_INDEX_BUFFER_BIT + | VK_BUFFER_USAGE_TRANSFER_SRC_BIT + | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + + private final VkFrameCtx ctx; + public final long buffer; + public final long memory; + private final long size; + + private static int COUNT; + private static long TOTAL_SIZE; + + public VkBuffer(VkFrameCtx ctx, long size) { + this(ctx, size, USAGE_DEFAULT, false); + } + + public VkBuffer(VkFrameCtx ctx, long size, int usage, boolean hostVisible) { + this.ctx = ctx; + this.size = size; + var vctx = ctx.vk(); + try (MemoryStack stack = stackPush()) { + var bci = VkBufferCreateInfo.calloc(stack).sType$Default() + .size(size).usage(usage).sharingMode(VK_SHARING_MODE_EXCLUSIVE); + var pBuf = stack.mallocLong(1); + check(vkCreateBuffer(vctx.device, bci, null, pBuf), "vkCreateBuffer"); + this.buffer = pBuf.get(0); + + var req = VkMemoryRequirements.calloc(stack); + vkGetBufferMemoryRequirements(vctx.device, this.buffer, req); + int props = hostVisible + ? (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) + : VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default() + .allocationSize(req.size()) + .memoryTypeIndex(vctx.findMemoryType(req.memoryTypeBits(), props)); + var pMem = stack.mallocLong(1); + check(vkAllocateMemory(vctx.device, mai, null, pMem), "vkAllocateMemory"); + this.memory = pMem.get(0); + check(vkBindBufferMemory(vctx.device, this.buffer, this.memory, 0), "vkBindBufferMemory"); + } + COUNT++; + TOTAL_SIZE += size; + } + + /** Maps the whole buffer; only valid for hostVisible buffers. */ + public long map() { + try (MemoryStack stack = stackPush()) { + var pp = stack.mallocPointer(1); + check(vkMapMemory(this.ctx.vk().device, this.memory, 0, this.size, 0, pp), "vkMapMemory"); + return pp.get(0); + } + } + + @Override + public long sizeBytes() { + return this.size; + } + + public long size() { + return this.size; + } + + @Override + public int glId() { + throw new UnsupportedOperationException("VkBuffer has no GL id"); + } + + /** Records a fill of 0 across the given range into the current frame commands. */ + public VkBuffer zeroRange(long offset, long len) { + this.ctx.fillBuffer(this, offset, len, 0); + return this; + } + + public VkBuffer zero() { + return this.zeroRange(0, VK_WHOLE_SIZE); + } + + public VkBuffer fill(int value) { + this.ctx.fillBuffer(this, 0, VK_WHOLE_SIZE, value); + return this; + } + + @Override + public void free() { + this.free0(); + COUNT--; + TOTAL_SIZE -= this.size; + this.ctx.deferDestroy(this.buffer, this.memory); + } + + public static int getCount() { + return COUNT; + } + + public static long getTotalSize() { + return TOTAL_SIZE; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkCmd.java b/src/main/java/me/cortex/voxy/client/core/vk/VkCmd.java new file mode 100644 index 000000000..4c9fa1909 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkCmd.java @@ -0,0 +1,42 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkRect2D; +import org.lwjgl.vulkan.VkViewport; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +/** Small shared Vulkan command/setup helpers reused across the render subsystems. */ +public final class VkCmd { + private VkCmd() {} + + /** Full-frame dynamic viewport + scissor covering [0,0]..[width,height]. */ + public static void setViewportScissor(VkCommandBuffer cmd, int width, int height) { + try (MemoryStack stack = stackPush()) { + var vp = VkViewport.calloc(1, stack).x(0).y(0).width(width).height(height).minDepth(0).maxDepth(1); + vkCmdSetViewport(cmd, 0, vp); + var sc = VkRect2D.calloc(1, stack); + sc.extent(e -> e.width(width).height(height)); + vkCmdSetScissor(cmd, 0, sc); + } + } + + /** The reverse-Z-aware "closer or equal" depth compare op the terrain/composite passes use. */ + public static int closerEqual(RenderProperties properties) { + return properties.isReverseZ() ? VK_COMPARE_OP_GREATER_OR_EQUAL : VK_COMPARE_OP_LESS_OR_EQUAL; + } + + /** Expands the shared u8 cube index buffer (36 indices) to u16 at {@code dst}. */ + public static void writeCubeIndicesU16(long dst) { + var cube = SharedIndexBuffer.generateCubeIndexBuffer();//u8 source + for (int i = 0; i < 6 * 2 * 3; i++) { + MemoryUtil.memPutShort(dst + i * 2L, (short) (MemoryUtil.memGetByte(cube.address + i) & 0xFF)); + } + cube.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java b/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java deleted file mode 100644 index 2d08c2bc2..000000000 --- a/src/main/java/me/cortex/voxy/client/core/vk/VkComputePipeline.java +++ /dev/null @@ -1,82 +0,0 @@ -package me.cortex.voxy.client.core.vk; - -import me.cortex.voxy.client.core.gl.shader.ShaderType; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.vulkan.*; - -import java.nio.ByteBuffer; - -import static me.cortex.voxy.client.core.vk.VkUtil.check; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.vulkan.VK10.*; - -/** - * Compute pipeline for the pure-VK path: prep/cull/prefixsum/cmdgen and, - * phase-3, the hierarchical traversal itself — the same GLSL compute sources - * as GL, compiled via shaderc. Descriptors: one set of N storage buffers + - * one UBO at binding 0, mirroring the GL binding-base layout so the shared - * shader sources keep a single binding scheme under VOXY_VULKAN. - */ -public final class VkComputePipeline { - private final VulkanContext ctx; - public final long descriptorSetLayout; - public final long pipelineLayout; - public final long pipeline; - private final long shaderModule; - - public VkComputePipeline(VulkanContext ctx, String glslSource, String name, int uboCount, int ssboCount) { - this.ctx = ctx; - ByteBuffer spv = ShadercCompiler.compile(glslSource, ShaderType.COMPUTE, name); - try (MemoryStack stack = stackPush()) { - var smci = VkShaderModuleCreateInfo.calloc(stack).sType$Default().pCode(spv); - var pMod = stack.mallocLong(1); - check(vkCreateShaderModule(ctx.device, smci, null, pMod), "vkCreateShaderModule(" + name + ")"); - this.shaderModule = pMod.get(0); - - var bindings = VkDescriptorSetLayoutBinding.calloc(uboCount + ssboCount, stack); - for (int i = 0; i < uboCount; i++) { - bindings.get(i).binding(i).descriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) - .descriptorCount(1).stageFlags(VK_SHADER_STAGE_COMPUTE_BIT); - } - for (int i = 0; i < ssboCount; i++) { - bindings.get(uboCount + i).binding(uboCount + i) - .descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) - .descriptorCount(1).stageFlags(VK_SHADER_STAGE_COMPUTE_BIT); - } - var dslci = VkDescriptorSetLayoutCreateInfo.calloc(stack).sType$Default().pBindings(bindings); - var pDsl = stack.mallocLong(1); - check(vkCreateDescriptorSetLayout(ctx.device, dslci, null, pDsl), "vkCreateDescriptorSetLayout(" + name + ")"); - this.descriptorSetLayout = pDsl.get(0); - - var plci = VkPipelineLayoutCreateInfo.calloc(stack).sType$Default() - .pSetLayouts(stack.longs(this.descriptorSetLayout)); - var pPl = stack.mallocLong(1); - check(vkCreatePipelineLayout(ctx.device, plci, null, pPl), "vkCreatePipelineLayout(" + name + ")"); - this.pipelineLayout = pPl.get(0); - - var cpci = VkComputePipelineCreateInfo.calloc(1, stack).sType$Default() - .layout(this.pipelineLayout); - cpci.stage().sType$Default().stage(VK_SHADER_STAGE_COMPUTE_BIT) - .module(this.shaderModule).pName(stack.UTF8("main")); - var pPipe = stack.mallocLong(1); - check(vkCreateComputePipelines(ctx.device, VK_NULL_HANDLE, cpci, null, pPipe), "vkCreateComputePipelines(" + name + ")"); - this.pipeline = pPipe.get(0); - } - } - - public void bindAndDispatch(VkCommandBuffer cmd, long descriptorSet, int gx, int gy, int gz) { - try (MemoryStack stack = stackPush()) { - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, this.pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, this.pipelineLayout, - 0, stack.longs(descriptorSet), null); - vkCmdDispatch(cmd, gx, gy, gz); - } - } - - public void free() { - vkDestroyPipeline(this.ctx.device, this.pipeline, null); - vkDestroyPipelineLayout(this.ctx.device, this.pipelineLayout, null); - vkDestroyDescriptorSetLayout(this.ctx.device, this.descriptorSetLayout, null); - vkDestroyShaderModule(this.ctx.device, this.shaderModule, null); - } -} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkDownloadStream.java b/src/main/java/me/cortex/voxy/client/core/vk/VkDownloadStream.java new file mode 100644 index 000000000..2e92f70ab --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkDownloadStream.java @@ -0,0 +1,162 @@ +package me.cortex.voxy.client.core.vk; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.AllocationArena; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkBufferCopy; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; + +import static me.cortex.voxy.common.util.AllocationArena.SIZE_LIMIT; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK GPU->CPU readback: vkCmdCopyBuffer into a host-visible readback +// buffer at commit(); callbacks fire from tick() once the producing frame's +// event has signaled (the copy provably completed). Mirrors the GL +// DownloadStream fence/frame model. +public class VkDownloadStream extends AbstractDownloadStream { + private final VkFrameCtx ctx; + private final VkBuffer readbackBuffer; + private final long readbackPtr; + + private final AllocationArena allocationArena = new AllocationArena(); + private final Deque frames = new ArrayDeque<>(); + private final LongArrayList thisFrameAllocations = new LongArrayList(); + private final Deque downloadList = new ArrayDeque<>(); + private final ArrayList thisFrameDownloadList = new ArrayList<>(); + + private long caddr = -1; + private long offset = 0; + //The frame whose command buffer actually recorded the pending copies. tick() + // runs at the START of the next render frame (after endFrame advanced the + // counter), so currentFrame() there is one AHEAD of the frame the copy was + // recorded into — tagging with it made readbacks retire (and fire their + // node-visibility callbacks) a whole frame late. Capture the true recording + // frame at commit() instead. + private long recordFrame = -1; + + public VkDownloadStream(VkFrameCtx ctx, long size) { + this.ctx = ctx; + this.readbackBuffer = new VkBuffer(ctx, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, true); + this.readbackPtr = this.readbackBuffer.map(); + this.allocationArena.setLimit(size); + ctx.addRetireListener(this::retireUpTo); + } + + @Override + public void download(IDeviceBuffer buffer, long downloadOffset, long size, DownloadResultConsumer resultConsumer) { + if (size > Integer.MAX_VALUE || size <= 0) throw new IllegalArgumentException(); + if (downloadOffset + size > buffer.sizeBytes()) throw new IllegalArgumentException(); + + long addr; + if (this.caddr == -1 || !this.allocationArena.expand(this.caddr, (int) size)) { + this.caddr = this.allocationArena.alloc((int) size); + if (this.caddr == SIZE_LIMIT) { + Logger.warn("VK download stream full, force-idling the device to recover; this will hitch"); + this.commit(); + int attempts = 10; + while (--attempts != 0 && this.caddr == SIZE_LIMIT) { + this.ctx.waitIdleRetireAll(); + this.tick(); + this.caddr = this.allocationArena.alloc((int) size); + } + if (this.caddr == SIZE_LIMIT) { + throw new IllegalStateException("Could not allocate readback space even after device idle"); + } + } + this.thisFrameAllocations.add(this.caddr); + this.offset = size; + addr = this.caddr; + } else { + addr = this.caddr + this.offset; + this.offset += size; + } + this.downloadList.add(new DownloadData((VkBuffer) buffer, addr, downloadOffset, size, resultConsumer)); + this.commit(); + } + + @Override + public void commit() { + if (this.downloadList.isEmpty()) return; + //Capture the frame currently being recorded — the copies below land in its + // command buffer and complete when its event signals. + this.recordFrame = this.ctx.currentFrame(); + var cmd = this.ctx.cmd(); + //Source buffers are compute/raster/transfer outputs; narrow from ALL_COMMANDS + // to the actual producing stages so unrelated GPU work overlaps the copy. + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + try (MemoryStack stack = stackPush()) { + for (var entry : this.downloadList) { + var region = VkBufferCopy.calloc(1, stack) + .srcOffset(entry.targetOffset).dstOffset(entry.downloadStreamOffset).size(entry.size); + vkCmdCopyBuffer(cmd, entry.target.buffer, this.readbackBuffer.buffer, region); + } + } + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_HOST_READ_BIT); + this.thisFrameDownloadList.addAll(this.downloadList); + this.downloadList.clear(); + this.caddr = -1; + this.offset = 0; + } + + @Override + public void tick() { + this.commit(); + if (!this.thisFrameAllocations.isEmpty()) { + //Tag with the frame the copies were RECORDED in (captured at commit), + // not currentFrame() — which at this early-in-frame tick() is one ahead. + this.frames.add(new DownloadFrame(this.recordFrame, + new LongArrayList(this.thisFrameAllocations), new ArrayList<>(this.thisFrameDownloadList))); + this.thisFrameAllocations.clear(); + this.thisFrameDownloadList.clear(); + } + //pollRetired() is called once at the end of each frame by VkRenderCore; + // polling here too was redundant (3x/frame). + } + + private void retireUpTo(long retiredFrame) { + while (!this.frames.isEmpty() && this.frames.peek().frameIdx <= retiredFrame) { + var frame = this.frames.pop(); + for (var data : frame.data) { + data.resultConsumer.consume(this.readbackPtr + data.downloadStreamOffset, data.size); + } + frame.allocations.forEach(this.allocationArena::free); + } + } + + @Override + public void waitDiscard() { + this.ctx.waitIdleRetireAll(); + while (!this.frames.isEmpty()) { + var frame = this.frames.pop(); + frame.allocations.forEach(this.allocationArena::free); + } + } + + @Override + public void flushWaitClear() { + this.tick(); + this.ctx.waitIdleRetireAll(); + if (!this.frames.isEmpty()) { + //waitIdleRetireAll retires via listener; anything left means listener ordering broke + throw new IllegalStateException(); + } + } + + @Override + public void free() { + this.readbackBuffer.free(); + } + + private record DownloadFrame(long frameIdx, LongArrayList allocations, ArrayList data) {} + private record DownloadData(VkBuffer target, long downloadStreamOffset, long targetOffset, long size, DownloadResultConsumer resultConsumer) {} +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkFrameCtx.java b/src/main/java/me/cortex/voxy/client/core/vk/VkFrameCtx.java new file mode 100644 index 000000000..14810607a --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkFrameCtx.java @@ -0,0 +1,264 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.common.Logger; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkCommandBufferAllocateInfo; +import org.lwjgl.vulkan.VkCommandBufferBeginInfo; +import org.lwjgl.vulkan.VkEventCreateInfo; +import org.lwjgl.vulkan.VkFenceCreateInfo; +import org.lwjgl.vulkan.VkMemoryBarrier; +import org.lwjgl.vulkan.VkSubmitInfo; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//The per-frame Vulkan recording context for the pure-VK path. +// +//ALL of Voxy's GPU work is recorded into MC's live frame command buffer between +// MC's own render passes (upload copies -> compute -> raster passes -> draws -> +// readback copies), ordered with pipeline barriers. This class owns: +// +// - the current recording target (cmd()), either MC's frame command buffer +// (inside the render hook) or a one-shot immediate buffer (resource +// construction outside a frame); +// - frame completion tracking WITHOUT touching MC's encoder internals: each +// Voxy frame ends with vkCmdSetEvent; the event provably signals only when +// MC has submitted the frame and the GPU reached Voxy's commands. Upload / +// download streams and deferred destruction retire on those events; +// - deferred destruction of buffers/images still referenced by frames in +// flight. +// +//Thread model: render thread only (MC records its VK frame on its render +// thread, and every Voxy entry point here is called from that thread). +public final class VkFrameCtx { + public interface FrameRetireListener { + /** Called when GPU work for frame {@code frameIdx} has provably completed. */ + void onFramesRetired(long retiredUpToInclusive); + } + + private final VulkanContext ctx; + private VkCommandBuffer frameCmd; //MC's frame command buffer while inside the hook + private VkCommandBuffer immediateCmd; //one-shot fallback outside the hook + private boolean anyWorkThisFrame; + + private long frameCounter = 0; //index of the frame currently being recorded + private long retiredCounter = -1; //all frames <= this have completed on the GPU + + private final Deque inFlight = new ArrayDeque<>(); + private final ArrayList eventPool = new ArrayList<>(); + private final ArrayList pendingDestroys = new ArrayList<>(); + private final ArrayList retireListeners = new ArrayList<>(); + + private record InFlightFrame(long frameIdx, long event) {} + private record PendingDestroy(long frameIdx, long buffer, long image, long imageView, long memory) {} + + public VkFrameCtx(VulkanContext ctx) { + this.ctx = ctx; + } + + public VulkanContext vk() { + return this.ctx; + } + + public void addRetireListener(FrameRetireListener listener) { + this.retireListeners.add(listener); + } + + /** Frame index currently being recorded; work recorded now completes when this frame retires. */ + public long currentFrame() { + return this.frameCounter; + } + + //================================================================================== + // Recording targets + + /** Enter the render hook: record into MC's frame command buffer. */ + public void beginFrame(VkCommandBuffer mcFrameCommandBuffer) { + if (this.frameCmd != null) throw new IllegalStateException("Frame already begun"); + this.frameCmd = mcFrameCommandBuffer; + } + + /** Leave the render hook: stamp the frame event and advance the frame counter. */ + public void endFrame() { + if (this.frameCmd == null) throw new IllegalStateException("No frame begun"); + if (this.anyWorkThisFrame) { + long event = this.obtainEvent(); + vkCmdSetEvent(this.frameCmd, event, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); + this.inFlight.add(new InFlightFrame(this.frameCounter, event)); + this.frameCounter++; + this.anyWorkThisFrame = false; + } + this.frameCmd = null; + } + + //The command buffer to record into. Inside the render hook this is MC's + // frame command buffer; outside it, a one-shot immediate command buffer is + // begun on demand and submitted synchronously by flushImmediate(). + public VkCommandBuffer cmd() { + this.anyWorkThisFrame = true; + if (this.frameCmd != null) return this.frameCmd; + if (this.immediateCmd == null) { + try (MemoryStack stack = stackPush()) { + var cbai = VkCommandBufferAllocateInfo.calloc(stack).sType$Default() + .commandPool(this.ctx.commandPool) + .level(VK_COMMAND_BUFFER_LEVEL_PRIMARY) + .commandBufferCount(1); + var pCmd = stack.mallocPointer(1); + check(vkAllocateCommandBuffers(this.ctx.device, cbai, pCmd), "vkAllocateCommandBuffers(immediate)"); + this.immediateCmd = new VkCommandBuffer(pCmd.get(0), this.ctx.device); + var begin = VkCommandBufferBeginInfo.calloc(stack).sType$Default() + .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + check(vkBeginCommandBuffer(this.immediateCmd, begin), "vkBeginCommandBuffer(immediate)"); + } + } + return this.immediateCmd; + } + + /** Submits + waits any pending immediate commands (resource init outside a frame). */ + public void flushImmediate() { + if (this.immediateCmd == null) return; + var cmd = this.immediateCmd; + this.immediateCmd = null; + try (MemoryStack stack = stackPush()) { + check(vkEndCommandBuffer(cmd), "vkEndCommandBuffer(immediate)"); + var fci = VkFenceCreateInfo.calloc(stack).sType$Default(); + var pFence = stack.mallocLong(1); + check(vkCreateFence(this.ctx.device, fci, null, pFence), "vkCreateFence(immediate)"); + long fence = pFence.get(0); + var submit = VkSubmitInfo.calloc(stack).sType$Default() + .pCommandBuffers(stack.pointers(cmd)); + check(vkQueueSubmit(this.ctx.queue, submit, fence), "vkQueueSubmit(immediate)"); + check(vkWaitForFences(this.ctx.device, fence, true, Long.MAX_VALUE), "vkWaitForFences(immediate)"); + vkDestroyFence(this.ctx.device, fence, null); + vkFreeCommandBuffers(this.ctx.device, this.ctx.commandPool, cmd); + } + } + + //================================================================================== + // Frame retirement + + /** Poll frame events; retire completed frames (recycles staging space, runs destroys). */ + public void pollRetired() { + boolean any = false; + while (!this.inFlight.isEmpty()) { + var frame = this.inFlight.peek(); + int status = vkGetEventStatus(this.ctx.device, frame.event); + if (status != VK_EVENT_SET) break; + this.inFlight.pop(); + check(vkResetEvent(this.ctx.device, frame.event), "vkResetEvent"); + this.eventPool.add(frame.event); + this.retiredCounter = frame.frameIdx; + any = true; + } + if (any) { + this.runRetirement(); + } + } + + /** Hard sync: device idle, then retire EVERYTHING (shutdown / teardown). */ + public void waitIdleRetireAll() { + this.flushImmediate(); + vkDeviceWaitIdle(this.ctx.device); + while (!this.inFlight.isEmpty()) { + vkDestroyEvent(this.ctx.device, this.inFlight.pop().event, null); + } + //Device is idle: every recorded frame — including the one currently being + // recorded — has completed, so even current-frame-tagged destroys are safe. + this.retiredCounter = this.frameCounter; + this.runRetirement(); + } + + private void runRetirement() { + for (var l : this.retireListeners) { + l.onFramesRetired(this.retiredCounter); + } + this.pendingDestroys.removeIf(d -> { + if (d.frameIdx <= this.retiredCounter) { + if (d.buffer != VK_NULL_HANDLE) vkDestroyBuffer(this.ctx.device, d.buffer, null); + if (d.imageView != VK_NULL_HANDLE) vkDestroyImageView(this.ctx.device, d.imageView, null); + if (d.image != VK_NULL_HANDLE) vkDestroyImage(this.ctx.device, d.image, null); + if (d.memory != VK_NULL_HANDLE) vkFreeMemory(this.ctx.device, d.memory, null); + return true; + } + return false; + }); + } + + private long obtainEvent() { + if (!this.eventPool.isEmpty()) { + return this.eventPool.remove(this.eventPool.size() - 1); + } + try (MemoryStack stack = stackPush()) { + var eci = VkEventCreateInfo.calloc(stack).sType$Default(); + var pEvent = stack.mallocLong(1); + check(vkCreateEvent(this.ctx.device, eci, null, pEvent), "vkCreateEvent"); + return pEvent.get(0); + } + } + + //================================================================================== + // Deferred destruction + + public void deferDestroy(long buffer, long memory) { + this.pendingDestroys.add(new PendingDestroy(this.frameCounter, buffer, VK_NULL_HANDLE, VK_NULL_HANDLE, memory)); + } + + public void deferDestroyImage(long image, long view, long memory) { + this.pendingDestroys.add(new PendingDestroy(this.frameCounter, VK_NULL_HANDLE, image, view, memory)); + } + + //================================================================================== + // Command helpers + + public void fillBuffer(VkBuffer buffer, long offset, long size, int value) { + vkCmdFillBuffer(this.cmd(), buffer.buffer, offset, size, value); + this.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + } + + /** Global memory barrier in the current command buffer. */ + public void barrier(int srcStage, int srcAccess, int dstStage, int dstAccess) { + try (MemoryStack stack = stackPush()) { + var mb = VkMemoryBarrier.calloc(1, stack).sType$Default() + .srcAccessMask(srcAccess).dstAccessMask(dstAccess); + vkCmdPipelineBarrier(this.cmd(), srcStage, dstStage, 0, mb, null, null); + } + } + + /** Compute-write -> compute-read barrier (compute-to-compute chaining). */ + public void computeToComputeBarrier() { + this.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + } + + /** Compute-write -> draw-indirect/vertex-read barrier (cmdgen -> raster draw). */ + public void computeToDrawBarrier() { + this.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, + VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + } + + /** Compute-write -> transfer-read barrier (compute output -> download copy). */ + public void computeToTransferBarrier() { + this.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + + public void free() { + this.waitIdleRetireAll(); + for (long event : this.eventPool) { + vkDestroyEvent(this.ctx.device, event, null); + } + this.eventPool.clear(); + if (!this.pendingDestroys.isEmpty()) { + Logger.warn("VkFrameCtx freed with " + this.pendingDestroys.size() + " pending destroys remaining"); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java new file mode 100644 index 000000000..ce5e1a879 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java @@ -0,0 +1,195 @@ +package me.cortex.voxy.client.core.vk; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkImageCreateInfo; +import org.lwjgl.vulkan.VkImageFormatListCreateInfo; +import org.lwjgl.vulkan.VkImageMemoryBarrier; +import org.lwjgl.vulkan.VkImageViewCreateInfo; +import org.lwjgl.vulkan.VkMemoryAllocateInfo; +import org.lwjgl.vulkan.VkMemoryRequirements; +import org.lwjgl.vulkan.VkSamplerCreateInfo; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//2D image + full view (+ optional per-mip views) for the pure-VK path: +// offscreen colour/depth targets, the HiZ mip pyramid, and the model atlas. +// Tracks the current layout for whole-image transitions (Voxy transitions +// whole subresource ranges only, keeping parity with the GL path's coarse +// barrier usage). +public final class VkImage2D { + private final VkFrameCtx ctx; + public final long image; + public final long memory; + public final long view; + public final long[] mipViews;//null unless requested + public final int width, height, mipLevels; + public final int format; + public final int aspect; + private int currentLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + public VkImage2D(VkFrameCtx ctx, int width, int height, int mipLevels, int format, int usage, int aspect, boolean perMipViews) { + this(ctx, width, height, mipLevels, format, usage, aspect, perMipViews, null); + } + + //Creates a 2D image with optional VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT + a + // VkImageFormatListCreateInfo listing the formats the image will be viewed + // as. Required for sampling a depth-only aspect of a packed D32_SFLOAT_S8_UINT + // image on MoltenVK (so the depth-only view aliases the image without a + // separate staging texture). viewFormats may be null (no mutable-format + // flag, classic path). + public VkImage2D(VkFrameCtx ctx, int width, int height, int mipLevels, int format, int usage, int aspect, + boolean perMipViews, int[] viewFormats) { + this.ctx = ctx; + this.width = width; + this.height = height; + this.mipLevels = mipLevels; + this.format = format; + this.aspect = aspect; + var vctx = ctx.vk(); + try (MemoryStack stack = stackPush()) { + var ici = VkImageCreateInfo.calloc(stack).sType$Default() + .imageType(VK_IMAGE_TYPE_2D) + .format(format) + .extent(e -> e.width(width).height(height).depth(1)) + .mipLevels(mipLevels).arrayLayers(1) + .samples(VK_SAMPLE_COUNT_1_BIT) + .tiling(VK_IMAGE_TILING_OPTIMAL) + .usage(usage) + .sharingMode(VK_SHARING_MODE_EXCLUSIVE) + .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED); + if (viewFormats != null && viewFormats.length > 0) { + ici.flags(VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT); + var formatList = VkImageFormatListCreateInfo.calloc(stack).sType$Default() + .pViewFormats(stack.ints(viewFormats)); + ici.pNext(formatList.address()); + } + var pImg = stack.mallocLong(1); + check(vkCreateImage(vctx.device, ici, null, pImg), "vkCreateImage"); + this.image = pImg.get(0); + + var req = VkMemoryRequirements.calloc(stack); + vkGetImageMemoryRequirements(vctx.device, this.image, req); + var mai = VkMemoryAllocateInfo.calloc(stack).sType$Default() + .allocationSize(req.size()) + .memoryTypeIndex(vctx.findMemoryType(req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); + var pMem = stack.mallocLong(1); + check(vkAllocateMemory(vctx.device, mai, null, pMem), "vkAllocateMemory(image)"); + this.memory = pMem.get(0); + check(vkBindImageMemory(vctx.device, this.image, this.memory, 0), "vkBindImageMemory"); + + this.view = createView(stack, vctx, 0, mipLevels); + if (perMipViews) { + this.mipViews = new long[mipLevels]; + for (int i = 0; i < mipLevels; i++) { + this.mipViews[i] = createView(stack, vctx, i, 1); + } + } else { + this.mipViews = null; + } + } + } + + private long createView(MemoryStack stack, VulkanContext vctx, int baseMip, int mipCount) { + var vci = VkImageViewCreateInfo.calloc(stack).sType$Default() + .image(this.image).viewType(VK_IMAGE_VIEW_TYPE_2D).format(this.format); + vci.subresourceRange().aspectMask(this.aspect).baseMipLevel(baseMip).levelCount(mipCount).baseArrayLayer(0).layerCount(1); + var pView = stack.mallocLong(1); + check(vkCreateImageView(vctx.device, vci, null, pView), "vkCreateImageView"); + return pView.get(0); + } + + /** Whole-image layout transition recorded into the current frame commands. */ + public void transition(int newLayout, int srcStage, int srcAccess, int dstStage, int dstAccess) { + try (MemoryStack stack = stackPush()) { + var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() + .srcAccessMask(srcAccess).dstAccessMask(dstAccess) + .oldLayout(this.currentLayout).newLayout(newLayout) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED).dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .image(this.image); + imb.subresourceRange().aspectMask(this.aspect).levelCount(this.mipLevels).layerCount(1); + vkCmdPipelineBarrier(this.ctx.cmd(), srcStage, dstStage, 0, null, null, imb); + this.currentLayout = newLayout; + } + } + + //Batched transition: records multiple images' layout changes in a single + // vkCmdPipelineBarrier (one call instead of N). Each image's transition is + // described by its own (newLayout, srcStage, srcAccess, dstStage, dstAccess) + // tuple; the call uses the union of all src stages -> union of all dst stages + // so the single barrier covers every image's dependency. + public record BatchEntry(VkImage2D image, int newLayout, int srcAccess, int dstAccess) {} + public static void transitionBatch(java.util.List entries, int unionSrcStage, int unionDstStage) { + if (entries.isEmpty()) return; + try (MemoryStack stack = stackPush()) { + var imbs = VkImageMemoryBarrier.calloc(entries.size(), stack); + for (int i = 0; i < entries.size(); i++) { + var e = entries.get(i); + imbs.get(i).sType$Default() + .srcAccessMask(e.srcAccess).dstAccessMask(e.dstAccess) + .oldLayout(e.image.currentLayout).newLayout(e.newLayout) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED).dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .image(e.image.image) + .subresourceRange().aspectMask(e.image.aspect).levelCount(e.image.mipLevels).layerCount(1); + } + //Use the first image's ctx (all images share the same VkFrameCtx in Voxy). + var cmd = entries.get(0).image.ctx.cmd(); + vkCmdPipelineBarrier(cmd, unionSrcStage, unionDstStage, 0, null, null, imbs); + for (var e : entries) e.image.currentLayout = e.newLayout; + } + } + + private final java.util.ArrayList extraViews = new java.util.ArrayList<>(); + + /** Additional full-image view with a different aspect (e.g. DEPTH-only sampling view of a depth-stencil image). */ + public long createAspectView(int viewAspect) { + try (MemoryStack stack = stackPush()) { + var vci = VkImageViewCreateInfo.calloc(stack).sType$Default() + .image(this.image).viewType(VK_IMAGE_VIEW_TYPE_2D).format(this.format); + vci.subresourceRange().aspectMask(viewAspect).baseMipLevel(0).levelCount(this.mipLevels).baseArrayLayer(0).layerCount(1); + var pView = stack.mallocLong(1); + check(vkCreateImageView(this.ctx.vk().device, vci, null, pView), "vkCreateImageView(aspect)"); + long view = pView.get(0); + this.extraViews.add(view); + return view; + } + } + + public void free() { + if (this.mipViews != null) { + for (long v : this.mipViews) { + this.ctx.deferDestroyImage(0, v, 0); + } + } + for (long v : this.extraViews) { + this.ctx.deferDestroyImage(0, v, 0); + } + this.ctx.deferDestroyImage(this.image, this.view, this.memory); + } + + /** Simple sampler factory (nearest/clamped or nearest-mipmap for HiZ etc). + * Cached by (mipmapNearest, linear) so the ~9 call sites across the renderer + * share ~4 sampler handles instead of creating one each. */ + private static final java.util.Map SAMPLER_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + public static long createSampler(VulkanContext ctx, boolean mipmapNearest, boolean linear) { + long key = ctx.device.address() ^ (mipmapNearest ? 1L : 0L) ^ (linear ? 2L : 0L); + Long cached = SAMPLER_CACHE.get(key); + if (cached != null) return cached; + try (MemoryStack stack = stackPush()) { + var sci = VkSamplerCreateInfo.calloc(stack).sType$Default() + .magFilter(linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST) + .minFilter(linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST) + .mipmapMode(VK_SAMPLER_MIPMAP_MODE_NEAREST) + .addressModeU(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .addressModeV(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .addressModeW(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .minLod(0).maxLod(mipmapNearest ? VK_LOD_CLAMP_NONE : 0.25f); + var pSampler = stack.mallocLong(1); + check(vkCreateSampler(ctx.device, sci, null, pSampler), "vkCreateSampler"); + long handle = pSampler.get(0); + SAMPLER_CACHE.put(key, handle); + return handle; + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkShaderPipeline.java b/src/main/java/me/cortex/voxy/client/core/vk/VkShaderPipeline.java new file mode 100644 index 000000000..3a6f7d18a --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkShaderPipeline.java @@ -0,0 +1,315 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.gl.shader.ShaderType; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRPushDescriptor.VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR; +import static org.lwjgl.vulkan.KHRPushDescriptor.vkCmdPushDescriptorSetKHR; +import static org.lwjgl.vulkan.VK10.*; + +//Unified pipeline wrapper for the pure-VK path: an explicit binding table +// (set 0, push descriptors — required by MC 26.2's own Vulkan backend, so +// always present on an adopted device), an optional push-constant range, and +// either a compute stage or a vert+frag pair with dynamic rendering. +// +//Binding numbers mirror the (possibly remapped) layout(binding=N) declarations +// of the VK shader variants; see assets/voxy/shaders/lod/vk/. +public final class VkShaderPipeline { + public static final int T_UBO = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + public static final int T_SSBO = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + public static final int T_SAMPLER = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + public static final int T_IMAGE = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + + //Interned descriptorSetLayout cache keyed by (device, binding-hash) so + // pipelines sharing a binding table reuse one layout handle (and skip + // re-creating + later destroying it). Lives for the device lifetime. + private static final java.util.Map> LAYOUT_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + + private final VkFrameCtx ctx; + public final long descriptorSetLayout; + public final long pipelineLayout; + public final long pipeline; + private final long[] modules; + private final boolean compute; + private final int pushStages; + + public record Binding(int binding, int type) {} + + public static Binding ubo(int b) { return new Binding(b, T_UBO); } + public static Binding ssbo(int b) { return new Binding(b, T_SSBO); } + public static Binding sampler(int b) { return new Binding(b, T_SAMPLER); } + public static Binding image(int b) { return new Binding(b, T_IMAGE); } + + //=============================== Compute =============================== + + public VkShaderPipeline(VkFrameCtx ctx, String name, String computeGlsl, int pushConstantBytes, List bindings) { + this.ctx = ctx; + this.compute = true; + this.pushStages = VK_SHADER_STAGE_COMPUTE_BIT; + var vctx = ctx.vk(); + try (MemoryStack stack = stackPush()) { + long module = createModule(vctx, ShadercCompiler.compile(computeGlsl, ShaderType.COMPUTE, name), stack); + this.modules = new long[]{module}; + this.descriptorSetLayout = createSetLayout(vctx, stack, bindings, VK_SHADER_STAGE_COMPUTE_BIT); + this.pipelineLayout = createPipelineLayout(vctx, stack, this.descriptorSetLayout, pushConstantBytes, VK_SHADER_STAGE_COMPUTE_BIT); + + var cpci = VkComputePipelineCreateInfo.calloc(1, stack).sType$Default().layout(this.pipelineLayout); + cpci.stage().sType$Default().stage(VK_SHADER_STAGE_COMPUTE_BIT).module(module).pName(stack.UTF8("main")); + var pPipe = stack.mallocLong(1); + check(vkCreateComputePipelines(vctx.device, VK_NULL_HANDLE, cpci, null, pPipe), "vkCreateComputePipelines(" + name + ")"); + this.pipeline = pPipe.get(0); + } + } + + //=============================== Graphics =============================== + + public static final class GfxDesc { + public String name; + public String vertGlsl, fragGlsl; + public int pushConstantBytes; + public List bindings = new ArrayList<>(); + public int colorFormat; //VK_FORMAT_UNDEFINED for depth-only + public int depthFormat; //VK_FORMAT_UNDEFINED for no depth attachment + public int stencilFormat; //VK_FORMAT_UNDEFINED unless depth-stencil has stencil + public boolean depthTest = true, depthWrite = true; + public int depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + public boolean blend = false; //standard alpha blend when true + public boolean colorWrite = true; + public boolean stencilTestEqual1 = false;//stencil func EQUAL ref=1, keep (LOD terrain masking) + public boolean stencilWriteAlways1 = false;//stencil ALWAYS -> write stencilWriteRef (depth setup pass) + public int stencilWriteRef = 1; + public int topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + } + + private static boolean isStripTopology(int topology) { + return topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP + || topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP + || topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN + || topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY + || topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY; + } + + public VkShaderPipeline(VkFrameCtx ctx, GfxDesc d) { + this.ctx = ctx; + this.compute = false; + this.pushStages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + var vctx = ctx.vk(); + try (MemoryStack stack = stackPush()) { + long vertModule = createModule(vctx, ShadercCompiler.compile(d.vertGlsl, ShaderType.VERTEX, d.name + ".vert"), stack); + long fragModule = createModule(vctx, ShadercCompiler.compile(d.fragGlsl, ShaderType.FRAGMENT, d.name + ".frag"), stack); + this.modules = new long[]{vertModule, fragModule}; + this.descriptorSetLayout = createSetLayout(vctx, stack, d.bindings, this.pushStages); + this.pipelineLayout = createPipelineLayout(vctx, stack, this.descriptorSetLayout, d.pushConstantBytes, this.pushStages); + + var stages = VkPipelineShaderStageCreateInfo.calloc(2, stack); + stages.get(0).sType$Default().stage(VK_SHADER_STAGE_VERTEX_BIT).module(vertModule).pName(stack.UTF8("main")); + stages.get(1).sType$Default().stage(VK_SHADER_STAGE_FRAGMENT_BIT).module(fragModule).pName(stack.UTF8("main")); + + var vertexInput = VkPipelineVertexInputStateCreateInfo.calloc(stack).sType$Default();//vertex pulling + //MoltenVK/Metal cannot disable primitive restart for strip/fan topologies (VK_ERROR_FEATURE_NOT_PRESENT), + //so enable it for those. It must stay VK_FALSE for list topologies (spec VUID-...-topology-06252 without + //primitiveTopologyListRestart). Restart has no effect on our non-indexed strip draws, so this is safe. + var inputAssembly = VkPipelineInputAssemblyStateCreateInfo.calloc(stack).sType$Default() + .topology(d.topology) + .primitiveRestartEnable(isStripTopology(d.topology)); + //Dynamic viewport+scissor: one pipeline survives resizes + var dynamicState = VkPipelineDynamicStateCreateInfo.calloc(stack).sType$Default() + .pDynamicStates(stack.ints(VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR)); + var viewportState = VkPipelineViewportStateCreateInfo.calloc(stack).sType$Default() + .viewportCount(1).scissorCount(1); + var raster = VkPipelineRasterizationStateCreateInfo.calloc(stack).sType$Default() + .polygonMode(VK_POLYGON_MODE_FILL).cullMode(VK_CULL_MODE_NONE)//GL path disables cull face + .frontFace(VK_FRONT_FACE_COUNTER_CLOCKWISE).lineWidth(1); + var msaa = VkPipelineMultisampleStateCreateInfo.calloc(stack).sType$Default() + .rasterizationSamples(VK_SAMPLE_COUNT_1_BIT); + + var depthState = VkPipelineDepthStencilStateCreateInfo.calloc(stack).sType$Default() + .depthTestEnable(d.depthTest).depthWriteEnable(d.depthWrite).depthCompareOp(d.depthCompare); + if (d.stencilTestEqual1 || d.stencilWriteAlways1) { + depthState.stencilTestEnable(true); + var op = depthState.front(); + if (d.stencilWriteAlways1) { + op.failOp(VK_STENCIL_OP_KEEP).passOp(VK_STENCIL_OP_REPLACE).depthFailOp(VK_STENCIL_OP_KEEP) + .compareOp(VK_COMPARE_OP_ALWAYS).compareMask(0xFF).writeMask(0xFF).reference(d.stencilWriteRef); + } else { + op.failOp(VK_STENCIL_OP_KEEP).passOp(VK_STENCIL_OP_KEEP).depthFailOp(VK_STENCIL_OP_KEEP) + .compareOp(VK_COMPARE_OP_EQUAL).compareMask(0xFF).writeMask(0x00).reference(1); + } + depthState.back(depthState.front()); + } + + var blendAttach = VkPipelineColorBlendAttachmentState.calloc(1, stack) + .colorWriteMask(d.colorWrite ? 0xF : 0) + .blendEnable(d.blend); + if (d.blend) { + blendAttach.srcColorBlendFactor(VK_BLEND_FACTOR_SRC_ALPHA) + .dstColorBlendFactor(VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) + .colorBlendOp(VK_BLEND_OP_ADD) + .srcAlphaBlendFactor(VK_BLEND_FACTOR_ONE) + .dstAlphaBlendFactor(VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) + .alphaBlendOp(VK_BLEND_OP_ADD); + } + var blend = VkPipelineColorBlendStateCreateInfo.calloc(stack).sType$Default(); + if (d.colorFormat != VK_FORMAT_UNDEFINED) { + blend.pAttachments(blendAttach); + } + + var rendering = VkPipelineRenderingCreateInfoKHR.calloc(stack).sType$Default() + .depthAttachmentFormat(d.depthFormat) + .stencilAttachmentFormat(d.stencilFormat); + if (d.colorFormat != VK_FORMAT_UNDEFINED) { + rendering.colorAttachmentCount(1).pColorAttachmentFormats(stack.ints(d.colorFormat)); + } + + var gpci = VkGraphicsPipelineCreateInfo.calloc(1, stack).sType$Default() + .pNext(rendering) + .pStages(stages) + .pVertexInputState(vertexInput) + .pInputAssemblyState(inputAssembly) + .pViewportState(viewportState) + .pRasterizationState(raster) + .pMultisampleState(msaa) + .pDepthStencilState(depthState) + .pColorBlendState(blend) + .pDynamicState(dynamicState) + .layout(this.pipelineLayout); + var pPipe = stack.mallocLong(1); + check(vkCreateGraphicsPipelines(vctx.device, VK_NULL_HANDLE, gpci, null, pPipe), "vkCreateGraphicsPipelines(" + d.name + ")"); + this.pipeline = pPipe.get(0); + } + } + + //=============================== Shared =============================== + + private static long createModule(VulkanContext vctx, ByteBuffer spv, MemoryStack stack) { + var smci = VkShaderModuleCreateInfo.calloc(stack).sType$Default().pCode(spv); + var pMod = stack.mallocLong(1); + check(vkCreateShaderModule(vctx.device, smci, null, pMod), "vkCreateShaderModule"); + return pMod.get(0); + } + + private static long createSetLayout(VulkanContext vctx, MemoryStack stack, List bindings, int stages) { + //Intern by (bindings hash + stages): pipelines with identical binding + // tables (e.g. terrainOpaque and terrainTranslucent share the same + // bindings list) reuse one VkDescriptorSetLayout handle instead of each + // building + destroying its own. + long key = stages; + for (var b : bindings) { + key = key * 31L + b.binding() * 7L + b.type(); + } + long deviceAddr = vctx.device.address(); + var perDevice = LAYOUT_CACHE.computeIfAbsent(deviceAddr, k -> new java.util.concurrent.ConcurrentHashMap<>()); + Long cached = perDevice.get(key); + if (cached != null) return cached; + + var lb = VkDescriptorSetLayoutBinding.calloc(bindings.size(), stack); + for (int i = 0; i < bindings.size(); i++) { + var b = bindings.get(i); + lb.get(i).binding(b.binding()).descriptorType(b.type()).descriptorCount(1).stageFlags(stages); + } + var dslci = VkDescriptorSetLayoutCreateInfo.calloc(stack).sType$Default() + .flags(VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR) + .pBindings(lb); + var pDsl = stack.mallocLong(1); + check(vkCreateDescriptorSetLayout(vctx.device, dslci, null, pDsl), "vkCreateDescriptorSetLayout"); + long handle = pDsl.get(0); + perDevice.put(key, handle); + return handle; + } + + private static long createPipelineLayout(VulkanContext vctx, MemoryStack stack, long setLayout, int pushBytes, int pushStages) { + var plci = VkPipelineLayoutCreateInfo.calloc(stack).sType$Default().pSetLayouts(stack.longs(setLayout)); + if (pushBytes > 0) { + var range = VkPushConstantRange.calloc(1, stack).stageFlags(pushStages).offset(0).size(pushBytes); + plci.pPushConstantRanges(range); + } + var pPl = stack.mallocLong(1); + check(vkCreatePipelineLayout(vctx.device, plci, null, pPl), "vkCreatePipelineLayout"); + return pPl.get(0); + } + + public void bind(VkCommandBuffer cmd) { + vkCmdBindPipeline(cmd, this.compute ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS, this.pipeline); + } + + public void pushConstants(VkCommandBuffer cmd, ByteBuffer data) { + vkCmdPushConstants(cmd, this.pipelineLayout, this.pushStages, 0, data); + } + + /** Accumulates descriptor writes then pushes them; render-thread scratch use only. */ + public Binder binder() { + return new Binder(this); + } + + public static final class Binder implements AutoCloseable { + private final VkShaderPipeline owner; + private final MemoryStack stack = stackPush(); + private final List writes = new ArrayList<>(); + + private Binder(VkShaderPipeline owner) { + this.owner = owner; + } + + public Binder buffer(int binding, int type, long buffer, long offset, long range) { + var info = VkDescriptorBufferInfo.calloc(1, this.stack).buffer(buffer).offset(offset).range(range); + var write = VkWriteDescriptorSet.calloc(this.stack).sType$Default() + .dstBinding(binding).descriptorType(type).descriptorCount(1).pBufferInfo(info); + this.writes.add(write); + return this; + } + + public Binder ubo(int binding, VkBuffer buffer) { return this.buffer(binding, T_UBO, buffer.buffer, 0, buffer.size()); } + public Binder ssbo(int binding, VkBuffer buffer) { return this.buffer(binding, T_SSBO, buffer.buffer, 0, buffer.size()); } + public Binder ssbo(int binding, long rawBuffer, long offset, long range) { return this.buffer(binding, T_SSBO, rawBuffer, offset, range); } + + public Binder sampler(int binding, long imageView, long sampler, int layout) { + var info = VkDescriptorImageInfo.calloc(1, this.stack).sampler(sampler).imageView(imageView).imageLayout(layout); + var write = VkWriteDescriptorSet.calloc(this.stack).sType$Default() + .dstBinding(binding).descriptorType(T_SAMPLER).descriptorCount(1).pImageInfo(info); + this.writes.add(write); + return this; + } + + public Binder image(int binding, long imageView) { + var info = VkDescriptorImageInfo.calloc(1, this.stack).imageView(imageView).imageLayout(VK_IMAGE_LAYOUT_GENERAL); + var write = VkWriteDescriptorSet.calloc(this.stack).sType$Default() + .dstBinding(binding).descriptorType(T_IMAGE).descriptorCount(1).pImageInfo(info); + this.writes.add(write); + return this; + } + + public void push(VkCommandBuffer cmd) { + var buf = VkWriteDescriptorSet.calloc(this.writes.size(), this.stack); + for (int i = 0; i < this.writes.size(); i++) { + buf.put(i, this.writes.get(i)); + } + vkCmdPushDescriptorSetKHR(cmd, + this.owner.compute ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS, + this.owner.pipelineLayout, 0, buf); + } + + @Override + public void close() { + this.stack.close(); + } + } + + public void free() { + var device = this.ctx.vk().device; + vkDestroyPipeline(device, this.pipeline, null); + vkDestroyPipelineLayout(device, this.pipelineLayout, null); + //descriptorSetLayout is interned (shared across pipelines with identical + // bindings); never freed per-pipeline. Leaks are bounded by the device + // lifetime and the binding-table cardinality (a handful of distinct layouts). + for (long module : this.modules) { + vkDestroyShaderModule(device, module, null); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkShaderSource.java b/src/main/java/me/cortex/voxy/client/core/vk/VkShaderSource.java new file mode 100644 index 000000000..05af55f95 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkShaderSource.java @@ -0,0 +1,74 @@ +package me.cortex.voxy.client.core.vk; + +import me.cortex.voxy.client.core.gl.shader.ShaderLoader; + +import java.util.LinkedHashMap; +import java.util.Map; + +//Loads a Voxy shader asset for the Vulkan backend: expands #imports via the +// shared ShaderLoader, forces a Vulkan-capable #version, and injects #defines +// (binding remaps, feature flags) right after the version/extension header — +// the same define-injection model the GL Shader builder uses. VOXY_VULKAN is +// added by ShadercCompiler itself, activating the guards in shared sources. +public final class VkShaderSource { + //Shader printf debugging is a GL-path feature (PrintfInjector rewrites the calls); + // on VK the statements are stripped so the shared sources stay single-source. + private static final java.util.regex.Pattern PRINTF = java.util.regex.Pattern.compile("printf\\s*\\([^;]*?\\)\\s*;", java.util.regex.Pattern.DOTALL); + + public static String load(String id, Map defines) { + String src = PRINTF.matcher(ShaderLoader.parse(id)).replaceAll(""); + StringBuilder header = new StringBuilder(); + StringBuilder body = new StringBuilder(); + boolean versionDone = false; + for (String line : src.split("\n", -1)) { + String trimmed = line.trim(); + if (!versionDone && trimmed.startsWith("#version")) { + //Force a Vulkan-legal version (some fullscreen shaders are #version 330) + header.append("#version 460 core\n"); + versionDone = true; + continue; + } + if (!versionDone || trimmed.startsWith("#extension")) { + header.append(line).append('\n'); + continue; + } + body.append(line).append('\n'); + } + StringBuilder out = new StringBuilder(header); + if (!versionDone) { + out.insert(0, "#version 460 core\n"); + } + for (var e : defines.entrySet()) { + out.append("#define ").append(e.getKey()); + if (e.getValue() != null && !e.getValue().isEmpty()) { + out.append(' ').append(e.getValue()); + } + out.append('\n'); + } + out.append(body); + return out.toString(); + } + + public static DefineBuilder defs() { + return new DefineBuilder(); + } + + public static final class DefineBuilder { + private final Map map = new LinkedHashMap<>(); + + public DefineBuilder def(String k) { this.map.put(k, ""); return this; } + public DefineBuilder def(String k, int v) { this.map.put(k, Integer.toString(v)); return this; } + public DefineBuilder def(String k, float v) { this.map.put(k, Float.toString(v)); return this; } + public DefineBuilder def(String k, String v) { this.map.put(k, v); return this; } + public DefineBuilder defIf(String k, boolean condition) { if (condition) this.map.put(k, ""); return this; } + + /** USE_ZERO_ONE_DEPTH / USE_REVERSE_Z from render properties. */ + public DefineBuilder props(me.cortex.voxy.client.core.RenderProperties properties) { + this.defIf("USE_ZERO_ONE_DEPTH", properties.isZero2One()); + this.defIf("USE_REVERSE_Z", properties.isReverseZ()); + return this; + } + + public Map build() { return this.map; } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkUploadStream.java b/src/main/java/me/cortex/voxy/client/core/vk/VkUploadStream.java new file mode 100644 index 000000000..d9f3df5cf --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkUploadStream.java @@ -0,0 +1,164 @@ +package me.cortex.voxy.client.core.vk; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.AllocationArena; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkBufferCopy; + +import java.util.ArrayDeque; +import java.util.Deque; + +import static me.cortex.voxy.common.util.AllocationArena.SIZE_LIMIT; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK implementation of the streaming upload contract: a persistently +// mapped host-visible staging buffer + vkCmdCopyBuffer batches recorded into +// the current frame commands at commit(). Staging space is recycled when the +// frame that consumed it retires (VkFrameCtx events), mirroring the GL +// fence-per-frame model 1:1. +public class VkUploadStream extends AbstractUploadStream { + private final VkFrameCtx ctx; + private final VkBuffer stagingBuffer; + private final long stagingPtr; + private final int alignment; + + private final AllocationArena allocationArena = new AllocationArena(); + private final Deque frames = new ArrayDeque<>(); + private final LongArrayList thisFrameAllocations = new LongArrayList(); + private final Deque uploadList = new ArrayDeque<>(); + + private long caddr = -1; + private long offset = 0; + + public VkUploadStream(VkFrameCtx ctx, long size) { + this.ctx = ctx; + this.stagingBuffer = new VkBuffer(ctx, size, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true); + this.stagingPtr = this.stagingBuffer.map(); + this.allocationArena.setLimit(size); + long minAlign = Math.max(16, ctx.vk().storageBufferOffsetAlignment()); + //Also honour minUniformBufferOffsetAlignment defensively: uniform uploads + // are offset-0 today, but a packed-uniform sub-allocation path would break + // without this. storageBufferOffsetAlignment is typically >= uniformAlign + // so this is usually a no-op. + minAlign = Math.max(minAlign, ctx.vk().uniformBufferOffsetAlignment()); + this.alignment = (int) minAlign; + ctx.addRetireListener(this::retireUpTo); + } + + /** VK handle of the staging buffer, for binding staged regions directly as SSBOs. */ + public long stagingBufferHandle() { + return this.stagingBuffer.buffer; + } + + @Override + public long upload(IDeviceBuffer buffer, long destOffset, long size) { + long addr = this.rawUploadAddress((int) size); + this.uploadList.add(new UploadData((VkBuffer) buffer, addr, destOffset, size)); + return this.stagingPtr + addr; + } + + @Override + public long rawUploadAddress(int size) { + if (size < 0) throw new IllegalStateException("Negative size"); + size = this.alignUpAlloc(size); + if (size > this.stagingBuffer.size()) throw new IllegalArgumentException(); + + long addr; + if (this.caddr == -1 || !this.allocationArena.expand(this.caddr, size)) { + this.caddr = this.allocationArena.alloc(size); + if (this.caddr == SIZE_LIMIT) { + Logger.error("VK upload stream full, force-idling the device to recover; this will hitch"); + int attempts = 10; + while (--attempts != 0 && this.caddr == SIZE_LIMIT) { + this.ctx.waitIdleRetireAll(); + this.caddr = this.allocationArena.alloc(size); + } + if (this.caddr == SIZE_LIMIT) { + throw new IllegalStateException("Could not allocate upload staging space even after device idle"); + } + } + this.thisFrameAllocations.add(this.caddr); + this.offset = size; + addr = this.caddr; + } else { + addr = this.caddr + this.offset; + this.offset += size; + } + if (this.caddr + size > this.stagingBuffer.size()) throw new IllegalStateException(); + return addr; + } + + @Override + public void commit() { + if (this.uploadList.isEmpty()) { + this.caddr = -1; + this.offset = 0; + return; + } + var cmd = this.ctx.cmd(); + //Upload copies must not race preceding GPU writes of the destination + // buffers (the targets are compute/raster/transfer outputs), and must + // complete before the consuming compute/vertex/fragment stages read + // them. Scoped stage masks let unrelated GPU work overlap the copies, + // unlike the previous ALL_COMMANDS barriers which forced a full stall + // on every commit (~6/frame). + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + try (MemoryStack stack = stackPush()) { + for (var entry : this.uploadList) { + var region = VkBufferCopy.calloc(1, stack) + .srcOffset(entry.uploadOffset).dstOffset(entry.targetOffset).size(entry.size); + vkCmdCopyBuffer(cmd, this.stagingBuffer.buffer, entry.target.buffer, region); + } + } + this.uploadList.clear(); + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + this.caddr = -1; + this.offset = 0; + } + + @Override + public void tick() { + this.commit(); + if (!this.thisFrameAllocations.isEmpty()) { + this.frames.add(new UploadFrame(this.ctx.currentFrame(), new LongArrayList(this.thisFrameAllocations))); + this.thisFrameAllocations.clear(); + } + //pollRetired() is called once at the end of each frame by VkRenderCore; + // polling here too was redundant (3x/frame) with no extra retirement benefit. + } + + private void retireUpTo(long retiredFrame) { + while (!this.frames.isEmpty() && this.frames.peek().frameIdx <= retiredFrame) { + var frame = this.frames.pop(); + frame.allocations.forEach(this.allocationArena::free); + } + } + + @Override + public long getBaseAddress() { + return this.stagingPtr; + } + + @Override + public int baseAlignment() { + return this.alignment; + } + + @Override + public void free() { + this.retireUpTo(Long.MAX_VALUE); + this.stagingBuffer.free(); + } + + private record UploadFrame(long frameIdx, LongArrayList allocations) {} + private record UploadData(VkBuffer target, long uploadOffset, long targetOffset, long size) {} +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java index ed2332a24..8120f44a1 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanBackend.java @@ -1,28 +1,47 @@ package me.cortex.voxy.client.core.vk; -import me.cortex.voxy.client.core.util.IrisUtil; import me.cortex.voxy.common.Logger; -/** - * Capability detection + lifecycle for the optional Vulkan backend. - * GL remains the default; VK is only used when (a) the config toggle asks for it, - * (b) a suitable device with GL-interop extensions exists, and (c) an Iris - * shaderpack is NOT active (Iris patches Voxy's GLSL fragment path, which the - * phase-1 VK backend cannot honor, so it is explicitly gated out). - */ +//Capability detection + lifecycle for the Vulkan backend. +//Voxy follows Minecraft's own graphics API: when MC runs on its 26.2 Vulkan +// backend every rendering mod draws through Blaze3D on Vulkan (no GL context +// exists in the process), so Voxy adopts MC's VkDevice/queue via IVkHost instead +// of creating its own. When MC is on OpenGL Voxy uses its OpenGL (MDIC) backend. +//There is no fallback either way: a GL context cannot exist while MC is on Vulkan. public final class VulkanBackend { private static Boolean supported; private static VulkanContext context; private static String unsupportedReason = "not probed"; + + //True when MC is on Vulkan AND the host adapter is registered AND the LWJGL + // Vulkan bindings + MC's device could be adopted. + public static boolean shouldUseVulkan() { + if (!MinecraftVkHost.isMinecraftOnVulkan()) { + return false;//MC is on OpenGL -> Voxy follows it onto OpenGL + } + if (MinecraftVkHost.get() == null) { + //MC reports Vulkan but the Blaze3D-VK adapter has not registered a host yet + Logger.info("Voxy: Minecraft on Vulkan but host adapter not yet registered"); + return false; + } + return isSupported(); + } + public static synchronized boolean isSupported() { if (supported == null) { try { Class.forName("org.lwjgl.vulkan.VK10"); - var probe = new VulkanContext(); - context = probe; - supported = true; - unsupportedReason = null; + var host = MinecraftVkHost.get(); + if (host == null) { + supported = false; + unsupportedReason = "no Minecraft Vulkan host adapter"; + } else { + context = VulkanContext.adopt(host); + supported = true; + unsupportedReason = null; + Logger.info("Voxy Vulkan backend adopting Minecraft's device: " + context.deviceName); + } } catch (Throwable t) { supported = false; unsupportedReason = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(); @@ -32,22 +51,6 @@ public static synchronized boolean isSupported() { return supported; } - public static boolean shouldUseVulkan(boolean configWantsVulkan) { - if (!configWantsVulkan) return false; - if (IrisUtil.IRIS_INSTALLED && IrisUtil.irisShaderpackActiveSafe()) { - Logger.info("Voxy: Vulkan requested but Iris shaderpack active -> staying on OpenGL"); - return false; - } - if (org.lwjgl.system.Platform.get() == org.lwjgl.system.Platform.MACOSX - && !MinecraftVkHost.isMinecraftOnVulkan()) { - //macOS has no GL-interop (MoltenVK lacks external_memory_fd; Apple GL is 4.1), - //so the only viable VK path is riding Minecraft's own 26.2 Vulkan backend. - Logger.info("Voxy: Vulkan on macOS requires Minecraft's Graphics API set to 'Prefer Vulkan' -> staying on OpenGL"); - return false; - } - return isSupported(); - } - public static synchronized VulkanContext context() { if (!isSupported()) throw new IllegalStateException("Vulkan not supported: " + unsupportedReason); return context; @@ -55,11 +58,13 @@ public static synchronized VulkanContext context() { public static String statusLine() { if (supported == null) return "vk: unprobed"; - return supported ? ("vk: " + context.deviceName) : ("vk: unavailable (" + unsupportedReason + ")"); + return supported ? ("vk: host(" + context.deviceName + ")") : ("vk: unavailable (" + unsupportedReason + ")"); } public static synchronized void shutdown() { - if (context != null) { context.destroy(); context = null; supported = null; } + //Host-adopted: do not destroy MC's device (VulkanContext.destroy handles this) + if (context != null) { context.destroy(); context = null; } + supported = null; } private VulkanBackend() {} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java index 673c0552c..41dec05d9 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java @@ -1,29 +1,29 @@ package me.cortex.voxy.client.core.vk; import me.cortex.voxy.common.Logger; -import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.Platform; -import org.lwjgl.vulkan.*; - -import java.nio.IntBuffer; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import org.lwjgl.vulkan.VK11; +import org.lwjgl.vulkan.VkCommandPoolCreateInfo; +import org.lwjgl.vulkan.VkDevice; +import org.lwjgl.vulkan.VkInstance; +import org.lwjgl.vulkan.VkPhysicalDevice; +import org.lwjgl.vulkan.VkPhysicalDeviceFeatures2; +import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; +import org.lwjgl.vulkan.VkPhysicalDeviceProperties; +import org.lwjgl.vulkan.VkPhysicalDeviceSubgroupProperties; +import org.lwjgl.vulkan.VkPhysicalDeviceVulkan12Features; +import org.lwjgl.vulkan.VkQueue; import static me.cortex.voxy.client.core.vk.VkUtil.check; import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.system.MemoryUtil.NULL; import static org.lwjgl.vulkan.VK10.*; -import static org.lwjgl.vulkan.VK12.VK_API_VERSION_1_2; +import static org.lwjgl.vulkan.VK11.*; -/** - * Owns Voxy's private VkInstance/VkDevice. Voxy does NOT take over Minecraft's - * swapchain: the interop model is VK renders LODs offscreen into images whose - * memory is exported (VK_KHR_external_memory) and imported into the game's GL - * context (GL_EXT_memory_object), synchronized with exported semaphores. - */ +//Wraps the Vulkan device Voxy renders on. Voxy never creates its own device: +// when MC 26.2 runs on its native Vulkan backend, Voxy ADOPTS the game's +// VkInstance/VkDevice/queue via IVkHost and allocates only its own command pool. +// MC owns (and destroys) the device/instance, so destroy() tears down only the +// command pool Voxy created here. public final class VulkanContext { public final VkInstance instance; public final VkPhysicalDevice physicalDevice; @@ -31,171 +31,117 @@ public final class VulkanContext { public final VkQueue queue; public final int queueFamily; public final boolean hasDrawIndirectCount; + public final boolean subgroupArithmetic; + public final int subgroupSize; public final String deviceName; public final long commandPool; - - private static final String[] REQUIRED_DEVICE_EXTS_COMMON = { - "VK_KHR_external_memory", - "VK_KHR_external_semaphore", - }; - - public static String[] platformInteropExts() { - return Platform.get() == Platform.WINDOWS - ? new String[]{"VK_KHR_external_memory_win32", "VK_KHR_external_semaphore_win32"} - : new String[]{"VK_KHR_external_memory_fd", "VK_KHR_external_semaphore_fd"}; - } - - /** True when this context must support the GL-interop hybrid mode (Windows/Linux only). */ - public final boolean glInterop; - /** True when running on a portability (MoltenVK/Metal) implementation. */ - public boolean isPortability; - - public VulkanContext() { this(Platform.get() != Platform.MACOSX); } - - public VulkanContext(boolean glInterop) { - this.glInterop = glInterop; + private VkPhysicalDeviceSubgroupProperties subgroupProps; + + public static VulkanContext adopt(IVkHost host) { return new VulkanContext(host); } + + private VulkanContext(IVkHost host) { + this.instance = host.instance(); + this.physicalDevice = host.physicalDevice(); + this.device = host.device(); + this.queue = host.graphicsQueue(); + this.queueFamily = host.graphicsQueueFamily(); + this.hasDrawIndirectCount = queryDrawIndirectCount(this.physicalDevice); + var subgroup = querySubgroupProperties(this.physicalDevice); + this.subgroupProps = subgroup; + this.subgroupArithmetic = subgroup != null && (subgroup.supportedOperations() & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0; + this.subgroupSize = subgroup != null ? subgroup.subgroupSize() : 1; + String name; try (MemoryStack stack = stackPush()) { - var appInfo = VkApplicationInfo.calloc(stack) - .sType$Default() - .pApplicationName(stack.UTF8("voxy")) - .pEngineName(stack.UTF8("voxy-vk")) - .apiVersion(VK_API_VERSION_1_2); - //MoltenVK (macOS): the loader only lists portability devices when - //VK_KHR_portability_enumeration is enabled with the ENUMERATE_PORTABILITY flag. - boolean portability = Platform.get() == Platform.MACOSX; - PointerBuffer instExts = null; - if (portability) { - instExts = stack.mallocPointer(2); - instExts.put(stack.UTF8("VK_KHR_portability_enumeration")); - instExts.put(stack.UTF8("VK_KHR_get_physical_device_properties2")); - instExts.flip(); - } - var ici = VkInstanceCreateInfo.calloc(stack).sType$Default().pApplicationInfo(appInfo); - if (portability) { - ici.flags(0x00000001 /*VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR*/) - .ppEnabledExtensionNames(instExts); - } - PointerBuffer pInstance = stack.mallocPointer(1); - check(vkCreateInstance(ici, null, pInstance), "vkCreateInstance"); - this.instance = new VkInstance(pInstance.get(0), ici); - - IntBuffer count = stack.mallocInt(1); - check(vkEnumeratePhysicalDevices(this.instance, count, null), "enumerate devices (count)"); - if (count.get(0) == 0) throw new IllegalStateException("No Vulkan physical devices"); - PointerBuffer devices = stack.mallocPointer(count.get(0)); - check(vkEnumeratePhysicalDevices(this.instance, count, devices), "enumerate devices"); - - boolean requireGlInterop = this.glInterop; - VkPhysicalDevice chosen = null; int chosenFamily = -1; boolean chosenDic = false; - boolean chosenPortability = false; boolean chosenDiscrete = false; String chosenName = null; - for (int i = 0; i < devices.capacity(); i++) { - var pd = new VkPhysicalDevice(devices.get(i), this.instance); - Set exts = deviceExtensions(pd, stack); - if (requireGlInterop && (!hasAll(exts, REQUIRED_DEVICE_EXTS_COMMON) || !hasAll(exts, platformInteropExts()))) continue; - int family = findGraphicsQueueFamily(pd, stack); - if (family < 0) continue; - var props = VkPhysicalDeviceProperties.calloc(stack); - vkGetPhysicalDeviceProperties(pd, props); - boolean discrete = props.deviceType() == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; - if (chosen != null && (chosenDiscrete || !discrete)) continue;//prefer discrete, like vanilla 26.2 - chosen = pd; chosenFamily = family; chosenName = props.deviceNameString(); chosenDiscrete = discrete; - //NOTE: drawIndirectCount is a FEATURE in core 1.2, not implied by apiVersion — - //MoltenVK reports 1.2 without it. Must query VkPhysicalDeviceVulkan12Features. - var f12q = VkPhysicalDeviceVulkan12Features.calloc(stack).sType$Default(); - var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(f12q); - VK11.vkGetPhysicalDeviceFeatures2(pd, f2); - chosenDic = f12q.drawIndirectCount() || exts.contains("VK_KHR_draw_indirect_count"); - chosenPortability = exts.contains("VK_KHR_portability_subset"); - } - if (chosen == null) throw new IllegalStateException(requireGlInterop - ? "No VK device with GL-interop extensions (hybrid mode; unavailable on macOS by design)" - : "No suitable VK device"); - this.physicalDevice = chosen; this.queueFamily = chosenFamily; - this.hasDrawIndirectCount = chosenDic; this.deviceName = chosenName; - this.isPortability = chosenPortability; - - List devExts = new ArrayList<>(); - if (requireGlInterop) { - devExts.addAll(List.of(REQUIRED_DEVICE_EXTS_COMMON)); - devExts.addAll(List.of(platformInteropExts())); - } - if (chosenPortability) devExts.add("VK_KHR_portability_subset");//spec: must enable if supported - if (chosenDic) devExts.add("VK_KHR_draw_indirect_count"); - PointerBuffer pExts = stack.mallocPointer(devExts.size()); - for (String e : devExts) pExts.put(stack.UTF8(e)); - pExts.flip(); - - var queueCI = VkDeviceQueueCreateInfo.calloc(1, stack).sType$Default() - .queueFamilyIndex(this.queueFamily) - .pQueuePriorities(stack.floats(1.0f)); - var features12 = VkPhysicalDeviceVulkan12Features.calloc(stack).sType$Default() - .drawIndirectCount(chosenDic); - var features = VkPhysicalDeviceFeatures.calloc(stack).multiDrawIndirect(true); - var dci = VkDeviceCreateInfo.calloc(stack).sType$Default() - .pNext(features12) - .pQueueCreateInfos(queueCI) - .ppEnabledExtensionNames(pExts) - .pEnabledFeatures(features); - PointerBuffer pDevice = stack.mallocPointer(1); - check(vkCreateDevice(this.physicalDevice, dci, null, pDevice), "vkCreateDevice"); - this.device = new VkDevice(pDevice.get(0), this.physicalDevice, dci); - - PointerBuffer pQueue = stack.mallocPointer(1); - vkGetDeviceQueue(this.device, this.queueFamily, 0, pQueue); - this.queue = new VkQueue(pQueue.get(0), this.device); - + var props = VkPhysicalDeviceProperties.calloc(stack); + vkGetPhysicalDeviceProperties(this.physicalDevice, props); + name = props.deviceNameString(); var cpci = VkCommandPoolCreateInfo.calloc(stack).sType$Default() .flags(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) .queueFamilyIndex(this.queueFamily); var pPool = stack.mallocLong(1); - check(vkCreateCommandPool(this.device, cpci, null, pPool), "vkCreateCommandPool"); + check(vkCreateCommandPool(this.device, cpci, null, pPool), "vkCreateCommandPool(adopted)"); this.commandPool = pPool.get(0); - Logger.info("Voxy Vulkan context created on device: " + this.deviceName - + " (drawIndirectCount=" + this.hasDrawIndirectCount + ")"); } + this.deviceName = name + " (MC host)"; + Logger.info("Voxy Vulkan context adopted Minecraft device: " + this.deviceName + + " (drawIndirectCount=" + this.hasDrawIndirectCount + + ", subgroupArithmetic=" + this.subgroupArithmetic + + ", subgroupSize=" + this.subgroupSize + ")"); } - private static Set deviceExtensions(VkPhysicalDevice pd, MemoryStack stack) { - IntBuffer c = stack.mallocInt(1); - vkEnumerateDeviceExtensionProperties(pd, (String) null, c, null); - var props = VkExtensionProperties.calloc(c.get(0), stack); - vkEnumerateDeviceExtensionProperties(pd, (String) null, c, props); - Set out = new HashSet<>(); - for (int i = 0; i < props.capacity(); i++) out.add(props.get(i).extensionNameString()); - return out; + private static boolean queryDrawIndirectCount(VkPhysicalDevice pd) { + try (MemoryStack stack = stackPush()) { + var f12q = VkPhysicalDeviceVulkan12Features.calloc(stack).sType$Default(); + var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(f12q); + VK11.vkGetPhysicalDeviceFeatures2(pd, f2); + return f12q.drawIndirectCount(); + } } - private static boolean hasAll(Set have, String[] want) { - for (String w : want) if (!have.contains(w)) return false; - return true; + private static VkPhysicalDeviceSubgroupProperties querySubgroupProperties(VkPhysicalDevice pd) { + try (MemoryStack stack = stackPush()) { + var sg = VkPhysicalDeviceSubgroupProperties.calloc(stack).sType$Default(); + var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(sg.address()); + VK11.vkGetPhysicalDeviceFeatures2(pd, f2); + // Return a malloc'd copy so the caller can read it past the stack frame. + var copy = VkPhysicalDeviceSubgroupProperties.malloc(); + copy.set(sg); + return copy; + } } - private static int findGraphicsQueueFamily(VkPhysicalDevice pd, MemoryStack stack) { - IntBuffer c = stack.mallocInt(1); - vkGetPhysicalDeviceQueueFamilyProperties(pd, c, null); - var fams = VkQueueFamilyProperties.calloc(c.get(0), stack); - vkGetPhysicalDeviceQueueFamilyProperties(pd, c, fams); - for (int i = 0; i < fams.capacity(); i++) { - if ((fams.get(i).queueFlags() & VK_QUEUE_GRAPHICS_BIT) != 0) return i; + private long storageAlign = -1; + /** minStorageBufferOffsetAlignment of the physical device. */ + public long storageBufferOffsetAlignment() { + if (this.storageAlign == -1) { + try (MemoryStack stack = stackPush()) { + var props = VkPhysicalDeviceProperties.calloc(stack); + vkGetPhysicalDeviceProperties(this.physicalDevice, props); + this.storageAlign = props.limits().minStorageBufferOffsetAlignment(); + } } - return -1; + return this.storageAlign; } - public int findMemoryType(int typeBits, int required) { - try (MemoryStack stack = stackPush()) { - var mem = VkPhysicalDeviceMemoryProperties.calloc(stack); - vkGetPhysicalDeviceMemoryProperties(this.physicalDevice, mem); - for (int i = 0; i < mem.memoryTypeCount(); i++) { - if ((typeBits & (1 << i)) != 0 && (mem.memoryTypes(i).propertyFlags() & required) == required) return i; + private long uniformAlign = -1; + /** minUniformBufferOffsetAlignment of the physical device. */ + public long uniformBufferOffsetAlignment() { + if (this.uniformAlign == -1) { + try (MemoryStack stack = stackPush()) { + var props = VkPhysicalDeviceProperties.calloc(stack); + vkGetPhysicalDeviceProperties(this.physicalDevice, props); + this.uniformAlign = props.limits().minUniformBufferOffsetAlignment(); } } + return this.uniformAlign; + } + + //Device memory properties are immutable for the device lifetime; cache them + // instead of re-querying the driver per allocation. Freed in destroy(). + private VkPhysicalDeviceMemoryProperties memoryProperties; + public int findMemoryType(int typeBits, int required) { + if (this.memoryProperties == null) { + this.memoryProperties = VkPhysicalDeviceMemoryProperties.malloc(); + vkGetPhysicalDeviceMemoryProperties(this.physicalDevice, this.memoryProperties); + } + var mem = this.memoryProperties; + for (int i = 0; i < mem.memoryTypeCount(); i++) { + if ((typeBits & (1 << i)) != 0 && (mem.memoryTypes(i).propertyFlags() & required) == required) return i; + } throw new IllegalStateException("No suitable VK memory type"); } public void destroy() { vkDeviceWaitIdle(this.device); vkDestroyCommandPool(this.device, this.commandPool, null); - vkDestroyDevice(this.device, null); - vkDestroyInstance(this.instance, null); + if (this.subgroupProps != null) { + this.subgroupProps.free(); + this.subgroupProps = null; + } + if (this.memoryProperties != null) { + this.memoryProperties.free(); + this.memoryProperties = null; + } + //Host mode: MC owns the device/instance — only the command pool above was ours. } } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkBoundRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkBoundRenderer.java new file mode 100644 index 000000000..3e9c9c672 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkBoundRenderer.java @@ -0,0 +1,165 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.bounding.IBoundStore; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkCmd; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import net.minecraft.client.Minecraft; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdBeginRenderingKHR; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdEndRenderingKHR; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK mirror of the GL BoundRenderer: rasterizes an AABB per Sodium-visible +// chunk section into the viewport's depth-bound image with a "further" depth +// test, capturing the far bound of the vanilla-covered volume. The terrain +// fragment shader (quads.frag, sampler binding 10) then discards LOD fragments +// that vanilla terrain will cover, saving overdraw. +// +//Differences from GL, both deliberate: +// - one 36-index box per instance instead of the 32-box batches (no +// baseInstance arithmetic — gl_InstanceIndex is the chunk id directly); +// - no face culling: with the further depth compare the back faces win the +// depth test anyway, which is exactly the bound the GL backface trick kept. +public class VkBoundRenderer { + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final RenderProperties properties; + + private final VkBuffer uniform; + private final VkBuffer boxIndexBuffer;//36 u16 indices, vertex ids 0..7 + private final VkShaderPipeline pipeline; + + //Per-frame uniform scratch (reused each render() to avoid heap allocs) + private final Vector3i cameraBlock = new Vector3i(); + private final Vector3f innerBlock = new Vector3f(); + private final Matrix4f mvpScratch = new Matrix4f(); + + public VkBoundRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, RenderProperties properties) { + this.ctx = ctx; + this.uploadStream = uploadStream; + this.properties = properties; + this.uniform = new VkBuffer(ctx, 128).zero(); + + this.boxIndexBuffer = new VkBuffer(ctx, 6 * 2 * 3 * 2L); + { + long ptr = uploadStream.upload(this.boxIndexBuffer, 0, this.boxIndexBuffer.size()); + VkCmd.writeCubeIndicesU16(ptr); + uploadStream.commit(); + ctx.flushImmediate(); + } + + var d = new VkShaderPipeline.GfxDesc(); + d.name = "chunk-bounds"; + d.vertGlsl = VkShaderSource.load("voxy:chunkoutline/outline.vsh", VkShaderSource.defs().props(properties).build()); + d.fragGlsl = VkShaderSource.load("voxy:chunkoutline/outline.fsh", VkShaderSource.defs().props(properties).build()); + d.colorFormat = VK_FORMAT_UNDEFINED; + d.depthFormat = VK_FORMAT_D32_SFLOAT;//the depth-bound image format + d.stencilFormat = VK_FORMAT_UNDEFINED; + d.depthTest = true; + d.depthWrite = true; + d.colorWrite = false; + //"further" compare: keep the farthest fragment (the AABB back face) + d.depthCompare = properties.isReverseZ() ? VK_COMPARE_OP_LESS : VK_COMPARE_OP_GREATER; + d.bindings = List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1)); + this.pipeline = new VkShaderPipeline(ctx, d); + } + + //Records the bound raster into the frame. The depth-bound image is cleared + // inline as LOAD_OP_CLEAR (inverseClearDepth) by this pass — the previous + // compositor vkCmdClearDepthStencilImage + TRANSFER_DST round-trip + + // LOAD_OP_LOAD was a redundant tile load on TBDR. With no visible sections + // we still issue the clear-only pass so the terrain sampler sees the + // "no bound" state. + public void render(VkViewport viewport, IBoundStore store) { + store.preRender(viewport); + int count = store.getCount(); + + if (count != 0) { + {//uniform: same 128-byte layout as the GL BoundRenderer (MVP', cameraBlockPos, fract, renderDistance) + final float renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance() * 16;//In blocks + long ptr = this.uploadStream.upload(this.uniform, 0, 128); + long matPtr = ptr; ptr += 4 * 4 * 4; + + int bx = (int) Math.floor(viewport.cameraX); + int by = (int) Math.floor(viewport.cameraY); + int bz = (int) Math.floor(viewport.cameraZ); + this.cameraBlock.set(bx, by, bz).getToAddress(ptr); ptr += 4 * 4; + + var negInnerBlock = this.innerBlock.set( + (float) (viewport.cameraX - bx), + (float) (viewport.cameraY - by), + (float) (viewport.cameraZ - bz)); + negInnerBlock.getToAddress(ptr); ptr += 4 * 3; + viewport.MVP.translate(negInnerBlock.negate(), this.mvpScratch).getToAddress(matPtr); + MemoryUtil.memPutFloat(ptr, renderDistance); + this.uploadStream.commit(); + } + + var cmd = this.ctx.cmd(); + //uniform/chunk-pos uploads + the store's SSBO must be visible to the draw + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, + VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + } + + var cmd = this.ctx.cmd(); + //Transition depthBound from whatever layout the previous frame left it in + // (SHADER_READ_ONLY_OPTIMAL after the post-render transition below, or + // UNDEFINED on first frame) to DEPTH_STENCIL_ATTACHMENT_OPTIMAL. + viewport.depthBound.transition(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + + try (MemoryStack stack = stackPush()) { + var depthAttach = org.lwjgl.vulkan.VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(viewport.depthBound.view) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR)//inline clear (inverseClearDepth) — saves the transfer-clear + tile load + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + depthAttach.clearValue().depthStencil().depth(this.properties.inverseClearDepth()).stencil(0); + var info = org.lwjgl.vulkan.VkRenderingInfoKHR.calloc(stack).sType$Default() + .renderArea(org.lwjgl.vulkan.VkRect2D.calloc(stack).extent(e -> e.width(viewport.width).height(viewport.height))) + .layerCount(1) + .pDepthAttachment(depthAttach); + vkCmdBeginRenderingKHR(cmd, info); + } + if (count != 0) { + this.pipeline.bind(cmd); + VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); + try (var b = this.pipeline.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, (VkBuffer) store.getBuffer()) + .push(cmd); + } + vkCmdBindIndexBuffer(cmd, this.boxIndexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexed(cmd, 6 * 2 * 3, count, 0, 0, 0); + } + vkCmdEndRenderingKHR(cmd); + + viewport.depthBound.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + + store.postRender(viewport); + } + + public void free() { + this.pipeline.free(); + this.uniform.free(); + this.boxIndexBuffer.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkCompositor.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkCompositor.java new file mode 100644 index 000000000..4febb610f --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkCompositor.java @@ -0,0 +1,299 @@ +package me.cortex.voxy.client.core.vk.render; + +import com.mojang.blaze3d.textures.GpuTextureView; +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.VoxyRenderSystem; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkCmd; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import org.joml.Matrix4f; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkRect2D; +import org.lwjgl.vulkan.VkRenderingAttachmentInfoKHR; +import org.lwjgl.vulkan.VkRenderingInfoKHR; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdBeginRenderingKHR; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdEndRenderingKHR; +import static org.lwjgl.vulkan.VK10.*; + +//The two fullscreen passes bracketing Voxy's VK frame, mirroring the GL +// NormalRenderPipeline: +// +// SETUP — clear Voxy's offscreen depth-stencil (depth=clear, stencil=1) and +// colour, then copy MC's depth in (transformed into Voxy's projection +// space by the fragment shader) writing stencil=0 where vanilla +// terrain exists. LOD terrain then renders with stencil==1 only. +// +// COMPOSITE — alpha-blend Voxy's offscreen colour into MC's frame, emitting +// depth transformed back into vanilla's projection space, with the +// environmental fog ramp applied. +public class VkCompositor { + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final RenderProperties properties; + private final boolean useEnvFog; + + private final VkBuffer compositeParams; + private final long depthSampler; + private final long colourSampler; + + private VkShaderPipeline depthSetup; + private VkShaderPipeline composite; + private int setupDepthFormat = -1; + private int compositeColorFormat = -1, compositeDepthFormat = -1; + + //Reused each frame for the composite-param matrices (no per-frame allocation) + private final Matrix4f scratchA = new Matrix4f(); + + public VkCompositor(VkFrameCtx ctx, VkUploadStream uploadStream, RenderProperties properties, boolean useEnvFog) { + this.ctx = ctx; + this.uploadStream = uploadStream; + this.properties = properties; + this.useEnvFog = useEnvFog; + this.compositeParams = new VkBuffer(ctx, 256).zero(); + ctx.flushImmediate(); + this.depthSampler = VkImage2D.createSampler(ctx.vk(), false, false); + this.colourSampler = VkImage2D.createSampler(ctx.vk(), false, false); + } + + private void ensureSetupPipeline(VkViewportRT viewport) { + if (this.depthSetup != null && this.setupDepthFormat == viewport.viewport.depthStencil.format) return; + if (this.depthSetup != null) this.depthSetup.free(); + var d = new VkShaderPipeline.GfxDesc(); + d.name = "depth-setup"; + d.vertGlsl = VkShaderSource.load("voxy:post/fullscreen2.vert", VkShaderSource.defs().props(this.properties).build()); + d.fragGlsl = VkShaderSource.load("voxy:post/setup_stencil_depth.frag", VkShaderSource.defs().props(this.properties).build()); + d.pushConstantBytes = 8; + d.colorFormat = viewport.viewport.colour.format; + d.depthFormat = viewport.viewport.depthStencil.format; + d.stencilFormat = viewport.viewport.depthStencil.format; + d.depthTest = true; + d.depthWrite = true; + d.depthCompare = VK_COMPARE_OP_ALWAYS; + d.colorWrite = false; + d.blend = false; + d.stencilWriteAlways1 = true; + d.stencilWriteRef = 0;//mark vanilla-covered pixels with 0; LOD renders where stencil==1 + d.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + d.bindings = List.of(VkShaderPipeline.sampler(0)); + this.depthSetup = new VkShaderPipeline(this.ctx, d); + this.setupDepthFormat = viewport.viewport.depthStencil.format; + } + + private void ensureCompositePipeline(int mcColorFormat, int mcDepthFormat) { + if (this.composite != null && this.compositeColorFormat == mcColorFormat && this.compositeDepthFormat == mcDepthFormat) return; + if (this.composite != null) this.composite.free(); + var d = new VkShaderPipeline.GfxDesc(); + d.name = "composite"; + d.vertGlsl = VkShaderSource.load("voxy:post/fullscreen2.vert", VkShaderSource.defs().props(this.properties).build()); + d.fragGlsl = VkShaderSource.load("voxy:post/blit_texture_depth_cutout.frag", VkShaderSource.defs().props(this.properties) + .def("EMIT_COLOUR") + .defIf("USE_ENV_FOG", this.useEnvFog) + .build()); + d.colorFormat = mcColorFormat; + d.depthFormat = mcDepthFormat; + d.stencilFormat = VK_FORMAT_UNDEFINED; + d.depthTest = true; + d.depthWrite = true; + d.depthCompare = VkCmd.closerEqual(this.properties); + d.blend = true; + d.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + d.bindings = List.of(VkShaderPipeline.sampler(0), VkShaderPipeline.ubo(1), VkShaderPipeline.sampler(3)); + this.composite = new VkShaderPipeline(this.ctx, d); + this.compositeColorFormat = mcColorFormat; + this.compositeDepthFormat = mcDepthFormat; + } + + /** Wrapper carrying the per-frame MC attachment info alongside Voxy's viewport. */ + public record VkViewportRT(VkViewport viewport, + GpuTextureView mcColour, GpuTextureView mcDepth, + int mcWidth, int mcHeight) {} + + //SETUP pass. MC's depth image is transitioned to shader-read for sampling + // and back to attachment afterwards. + public void setupDepthStencil(VkViewportRT rt) { + this.ensureSetupPipeline(rt); + var cmd = this.ctx.cmd(); + var viewport = rt.viewport; + + //Voxy offscreen images -> attachment layouts (first use each frame). + //Batched into a single vkCmdPipelineBarrier (colour + depthStencil together) + // instead of two separate transitions + VkImage2D.transitionBatch(java.util.List.of( + new VkImage2D.BatchEntry(viewport.colour, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT), + new VkImage2D.BatchEntry(viewport.depthStencil, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT)), + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT); + //depth-bound image: the clear happens inline as LOAD_OP_CLEAR inside + // VkBoundRenderer's render pass (or a no-section clear-only pass when + // there are no visible sections). Leave depthBound in its current layout; + // VkBoundRenderer transitions it to DEPTH_STENCIL_ATTACHMENT directly. + + //MC depth -> sample-able + VkFrameHost.transitionMcImage(cmd, rt.mcDepth, true, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + try (MemoryStack stack = stackPush()) { + var colorAttach = VkRenderingAttachmentInfoKHR.calloc(1, stack).sType$Default() + .imageView(viewport.colour.view) + .imageLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + colorAttach.clearValue().color().float32(0, 0).float32(1, 0).float32(2, 0).float32(3, 0); + var depthAttach = VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(viewport.depthStencil.view) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + depthAttach.clearValue().depthStencil().depth(this.properties.clearDepth()).stencil(1); + var stencilAttach = VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(viewport.depthStencil.view) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + stencilAttach.clearValue().depthStencil().depth(this.properties.clearDepth()).stencil(1); + var info = VkRenderingInfoKHR.calloc(stack).sType$Default() + .renderArea(VkRect2D.calloc(stack).extent(e -> e.width(viewport.width).height(viewport.height))) + .layerCount(1) + .pColorAttachments(colorAttach) + .pDepthAttachment(depthAttach) + .pStencilAttachment(stencilAttach); + vkCmdBeginRenderingKHR(cmd, info); + } + this.depthSetup.bind(cmd); + VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); + try (var b = this.depthSetup.binder()) { + b.sampler(0, VkFrameHost.vkView(rt.mcDepth), this.depthSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + var pc = stack.malloc(8); + pc.putFloat(0, ((float) viewport.width) / rt.mcWidth); + pc.putFloat(4, ((float) viewport.height) / rt.mcHeight); + this.depthSetup.pushConstants(cmd, pc); + } + vkCmdDraw(cmd, 4, 1, 0, 0); + vkCmdEndRenderingKHR(cmd); + + //MC depth back to attachment + VkFrameHost.transitionMcImage(cmd, rt.mcDepth, true, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + } + + /** Transition Voxy's offscreen targets for sampling (HiZ build / composite). */ + public void offscreenToSampled(VkViewport viewport) { + viewport.depthStencil.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + } + + public void offscreenToAttachment(VkViewport viewport) { + viewport.depthStencil.transition(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + } + + /** COMPOSITE pass into MC's frame attachments. */ + public void composite(VkViewportRT rt) { + var viewport = rt.viewport; + boolean fogCoversAllRendering = viewport.fogParameters != null + && viewport.fogParameters.environmentalEnd() < VoxyRenderSystem.getRenderDistance(); + if (fogCoversAllRendering) { + return;//mirrors the GL "fog covers everything -> skip blit" branch + } + int mcColorFormat = VkFrameHost.vkFormat(rt.mcColour); + int mcDepthFormat = VkFrameHost.vkFormat(rt.mcDepth); + this.ensureCompositePipeline(mcColorFormat, mcDepthFormat); + var cmd = this.ctx.cmd(); + + {//composite params UBO + long ptr = this.uploadStream.upload(this.compositeParams, 0, 256); + this.scratchA.set(viewport.MVP).invert().getToAddress(ptr); ptr += 64; + this.scratchA.set(viewport.vanillaProjection).mul(viewport.modelView).getToAddress(ptr); ptr += 64; + //endParams (vec4: invEndFogDelta, startDelta, clampedEnd, 0) + fog + // colour (vec4). Default all-zero unless environmental fog is active. + float e0 = 0, e1 = 0, e2 = 0, f0 = 0, f1 = 0, f2 = 0, f3 = 0; + if (this.useEnvFog && viewport.fogParameters != null) { + float start = viewport.fogParameters.environmentalStart(); + float endF = viewport.fogParameters.environmentalEnd(); + if (Math.abs(endF - start) > 1) { + float invEndFogDelta = 1f / (endF - start); + float endDistance = Math.max(VoxyRenderSystem.getRenderDistance(), 20 * 16) * (float) Math.sqrt(3); + float startDelta = -start * invEndFogDelta; + e0 = invEndFogDelta; + e1 = startDelta; + e2 = Math.clamp(endDistance * invEndFogDelta + startDelta, 0, 1); + f0 = viewport.fogParameters.red(); + f1 = viewport.fogParameters.green(); + f2 = viewport.fogParameters.blue(); + f3 = viewport.fogParameters.alpha(); + } + } + MemoryUtil.memPutFloat(ptr, e0); + MemoryUtil.memPutFloat(ptr + 4, e1); + MemoryUtil.memPutFloat(ptr + 8, e2); + MemoryUtil.memPutFloat(ptr + 12, 0f); + ptr += 16; + MemoryUtil.memPutFloat(ptr, f0); + MemoryUtil.memPutFloat(ptr + 4, f1); + MemoryUtil.memPutFloat(ptr + 8, f2); + MemoryUtil.memPutFloat(ptr + 12, f3); + this.uploadStream.commit(); + } + + //SSAO output colour (with translucents composited on top) -> sampled + viewport.colourSSAO.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + //(depth already SHADER_READ from the HiZ stage — offscreenToSampled) + + try (MemoryStack stack = stackPush()) { + var colorAttach = VkRenderingAttachmentInfoKHR.calloc(1, stack).sType$Default() + .imageView(VkFrameHost.vkView(rt.mcColour)) + .imageLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + var depthAttach = VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(VkFrameHost.vkView(rt.mcDepth)) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + var info = VkRenderingInfoKHR.calloc(stack).sType$Default() + .renderArea(VkRect2D.calloc(stack).extent(e -> e.width(rt.mcWidth).height(rt.mcHeight))) + .layerCount(1) + .pColorAttachments(colorAttach) + .pDepthAttachment(depthAttach); + vkCmdBeginRenderingKHR(cmd, info); + } + this.composite.bind(cmd); + VkCmd.setViewportScissor(cmd, rt.mcWidth, rt.mcHeight); + try (var b = this.composite.binder()) { + b.sampler(0, viewport.depthSampleView, this.depthSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .ubo(1, this.compositeParams) + .sampler(3, viewport.colourSSAO.view, this.colourSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .push(cmd); + } + vkCmdDraw(cmd, 4, 1, 0, 0); + vkCmdEndRenderingKHR(cmd); + } + + public void free() { + if (this.depthSetup != null) this.depthSetup.free(); + if (this.composite != null) this.composite.free(); + //depthSampler/colourSampler come from VkImage2D.createSampler's + // device-lifetime cache (shared across compositor/SSAO/terrain); never + // destroy them per-object (double vkDestroySampler -> SIGSEGV on unload) + this.compositeParams.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java new file mode 100644 index 000000000..8e80db0f7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java @@ -0,0 +1,89 @@ +package me.cortex.voxy.client.core.vk.render; + +import com.mojang.blaze3d.textures.GpuTextureView; +import com.mojang.blaze3d.vulkan.VulkanConst; +import com.mojang.blaze3d.vulkan.VulkanGpuTexture; +import com.mojang.blaze3d.vulkan.VulkanGpuTextureView; +import net.minecraft.client.Minecraft; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkImageMemoryBarrier; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Accessors for MC's live Vulkan frame resources at the render hook point: +// the world colour/depth attachment views MC is rendering into and the +// lightmap texture view. All calls are render-thread only. +public final class VkFrameHost { + private VkFrameHost() {} + + /** VkImageView of MC's lightmap (bound as Voxy's terrain light sampler). */ + public static long lightmapView() { + return ((VulkanGpuTextureView) Minecraft.getInstance().gameRenderer.levelLightmap()).vkImageView(); + } + + public static long vkView(GpuTextureView view) { + return ((VulkanGpuTextureView) view).vkImageView(); + } + + public static int vkFormat(GpuTextureView view) { + return VulkanConst.toVk(view.texture().getFormat()); + } + + //Layout-transition one of MC's own images (colour/depth attachment) with + // scoped stage/access masks matching the actual producer/consumer — MC just + // rendered into the depth attachment (LATE_FRAGMENT_TESTS write), and Voxy + // samples it (FRAGMENT_SHADER read), so the previous ALL_COMMANDS masks + // (which serialised the transition with unrelated compute) are narrowed. + // Used to bracket sampling of MC's attachments mid-frame (they live in + // ATTACHMENT_OPTIMAL otherwise). + public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, + boolean depth, int oldLayout, int newLayout) { + try (MemoryStack stack = stackPush()) { + long image = ((VulkanGpuTexture) view.texture()).vkImage(); + //MC's depth attachment is written by the late fragment tests; MC's + // colour by the fragment shader. Voxy reads both from the fragment + // shader (sampler). Reverse transitions are FRAGMENT_SHADER read -> + // late-fragment-tests write. + int srcStage, srcAccess, dstStage, dstAccess; + boolean toSampled = newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + if (depth) { + if (toSampled) { + srcStage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + srcAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dstAccess = VK_ACCESS_SHADER_READ_BIT; + } else { + srcStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + srcAccess = VK_ACCESS_SHADER_READ_BIT; + dstStage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dstAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + } + } else { + if (toSampled) { + srcStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + srcAccess = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dstAccess = VK_ACCESS_SHADER_READ_BIT; + } else { + srcStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + srcAccess = VK_ACCESS_SHADER_READ_BIT; + dstStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dstAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + } + } + var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() + .srcAccessMask(srcAccess).dstAccessMask(dstAccess) + .oldLayout(oldLayout).newLayout(newLayout) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .image(image); + imb.subresourceRange() + .aspectMask(depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT) + .levelCount(VK_REMAINING_MIP_LEVELS) + .layerCount(VK_REMAINING_ARRAY_LAYERS); + vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, null, null, imb); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkHiZ.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkHiZ.java new file mode 100644 index 000000000..e550e372e --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkHiZ.java @@ -0,0 +1,189 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import org.lwjgl.system.MemoryStack; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK HiZ pyramid: an R32F mip chain reduced with the same conservative +// REDUCTION as the GL path (min for reverse-Z), built by one small compute +// dispatch per level. Level 0 reduces from the offscreen depth image directly. +// The whole pyramid lives in GENERAL layout (written as storage image, read as +// sampled image by the traversal). +// +// When subgroup arithmetic is supported (MoltenVK/Metal, NVIDIA, AMD, Intel on +// Vulkan 1.1+), levels 1..6 are collapsed into a SINGLE dispatch by the +// subgroup reduce shader (hiz_subgroup.comp), cutting ~5 dispatches + 5 +// barriers per frame at 1080p. Level 0 still uses the per-level reduce (it +// handles the non-power-of-two source/dest ratio). Any levels beyond 6 (for +// pyramids larger than 64x64 base) fall back to the per-level loop. +public class VkHiZ { + private final VkFrameCtx ctx; + private final VkShaderPipeline reduce; + private final VkShaderPipeline subgroupReduce; //null when subgroup unsupported + public final long sampler;//nearest, nearest-mip, clamped + + private VkImage2D pyramid; + private int levels; + private int width, height; + private boolean initialized; + + public VkHiZ(VkFrameCtx ctx, RenderProperties properties) { + this.ctx = ctx; + this.reduce = new VkShaderPipeline(ctx, "hiz_reduce.comp", + VkShaderSource.load("voxy:hiz/vk/hiz_reduce.comp", VkShaderSource.defs().props(properties).build()), + 16, + List.of(VkShaderPipeline.sampler(0), VkShaderPipeline.image(1))); + //Subgroup reduce: 7 bindings (mip_0 sampler + mip_1..mip_6 storage images). + //Only built when the device supports subgroup arithmetic AND the pyramid + // will have >= 7 levels. + if (ctx.vk().subgroupArithmetic) { + this.subgroupReduce = new VkShaderPipeline(ctx, "hiz_subgroup.comp", + VkShaderSource.load("voxy:hiz/vk/hiz_subgroup.comp", VkShaderSource.defs().props(properties).build()), + 16, + List.of(VkShaderPipeline.sampler(0), + VkShaderPipeline.image(1), VkShaderPipeline.image(2), VkShaderPipeline.image(3), + VkShaderPipeline.image(4), VkShaderPipeline.image(5), VkShaderPipeline.image(6))); + } else { + this.subgroupReduce = null; + } + this.sampler = VkImage2D.createSampler(ctx.vk(), true, false); + } + + private void alloc(int width, int height) { + if (this.pyramid != null) this.pyramid.free(); + this.levels = (int) Math.ceil(Math.log(Math.max(width, height)) / Math.log(2)); + this.levels = Math.max(this.levels, 1); + this.pyramid = new VkImage2D(this.ctx, width, height, this.levels, VK_FORMAT_R32_SFLOAT, + VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT, true); + this.width = width; + this.height = height; + this.initialized = false; + } + + /** + * Rebuild the pyramid from the offscreen depth image (must currently be in + * SHADER_READ_ONLY_OPTIMAL with a DEPTH-aspect sampling view). + */ + public void buildMipChain(long depthSampleView, int srcWidth, int srcHeight) { + int w = Integer.highestOneBit(srcWidth); + int h = Integer.highestOneBit(srcHeight); + if (this.width != w || this.height != h || this.pyramid == null) { + this.alloc(w, h); + } + var cmd = this.ctx.cmd(); + if (!this.initialized) { + this.pyramid.transition(VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); + this.initialized = true; + } + + //Level 0: always the per-level reduce (handles non-power-of-two source ratio). + this.reduce.bind(cmd); + int cw = this.width, ch = this.height; + int sw = srcWidth, sh = srcHeight; + { + try (var binder = this.reduce.binder()) { + binder.sampler(0, depthSampleView, this.sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .image(1, this.pyramid.mipViews[0]) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + var pc = stack.malloc(16); + pc.putInt(0, cw).putInt(4, ch).putInt(8, sw).putInt(12, sh); + this.reduce.pushConstants(cmd, pc); + } + vkCmdDispatch(cmd, (cw + 7) / 8, (ch + 7) / 8, 1); + } + //Level 0 write -> level 1 read + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + sw = cw; sh = ch; + cw = Math.max(cw / 2, 1); + ch = Math.max(ch / 2, 1); + + //Levels 1..6: single subgroup dispatch when available and pyramid has enough levels. + //The subgroup shader reduces 64x64 tiles of mip_0 to mip_1..mip_6 in one dispatch. + int subgroupEndLevel = 0; + if (this.subgroupReduce != null && this.levels >= 7) { + this.subgroupReduce.bind(cmd); + try (var binder = this.subgroupReduce.binder()) { + binder.sampler(0, this.pyramid.mipViews[0], this.sampler, VK_IMAGE_LAYOUT_GENERAL) + .image(1, this.pyramid.mipViews[1]) + .image(2, this.pyramid.mipViews[2]) + .image(3, this.pyramid.mipViews[3]) + .image(4, this.pyramid.mipViews[4]) + .image(5, this.pyramid.mipViews[5]) + .image(6, this.pyramid.mipViews[6]) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + var pc = stack.malloc(16); + pc.putFloat(0, 1.0f / this.width).putFloat(4, 1.0f / this.height) + .putInt(8, this.levels).putInt(12, 0); + this.subgroupReduce.pushConstants(cmd, pc); + } + //One dispatch per 64x64 tile of the pyramid base. + vkCmdDispatch(cmd, (this.width + 63) / 64, (this.height + 63) / 64, 1); + //mip_1..mip_6 writes -> subsequent level reads + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + subgroupEndLevel = 6; + //Advance cw/ch past the subgroup-covered levels for any remaining per-level loop. + cw = Math.max(this.width >> 6, 1); + ch = Math.max(this.height >> 6, 1); + sw = Math.max(this.width >> 5, 1); + sh = Math.max(this.height >> 5, 1); + } + + //Remaining levels (7+): per-level reduce loop. + for (int i = subgroupEndLevel + 1; i < this.levels; i++) { + this.reduce.bind(cmd); + try (var binder = this.reduce.binder()) { + long srcView = this.pyramid.mipViews[i - 1]; + binder.sampler(0, srcView, this.sampler, VK_IMAGE_LAYOUT_GENERAL) + .image(1, this.pyramid.mipViews[i]) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + var pc = stack.malloc(16); + pc.putInt(0, cw).putInt(4, ch).putInt(8, sw).putInt(12, sh); + this.reduce.pushConstants(cmd, pc); + } + vkCmdDispatch(cmd, (cw + 7) / 8, (ch + 7) / 8, 1); + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + sw = cw; sh = ch; + cw = Math.max(cw / 2, 1); + ch = Math.max(ch / 2, 1); + } + + //Pyramid -> traversal sampling + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + } + + public long pyramidView() { + return this.pyramid.view; + } + + public int getPackedLevels() { + return (this.width << 16) | this.height; + } + + public void free() { + if (this.pyramid != null) this.pyramid.free(); + //sampler comes from VkImage2D.createSampler's device-lifetime cache (shared + // handle); never destroy it per-object (multi-free vkDestroySampler -> SIGSEGV). + this.reduce.free(); + if (this.subgroupReduce != null) this.subgroupReduce.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkModelStore.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkModelStore.java new file mode 100644 index 000000000..4de6440d7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkModelStore.java @@ -0,0 +1,134 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.model.IModelStore; +import me.cortex.voxy.client.core.model.ModelFactory; +import me.cortex.voxy.client.core.model.ModelStore; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.common.util.MemoryBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkBufferImageCopy; +import org.lwjgl.vulkan.VkSamplerCreateInfo; + +import static me.cortex.voxy.client.core.vk.VkUtil.check; +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK model store: block-model data + biome colour VkBuffers and the baked +// model texture atlas as a mipped RGBA8 VkImage. Texture tiles stream through +// the upload staging buffer with vkCmdCopyBufferToImage per mip; the batch is +// bracketed by TRANSFER_DST <-> SHADER_READ_ONLY transitions. +public class VkModelStore implements IModelStore { + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + final VkBuffer modelBuffer; + final VkBuffer modelColourBuffer; + final VkImage2D atlas; + public final long atlasSampler; + private boolean inUploadBatch; + + public VkModelStore(VkFrameCtx ctx, VkUploadStream uploadStream) { + this.ctx = ctx; + this.uploadStream = uploadStream; + this.modelBuffer = new VkBuffer(ctx, ModelStore.MODEL_SIZE * (1L << 16)).zero(); + this.modelColourBuffer = new VkBuffer(ctx, 4L * (1 << 16)).zero(); + this.atlas = new VkImage2D(ctx, + ModelFactory.MODEL_TEXTURE_SIZE * 3 * 256, + ModelFactory.MODEL_TEXTURE_SIZE * 2 * 256, + ModelFactory.LAYERS, + VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, false); + //Start life in shader-read so the first frame can bind it even with no uploads yet + this.atlas.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + ctx.flushImmediate(); + + try (MemoryStack stack = stackPush()) { + //Mirror the GL sampler: nearest mag, nearest-within-mip + linear-between-mips min + var sci = VkSamplerCreateInfo.calloc(stack).sType$Default() + .magFilter(VK_FILTER_NEAREST) + .minFilter(VK_FILTER_NEAREST) + .mipmapMode(VK_SAMPLER_MIPMAP_MODE_LINEAR) + .addressModeU(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .addressModeV(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .addressModeW(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) + .minLod(0).maxLod(ModelFactory.LAYERS - 1); + var pSampler = stack.mallocLong(1); + check(vkCreateSampler(ctx.vk().device, sci, null, pSampler), "vkCreateSampler(modelAtlas)"); + this.atlasSampler = pSampler.get(0); + } + } + + @Override + public IDeviceBuffer modelBufferHandle() { + return this.modelBuffer; + } + + @Override + public IDeviceBuffer colourBufferHandle() { + return this.modelColourBuffer; + } + + @Override + public void beginTextureUploads() { + this.atlas.transition(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + this.inUploadBatch = true; + } + + @Override + public void uploadModelTexture(int modelId, MemoryBuffer texture) { + if (!this.inUploadBatch) throw new IllegalStateException("Texture upload outside batch"); + final int TS = ModelFactory.MODEL_TEXTURE_SIZE; + int X = (modelId & 0xFF) * TS * 3; + int Y = ((modelId >> 8) & 0xFF) * TS * 2; + + //Stage the full mip chain in one staging allocation + int totalBytes = 0; + for (int lvl = 0; lvl < ModelFactory.LAYERS; lvl++) { + totalBytes += (TS * TS * 3 * 2 * 4) >> (lvl << 1); + } + long stageOff = this.uploadStream.rawUploadAddress(totalBytes); + MemoryUtil.memCopy(texture.address, this.uploadStream.getBaseAddress() + stageOff, totalBytes); + + var cmd = this.ctx.cmd(); + try (MemoryStack stack = stackPush()) { + var regions = VkBufferImageCopy.calloc(ModelFactory.LAYERS, stack); + long srcOff = stageOff; + for (int lvl = 0; lvl < ModelFactory.LAYERS; lvl++) { + final int flvl = lvl; + var r = regions.get(lvl); + r.bufferOffset(srcOff).bufferRowLength(0).bufferImageHeight(0); + r.imageSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(lvl).baseArrayLayer(0).layerCount(1); + r.imageOffset(o -> o.x(X >> flvl).y(Y >> flvl).z(0)); + r.imageExtent(e -> e.width((TS * 3) >> flvl).height((TS * 2) >> flvl).depth(1)); + srcOff += (TS * TS * 3 * 2 * 4) >> (lvl << 1); + } + vkCmdCopyBufferToImage(cmd, this.uploadStream.stagingBufferHandle(), this.atlas.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, regions); + } + } + + @Override + public void endTextureUploads() { + this.atlas.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + this.inUploadBatch = false; + } + + @Override + public void free() { + vkDestroySampler(this.ctx.vk().device, this.atlasSampler, null); + this.modelBuffer.free(); + this.modelColourBuffer.free(); + this.atlas.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeCleaner.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeCleaner.java new file mode 100644 index 000000000..deaaf96b9 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeCleaner.java @@ -0,0 +1,168 @@ +package me.cortex.voxy.client.core.vk.render; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager; +import me.cortex.voxy.client.core.rendering.hierachical.INodeCleaner; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkDownloadStream; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.vkCmdDispatch; + +//Pure-VK port of NodeCleaner: finds the least-recently-rendered nodes on the +// GPU (sort + transform computes) and feeds them back to the AsyncNodeManager +// for geometry eviction when the geometry buffer runs low. Mirrors the GL +// implementation pass-for-pass. +public class VkNodeCleaner implements INodeCleaner { + private static final int SORTING_WORKER_SIZE = 64; + private static final int WORK_PER_THREAD = 8; + static final int OUTPUT_COUNT = 256; + + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final VkDownloadStream downloadStream; + private final AsyncNodeManager nodeManager; + + private final VkShaderPipeline sorter; + private final VkShaderPipeline resultTransformer; + private final VkShaderPipeline batchClear; + + public final VkBuffer visibilityBuffer; + private final VkBuffer outputBuffer; + int visibilityId = 0; + + public VkNodeCleaner(VkFrameCtx ctx, VkUploadStream up, VkDownloadStream down, AsyncNodeManager nodeManager) { + this.ctx = ctx; + this.uploadStream = up; + this.downloadStream = down; + this.nodeManager = nodeManager; + this.visibilityBuffer = new VkBuffer(ctx, nodeManager.maxNodeCount * 4L).fill(-1); + this.outputBuffer = new VkBuffer(ctx, OUTPUT_COUNT * 4 + OUTPUT_COUNT * 8); + ctx.flushImmediate(); + + this.sorter = new VkShaderPipeline(ctx, "sort_visibility.comp", + VkShaderSource.load("voxy:lod/hierarchical/cleaner/sort_visibility.comp", VkShaderSource.defs() + .def("WORK_SIZE", SORTING_WORKER_SIZE) + .def("ELEMS_PER_THREAD", WORK_PER_THREAD) + .def("OUTPUT_SIZE", OUTPUT_COUNT) + .def("VISIBILITY_BUFFER_BINDING", 1) + .def("OUTPUT_BUFFER_BINDING", 2) + .def("NODE_DATA_BINDING", 3) + .build()), + 0, + List.of(VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), VkShaderPipeline.ssbo(3))); + + this.resultTransformer = new VkShaderPipeline(ctx, "result_transformer.comp", + VkShaderSource.load("voxy:lod/hierarchical/cleaner/result_transformer.comp", VkShaderSource.defs() + .def("OUTPUT_SIZE", OUTPUT_COUNT) + .def("MIN_ID_BUFFER_BINDING", 0) + .def("NODE_BUFFER_BINDING", 1) + .def("OUTPUT_BUFFER_BINDING", 2) + .def("VISIBILITY_BUFFER_BINDING", 3) + .build()), + 4, + List.of(VkShaderPipeline.ssbo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), VkShaderPipeline.ssbo(3))); + + this.batchClear = new VkShaderPipeline(ctx, "batch_visibility_set.comp", + VkShaderSource.load("voxy:lod/hierarchical/cleaner/batch_visibility_set.comp", VkShaderSource.defs() + .def("VISIBILITY_BUFFER_BINDING", 0) + .def("LIST_BUFFER_BINDING", 1) + .build()), + 8, + List.of(VkShaderPipeline.ssbo(0), VkShaderPipeline.ssbo(1))); + } + + @Override + public void tick(IDeviceBuffer nodeDataBufferIn) { + var nodeDataBuffer = (VkBuffer) nodeDataBufferIn; + this.visibilityId++; + if (this.shouldCleanGeometry()) { + this.outputBuffer.fill(this.nodeManager.maxNodeCount - 2); + var cmd = this.ctx.cmd(); + + this.sorter.bind(cmd); + try (var binder = this.sorter.binder()) { + binder.ssbo(1, this.visibilityBuffer) + .ssbo(2, this.outputBuffer) + .ssbo(3, nodeDataBuffer) + .push(cmd); + } + vkCmdDispatch(cmd, (this.nodeManager.getCurrentMaxNodeId() + (SORTING_WORKER_SIZE * WORK_PER_THREAD) - 1) / (SORTING_WORKER_SIZE * WORK_PER_THREAD), 1, 1); + //Sort -> resultTransformer: both compute. + this.ctx.computeToComputeBarrier(); + + this.resultTransformer.bind(cmd); + try (var binder = this.resultTransformer.binder()) { + binder.ssbo(0, this.outputBuffer.buffer, 0, 4L * OUTPUT_COUNT) + .ssbo(1, nodeDataBuffer) + .ssbo(2, this.outputBuffer.buffer, 4L * OUTPUT_COUNT, 8L * OUTPUT_COUNT) + .ssbo(3, this.visibilityBuffer) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + this.resultTransformer.pushConstants(cmd, stack.malloc(4).putInt(0, this.visibilityId)); + } + vkCmdDispatch(cmd, 1, 1, 1); + //resultTransformer -> download copy: compute -> transfer. + this.ctx.computeToTransferBarrier(); + + this.downloadStream.download(this.outputBuffer, 4L * OUTPUT_COUNT, 8L * OUTPUT_COUNT, + buffer -> this.nodeManager.submitRemoveBatch(buffer.copy())); + } + } + + private boolean shouldCleanGeometry() { + long remaining = this.nodeManager.getGeometryCapacity() - this.nodeManager.getUsedGeometryCapacity(); + return remaining < 256_000_000;//If less than 256 mb free memory + } + + @Override + public void updateIds(IntOpenHashSet collection) { + if (!collection.isEmpty()) { + int count = collection.size(); + long off = this.uploadStream.rawUploadAddress(count * 4); + long ptr = this.uploadStream.getBaseAddress() + off; + var iter = collection.iterator(); + while (iter.hasNext()) { + MemoryUtil.memPutInt(ptr, iter.nextInt()); + ptr += 4; + } + this.uploadStream.commit(); + + var cmd = this.ctx.cmd(); + this.batchClear.bind(cmd); + try (var binder = this.batchClear.binder()) { + binder.ssbo(0, this.visibilityBuffer) + .ssbo(1, this.uploadStream.stagingBufferHandle(), off, this.uploadStream.alignUpAlloc(count * 4)) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + var pc = stack.malloc(8); + pc.putInt(0, count); + pc.putInt(4, this.visibilityId); + this.batchClear.pushConstants(cmd, pc); + } + vkCmdDispatch(cmd, (count + 127) / 128, 1, 1); + //batchClear -> next-frame traversal/sorter (compute -> compute). + this.ctx.computeToComputeBarrier(); + } + } + + @Override + public void free() { + this.sorter.free(); + this.resultTransformer.free(); + this.batchClear.free(); + this.visibilityBuffer.free(); + this.outputBuffer.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeGpuOps.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeGpuOps.java new file mode 100644 index 000000000..13ef64cfa --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkNodeGpuOps.java @@ -0,0 +1,104 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.rendering.hierachical.INodeGpuOps; +import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.UnsafeUtil; +import org.lwjgl.system.MemoryStack; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.vkCmdDispatch; + +//Vulkan implementation of the node-manager GPU ops (scatter + multi-memcpy computes). +public class VkNodeGpuOps implements INodeGpuOps { + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final VkShaderPipeline scatterWrite; + private final VkShaderPipeline multiMemcpy; + + public VkNodeGpuOps(VkFrameCtx ctx, VkUploadStream uploadStream) { + this.ctx = ctx; + this.uploadStream = uploadStream; + this.scatterWrite = new VkShaderPipeline(ctx, "scatter.comp", + VkShaderSource.load("voxy:util/scatter.comp", VkShaderSource.defs() + .def("INPUT_BUFFER_BINDING", 0) + .def("OUTPUT_BUFFER1_BINDING", 1) + .def("OUTPUT_BUFFER2_BINDING", 2) + .build()), + 4, + List.of(VkShaderPipeline.ssbo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2))); + this.multiMemcpy = new VkShaderPipeline(ctx, "memcpy.comp", + VkShaderSource.load("voxy:util/memcpy.comp", VkShaderSource.defs() + .def("INPUT_HEADER_BUFFER_BINDING", 0) + .def("INPUT_DATA_BUFFER_BINDING", 1) + .def("OUTPUT_BUFFER_BINDING", 2) + .build()), + 0, + List.of(VkShaderPipeline.ssbo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2))); + } + + @Override + public void multiMemcpy(long headerPtr, int copies, long dataPtr, int dataSize, IBasicGeometryData geometry) { + int upCopies = this.uploadStream.alignUpAlloc(copies * 16); + int upScratchSize = this.uploadStream.alignUpAlloc(dataSize); + long off = this.uploadStream.rawUploadAddress(upScratchSize + upCopies); + UnsafeUtil.memcpy(headerPtr, this.uploadStream.getBaseAddress() + off, copies * 16L); + UnsafeUtil.memcpy(dataPtr, this.uploadStream.getBaseAddress() + off + upCopies, dataSize); + this.uploadStream.commit(); + + if (copies > 500) { + Logger.warn("Large amount of copies, lag will probably happen: " + copies); + } + + var cmd = this.ctx.cmd(); + this.multiMemcpy.bind(cmd); + try (var binder = this.multiMemcpy.binder()) { + binder.ssbo(0, this.uploadStream.stagingBufferHandle(), off, upCopies) + .ssbo(1, this.uploadStream.stagingBufferHandle(), off + upCopies, upScratchSize) + .ssbo(2, (VkBuffer) geometry.geometryBufferHandle()) + .push(cmd); + } + vkCmdDispatch(cmd, copies, 1, 1); + //Next consumer is the scatterWrite compute pass (or the cleaner); scope to + // COMPUTE -> COMPUTE so unrelated raster/transfer can overlap. + this.ctx.computeToComputeBarrier(); + } + + @Override + public void scatterWrite(long chunksPtr, int count, IDeviceBuffer nodeBuffer, IBasicGeometryData geometry) { + int chunks = (count + 3) / 4; + int streamSize = chunks * 80;//80 bytes per chunk + long off = this.uploadStream.rawUploadAddress(streamSize); + UnsafeUtil.memcpy(chunksPtr, this.uploadStream.getBaseAddress() + off, streamSize); + this.uploadStream.commit(); + + var cmd = this.ctx.cmd(); + this.scatterWrite.bind(cmd); + try (var binder = this.scatterWrite.binder()) { + binder.ssbo(0, this.uploadStream.stagingBufferHandle(), off, this.uploadStream.alignUpAlloc(streamSize)) + .ssbo(1, (VkBuffer) nodeBuffer) + .ssbo(2, (VkBuffer) geometry.metadataBufferHandle()) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + this.scatterWrite.pushConstants(cmd, stack.malloc(4).putInt(0, count)); + } + vkCmdDispatch(cmd, (count + 127) / 128, 1, 1); + //Next consumer is the node cleaner / next-frame traversal (both compute). + this.ctx.computeToComputeBarrier(); + } + + @Override + public void free() { + this.scatterWrite.free(); + this.multiMemcpy.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java new file mode 100644 index 000000000..71d9c47fb --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java @@ -0,0 +1,340 @@ +package me.cortex.voxy.client.core.vk.render; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import me.cortex.voxy.client.config.VoxyConfig; +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.VoxyRenderSystem; +import me.cortex.voxy.client.core.model.ModelBakerySubsystem; +import me.cortex.voxy.client.core.rendering.RenderDistanceTracker; +import me.cortex.voxy.client.core.rendering.ViewportSelector; +import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; +import me.cortex.voxy.client.core.model.bakery.IAtlasTextureReader; +import me.cortex.voxy.client.core.rendering.bounding.StreamedBoundStore; +import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager; +import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; +import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; +import me.cortex.voxy.client.core.vk.MinecraftVkHost; +import me.cortex.voxy.client.core.vk.MinecraftVkHostAdapter; +import me.cortex.voxy.client.core.vk.VkAtlasTextureReader; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkDownloadStream; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.client.core.vk.VulkanBackend; +import me.cortex.voxy.common.CmpLog; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.thread.ServiceManager; +import me.cortex.voxy.common.world.WorldEngine; +import net.caffeinemc.mods.sodium.client.render.chunk.ChunkRenderMatrices; +import net.caffeinemc.mods.sodium.client.util.FogParameters; +import net.minecraft.client.Minecraft; + +import java.util.Arrays; +import java.util.List; + +//The pure-Vulkan render core: constructed instead of the GL pipeline when MC +// itself runs on Vulkan. Owns every GPU-facing subsystem (streams, geometry, +// models, traversal, terrain renderer, compositor) on MC's adopted device, and +// shares the CPU-side services (node manager, mesh generation, model bakery, +// render-distance tracking) with the GL path. +// +//Everything records into MC's frame command buffer from the render hook +// (MixinSodiumOpaqueVkFrame, TAIL of Sodium's opaque terrain draw), between +// MC's opaque terrain pass and the rest of its frame. No OpenGL is touched. +public class VkRenderCore { + private final WorldEngine worldIn; + private final VkFrameCtx frameCtx; + private final VkUploadStream uploadStream; + private final VkDownloadStream downloadStream; + + private final RenderProperties properties; + private final VkModelStore modelStore; + private final ModelBakerySubsystem modelService; + private final RenderGenerationService renderGen; + private final VkSectionGeometryData geometryData; + private final AsyncNodeManager nodeManager; + private final VkNodeCleaner nodeCleaner; + private final VkTraversal traversal; + private final VkTerrainRenderer terrainRenderer; + private final VkCompositor compositor; + private final VkSSAO ssao; + private final VkBoundRenderer boundRenderer; + private final StreamedBoundStore visibleSectionStream; + private boolean shutDown = false; + private boolean firstFrameLogged = false; + private final RenderDistanceTracker renderDistanceTracker; + private final ViewportSelector viewportSelector; + + public VkRenderCore(WorldEngine world, ServiceManager sm) { + world.acquireRef(); + CmpLog.backend = "vulkan"; + Logger.info("Creating Voxy pure-Vulkan render core"); + try { + this.worldIn = world; + var host = MinecraftVkHost.get(); + if (host == null) throw new IllegalStateException("No Minecraft Vulkan host adapter registered"); + var vctx = VulkanBackend.context();//adopts MC's device + this.frameCtx = new VkFrameCtx(vctx); + + //Install the VK streams BEFORE any shared class touches the singletons + this.uploadStream = new VkUploadStream(this.frameCtx, 1 << 26);//64 mb, same as GL + this.downloadStream = new VkDownloadStream(this.frameCtx, 1 << 25);//32 mb, same as GL + AbstractUploadStream.setInstance(this.uploadStream); + AbstractDownloadStream.setInstance(this.downloadStream); + + this.properties = RenderProperties.getRenderProperties(); + + //Install the VK atlas readback BEFORE the model bakery reads the block atlas + // (its constructor does a synchronous GPU->CPU copy) + IAtlasTextureReader.setInstance( + new VkAtlasTextureReader(this.frameCtx)); + + this.modelStore = new VkModelStore(this.frameCtx, this.uploadStream); + this.modelService = new ModelBakerySubsystem(world.getMapper(), this.modelStore); + this.renderGen = new RenderGenerationService(world, this.modelService, sm, false); + + this.geometryData = new VkSectionGeometryData(this.frameCtx, 1 << 20, geometryCapacity()); + this.nodeManager = new AsyncNodeManager(1 << 21, this.geometryData, this.renderGen, + new VkNodeGpuOps(this.frameCtx, this.uploadStream)); + this.nodeCleaner = new VkNodeCleaner(this.frameCtx, this.uploadStream, this.downloadStream, this.nodeManager); + this.traversal = new VkTraversal(this.frameCtx, this.uploadStream, this.downloadStream, + this.properties, this.nodeManager, this.nodeCleaner, this.renderGen); + this.terrainRenderer = new VkTerrainRenderer(this.frameCtx, this.uploadStream, this.downloadStream, + this.properties, this.geometryData, this.modelStore); + this.compositor = new VkCompositor(this.frameCtx, this.uploadStream, this.properties, + VoxyConfig.CONFIG.useEnvironmentalFog); + this.ssao = new VkSSAO(this.frameCtx, this.uploadStream, this.properties, VoxyConfig.CONFIG.getSSAOMode()); + //Depth-bound culling: Sodium's visibility mixins feed the store; the bound + // renderer rasters visible-chunk AABBs into the depth-bound image so the + // terrain shaders can discard LOD fragments vanilla terrain will cover. + this.visibleSectionStream = new StreamedBoundStore( + size -> new VkBuffer(this.frameCtx, size)); + this.boundRenderer = new VkBoundRenderer(this.frameCtx, this.uploadStream, this.properties); + + world.setDirtyCallback(this.nodeManager::worldEvent); + Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome); + world.getMapper().setBiomeCallback(this.modelService::addBiome); + this.nodeManager.start(); + + this.viewportSelector = new ViewportSelector<>(() -> + new VkViewport(this.frameCtx, this.properties, this.geometryData.getMaxSectionCount())); + + int minSec = Minecraft.getInstance().level.getMinSectionY() >> 5; + int maxSec = (Minecraft.getInstance().level.getMaxSectionY() - 1) >> 5; + this.renderDistanceTracker = new RenderDistanceTracker(40, minSec, maxSec, + this.nodeManager::addTopLevel, this.nodeManager::removeTopLevel); + this.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); + + this.frameCtx.flushImmediate(); + Logger.info("Voxy pure-Vulkan render core created with " + this.geometryData.getMaxCapacity() + " geometry capacity"); + Logger.info("Voxy VK diagnostics: isZero2One=" + this.properties.isZero2One() + + " isReverseZ=" + this.properties.isReverseZ() + + " clearDepth=" + this.properties.clearDepth() + + " inverseClearDepth=" + this.properties.inverseClearDepth() + + " drawIndirectCount=" + vctx.hasDrawIndirectCount + + " device=" + vctx.deviceName); + } catch (RuntimeException e) { + world.releaseRef(); + throw e; + } + } + + private static long geometryCapacity() { + //Conservative fixed allocation (no sparse residency tricks on VK): 2GB, halved on failure inside VkSectionGeometryData + return 2048L << 20; + } + + //Renders one Voxy frame into MC's frame command buffer. Called from the + // render hook right after MC's opaque terrain pass, on the render thread. + // + //matrices are Sodium's per-frame ChunkRenderMatrices — the exact + // projection+modelView MC/Sodium just drew the terrain with, INCLUDING + // per-frame view bobbing/nausea/portal warps. They must be used instead of + // the raw cameraRenderState matrices: computeProjectionMat extracts the + // bob delta as rawMCProj^-1 x base, which collapses to identity if base IS + // rawMCProj, and viewRotationMatrix is rotation-only — both of which made + // the LODs bounce relative to vanilla terrain while walking. + public void renderFrame(RenderTarget target, MinecraftVkHostAdapter adapter, ChunkRenderMatrices matrices, + double camX, double camY, double camZ) { + var frameCmd = adapter.frameCommandBuffer(); + if (frameCmd == null) { + Logger.warn("Voxy VK: no frame command buffer at hook point, skipping frame"); + return; + } + var crs = Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState; + if (crs == null || !crs.initialized) return; + + if (!this.firstFrameLogged) { + this.firstFrameLogged = true; + Logger.info("Voxy VK first frame: target=" + target.width + "x" + target.height + + " colorFormat=" + VkFrameHost.vkFormat(target.getColorTextureView()) + + " depthFormat=" + VkFrameHost.vkFormat(target.getDepthTextureView()) + + " sectionCount=" + this.geometryData.getSectionCount() + + " hasMatrices=" + (matrices != null)); + } + + this.frameCtx.flushImmediate(); + this.frameCtx.beginFrame(frameCmd); + try { + if (me.cortex.voxy.commonImpl.VoxyCommon.IS_MINE_IN_ABYSS) {//same camera trickery as the GL setupViewport + int sector = (((int) Math.floor(camX) >> 4) + 512) >> 10; + camX -= sector << 14;//10+4 + camY += (16 + (256 - 32 - sector * 30)) * 16; + } + + var viewport = this.viewportSelector.getViewport(); + var voxyProjection = VoxyRenderSystem.computeProjectionMat(this.properties, matrices.projection()); + var fog = crs.fogData == null ? null : new FogParameters( + crs.fogData.color.x, crs.fogData.color.y, crs.fogData.color.z, crs.fogData.color.w, + crs.fogData.environmentalStart, crs.fogData.environmentalEnd, + crs.fogData.renderDistanceStart, crs.fogData.renderDistanceEnd); + viewport.setVanillaProjection(matrices.projection()) + .setProjection(voxyProjection) + .setModelView(matrices.modelView())//setModelView copies into the viewport's own matrix + .setCamera(camX, camY, camZ) + .setScreenSize(target.width, target.height) + .setFogParameters(fog) + .update(); + viewport.frameId++; + if (viewport.width <= 0 || viewport.height <= 0) return; + viewport.ensureTargets(); + + var rt = new VkCompositor.VkViewportRT(viewport, + target.getColorTextureView(), target.getDepthTextureView(), target.width, target.height); + + //1. copy MC depth in + stencil mask (also clears the offscreen targets) + this.compositor.setupDepthStencil(rt); + + //1.5 raster the vanilla-visible chunk bounds into the depth-bound image + // (sampled by the terrain draws below to cull LOD fragments behind vanilla) + this.boundRenderer.render(viewport, this.visibleSectionStream); + + //2. opaque LOD terrain (draw calls generated LAST frame) + this.terrainRenderer.renderOpaque(viewport, false); + + //3. HiZ + node management + hierarchical traversal + this.compositor.offscreenToSampled(viewport); + viewport.hiZ.buildMipChain(viewport.depthSampleView, viewport.width, viewport.height); + this.compositor.offscreenToAttachment(viewport); + + this.downloadStream.tick(); + this.nodeManager.tick(this.traversal.getNodeBuffer(), this.nodeCleaner); + this.nodeCleaner.tick(this.traversal.getNodeBuffer()); + this.nodeManager.logCompareStats(); + this.traversal.doTraversal(viewport); + + //4. build the draw commands for this frame (prep, raster cull, cmdgen, translucency sort) + this.terrainRenderer.buildDrawCalls(viewport); + + //5. temporal, then SSAO (reads colour+depth, writes colourSSAO with + // sanitized alpha), then translucents onto the SSAO output — the same + // opaque->temporal->SSAO->translucent order as the GL pipeline + this.terrainRenderer.renderTemporal(viewport); + this.ssao.compute(viewport, rt); + this.terrainRenderer.renderTranslucent(viewport); + + //6. composite into MC's frame + this.compositor.offscreenToSampled(viewport); + this.compositor.composite(rt); + + //7. dynamic CPU work (uploads recycled, model baking, render distance tracking) + this.uploadStream.tick(); + this.renderDistanceTracker.setCenterAndProcess(viewport.cameraX, viewport.cameraZ); + this.modelService.tick(900_000); + } finally { + this.frameCtx.endFrame(); + this.frameCtx.pollRetired(); + } + } + + public void setRenderDistance(float renderDistance) { + this.renderDistanceTracker.setRenderDistance((int) Math.ceil(renderDistance + 1)); + } + + public void addDebugInfo(List debug) { + debug.add("VK host mode: " + VulkanBackend.statusLine()); + debug.add("VkBuf [#/Mb]: [" + VkBuffer.getCount() + "/" + + (VkBuffer.getTotalSize() / 1_000_000) + "]"); + this.modelService.addDebugData(debug); + this.renderGen.addDebugData(debug); + this.nodeManager.addDebug(debug); + this.ssao.addDebugInfo(debug); + } + + public void shutdown() { + if (this.shutDown) { + //Idempotent: a second teardown would double-free / re-idle the device + return; + } + this.shutDown = true; + Logger.info("Shutting down Voxy pure-Vulkan render core"); + + //CPU-only stop first: detach world callbacks and join the node/gen worker + // threads (both produce CPU data only — GPU upload happens on the render + // thread), so nothing can enqueue more work while we tear down. The model + // bakery is NOT stopped here — its shutdown() frees GPU resources + // (VkModelStore), so it must run AFTER the device is idle. + try { + this.worldIn.setDirtyCallback(null); + this.worldIn.getMapper().setBiomeCallback(null); + this.worldIn.getMapper().setStateCallback(null); + this.nodeManager.stop(); + this.renderGen.shutdown(); + } catch (Exception e) { + Logger.error("Error stopping VK render core CPU services", e); + } + + //Only touch the GPU if MC's adopted device is still alive. On full game + // exit MC's VulkanDevice.close() (which clears the host) can run before + // the level renderer closes; issuing vkDeviceWaitIdle / vkDestroy* + // against a destroyed device is a use-after-free in the driver. If the + // host is already gone the objects are unreachable anyway, so leaking + // them is strictly safer than a native crash. + boolean deviceAlive = MinecraftVkHost.get() != null; + if (deviceAlive) { + try { + //Idle the device BEFORE destroying anything, so no destroy races + // GPU work still referencing these objects + this.frameCtx.waitIdleRetireAll(); + //modelService.shutdown() joins the (CPU) baking thread and frees + // the VkModelStore exactly once. It OWNS the store's lifetime — + // VkRenderCore must not free modelStore itself (double + // vkDestroySampler, observed NVIDIA SIGSEGV on world unload). + this.modelService.shutdown(); + this.boundRenderer.free(); + this.visibleSectionStream.free(); + this.traversal.free(); + this.nodeCleaner.free(); + this.geometryData.free(); + this.terrainRenderer.free(); + this.ssao.free(); + this.compositor.free(); + this.viewportSelector.free(); + this.downloadStream.flushWaitClear(); + this.uploadStream.free(); + this.downloadStream.free(); + this.frameCtx.free(); + } catch (Exception e) { + Logger.error("Error shutting down VK render core GPU resources", e); + } + } else { + Logger.warn("Voxy VK: Minecraft's Vulkan device is already gone at shutdown; " + + "skipping GPU teardown to avoid destroying objects on a dead device"); + } + + AbstractUploadStream.clearInstance(); + AbstractDownloadStream.clearInstance(); + IAtlasTextureReader.clearInstance(); + + this.worldIn.releaseRef(); + Logger.info("VK render core shutdown completed"); + } + + public StreamedBoundStore getVisibleSectionStream() { + return this.visibleSectionStream; + } + + public WorldEngine getEngine() { + return this.worldIn; + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java new file mode 100644 index 000000000..e4508aea0 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java @@ -0,0 +1,189 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.SSAO; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.common.Logger; +import org.joml.Matrix4f; +import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK port of the GL SSAO pass (post/ssao.comp): between the temporal and +// translucent draws, a compute dispatch reads the opaque LOD colour+depth and +// writes the AO-modulated colour into viewport.colourSSAO. Beyond the AO itself +// this pass is load-bearing for compositing: the raw colour target carries +// per-pixel METADATA in its alpha channel, and this pass rewrites alpha to 1 +// (LOD present) / 0 (empty), which is what the composite blend expects. +// +// The BETTER/BEST modes additionally sample MC's own depth attachment (for AO +// across the vanilla/LOD seam), transitioned around the dispatch. AUTO mode is +// resolved from the device-local heap size (VK always exposes heap sizes, +// unlike the GL Capabilities memory query). +public class VkSSAO { + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + + private final boolean isBetterSSAO; + private final int spp; + private final VkShaderPipeline pipeline; + private final VkBuffer params; + private final long colourSampler; + private final long depthSampler; + private final Matrix4f invScratch = new Matrix4f();//per-frame matrix inversion scratch (avoids a heap alloc each compute()) + + public VkSSAO(VkFrameCtx ctx, VkUploadStream uploadStream, RenderProperties properties, SSAO.SSAOMode mode) { + this.ctx = ctx; + this.uploadStream = uploadStream; + + mode = resolveAuto(ctx, mode); + this.isBetterSSAO = mode != SSAO.SSAOMode.BASIC; + this.spp = switch (mode) { + case BETTER -> 12; + case BEST -> 24; + default -> 0; + }; + Logger.info("Voxy VK SSAO mode: " + mode); + + var defs = VkShaderSource.defs().props(properties); + if (this.isBetterSSAO) { + defs.def("BETTER_SSAO").def("SSAO_STEPS", this.spp).def("USE_GENERATED_SAMPLE_POINTS"); + } + String src = VkShaderSource.load("voxy:post/ssao.comp", defs.build()); + if (this.isBetterSSAO) { + //Same compile-time sample disk the GL builder splices in via replace() + src = src.replace("%%CONST_ARRAY%%", generateSamplePoints(this.spp)); + } + + var bindings = this.isBetterSSAO + ? List.of(VkShaderPipeline.image(0), VkShaderPipeline.sampler(1), VkShaderPipeline.sampler(2), + VkShaderPipeline.sampler(3), VkShaderPipeline.ubo(4)) + : List.of(VkShaderPipeline.image(0), VkShaderPipeline.sampler(1), VkShaderPipeline.sampler(2), + VkShaderPipeline.ubo(4)); + this.pipeline = new VkShaderPipeline(ctx, "ssao.comp", src, 0, bindings); + + this.params = new VkBuffer(ctx, 256).zero(); + ctx.flushImmediate(); + this.colourSampler = VkImage2D.createSampler(ctx.vk(), false, false);//nearest (GL colourTex is NEAREST) + //GL: better -> NEAREST(+mip), basic -> LINEAR; our targets are single-mip + this.depthSampler = VkImage2D.createSampler(ctx.vk(), false, !this.isBetterSSAO); + } + + private static SSAO.SSAOMode resolveAuto(VkFrameCtx ctx, SSAO.SSAOMode mode) { + if (mode != SSAO.SSAOMode.AUTO) return mode; + //Budget-based heuristic: pick the highest tier whose sample count fits a + // per-frame sample budget = screenPixels * spp. Downgrades high-resolution + // devices (more pixels = more samples at the same spp) and unified-memory + // Macs (where the previous heap-size heuristic always picked BEST because + // the "device-local heap" is the whole RAM). + long w = net.minecraft.client.Minecraft.getInstance().getWindow().getWidth(); + long h = net.minecraft.client.Minecraft.getInstance().getWindow().getHeight(); + long pixels = Math.max(1, w * h); + //BEST = 24 spp, BETTER = 12 spp; budget thresholds in samples/frame. + if (pixels * 24 < 50_000_000L) return SSAO.SSAOMode.BEST; + if (pixels * 12 < 150_000_000L) return SSAO.SSAOMode.BETTER; + return SSAO.SSAOMode.BASIC; + } + + private static String generateSamplePoints(int samples) { + var array = new StringBuilder(); + for (int i = 0; i < samples; i++) { + array.append("vec2("); + float a = (((float) i) + 0.5f) * (1.0f / samples); + float base = (float) (i * (1.0 / 1.6180339887) + 0.5); + float r = (float) Math.sqrt(base % 1); + float theta = a * 6.2831853f; + array.append((float) (r * Math.cos(theta))).append("f, ") + .append((float) (r * Math.sin(theta))).append("f)"); + if (i != samples - 1) array.append(", "); + } + return array.toString(); + } + + /** + * Records the SSAO dispatch: colour+depth (and MC depth for BETTER) sampled, + * colourSSAO written. Leaves colourSSAO in COLOR_ATTACHMENT layout for the + * translucent pass and the offscreen depth back in attachment layout. + */ + public void compute(VkViewport viewport, VkCompositor.VkViewportRT rt) { + var cmd = this.ctx.cmd(); + + {//params UBO: BASIC = MVP,invMVP; BETTER = Proj,invProj,MV,sourceInvProj + long ptr = this.uploadStream.upload(this.params, 0, 256); + var scratch = this.invScratch; + if (this.isBetterSSAO) { + viewport.projection.getToAddress(ptr); + viewport.projection.invert(scratch).getToAddress(ptr + 64); + viewport.modelView.getToAddress(ptr + 128); + viewport.vanillaProjection.invert(scratch).getToAddress(ptr + 192); + } else { + viewport.MVP.getToAddress(ptr); + viewport.MVP.invert(scratch).getToAddress(ptr + 64); + } + this.uploadStream.commit(); + } + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + + //opaque+temporal colour -> sampled; offscreen depth -> sampled; SSAO target -> storage + viewport.colour.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + viewport.depthStencil.transition(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + viewport.colourSSAO.transition(VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT); + if (this.isBetterSSAO) { + VkFrameHost.transitionMcImage(cmd, rt.mcDepth(), true, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + + this.pipeline.bind(cmd); + try (var b = this.pipeline.binder()) { + b.image(0, viewport.colourSSAO.view) + .sampler(1, viewport.colour.view, this.colourSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .sampler(2, viewport.depthSampleView, this.depthSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + if (this.isBetterSSAO) { + b.sampler(3, VkFrameHost.vkView(rt.mcDepth()), this.depthSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + b.ubo(4, this.params).push(cmd); + } + vkCmdDispatch(cmd, (viewport.width + 7) / 8, (viewport.height + 7) / 8, 1); + + if (this.isBetterSSAO) { + VkFrameHost.transitionMcImage(cmd, rt.mcDepth(), true, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + } + //SSAO output -> colour attachment for the translucent pass + viewport.colourSSAO.transition(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + //offscreen depth back to attachment for the translucent pass + viewport.depthStencil.transition(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + } + + public void addDebugInfo(List debugLines) { + debugLines.add("SSAO(VK): " + (this.isBetterSSAO ? ("new (" + this.spp + " spp)") : "basic")); + } + + public void free() { + this.pipeline.free(); + this.params.free(); + //colourSampler/depthSampler come from VkImage2D.createSampler's device-lifetime + // cache (shared handles); never destroy them per-object (multi-free -> SIGSEGV). + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkSectionGeometryData.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSectionGeometryData.java new file mode 100644 index 000000000..a8888afc3 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSectionGeometryData.java @@ -0,0 +1,94 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; +import me.cortex.voxy.client.core.rendering.util.IDeviceBuffer; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.common.Logger; + +//Pure-VK geometry store: the quad geometry buffer + per-section metadata as +// plain device-local VkBuffers (no sparse tricks — Vulkan allocation succeeds +// or fails up front; on failure we halve the capacity and retry). +public class VkSectionGeometryData implements IBasicGeometryData { + private final VkBuffer sectionMetadataBuffer; + private final VkBuffer geometryBuffer; + private final int maxSectionCount; + private int currentSectionCount; + + public VkSectionGeometryData(VkFrameCtx ctx, int maxSectionCount, long geometryCapacity) { + this.maxSectionCount = maxSectionCount; + if ((geometryCapacity % 8) != 0) throw new IllegalStateException(); + this.sectionMetadataBuffer = new VkBuffer(ctx, (long) maxSectionCount * SECTION_METADATA_SIZE); + VkBuffer buffer = null; + long capacity = geometryCapacity; + while (buffer == null) { + try { + Logger.info("Allocating " + (capacity / (1024 * 1024)) + "MB VK geometry buffer"); + buffer = new VkBuffer(ctx, capacity); + } catch (RuntimeException e) { + if (capacity <= (256L << 20)) throw e; + capacity /= 2; + Logger.warn("VK geometry allocation failed, retrying with " + (capacity / (1024 * 1024)) + "MB"); + } + } + this.geometryBuffer = buffer; + //Match the GL path's zeroed geometry buffer + this.geometryBuffer.zero(); + this.sectionMetadataBuffer.zero(); + ctx.flushImmediate(); + } + + @Override + public IDeviceBuffer geometryBufferHandle() { + return this.geometryBuffer; + } + + @Override + public IDeviceBuffer metadataBufferHandle() { + return this.sectionMetadataBuffer; + } + + public VkBuffer geometryBuffer() { + return this.geometryBuffer; + } + + public VkBuffer metadataBuffer() { + return this.sectionMetadataBuffer; + } + + @Override + public void ensureAccessable(int maxElementAccess) { + //Fully-resident allocation: nothing to commit + } + + @Override + public int getSectionCount() { + return this.currentSectionCount; + } + + @Override + public void setSectionCount(int count) { + this.currentSectionCount = count; + } + + @Override + public int getMaxSectionCount() { + return this.maxSectionCount; + } + + @Override + public long getGeometryCapacityBytes() { + return this.geometryBuffer.size(); + } + + @Override + public long getMaxCapacity() { + return this.geometryBuffer.size(); + } + + @Override + public void free() { + this.sectionMetadataBuffer.free(); + this.geometryBuffer.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java new file mode 100644 index 000000000..b4d387253 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java @@ -0,0 +1,504 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkCmd; +import me.cortex.voxy.client.core.vk.VkDownloadStream; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.common.Logger; +import org.joml.Matrix4f; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkRect2D; +import org.lwjgl.vulkan.VkRenderingAttachmentInfoKHR; +import org.lwjgl.vulkan.VkRenderingInfoKHR; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdBeginRenderingKHR; +import static org.lwjgl.vulkan.KHRDynamicRendering.vkCmdEndRenderingKHR; +import static org.lwjgl.vulkan.VK10.*; +import static org.lwjgl.vulkan.VK12.vkCmdDrawIndexedIndirectCount; + +//Pure-VK mirror of MDICSectionRenderer: the same six GPU passes (prep, +// raster-cull, command generation, translucency prefix sort + build, then +// opaque / temporal / translucent indexed-indirect-count draws) against the +// same buffer layouts, recorded into MC's frame command buffer with dynamic +// rendering over Voxy's offscreen colour + depth-stencil targets. +public class VkTerrainRenderer { + //Draw-command offsets shared with the cmdgen/translucent shader defines. + private static final int TRANSLUCENT_OFFSET = VkViewport.OPAQUE_DRAW_COUNT; + private static final int TEMPORAL_OFFSET = TRANSLUCENT_OFFSET + VkViewport.TRANSLUCENT_DRAW_COUNT; + + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final VkDownloadStream downloadStream; + private final RenderProperties properties; + private final VkSectionGeometryData geometry; + private final VkModelStore modelStore; + + //MoltenVK fixed-count fallback state: the last-known REAL per-pass draw counts, + // read back from drawCountCallBuffer each frame. On desktop the GPU sources + // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to the + // per-pass caps so the first few frames — before the first readback lands — + // behave like the old section-count fallback, then converge to the true + // visible counts. + private int fbOpaqueDraws = VkViewport.OPAQUE_DRAW_COUNT; + private int fbTranslucentDraws = VkViewport.TRANSLUCENT_DRAW_COUNT; + private int fbTemporalDraws = VkViewport.TEMPORAL_DRAW_COUNT; + + private final VkBuffer uniform; + private final VkBuffer distanceCountBuffer; + private final VkBuffer indexBuffer; + private final long depthBoundSampler; + private final long lightmapSampler; + + private final VkShaderPipeline prep; + private final VkShaderPipeline cmdGen; + private final VkShaderPipeline prefixSum; + private final VkShaderPipeline translucentGen; + private final VkShaderPipeline cullRaster; + private VkShaderPipeline terrainOpaque; + private VkShaderPipeline terrainTranslucent; + private int pipelineColorFormat = -1, pipelineDepthFormat = -1; + + public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownloadStream downloadStream, + RenderProperties properties, VkSectionGeometryData geometry, VkModelStore modelStore) { + this.ctx = ctx; + this.uploadStream = uploadStream; + this.downloadStream = downloadStream; + this.properties = properties; + this.geometry = geometry; + this.modelStore = modelStore; + + this.uniform = new VkBuffer(ctx, 1024).zero(); + this.distanceCountBuffer = new VkBuffer(ctx, 1024L * 4 + VkViewport.TRANSLUCENT_DRAW_COUNT * 4L).zero(); + + //Shared index buffer: u16 quad pattern + u16 cube indices at CUBE_INDEX_OFFSET + this.indexBuffer = new VkBuffer(ctx, SharedIndexBuffer.CUBE_INDEX_OFFSET + 6 * 2 * 3 * 2L); + { + var quads = SharedIndexBuffer.generateQuadIndicesShort(16380); + long ptr = uploadStream.upload(this.indexBuffer, 0, this.indexBuffer.size()); + quads.cpyTo(ptr); + VkCmd.writeCubeIndicesU16(ptr + SharedIndexBuffer.CUBE_INDEX_OFFSET); + quads.free(); + uploadStream.commit(); + ctx.flushImmediate(); + } + this.depthBoundSampler = VkImage2D.createSampler(ctx.vk(), false, false); + this.lightmapSampler = VkImage2D.createSampler(ctx.vk(), false, true); + + //================= compute pipelines ================= + this.prep = new VkShaderPipeline(ctx, "prep.comp", + VkShaderSource.load("voxy:lod/gl46/prep.comp", VkShaderSource.defs().build()), + 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2))); + + this.cmdGen = new VkShaderPipeline(ctx, "cmdgen.comp", + VkShaderSource.load("voxy:lod/gl46/cmdgen.comp", VkShaderSource.defs() + .def("TRANSLUCENT_WRITE_BASE", 1024) + .def("TEMPORAL_OFFSET", TEMPORAL_OFFSET) + .def("TRANSLUCENT_DISTANCE_BUFFER_BINDING", 7) + .build()), + 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), + VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5), + VkShaderPipeline.ssbo(6), VkShaderPipeline.ssbo(7))); + + //Subgroup prefix sum on VK when the device advertises subgroup arithmetic + // (MoltenVK/Metal simdgroups, NVIDIA, AMD, Intel — virtually every VK 1.1+ + // device). Falls back to the shared-memory Hillis-Steele scan otherwise. + boolean useSubgroup = ctx.vk().subgroupArithmetic; + this.prefixSum = new VkShaderPipeline(ctx, "prefixsum.comp", + VkShaderSource.load(useSubgroup ? "voxy:util/prefixsum/inital3_vk.comp" : "voxy:util/prefixsum/simple.comp", + VkShaderSource.defs().def("IO_BUFFER", 0).build()), + 0, List.of(VkShaderPipeline.ssbo(0))); + + this.translucentGen = new VkShaderPipeline(ctx, "buildtranslucents.comp", + VkShaderSource.load("voxy:lod/gl46/buildtranslucents.comp", VkShaderSource.defs() + .def("TRANSLUCENT_WRITE_BASE", 1024) + .def("TRANSLUCENT_DISTANCE_BUFFER_BINDING", 5) + .def("TRANSLUCENT_OFFSET", TRANSLUCENT_OFFSET) + .build()), + 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), + VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5))); + + //================= raster cull pipeline (depth-only, no writes) ================= + var cullDesc = new VkShaderPipeline.GfxDesc(); + cullDesc.name = "cullraster"; + cullDesc.vertGlsl = VkShaderSource.load("voxy:lod/gl46/cull/raster.vert", VkShaderSource.defs().props(properties).build()); + cullDesc.fragGlsl = VkShaderSource.load("voxy:lod/gl46/cull/raster.frag", VkShaderSource.defs().props(properties).build()); + cullDesc.colorFormat = VK_FORMAT_UNDEFINED; + cullDesc.depthFormat = VK_FORMAT_D32_SFLOAT_S8_UINT; + cullDesc.stencilFormat = VK_FORMAT_D32_SFLOAT_S8_UINT; + cullDesc.depthTest = true; + cullDesc.depthWrite = false; + cullDesc.colorWrite = false; + cullDesc.depthCompare = VkCmd.closerEqual(this.properties); + cullDesc.bindings = List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), + VkShaderPipeline.ssbo(2), VkShaderPipeline.ssbo(3)); + this.cullRaster = new VkShaderPipeline(ctx, cullDesc); + } + + private void ensureTerrainPipelines(VkViewport viewport) { + int cf = viewport.colour.format; + int df = viewport.depthStencil.format; + if (this.terrainOpaque != null && cf == this.pipelineColorFormat && df == this.pipelineDepthFormat) return; + if (this.terrainOpaque != null) { + this.terrainOpaque.free(); + this.terrainTranslucent.free(); + } + var cardinalLight = net.minecraft.client.Minecraft.getInstance().level.cardinalLighting(); + String vert = VkShaderSource.load("voxy:lod/gl46/quads3.vert", VkShaderSource.defs().props(this.properties) + .def("NO_SHADE_FACE_TINT", cardinalLight.up()) + .def("UP_FACE_TINT", cardinalLight.up()) + .def("DOWN_FACE_TINT", cardinalLight.down()) + .def("Z_AXIS_FACE_TINT", cardinalLight.north()) + .def("X_AXIS_FACE_TINT", cardinalLight.east()) + .build()); + + var bindings = List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(3), + VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5), + VkShaderPipeline.sampler(8), VkShaderPipeline.sampler(9), VkShaderPipeline.sampler(10)); + + var opaque = new VkShaderPipeline.GfxDesc(); + opaque.name = "terrain-opaque"; + opaque.vertGlsl = vert; + opaque.fragGlsl = VkShaderSource.load("voxy:lod/gl46/quads.frag", VkShaderSource.defs().props(this.properties).build()); + opaque.colorFormat = cf; + opaque.depthFormat = df; + opaque.stencilFormat = df; + opaque.depthTest = true; + opaque.depthWrite = true; + opaque.depthCompare = VkCmd.closerEqual(this.properties); + opaque.blend = false; + opaque.stencilTestEqual1 = true;//only render where vanilla terrain is absent + opaque.bindings = bindings; + this.terrainOpaque = new VkShaderPipeline(this.ctx, opaque); + + var translucent = new VkShaderPipeline.GfxDesc(); + translucent.name = "terrain-translucent"; + translucent.vertGlsl = vert; + translucent.fragGlsl = VkShaderSource.load("voxy:lod/gl46/quads.frag", VkShaderSource.defs().props(this.properties) + .def("TRANSLUCENT").build()); + translucent.colorFormat = cf; + translucent.depthFormat = df; + translucent.stencilFormat = df; + translucent.depthTest = true; + translucent.depthWrite = true; + translucent.depthCompare = VkCmd.closerEqual(this.properties); + translucent.blend = true; + translucent.stencilTestEqual1 = true; + translucent.bindings = bindings; + this.terrainTranslucent = new VkShaderPipeline(this.ctx, translucent); + + this.pipelineColorFormat = cf; + this.pipelineDepthFormat = df; + } + + //================================================================================== + + private final Matrix4f uniformScratch = new Matrix4f(); + public void uploadUniform(VkViewport viewport) { + long ptr = this.uploadStream.upload(this.uniform, 0, 1024); + var mat = this.uniformScratch.set(viewport.MVP); + mat.translate(-viewport.innerTranslation.x, -viewport.innerTranslation.y, -viewport.innerTranslation.z); + mat.getToAddress(ptr); ptr += 4 * 4 * 4; + viewport.section.getToAddress(ptr); ptr += 4 * 3; + if (viewport.frameId < 0) { + Logger.error("Frame ID negative, this will cause things to break, wrapping around"); + viewport.frameId &= 0x7fffffff; + } + MemoryUtil.memPutInt(ptr, viewport.frameId & 0x7fffffff); ptr += 4; + viewport.innerTranslation.getToAddress(ptr); + this.uploadStream.commit(); + } + + /** Mirrors MDIC buildDrawCalls: prep -> raster cull -> cmdgen -> translucency sort. */ + public void buildDrawCalls(VkViewport viewport) { + if (this.geometry.getSectionCount() == 0) return; + var cmd = this.ctx.cmd(); + this.uploadUniform(viewport); + + if (!this.ctx.vk().hasDrawIndirectCount) { + //Fixed-count fallback (MoltenVK): renderTerrain issues + // vkCmdDrawIndexedIndirect with maxDrawCount rather than a GPU-sourced + // count, so trailing slots past the actual command count would be read + // as stale draw commands from the previous frame. Zero the three + // drawCallBuffer slices (opaque / temporal / translucent) here so any + // un-overwritten slot reads as instanceCount=0 (a no-op draw). The + // cmdgen compute below then writes the real commands on top; the fill + // completes (with a barrier) before the cmdgen dispatch reads the buffer. + // Gated to the fallback path only — the tight-count path (desktop Vulkan) + // never reads past the actual count, so it pays nothing here. + long stride = 5L * 4; + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, 0L, VkViewport.OPAQUE_DRAW_COUNT * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TEMPORAL_OFFSET * stride, VkViewport.TEMPORAL_DRAW_COUNT * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TRANSLUCENT_OFFSET * stride, VkViewport.TRANSLUCENT_DRAW_COUNT * stride, 0); + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); + } + + {//prep + this.prep.bind(cmd); + try (var b = this.prep.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, viewport.drawCountCallBuffer) + .ssbo(2, viewport.indirectLookupBuffer) + .push(cmd); + } + vkCmdDispatch(cmd, 1, 1, 1); + //prep (compute) wrote the cull indirect args; the raster-cull draw + // reads them. Scope to COMPUTE -> DRAW_INDIRECT|VERTEX_INPUT. + this.ctx.computeToDrawBarrier(); + } + + {//raster occlusion test into the visibility buffer (depth-tested box draw, no writes) + this.beginRendering(cmd, viewport, 0L, true);//depth-only + this.cullRaster.bind(cmd); + VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); + try (var b = this.cullRaster.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, this.geometry.metadataBuffer()) + .ssbo(2, viewport.visibilityBuffer) + .ssbo(3, viewport.indirectLookupBuffer) + .push(cmd); + } + vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdDrawIndexedIndirect(cmd, viewport.drawCountCallBuffer.buffer, 6 * 4, 1, 20); + vkCmdEndRenderingKHR(cmd); + //The raster-cull draw wrote visibilityData (SSBO) from the fragment + // shader; the consumer is the cmdgen compute. Scope to those stages + // instead of the previous fullBarrier (ALL_COMMANDS -> ALL_COMMANDS) + // which forced a full pipeline stall on every frame. + this.ctx.barrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + } + + {//command generation (indirect dispatch sized by prep) + vkCmdFillBuffer(cmd, this.distanceCountBuffer.buffer, 0, 1024L * 4, 0); + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + this.cmdGen.bind(cmd); + try (var b = this.cmdGen.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, viewport.drawCallBuffer) + .ssbo(2, viewport.drawCountCallBuffer) + .ssbo(3, this.geometry.metadataBuffer()) + .ssbo(4, viewport.visibilityBuffer) + .ssbo(5, viewport.indirectLookupBuffer) + .ssbo(6, viewport.positionScratchBuffer) + .ssbo(7, this.distanceCountBuffer) + .push(cmd); + } + vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); + //cmdgen -> prefixsum: both compute. The draw that consumes these + // indirect commands is guarded by renderTerrain's own barrier. + this.ctx.computeToComputeBarrier(); + } + + {//translucency sorting + this.prefixSum.bind(cmd); + try (var b = this.prefixSum.binder()) { + b.ssbo(0, this.distanceCountBuffer).push(cmd); + } + vkCmdDispatch(cmd, 1, 1, 1); + //prefixsum -> translucentGen: both compute. + this.ctx.computeToComputeBarrier(); + + this.translucentGen.bind(cmd); + try (var b = this.translucentGen.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, viewport.drawCallBuffer) + .ssbo(2, viewport.drawCountCallBuffer) + .ssbo(3, this.geometry.metadataBuffer()) + .ssbo(4, viewport.indirectLookupBuffer) + .ssbo(5, this.distanceCountBuffer) + .push(cmd); + } + vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); + //No trailing barrier here: the translucent commands written into + // drawCallBuffer are read by renderTranslucent's renderTerrain, which + // issues its own COMPUTE|TRANSFER -> DRAW_INDIRECT|VERTEX|FRAGMENT + // barrier (line ~355) before drawing. The previous computeToAllBarrier + // here was redundant with that draw barrier and serialised the GPU. + } + + if (!this.ctx.vk().hasDrawIndirectCount) { + //Read the three real per-pass draw counts back to the CPU so next frame's + // fixed-count multi-draws track them instead of the worst-case section + // cap (see fixedCountBudget). The counts live at opaque@12 / translucent@16 + // / temporal@20 in drawCountCallBuffer; download those 12 bytes on the same + // async, event-retired path the traversal request readback already uses + // (commit() scopes its own COMPUTE->TRANSFER->HOST barriers). The callback + // runs on the render thread from pollRetired, so the plain field writes are + // race-free. Desktop (drawIndirectCount) neither needs nor issues this. + this.downloadStream.download(viewport.drawCountCallBuffer, 12, 12, (ptr, size) -> { + this.fbOpaqueDraws = clampCount(MemoryUtil.memGetInt(ptr), VkViewport.OPAQUE_DRAW_COUNT); + this.fbTranslucentDraws = clampCount(MemoryUtil.memGetInt(ptr + 4), VkViewport.TRANSLUCENT_DRAW_COUNT); + this.fbTemporalDraws = clampCount(MemoryUtil.memGetInt(ptr + 8), VkViewport.TEMPORAL_DRAW_COUNT); + }); + } + } + + private static int clampCount(int value, int cap) { + return value < 0 ? 0 : Math.min(value, cap); + } + + /** + * Draw-count upper bound for one terrain pass. On desktop Vulkan the GPU sources + * the real count from drawCountCallBuffer (vkCmdDrawIndexedIndirectCount), so the + * section-derived {@code cap} is only a ceiling and is returned unchanged. On + * MoltenVK — which lacks drawIndirectCount — the fixed-count multi-draw instead + * iterates whatever count it is handed, encoding one Metal draw per slot; handing + * it the section-count ceiling means tens of thousands of no-op draws every frame, + * invariant to where the camera looks (this is why sky/occlusion culling produced + * no Mac speed-up). Bounding it to the last-known real count plus headroom lets the + * culling actually reduce Mac draw cost. The full drawCallBuffer is still zeroed per + * frame, so every slot within the ceiling reads either a real command or a no-op — + * a transient under-estimate during fast camera motion only drops a few LOD draws + * for a frame or two, never reads stale geometry. + */ + private int fixedCountBudget(int lastKnownCount, int headroom, int cap) { + if (this.ctx.vk().hasDrawIndirectCount) return cap; + int budget = (int) (lastKnownCount * 1.5f) + headroom; + return Math.min(cap, Math.max(0, budget)); + } + + public void renderOpaque(VkViewport viewport, boolean clearTargets) { + //Build pipelines BEFORE the section-count guard: within a frame geometry is + // uploaded after this call (nodeManager.tick/buildDrawCalls), so renderTemporal/ + // renderTranslucent can see sectionCount>0 later this same frame. If we skipped + // ensure here when the count is momentarily 0, those calls would bind a null + // pipeline. ensureTerrainPipelines is idempotent (no-op once built for the format). + this.ensureTerrainPipelines(viewport); + if (this.geometry.getSectionCount() == 0) return; + this.uploadUniform(viewport); + int cap = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbOpaqueDraws, 1024, cap); + this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, 0, 4 * 3, maxDraw, clearTargets); + } + + public void renderTemporal(VkViewport viewport) { + this.ensureTerrainPipelines(viewport); + if (this.geometry.getSectionCount() == 0) return; + int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbTemporalDraws, 256, cap); + this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, TEMPORAL_OFFSET * 5L * 4, 4 * 5, maxDraw, false); + } + + /** Translucents draw onto the SSAO output (mirrors the GL fbSSAO target). */ + public void renderTranslucent(VkViewport viewport) { + this.ensureTerrainPipelines(viewport); + if (this.geometry.getSectionCount() == 0) return; + int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbTranslucentDraws, 256, cap); + this.renderTerrain(viewport, viewport.colourSSAO.view, this.terrainTranslucent, TRANSLUCENT_OFFSET * 5L * 4, 4 * 4, maxDraw, false); + } + + private void renderTerrain(VkViewport viewport, long colorView, VkShaderPipeline pipeline, + long indirectOffset, long drawCountOffset, int maxDrawCount, boolean clear) { + var cmd = this.ctx.cmd(); + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, + VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDEX_READ_BIT); + + this.beginRendering(cmd, viewport, colorView, !clear);//LOAD unless first pass (which cleared via compositor setup) + pipeline.bind(cmd); + VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); + long lightmapView = VkFrameHost.lightmapView(); + try (var b = pipeline.binder()) { + b.ubo(0, this.uniform) + .ssbo(1, this.geometry.geometryBuffer()) + .ssbo(3, this.modelStore.modelBuffer) + .ssbo(4, this.modelStore.modelColourBuffer) + .ssbo(5, viewport.positionScratchBuffer) + .sampler(8, this.modelStore.atlas.view, this.modelStore.atlasSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .sampler(9, lightmapView, this.lightmapSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .sampler(10, viewport.depthBoundSampleView, this.depthBoundSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .push(cmd); + } + vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); + if (this.ctx.vk().hasDrawIndirectCount) { + //Tight-count path: the GPU reads the actual draw count from the count + // buffer each frame. Requires the drawIndirectCount Vulkan 1.2 feature + // to be enabled on the device — desktop Vulkan (NVIDIA/AMD/Intel) enables + // it; MoltenVK does not (see the else branch). + vkCmdDrawIndexedIndirectCount(cmd, + viewport.drawCallBuffer.buffer, indirectOffset, + viewport.drawCountCallBuffer.buffer, drawCountOffset, + maxDrawCount, 5 * 4); + } else { + //MoltenVK/macOS fixed-count fallback: drawIndirectCount is not enabled + // on MC's adopted Vulkan device (MC's DeviceFeatures record does not + // even track it), and calling the function without the feature enabled + // is invalid usage that MoltenVK degenerates. The drawCallBuffer slice + // for this pass is zeroed per frame in buildDrawCalls (gated to this + // fallback path) so trailing slots past the actual command count read + // instanceCount=0 (no-op draws); a fixed-count multi-draw over the + // clamped maxDrawCount is therefore correct, just less tight. + vkCmdDrawIndexedIndirect(cmd, viewport.drawCallBuffer.buffer, indirectOffset, maxDrawCount, 5 * 4); + } + vkCmdEndRenderingKHR(cmd); + } + + /** + * Begin dynamic rendering over the offscreen targets (always LOAD; clears + * happen in the depth-setup pass). {@code colorView} selects the colour + * attachment (main colour vs SSAO output); 0 = depth-only. + */ + private void beginRendering(VkCommandBuffer cmd, VkViewport viewport, + long colorView, boolean load) { + try (MemoryStack stack = stackPush()) { + var depthAttach = VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(viewport.depthStencil.view) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + var stencilAttach = VkRenderingAttachmentInfoKHR.calloc(stack).sType$Default() + .imageView(viewport.depthStencil.view) + .imageLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + var info = VkRenderingInfoKHR.calloc(stack).sType$Default() + .renderArea(VkRect2D.calloc(stack).extent(e -> e.width(viewport.width).height(viewport.height))) + .layerCount(1) + .pDepthAttachment(depthAttach) + .pStencilAttachment(stencilAttach); + if (colorView != 0L) { + var colorAttach = VkRenderingAttachmentInfoKHR.calloc(1, stack).sType$Default() + .imageView(colorView) + .imageLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) + .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE); + info.pColorAttachments(colorAttach); + } + vkCmdBeginRenderingKHR(cmd, info); + } + } + + public void free() { + this.prep.free(); + this.cmdGen.free(); + this.prefixSum.free(); + this.translucentGen.free(); + this.cullRaster.free(); + if (this.terrainOpaque != null) { + this.terrainOpaque.free(); + this.terrainTranslucent.free(); + } + //depthBoundSampler/lightmapSampler come from VkImage2D.createSampler's + // device-lifetime cache (shared handles); never destroy them per-object + // (multi-free vkDestroySampler -> SIGSEGV on world unload). + this.uniform.free(); + this.distanceCountBuffer.free(); + this.indexBuffer.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java new file mode 100644 index 000000000..047b4383a --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java @@ -0,0 +1,287 @@ +package me.cortex.voxy.client.core.vk.render; + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import me.cortex.voxy.client.config.VoxyConfig; +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.Viewport; +import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; +import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager; +import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkDownloadStream; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkShaderPipeline; +import me.cortex.voxy.client.core.vk.VkShaderSource; +import me.cortex.voxy.client.core.vk.VkUploadStream; +import me.cortex.voxy.common.CmpLog; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.MemoryBuffer; +import me.cortex.voxy.common.world.WorldEngine; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.util.List; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK port of HierarchicalOcclusionTraverser: the same iterative BFS over +// the LOD node tree (flip-flop GPU queues, one indirect dispatch per LOD layer), +// HiZ-tested, emitting the render list + node requests. CPU-side TLN bookkeeping +// and request readback are identical to the GL implementation. +public class VkTraversal { + public static final int MAX_REQUEST_QUEUE_SIZE = HierarchicalOcclusionTraverser.MAX_REQUEST_QUEUE_SIZE; + public static final int MAX_QUEUE_SIZE = HierarchicalOcclusionTraverser.MAX_QUEUE_SIZE; + private static final int MAX_ITERATIONS = WorldEngine.MAX_LOD_LAYER + 1; + //Traversal workgroup size: 64 threads (2 subgroups on 32-wide devices, 1 on + // 64-wide AMD) improves HiZ texture cache locality and amortises dispatch + // overhead over the 32-thread default. Falls back to 32 (one subgroup) when + // subgroup support is unavailable. The shader's LOCAL_SIZE is driven by this + // define, and queue.glsl's push math is LOCAL_SIZE-parametric, so no shader + // change is needed. + private final int localWorkSizeBits; + + private static int resolveLocalWorkSizeBits(VkFrameCtx ctx) { + //subgroupSize >= 32 on every conformant Vulkan 1.1+ device; bump to 64 + // (2 subgroups) when subgroupSize >= 32. Conservative 32 fallback otherwise. + return ctx.vk().subgroupArithmetic && ctx.vk().subgroupSize >= 32 ? 6 : 5; + } + + //Bindings (single Vulkan namespace: HiZ sampler at 0, UBO at 1, SSBOs from 2) + private static final int HIZ_BINDING = 0; + private static final int SCENE_UNIFORM_BINDING = 1; + private static final int REQUEST_QUEUE_BINDING = 2; + private static final int RENDER_QUEUE_BINDING = 3; + private static final int NODE_DATA_BINDING = 4; + private static final int NODE_QUEUE_META_BINDING = 5; + private static final int NODE_QUEUE_SOURCE_BINDING = 6; + private static final int NODE_QUEUE_SINK_BINDING = 7; + private static final int RENDER_TRACKER_BINDING = 8; + + private final VkFrameCtx ctx; + private final VkUploadStream uploadStream; + private final VkDownloadStream downloadStream; + private final AsyncNodeManager nodeManager; + private final VkNodeCleaner nodeCleaner; + private final RenderGenerationService meshGen; + + private final VkBuffer requestBuffer; + private final VkBuffer nodeBuffer; + private final VkBuffer uniformBuffer; + private final VkBuffer topNodeIds; + private final VkBuffer queueMetaBuffer; + private final VkBuffer scratchQueueA; + private final VkBuffer scratchQueueB; + + private final VkShaderPipeline traversal; + + private int topNodeCount; + private final Int2IntOpenHashMap topNode2idxMapping = new Int2IntOpenHashMap(); + private final int[] idx2topNodeMapping = new int[MAX_QUEUE_SIZE]; + + public VkTraversal(VkFrameCtx ctx, VkUploadStream up, VkDownloadStream down, RenderProperties properties, + AsyncNodeManager nodeManager, VkNodeCleaner nodeCleaner, RenderGenerationService meshGen) { + this.ctx = ctx; + this.uploadStream = up; + this.downloadStream = down; + this.nodeManager = nodeManager; + this.nodeCleaner = nodeCleaner; + this.meshGen = meshGen; + this.localWorkSizeBits = resolveLocalWorkSizeBits(ctx); + + this.requestBuffer = new VkBuffer(ctx, MAX_REQUEST_QUEUE_SIZE * 8L + 8).zero(); + this.nodeBuffer = new VkBuffer(ctx, nodeManager.maxNodeCount * 16L).fill(-1); + this.uniformBuffer = new VkBuffer(ctx, 1024).zero(); + this.topNodeIds = new VkBuffer(ctx, MAX_QUEUE_SIZE * 4L).zero(); + this.queueMetaBuffer = new VkBuffer(ctx, 4L * 4 * MAX_ITERATIONS).zero(); + this.scratchQueueA = new VkBuffer(ctx, MAX_QUEUE_SIZE * 4L).zero(); + this.scratchQueueB = new VkBuffer(ctx, MAX_QUEUE_SIZE * 4L).zero(); + ctx.flushImmediate(); + + this.topNode2idxMapping.defaultReturnValue(-1); + this.nodeManager.setTLNAddRemoveCallbacks(this::addTLN, this::remTLN); + + this.traversal = new VkShaderPipeline(ctx, "traversal_dev.comp", + VkShaderSource.load("voxy:lod/hierarchical/traversal_dev.comp", VkShaderSource.defs() + .props(properties) + .def("MAX_ITERATIONS", MAX_ITERATIONS) + .def("LOCAL_SIZE_BITS", this.localWorkSizeBits) + .def("MAX_REQUEST_QUEUE_SIZE", MAX_REQUEST_QUEUE_SIZE) + .def("HIZ_BINDING", HIZ_BINDING) + .def("SCENE_UNIFORM_BINDING", SCENE_UNIFORM_BINDING) + .def("REQUEST_QUEUE_BINDING", REQUEST_QUEUE_BINDING) + .def("RENDER_QUEUE_BINDING", RENDER_QUEUE_BINDING) + .def("NODE_DATA_BINDING", NODE_DATA_BINDING) + .def("NODE_QUEUE_INDEX_BINDING", 0)//unused on VK (push constant) + .def("NODE_QUEUE_META_BINDING", NODE_QUEUE_META_BINDING) + .def("NODE_QUEUE_SOURCE_BINDING", NODE_QUEUE_SOURCE_BINDING) + .def("NODE_QUEUE_SINK_BINDING", NODE_QUEUE_SINK_BINDING) + .def("RENDER_TRACKER_BINDING", RENDER_TRACKER_BINDING) + .build()), + 4, + List.of(VkShaderPipeline.sampler(HIZ_BINDING), VkShaderPipeline.ubo(SCENE_UNIFORM_BINDING), + VkShaderPipeline.ssbo(REQUEST_QUEUE_BINDING), VkShaderPipeline.ssbo(RENDER_QUEUE_BINDING), + VkShaderPipeline.ssbo(NODE_DATA_BINDING), VkShaderPipeline.ssbo(NODE_QUEUE_META_BINDING), + VkShaderPipeline.ssbo(NODE_QUEUE_SOURCE_BINDING), VkShaderPipeline.ssbo(NODE_QUEUE_SINK_BINDING), + VkShaderPipeline.ssbo(RENDER_TRACKER_BINDING))); + } + + private void addTLN(int id) { + int aid = this.topNodeCount++; + if (this.topNodeCount > this.topNodeIds.size() / 4) { + throw new IllegalStateException("Top level node count greater than capacity"); + } + vkCmdFillBuffer(this.ctx.cmd(), this.topNodeIds.buffer, aid * 4L, 4, id); + if (this.topNode2idxMapping.put(id, aid) != -1) { + throw new IllegalStateException(); + } + this.idx2topNodeMapping[aid] = id; + } + + private void remTLN(int id) { + int idx = this.topNode2idxMapping.remove(id); + this.topNodeCount--; + if (idx == -1) throw new IllegalStateException(); + if (idx == this.topNodeCount) return; + int endTLNId = this.idx2topNodeMapping[this.topNodeCount]; + this.idx2topNodeMapping[idx] = endTLNId; + if (this.topNode2idxMapping.put(endTLNId, idx) == -1) throw new IllegalStateException(); + vkCmdFillBuffer(this.ctx.cmd(), this.topNodeIds.buffer, idx * 4L, 4, endTLNId); + } + + private void uploadUniform(Viewport viewport, VkViewport vkViewport) { + long ptr = this.uploadStream.upload(this.uniformBuffer, 0, 1024); + viewport.MVP.getToAddress(ptr); ptr += 4 * 4 * 4; + viewport.section.getToAddress(ptr); ptr += 4 * 3; + MemoryUtil.memPutInt(ptr, vkViewport.hiZ.getPackedLevels()); ptr += 4; + viewport.innerTranslation.getToAddress(ptr); ptr += 4 * 3; + + final float screenspaceAreaDecreasingSize = VoxyConfig.CONFIG.subDivisionSize * VoxyConfig.CONFIG.subDivisionSize; + MemoryUtil.memPutFloat(ptr, screenspaceAreaDecreasingSize / (viewport.width * viewport.height)); ptr += 4; + + for (int i = 0; i < 6; i++) { + viewport.frustumPlanes[i].getToAddress(ptr); ptr += 4 * 4; + } + + MemoryUtil.memPutInt(ptr, (int) (vkViewport.indirectLookupBuffer.size() / 4 - 1)); ptr += 4; + MemoryUtil.memPutInt(ptr, this.nodeCleaner.visibilityId); ptr += 4; + { + final double TARGET_COUNT = 4000; + double iFillness = Math.max(0, (TARGET_COUNT - this.meshGen.getTaskCount()) / TARGET_COUNT); + iFillness = Math.pow(iFillness, 2); + final int requestSize = (int) Math.ceil(iFillness * MAX_REQUEST_QUEUE_SIZE); + MemoryUtil.memPutInt(ptr, Math.max(0, Math.min(MAX_REQUEST_QUEUE_SIZE, requestSize))); ptr += 4; + } + MemoryUtil.memPutFloat(ptr, (float) Math.pow(VoxyConfig.CONFIG.sectionRenderDistance * 16 * 32, 2)); + } + + public void doTraversal(VkViewport viewport) { + this.uploadUniform(viewport, viewport); + var cmd = this.ctx.cmd(); + + //Clear the render output counter + vkCmdFillBuffer(cmd, viewport.indirectLookupBuffer.buffer, 0, 4, 0); + + //Prime the per-iteration queue metadata + int firstDispatchSize = (this.topNodeCount + (1 << this.localWorkSizeBits) - 1) >> this.localWorkSizeBits; + { + long ptr = this.uploadStream.upload(this.queueMetaBuffer, 0, 16L * MAX_ITERATIONS); + MemoryUtil.memPutInt(ptr, firstDispatchSize); + MemoryUtil.memPutInt(ptr + 4, 1); + MemoryUtil.memPutInt(ptr + 8, 1); + MemoryUtil.memPutInt(ptr + 12, this.topNodeCount); + for (int i = 1; i < MAX_ITERATIONS; i++) { + MemoryUtil.memPutInt(ptr + (i * 16L), 0); + MemoryUtil.memPutInt(ptr + (i * 16L) + 4, 1); + MemoryUtil.memPutInt(ptr + (i * 16L) + 8, 1); + MemoryUtil.memPutInt(ptr + (i * 16L) + 12, 0); + } + this.uploadStream.commit(); + } + //The upload commit just issued a scoped TRANSFER -> COMPUTE/VERTEX/etc + // barrier; this one orders the queueMetaBuffer upload + the + // indirectLookupBuffer vkCmdFillBuffer (TRANSFER writes) ahead of the + // traversal compute reads. Scoped to TRANSFER -> COMPUTE. + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + + this.traversal.bind(cmd); + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + var source = iter == 0 ? this.topNodeIds : ((iter & 1) == 0 ? this.scratchQueueA : this.scratchQueueB); + var sink = (iter & 1) == 0 ? this.scratchQueueB : this.scratchQueueA; + try (var b = this.traversal.binder()) { + b.sampler(HIZ_BINDING, viewport.hiZ.pyramidView(), viewport.hiZ.sampler, VK_IMAGE_LAYOUT_GENERAL) + .ubo(SCENE_UNIFORM_BINDING, this.uniformBuffer) + .ssbo(REQUEST_QUEUE_BINDING, this.requestBuffer) + .ssbo(RENDER_QUEUE_BINDING, viewport.indirectLookupBuffer) + .ssbo(NODE_DATA_BINDING, this.nodeBuffer) + .ssbo(NODE_QUEUE_META_BINDING, this.queueMetaBuffer) + .ssbo(NODE_QUEUE_SOURCE_BINDING, source) + .ssbo(NODE_QUEUE_SINK_BINDING, sink) + .ssbo(RENDER_TRACKER_BINDING, this.nodeCleaner.visibilityBuffer) + .push(cmd); + } + try (MemoryStack stack = stackPush()) { + this.traversal.pushConstants(cmd, stack.malloc(4).putInt(0, iter)); + } + if (iter == 0) { + if (firstDispatchSize != 0) { + vkCmdDispatch(cmd, firstDispatchSize, 1, 1); + } + } else { + vkCmdDispatchIndirect(cmd, this.queueMetaBuffer.buffer, iter * 16L); + } + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + } + //Traversal compute -> download copy (transfer) + requestBuffer fill. + this.ctx.computeToTransferBarrier(); + + //Download + reset the request queue + this.downloadStream.download(this.requestBuffer, this::forwardDownloadResult); + vkCmdFillBuffer(this.ctx.cmd(), this.requestBuffer.buffer, 0, 4, 0); + //The download copy read the requestBuffer (TRANSFER read); the fill + // resets it (TRANSFER write). The next reader is next frame's traversal + // compute, so this barrier only needs to order TRANSFER -> TRANSFER + + // COMPUTE. Removed the previous fullBarrier (ALL_COMMANDS -> ALL_COMMANDS) + // which was a full pipeline stall. + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT); + } + + private void forwardDownloadResult(long ptr, long size) { + int count = MemoryUtil.memGetInt(ptr); + ptr += 8;//skip the (empty) second value + if (count < 0 || count > 50000) { + Logger.error(new IllegalStateException("Count unexpected extreme value: " + count + " things may get weird")); + return; + } + if (count > (this.requestBuffer.size() >> 3) - 1) { + count = (int) ((this.requestBuffer.size() >> 3) - 1); + } + CmpLog.rec("traversal", "requestCount", count); + CmpLog.rec("traversal", "topNodeCount", this.topNodeCount); + if (count != 0) { + var buffer = new MemoryBuffer(count * 8L + 8).cpyFrom(ptr - 8); + MemoryUtil.memPutInt(buffer.address, count); + this.nodeManager.submitRequestBatch(buffer); + } + } + + public VkBuffer getNodeBuffer() { + return this.nodeBuffer; + } + + public void free() { + this.traversal.free(); + this.requestBuffer.free(); + this.nodeBuffer.free(); + this.uniformBuffer.free(); + this.queueMetaBuffer.free(); + this.topNodeIds.free(); + this.scratchQueueA.free(); + this.scratchQueueB.free(); + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkViewport.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkViewport.java new file mode 100644 index 000000000..655c14ae7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkViewport.java @@ -0,0 +1,116 @@ +package me.cortex.voxy.client.core.vk.render; + +import me.cortex.voxy.client.core.RenderProperties; +import me.cortex.voxy.client.core.rendering.IRenderList; +import me.cortex.voxy.client.core.rendering.Viewport; +import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser; +import me.cortex.voxy.client.core.vk.VkBuffer; +import me.cortex.voxy.client.core.vk.VkFrameCtx; +import me.cortex.voxy.client.core.vk.VkImage2D; + +import static org.lwjgl.vulkan.VK10.*; + +//Pure-VK viewport: the MDICViewport buffer set as VkBuffers, plus the +// offscreen render targets (colour + D32S8 depth-stencil), the depth-bound +// image (vanilla-coverage optimisation), and the HiZ pyramid. +public class VkViewport extends Viewport { + public static final int OPAQUE_DRAW_COUNT = 400_000; + public static final int TRANSLUCENT_DRAW_COUNT = 100_000; + public static final int TEMPORAL_DRAW_COUNT = 100_000; + + private final VkFrameCtx ctx; + + public final VkBuffer drawCountCallBuffer; + public final VkBuffer drawCallBuffer; + public final VkBuffer positionScratchBuffer; + public final VkBuffer indirectLookupBuffer; + public final VkBuffer visibilityBuffer; + + //Offscreen targets, lazily (re)created on resize + public VkImage2D colour; + public VkImage2D depthStencil; + public long depthSampleView;//DEPTH-aspect view of depthStencil for sampling + public VkImage2D depthBound; + public long depthBoundSampleView; + //SSAO output colour: the compute pass reads `colour` and writes the AO-modulated + // result (alpha sanitized to 1/0) here; translucents then draw onto it and the + // compositor samples it — mirroring the GL colourTex/colourSSAOTex pair. + public VkImage2D colourSSAO; + public final VkHiZ hiZ; + + public VkViewport(VkFrameCtx ctx, RenderProperties properties, int maxSectionCount) { + super(properties); + this.ctx = ctx; + this.drawCountCallBuffer = new VkBuffer(ctx, 1024).zero(); + this.drawCallBuffer = new VkBuffer(ctx, 5L * 4 * (OPAQUE_DRAW_COUNT + TRANSLUCENT_DRAW_COUNT + TEMPORAL_DRAW_COUNT)).zero(); + this.positionScratchBuffer = new VkBuffer(ctx, 8L * 400000).zero(); + this.indirectLookupBuffer = new VkBuffer(ctx, HierarchicalOcclusionTraverser.MAX_QUEUE_SIZE * 4L + 4).zero(); + this.visibilityBuffer = new VkBuffer(ctx, maxSectionCount * 4L).zero(); + this.hiZ = new VkHiZ(ctx, properties); + ctx.flushImmediate(); + } + + @Override + protected boolean useGlViewportHelpers() { + return false; + } + + /** (Re)creates the offscreen targets on size change; true if recreated. */ + public boolean ensureTargets() { + if (this.width <= 0 || this.height <= 0) return false; + if (this.colour != null && this.colour.width == this.width && this.colour.height == this.height) return false; + if (this.colour != null) { + this.colour.free(); + this.colourSSAO.free(); + this.depthStencil.free(); + this.depthBound.free(); + } + this.colour = new VkImage2D(this.ctx, this.width, this.height, 1, + VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, false); + this.colourSSAO = new VkImage2D(this.ctx, this.width, this.height, 1, + VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, false); + this.depthStencil = new VkImage2D(this.ctx, this.width, this.height, 1, + VK_FORMAT_D32_SFLOAT_S8_UINT, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, false, + //VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT + view-format list lets the + // depth-only aspect view alias the image directly. Without this, + // MoltenVK may have to synthesise a separate staging texture for + // depth-only sampling of a packed D32S8 image. The list must + // contain the original format plus the depth-only format. + new int[]{VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D32_SFLOAT}); + this.depthSampleView = this.depthStencil.createAspectView(VK_IMAGE_ASPECT_DEPTH_BIT); + this.depthBound = new VkImage2D(this.ctx, this.width, this.height, 1, + VK_FORMAT_D32_SFLOAT, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, false); + this.depthBoundSampleView = this.depthBound.view; + return true; + } + + @Override + protected void delete0() { + super.delete0(); + if (this.colour != null) { + this.colour.free(); + this.colourSSAO.free(); + this.depthStencil.free(); + this.depthBound.free(); + } + this.hiZ.free(); + this.visibilityBuffer.free(); + this.indirectLookupBuffer.free(); + this.drawCountCallBuffer.free(); + this.drawCallBuffer.free(); + this.positionScratchBuffer.free(); + } + + @Override + public IRenderList getRenderList() { + return this.indirectLookupBuffer; + } +} diff --git a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinDefaultChunkRenderer.java b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinDefaultChunkRenderer.java index 812c62494..8945f2022 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinDefaultChunkRenderer.java +++ b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinDefaultChunkRenderer.java @@ -60,6 +60,15 @@ private void doRender(ChunkRenderMatrices matrices, TerrainRenderPass renderPass if (renderPass == DefaultTerrainRenderPasses.CUTOUT) { var renderer = IVoxyRenderSystemHolder.getNullable(); if (renderer != null) { + if (renderer.isVulkanBackend()) { + //Vulkan backend: Voxy renders via its own frame hook + // (MixinSodiumOpaqueVkFrame). Sodium 0.9.1 also renders through MC's + // Vulkan device, so target.get*TextureView() returns a + // VulkanGpuTextureView with no glId() — the GL-interop path below + // must not run (the cast would ClassCastException before + // renderOpaque's own VK early-return is reached). + return; + } Viewport viewport = null; var target = renderPass.getTarget(); if (IrisUtil.irisShaderPackEnabled()) { diff --git a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinFallbackVisibleChunkCollector.java b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinFallbackVisibleChunkCollector.java index b88fb81fb..23a76811a 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinFallbackVisibleChunkCollector.java +++ b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinFallbackVisibleChunkCollector.java @@ -22,7 +22,7 @@ public class MixinFallbackVisibleChunkCollector { private static void voxy$injectVisibleStreamReset(CallbackInfo ci) { var vrs = IVoxyRenderSystemHolder.getNullable(); if (vrs != null) { - vrs.visbleSectionStream.reset(); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.reset(); } }*/ @@ -32,7 +32,7 @@ public class MixinFallbackVisibleChunkCollector { var section = instance.getCurrent(x,y,z); VoxyRenderSystem vrs; if (!IrisUtil.irisShadowActive() && (vrs = IVoxyRenderSystemHolder.getNullable()) != null && voxy$shouldUseForChunkBound(section, LocalSectionIndex.pack(x, y, z))) { - vrs.visbleSectionStream.put(SectionPos.asLong(x,y,z)); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.put(SectionPos.asLong(x,y,z)); } return section; } diff --git a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java index 5f606cbb1..84ac77d4e 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java +++ b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java @@ -44,7 +44,7 @@ public class MixinRenderSectionManager { private void voxy$injectReset1(Viewport viewport, FogParameters fogParameters, CallbackInfo ci) { var vrs = IVoxyRenderSystemHolder.getNullable(); if (vrs != null && !IrisUtil.irisShadowActive()) { - vrs.visbleSectionStream.reset(); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.reset(); } } @@ -52,7 +52,7 @@ public class MixinRenderSectionManager { private void voxy$injectReset2(Viewport viewport, FogParameters fogParameters, CallbackInfo ci) { var vrs = IVoxyRenderSystemHolder.getNullable(); if (vrs != null && !IrisUtil.irisShadowActive()) { - vrs.visbleSectionStream.reset(); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.reset(); } } diff --git a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinVisibleChunkCollector.java b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinVisibleChunkCollector.java index 675cc3444..589ee5bab 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinVisibleChunkCollector.java +++ b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinVisibleChunkCollector.java @@ -23,7 +23,7 @@ public class MixinVisibleChunkCollector { private static void voxy$injectVisibleStreamReset(CallbackInfo ci) { var vrs = IVoxyRenderSystemHolder.getNullable(); if (vrs != null) { - vrs.visbleSectionStream.reset(); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.reset(); } }*/ @@ -33,7 +33,7 @@ public class MixinVisibleChunkCollector { var region = instance.getForChunk(x,y,z); VoxyRenderSystem vrs; if (!IrisUtil.irisShadowActive() && (vrs = IVoxyRenderSystemHolder.getNullable()) != null && voxy$shouldUseForChunkBound(region, LocalSectionIndex.pack(x, y, z))) { - vrs.visbleSectionStream.put(SectionPos.asLong(x,y,z)); + if (vrs.visbleSectionStream != null) vrs.visbleSectionStream.put(SectionPos.asLong(x,y,z)); } return region; } diff --git a/src/main/java/me/cortex/voxy/client/mixin/vk/AccessorVulkanCommandEncoder.java b/src/main/java/me/cortex/voxy/client/mixin/vk/AccessorVulkanCommandEncoder.java new file mode 100644 index 000000000..4e884dc20 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/mixin/vk/AccessorVulkanCommandEncoder.java @@ -0,0 +1,16 @@ +package me.cortex.voxy.client.mixin.vk; + +import com.mojang.blaze3d.vulkan.VulkanCommandEncoder; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +//Exposes the command buffer MC's Vulkan encoder is currently recording into, so +// Voxy (in pure-VK host mode) can record its LOD draws into MC's frame instead +// of submitting a separate one. Null while MC has no open command buffer — +// callers must treat that as "not at a valid injection point yet". +@Mixin(VulkanCommandEncoder.class) +public interface AccessorVulkanCommandEncoder { + @Accessor("currentCommandBuffer") + VkCommandBuffer voxy$currentCommandBuffer(); +} diff --git a/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java new file mode 100644 index 000000000..7277b54f7 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java @@ -0,0 +1,51 @@ +package me.cortex.voxy.client.mixin.vk; + +import com.mojang.blaze3d.textures.GpuSampler; +import me.cortex.voxy.client.core.IVoxyRenderSystemHolder; +import me.cortex.voxy.client.core.vk.MinecraftVkHost; +import me.cortex.voxy.client.core.vk.MinecraftVkHostAdapter; +import me.cortex.voxy.common.Logger; +import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer; +import net.caffeinemc.mods.sodium.client.render.chunk.ChunkRenderMatrices; +import net.minecraft.client.renderer.chunk.ChunkSectionLayerGroup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +//The pure-Vulkan render entry point. +// +//NOTE: the earlier hook on the TAIL of vanilla ChunkSectionsToRender.renderGroup +// NEVER fired, because Sodium 0.9.1 — which renders terrain on MC's Vulkan +// device too — injects at the HEAD of renderGroup and calls +// CallbackInfo.cancel(), so the vanilla RETURN (and every @At("TAIL") injector) +// is bypassed. Sodium instead draws through SodiumWorldRenderer#drawChunkLayer. +// We therefore trigger Voxy's frame at the TAIL of drawChunkLayer for the +// OPAQUE group: Sodium's opaque terrain has just been drawn, its render pass +// is closed, the frame command buffer is recording, and MC's depth buffer +// holds vanilla terrain — exactly the state Voxy's VK frame needs. +@Mixin(value = SodiumWorldRenderer.class, remap = false) +public class MixinSodiumOpaqueVkFrame { + private static boolean voxy$loggedFirstFrame = false; + + @Inject(method = "drawChunkLayer", at = @At("TAIL"), remap = false) + private void voxy$renderVkFrame(ChunkSectionLayerGroup group, ChunkRenderMatrices matrices, + double x, double y, double z, GpuSampler sampler, CallbackInfo ci) { + if (group != ChunkSectionLayerGroup.OPAQUE) return; + if (!(MinecraftVkHost.get() instanceof MinecraftVkHostAdapter adapter)) return; + + var renderer = IVoxyRenderSystemHolder.getNullable(); + if (renderer == null || renderer.vkCore == null) return; + + if (!voxy$loggedFirstFrame) { + voxy$loggedFirstFrame = true; + Logger.info("Voxy VK frame hook fired (SodiumWorldRenderer.drawChunkLayer OPAQUE TAIL) - rendering LODs"); + } + try { + renderer.vkCore.renderFrame(group.outputTarget(), adapter, matrices, x, y, z); + } catch (Throwable t) { + //Never take down MC's frame; log loudly instead + Logger.error("Voxy VK frame failed", t); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/mixin/vk/MixinVulkanDevice.java b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinVulkanDevice.java new file mode 100644 index 000000000..0da09d06e --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinVulkanDevice.java @@ -0,0 +1,39 @@ +package me.cortex.voxy.client.mixin.vk; + +import com.mojang.blaze3d.vulkan.VulkanDevice; +import me.cortex.voxy.client.core.vk.MinecraftVkHost; +import me.cortex.voxy.client.core.vk.MinecraftVkHostAdapter; +import me.cortex.voxy.common.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +//The Blaze3D-VK adapter. When MC 26.2 initialises its own Vulkan device +// (Graphics API = Vulkan), this registers an IVkHost backed by that live +// device so Voxy adopts it (no second VkDevice) and records into MC's frame. +// Cleared when MC tears the device down (shutdown), after which Voxy has no +// host and is inactive until a device is registered again. +// +//If MC is on OpenGL this class is simply never instantiated, so the host stays +// unregistered and Voxy uses its OpenGL backend — no runtime cost on the GL path. +@Mixin(VulkanDevice.class) +public class MixinVulkanDevice { + @Inject(method = "", at = @At("TAIL")) + private void voxy$registerHost(CallbackInfo ci) { + try { + MinecraftVkHost.register(new MinecraftVkHostAdapter((VulkanDevice) (Object) this)); + Logger.info("Voxy: adopted Minecraft's Vulkan device (pure-VK host mode available)"); + } catch (Throwable t) { + //Never let Voxy's adapter break MC's device creation. With no host + // registered Voxy stays inactive. + Logger.warn("Voxy: failed to register Vulkan host adapter, Voxy will be inactive: " + t); + MinecraftVkHost.clear(); + } + } + + @Inject(method = "close", at = @At("HEAD")) + private void voxy$clearHost(CallbackInfo ci) { + MinecraftVkHost.clear(); + } +} diff --git a/src/main/java/me/cortex/voxy/common/CmpLog.java b/src/main/java/me/cortex/voxy/common/CmpLog.java new file mode 100644 index 000000000..d9ebd046b --- /dev/null +++ b/src/main/java/me/cortex/voxy/common/CmpLog.java @@ -0,0 +1,80 @@ +package me.cortex.voxy.common; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +//Opt-in structured logging for GL<-->VK A/B parity comparison. +// +//Enable with -Dvoxy.cmplog=. Both the OpenGL and Vulkan render paths then +// emit the SAME semantic per-frame quantities (section/geometry counts, traversal +// request counts, ...) as tab-separated records. For a stationary camera at a +// fixed viewpoint these values converge to identical integers when the VK +// translation is faithful, so ab_compare.py compare-logs can flag any divergence +// exactly, without the rounding noise that muddies a pixel diff. +// +//When the property is unset every method is a cheap no-op (one static-field +// read), so this stays compiled into normal builds at negligible cost. +// +//Record format (one per line): VOXYCMP\t\t\t\t +public final class CmpLog { + private static final String PATH = System.getProperty("voxy.cmplog"); + /** True when {@code -Dvoxy.cmplog=} was supplied. */ + public static final boolean ENABLED = PATH != null && !PATH.isEmpty(); + + /** Set by each render core so the log header records which backend produced it. */ + public static volatile String backend = "unknown"; + + private static BufferedWriter writer; + private static boolean broken = false; + private static int frame = 0; + + private CmpLog() {} + + private static BufferedWriter writer() { + if (writer == null && !broken) { + try { + Path p = Path.of(PATH).toAbsolutePath(); + if (p.getParent() != null) { + Files.createDirectories(p.getParent()); + } + writer = Files.newBufferedWriter(p, StandardCharsets.UTF_8); + writer.write("#VOXYCMP backend=" + backend + "\n"); + writer.flush(); + Logger.info("CmpLog: writing A/B comparison log to " + PATH + + " (backend=" + backend + ")"); + } catch (IOException e) { + broken = true; + Logger.error("CmpLog: failed to open " + PATH, e); + } + } + return writer; + } + + /** Advance to the next frame and flush; call once per rendered frame. */ + public static void nextFrame() { + if (!ENABLED) return; + synchronized (CmpLog.class) { + frame++; + if (writer != null) { + try { writer.flush(); } catch (IOException e) { broken = true; } + } + } + } + + /** Record a semantic quantity for the current frame. */ + public static void rec(String phase, String key, long value) { + if (!ENABLED) return; + synchronized (CmpLog.class) { + var w = writer(); + if (w == null) return; + try { + w.write("VOXYCMP\t" + frame + "\t" + phase + "\t" + key + "\t" + value + "\n"); + } catch (IOException e) { + broken = true; + } + } + } +} diff --git a/src/main/resources/assets/voxy/shaders/chunkoutline/outline.vsh b/src/main/resources/assets/voxy/shaders/chunkoutline/outline.vsh index b48cd6d5e..12775eb51 100644 --- a/src/main/resources/assets/voxy/shaders/chunkoutline/outline.vsh +++ b/src/main/resources/assets/voxy/shaders/chunkoutline/outline.vsh @@ -27,8 +27,20 @@ bool shouldRender(ivec3 icorner) { vec2 getTAA(); #endif +#ifdef VOXY_VULKAN +//VK draws one 36-index box per instance (no 32-batching, no baseInstance games): +// gl_InstanceIndex is the chunk id directly and the vertex index is the raw 0..7 corner. +#define VERT_ID gl_VertexIndex +#else +#define VERT_ID gl_VertexID +#endif + void main() { - uint id = (gl_InstanceID<<5)+gl_BaseInstance+(gl_VertexID>>3); +#ifdef VOXY_VULKAN + uint id = uint(gl_InstanceIndex); +#else + uint id = (gl_InstanceID<<5)+gl_BaseInstance+(VERT_ID>>3); +#endif ivec3 origin = unpackPos(chunkPos[id])*16; origin -= cameraBlockPos.xyz; @@ -38,7 +50,7 @@ void main() { return; } - ivec3 cubeCornerI = ivec3(gl_VertexID&1, (gl_VertexID>>2)&1, (gl_VertexID>>1)&1)*16; + ivec3 cubeCornerI = ivec3(VERT_ID&1, (VERT_ID>>2)&1, (VERT_ID>>1)&1)*16; //Expand the y height to be big (will be +- 8192) //TODO: make it W.R.T world height and offsets //cubeCornerI.y = cubeCornerI.y*1024-512; diff --git a/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_reduce.comp b/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_reduce.comp new file mode 100644 index 000000000..d6d0f3566 --- /dev/null +++ b/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_reduce.comp @@ -0,0 +1,49 @@ +#version 450 core +//Pure-VK HiZ mip reduction: one dispatch per destination mip level. Mirrors the +// GL blit.fsh conservative 2x2 reduction, sampling the source at the NORMALISED +// destination-texel centre (textureGather over [0,1]) so the reduction rescales +// correctly for ANY source:destination ratio. +// +// This matters at level 0, where the source is the full-screen depth image +// (e.g. 1920x1197) and the destination is the power-of-two pyramid base +// (1024x1024) -- NOT an exact 2:1 halving. The previous integer `pos*2` fetch +// assumed src == 2*dst, so it squished the full-res depth into the top-left +// ~58% of the base level and mis-aligned every occlusion test: distant LODs +// near the horizon were sampled against unrelated foreground depth (marked NEAR +// by the stencil-depth setup, i.e. a full occluder) and got wrongly culled, the +// holes shifting as the camera moved. Levels >=1 are exact 2:1 (powers of two) +// so this produces identical results there. +// +// REDUCTION comes from depthutils (min for reverse-Z: keep the FURTHEST value so +// occlusion tests stay conservative). +#import + +layout(local_size_x=8, local_size_y=8) in; + +layout(binding = 0) uniform sampler2D srcLevel; +layout(binding = 1, r32f) uniform restrict writeonly image2D dstLevel; + +layout(push_constant) uniform PushConstants { + ivec2 dstSize; + ivec2 srcSize;//unused: normalised gather handles the rescale +}; + +void main() { + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(pos, dstSize))) { + return; + } + //Normalised centre of this destination texel; the sampler maps [0,1] across + // the whole source level, exactly like the GL fullscreen-quad blit. + vec2 uv = (vec2(pos) + 0.5) / vec2(dstSize); + vec4 depths = textureGather(srcLevel, uv, 0); + + //Hole patch, kept identical to blit.fsh for parity (a no-op for min/max REDUCTION). + bvec4 cv = equal(vec4(FAR), depths); + if (any(cv)) { + depths = mix(vec4(NEAR), depths, cv); + } + + float res = REDUCTION(REDUCTION(depths.x, depths.y), REDUCTION(depths.z, depths.w)); + imageStore(dstLevel, pos, vec4(res)); +} diff --git a/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_subgroup.comp b/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_subgroup.comp new file mode 100644 index 000000000..54e8acedd --- /dev/null +++ b/src/main/resources/assets/voxy/shaders/hiz/vk/hiz_subgroup.comp @@ -0,0 +1,121 @@ +#version 460 core +//Pure-VK subgroup HiZ pyramid: collapses the whole mip chain (levels 1..N) into +// ONE dispatch per 64x64 tile of the pyramid base, using subgroupClusteredMax / +// subgroupMax reductions. Mirrors hiz/hiz.comp (the GL subgroup variant) with +// the VK binding model — each mip level is a separate image2D binding, pushed by +// the Java side at dispatch time. The level-0 reduction (full-screen depth -> +// pyramid base, non-power-of-two ratio) is handled separately by hiz_reduce.comp; +// this shader takes over from level 1 onwards. +// +//64x64 workgroup -> one dispatch per 64x64 tile covers the base in ceil(W/64) x +// ceil(H/64) workgroups. Each thread loads one 4x4 block (16 texels of mip_1), +// reduces to 1 value, then subgroup reductions build mip_2 (4x4 clusters) -> +// mip_3 (16x16) -> mip_4 (16x16 via clustered max) -> mip_5 (2x2) -> mip_6 (1x1). +// +//REDUCTION comes from depthutils (min for reverse-Z: keep the FURTHEST value). +#import + +#extension GL_KHR_shader_subgroup_arithmetic : require +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_clustered : require + +layout(local_size_x = 256) in; + +//Z-order swizzle so neighbouring subgroup invocations cover spatially-coherent +// texels (better texture-cache utilisation on the textureGather of mip_0). +const uint spread[64] = { + 0x11100100, 0x13120302, 0x31302120, 0x33322322, 0x15140504, 0x17160706, 0x35342524, 0x37362726, + 0x51504140, 0x53524342, 0x71706160, 0x73726362, 0x55544544, 0x57564746, 0x75746564, 0x77766766, + 0x19180908, 0x1b1a0b0a, 0x39382928, 0x3b3a2b2a, 0x1d1c0d0c, 0x1f1e0f0e, 0x3d3c2d2c, 0x3f3e2f2e, + 0x59584948, 0x5b5a4b4a, 0x79786968, 0x7b7a6b6a, 0x5d5c4d4c, 0x5f5e4f4e, 0x7d7c6d6c, 0x7f7e6f6e, + 0x91908180, 0x93928382, 0xb1b0a1a0, 0xb3b2a3a2, 0x95948584, 0x97968786, 0xb5b4a5a4, 0xb7b6a7a6, + 0xd1d0c1c0, 0xd3d2c3c2, 0xf1f0e1e0, 0xf3f2e3e2, 0xd5d4c5c4, 0xd7d6c7c6, 0xf5f4e5e4, 0xf7f6e7e6, + 0x99988988, 0x9b9a8b8a, 0xb9b8a9a8, 0xbbbaabaa, 0x9d9c8d8c, 0x9f9e8f8e, 0xbdbcadac, 0xbfbeafae, + 0xd9d8c9c8, 0xdbdacbca, 0xf9f8e9e8, 0xfbfaebea, 0xdddccdcc, 0xdfdecfce, 0xfdfcedec, 0xfffeefee +}; +uint swizzleId(uint id) { return bitfieldExtract(spread[id >> 2], (int(id) & 3) * 8, 8); } + +//Pyramid mip views. mip_0 is the base (already produced by hiz_reduce.comp); +// mip_1..mip_6 are written here by the subgroup reductions. The Java side binds +// only as many as exist (levels clamped), the rest are VK_NULL_HANDLE and never +// written. 7 mip bindings covers up to 64x64 -> 1x1 (6 reductions of base 64). +layout(set = 0, binding = 0) uniform sampler2D mip_0; +layout(set = 0, binding = 1, r32f) uniform restrict writeonly image2D mip_1; +layout(set = 0, binding = 2, r32f) uniform restrict writeonly image2D mip_2; +layout(set = 0, binding = 3, r32f) uniform restrict writeonly image2D mip_3; +layout(set = 0, binding = 4, r32f) uniform restrict writeonly image2D mip_4; +layout(set = 0, binding = 5, r32f) uniform restrict writeonly image2D mip_5; +layout(set = 0, binding = 6, r32f) uniform restrict writeonly image2D mip_6; + +layout(push_constant) uniform PushConstants { + vec2 invMip0Size; // 1.0 / vec2(mip_0 dimensions) + int levels; // number of mip levels actually present (mip_0=base) + int _pad; +}; + +//Cross-subgroup accumulation for the last two levels. 16 values (one per +// 4-subgroup chunk) -> 4 values (mip_5) -> 1 (mip_6). Declared at file scope +// (shared arrays cannot be nested in a function in GLSL). +shared float values[16]; + +float reduce2x2(ivec2 pos, out bool wroteMip1) { + //2x2 reduction of mip_0 -> mip_1, using textureGather at the normalised centre. + vec4 data = textureGather(mip_0, (vec2(pos * 2) + 1.0) * invMip0Size, 0); + float ret = REDUCTION(REDUCTION(data.x, data.y), REDUCTION(data.z, data.w)); + imageStore(mip_1, pos, vec4(ret)); + wroteMip1 = true; + return ret; +} + +float reduce4x4(ivec2 pos, out bool wroteMip2) { + ivec2 pos2 = pos * 2; + bool dummy; + float ret = REDUCTION( + REDUCTION(reduce2x2(pos2 + ivec2(0, 0), dummy), reduce2x2(pos2 + ivec2(0, 1), dummy)), + REDUCTION(reduce2x2(pos2 + ivec2(1, 0), dummy), reduce2x2(pos2 + ivec2(1, 1), dummy))); + imageStore(mip_2, pos, vec4(ret)); + wroteMip2 = true; + return ret; +} + +void main() { + uint id = swizzleId(gl_LocalInvocationID.x); + //Wave position in mip_2 coordinates (16x16 tile per workgroup at mip_2). + ivec2 wavePos = ivec2(gl_WorkGroupID.xy) * 16 + ivec2(id & 0xFU, id >> 4); + + bool wroteMip2; + float value = reduce4x4(wavePos, wroteMip2); + if (!wroteMip2) return; // thread was out of bounds for all its 2x2 gathers + + //Subgroup reductions within the 64-wide subgroup: clusters of 4 (4x4 -> 1) + // then 16 (16x16 -> 1) produce mip_3 and mip_4. + subgroupBarrier(); + + float reduced = subgroupClusteredMax(value, 4); + if ((gl_SubgroupInvocationID & 0x3) == 0) { + imageStore(mip_3, wavePos >> 1, vec4(reduced)); + } + subgroupBarrier(); + reduced = subgroupClusteredMax(value, 16); + if ((gl_SubgroupInvocationID & 0xF) == 0) { + imageStore(mip_4, wavePos >> 2, vec4(reduced)); + } + + //Cross-subgroup accumulation in shared memory for the last two levels. + //16 values (one per 4-subgroup chunk) -> 4 values (mip_5) -> 1 (mip_6). + if ((gl_LocalInvocationID.x & 0xFU) == 0) { + values[gl_LocalInvocationID.x >> 4] = reduced; + } + barrier(); + + if ((gl_LocalInvocationID.x >> 2) != 0) return; //keep only 4 threads + + uint i = gl_LocalInvocationID.x * 4; + value = REDUCTION(REDUCTION(values[i], values[i + 1]), REDUCTION(values[i + 2], values[i + 3])); + imageStore(mip_5, ivec2(gl_WorkGroupID.xy) * 2 + ivec2(gl_LocalInvocationID.x & 1u, gl_LocalInvocationID.x >> 1), vec4(value)); + subgroupBarrier(); + value = subgroupMax(value); + if (gl_LocalInvocationID.x == 0) { + imageStore(mip_6, ivec2(gl_WorkGroupID.xy), vec4(value)); + } +} \ No newline at end of file diff --git a/src/main/resources/assets/voxy/shaders/lod/gl46/cull/raster.vert b/src/main/resources/assets/voxy/shaders/lod/gl46/cull/raster.vert index 52e1232c2..c05f1edb9 100644 --- a/src/main/resources/assets/voxy/shaders/lod/gl46/cull/raster.vert +++ b/src/main/resources/assets/voxy/shaders/lod/gl46/cull/raster.vert @@ -2,6 +2,14 @@ #extension GL_ARB_gpu_shader_int64 : enable #define VISIBILITY_ACCESS +#ifdef VOXY_VULKAN +#define VERT_ID gl_VertexIndex +#define INSTANCE_ID gl_InstanceIndex +#else +#define VERT_ID gl_VertexID +#define INSTANCE_ID gl_InstanceID +#endif + #define SECTION_METADATA_BUFFER_BINDING 1 #define VISIBILITY_BUFFER_BINDING 2 #define INDIRECT_SECTION_LOOKUP_BINDING 3 @@ -19,7 +27,7 @@ vec2 getTAA(); #endif void main() { - uint sid = indirectLookup[gl_InstanceID]; + uint sid = indirectLookup[INSTANCE_ID]; SectionMeta section = sectionData[sid]; @@ -36,7 +44,7 @@ void main() { vec3 offset = aabbOffset-EXPANSION; - offset += vec3(gl_VertexID&1, (gl_VertexID>>2)&1, (gl_VertexID>>1)&1)*(size+2*EXPANSION); + offset += vec3(VERT_ID&1, (VERT_ID>>2)&1, (VERT_ID>>1)&1)*(size+2*EXPANSION); gl_Position = MVP * vec4(vec3(pos)+offset*(1<>2], pos, (gl_VertexID&3) == 1); + uvec2 pos = positionBuffer[BASE_INSTANCE]; + setupQuad(quad, quadData[uint(VERT_ID)>>2], pos, (VERT_ID&3) == 1); - uint cornerId = gl_VertexID&3; + uint cornerId = VERT_ID&3; gl_Position = #ifdef USE_NV_JANK @@ -75,7 +83,7 @@ void main() { #ifdef DEBUG_RENDER //quadDebug = uint(extractDetail(pos)); - quadDebug = uint(gl_VertexID)>>2; + quadDebug = uint(VERT_ID)>>2; #endif } diff --git a/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/batch_visibility_set.comp b/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/batch_visibility_set.comp index c5be1c7aa..e8560982d 100644 --- a/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/batch_visibility_set.comp +++ b/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/batch_visibility_set.comp @@ -10,8 +10,12 @@ layout(binding = LIST_BUFFER_BINDING, std430) restrict readonly buffer SetListBu uint[] ids; }; +#ifdef VOXY_VULKAN +layout(push_constant) uniform PushConstants { uint count; uint setTo; }; +#else layout(location=0) uniform uint count; layout(location=1) uniform uint setTo; +#endif void main() { uint id = gl_GlobalInvocationID.x; if (count <= id) { diff --git a/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/result_transformer.comp b/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/result_transformer.comp index c53978d12..b78f0cad5 100644 --- a/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/result_transformer.comp +++ b/src/main/resources/assets/voxy/shaders/lod/hierarchical/cleaner/result_transformer.comp @@ -17,7 +17,11 @@ layout(binding = VISIBILITY_BUFFER_BINDING, std430) restrict buffer VisibilityBu uint[] visibility; }; +#ifdef VOXY_VULKAN +layout(push_constant) uniform PushConstants { uint visibilityCounter; }; +#else layout(location=0) uniform uint visibilityCounter; +#endif void main() { uint id = minVisIds[gl_LocalInvocationID.x]; uvec4 node = nodes[id]; diff --git a/src/main/resources/assets/voxy/shaders/lod/hierarchical/queue.glsl b/src/main/resources/assets/voxy/shaders/lod/hierarchical/queue.glsl index 66f44e9bd..5610c7053 100644 --- a/src/main/resources/assets/voxy/shaders/lod/hierarchical/queue.glsl +++ b/src/main/resources/assets/voxy/shaders/lod/hierarchical/queue.glsl @@ -1,6 +1,10 @@ #define SENTINAL_OUT_OF_BOUNDS uint(-1) +#ifdef VOXY_VULKAN +layout(push_constant) uniform PushConstants { uint queueIdx; }; +#else layout(location = NODE_QUEUE_INDEX_BINDING) uniform uint queueIdx; +#endif layout(binding = NODE_QUEUE_META_BINDING, std430) restrict buffer NodeQueueMeta { uvec4 nodeQueueMetadata[MAX_ITERATIONS]; diff --git a/src/main/resources/assets/voxy/shaders/post/blit_texture_depth_cutout.frag b/src/main/resources/assets/voxy/shaders/post/blit_texture_depth_cutout.frag index f709b5015..a2e165a4a 100644 --- a/src/main/resources/assets/voxy/shaders/post/blit_texture_depth_cutout.frag +++ b/src/main/resources/assets/voxy/shaders/post/blit_texture_depth_cutout.frag @@ -1,6 +1,17 @@ #version 450 core layout(binding = 0) uniform sampler2D depthTex; +#ifdef VOXY_VULKAN +layout(binding = 1, std140) uniform CompositeParams { + mat4 invProjMat; + mat4 projMat; + vec4 endParams; + vec4 fogColour; +}; +#ifdef EMIT_COLOUR +layout(binding = 3) uniform sampler2D colourTex; +#endif +#else layout(location = 1) uniform mat4 invProjMat; layout(location = 2) uniform mat4 projMat; @@ -11,6 +22,7 @@ layout(location = 4) uniform vec4 endParams; layout(location = 5) uniform vec4 fogColour; #endif #endif +#endif #import @@ -39,7 +51,9 @@ void main() { depth = REDUCTION2(FAR+CLOSER_SIGN*(2.0f/((1<<24)-1)), depth); depth = NDC2SCREEN_DEPTH(depth); +#ifndef VOXY_VULKAN depth = gl_DepthRange.diff * depth + gl_DepthRange.near;//TODO: dont think this is right at all so should fix this +#endif gl_FragDepth = depth; diff --git a/src/main/resources/assets/voxy/shaders/post/fullscreen2.vert b/src/main/resources/assets/voxy/shaders/post/fullscreen2.vert index 4cf0df193..4e8f1797a 100644 --- a/src/main/resources/assets/voxy/shaders/post/fullscreen2.vert +++ b/src/main/resources/assets/voxy/shaders/post/fullscreen2.vert @@ -1,8 +1,13 @@ #version 330 core #import +#ifdef VOXY_VULKAN +#define VERT_ID gl_VertexIndex +#else +#define VERT_ID gl_VertexID +#endif out vec2 UV; void main() { - gl_Position = vec4(vec2(gl_VertexID&1, (gl_VertexID>>1)&1) * 2 - 1, FAR, 1); + gl_Position = vec4(vec2(VERT_ID&1, (VERT_ID>>1)&1) * 2 - 1, FAR, 1); UV = gl_Position.xy*0.5+0.5; } \ No newline at end of file diff --git a/src/main/resources/assets/voxy/shaders/post/setup_stencil_depth.frag b/src/main/resources/assets/voxy/shaders/post/setup_stencil_depth.frag index efbfb48eb..3ec502d35 100644 --- a/src/main/resources/assets/voxy/shaders/post/setup_stencil_depth.frag +++ b/src/main/resources/assets/voxy/shaders/post/setup_stencil_depth.frag @@ -1,7 +1,11 @@ #version 330 core layout(binding = 0) uniform sampler2D depthTex; +#ifdef VOXY_VULKAN +layout(push_constant) uniform PushConstants { vec2 scaleFactor; }; +#else layout(location = 1) uniform vec2 scaleFactor; +#endif #import diff --git a/src/main/resources/assets/voxy/shaders/post/ssao.comp b/src/main/resources/assets/voxy/shaders/post/ssao.comp index e3cbecaf2..8eaf561e8 100644 --- a/src/main/resources/assets/voxy/shaders/post/ssao.comp +++ b/src/main/resources/assets/voxy/shaders/post/ssao.comp @@ -7,6 +7,26 @@ layout(binding = 2) uniform sampler2D depthTex; #import +#ifdef VOXY_VULKAN +//Default-block uniforms are unavailable in Vulkan GLSL: the matrices live in a +// UBO (binding 4; 0..3 are the image + samplers in the unified namespace) +#ifdef BETTER_SSAO +layout(binding = 3) uniform sampler2D sourceDepthTex; +layout(binding = 4, std140) uniform SSAOParams { + mat4 Proj; + mat4 invProj; + mat4 MV; + mat4 sourceInvProj; +}; +#else +layout(binding = 4, std140) uniform SSAOParams { + mat4 MVP; + mat4 invMVP; + mat4 pad0; + mat4 pad1; +}; +#endif +#else #ifdef BETTER_SSAO layout(binding = 3) uniform sampler2D sourceDepthTex; layout(location = 4) uniform mat4 Proj; @@ -17,6 +37,7 @@ layout(location = 7) uniform mat4 sourceInvProj; layout(location = 3) uniform mat4 MVP; layout(location = 4) uniform mat4 invMVP; #endif +#endif vec3 rev3d(mat4 matrix, vec3 clip) { vec4 view = matrix * vec4(SCREEN2NDC(clip),1.0); diff --git a/src/main/resources/assets/voxy/shaders/util/prefixsum/inital3_vk.comp b/src/main/resources/assets/voxy/shaders/util/prefixsum/inital3_vk.comp new file mode 100644 index 000000000..393521a8e --- /dev/null +++ b/src/main/resources/assets/voxy/shaders/util/prefixsum/inital3_vk.comp @@ -0,0 +1,79 @@ +#version 460 + +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_arithmetic : require + +//Clean VK port of inital3.comp (the GL subgroup prefix sum), with the GL-era +// IS_INTEL/IS_WINDOWS driver-jank branches removed. On Vulkan the subgroup +// intrinsics (subgroupExclusiveAdd, gl_SubgroupID, gl_SubgroupInvocationID, +// gl_SubgroupSize) are reliable across MoltenVK/Metal, NVIDIA, AMD and Intel; +// the jank existed only for GL driver bugs that do not affect the VK path. +// +//Each thread loads one uvec4 from ioCount[gid] and produces the exclusive +// prefix sum across the workgroup's 256 uvec4 elements. The uvec4 is scanned +// internally (count.yzw = dat.xyz; count.z += count.y; count.w += count.z; +// sum = count.w + dat.w), so each thread contributes 4 sequential outputs +// with a single subgroup scan over `sum`. The cross-subgroup prefix is +// accumulated in a tiny shared array (one slot per subgroup) and scanned +// with one more subgroupExclusiveAdd over the first thread of each subgroup. + +#define WORK_SIZE 256 +layout(local_size_x = WORK_SIZE) in; + +layout(binding = IO_BUFFER, std430) buffer InputBuffer { + uvec4[] ioCount; +}; + +shared uint subgroupSums[8]; // WORK_SIZE / minSubgroupSize(32) = 8 slots max + +void main() { + uint gid = gl_GlobalInvocationID.x; + uint localId = gl_LocalInvocationID.x; + uint subgroupId = gl_SubgroupID; + uint subInv = gl_SubgroupInvocationID; + uint subSize = gl_SubgroupSize; + uint numSubgroups = gl_NumSubgroups; + + uvec4 count = uvec4(0); + uint sum = 0; + { + uvec4 dat = ioCount[gid]; + count.yzw = dat.xyz; + count.z += count.y; + count.w += count.z; + sum = count.w + dat.w; + } + subgroupBarrier(); //ensure all threads in the subgroup have read the buffer + + //Exclusive prefix sum of `sum` across the subgroup (1 subgroup op). + uint subgroupExclusive = subgroupExclusiveAdd(sum); + + //Last invocation of each subgroup writes its total into the shared array. + if (subInv == subSize - 1u) { + subgroupSums[subgroupId] = subgroupExclusive + sum; + } + + memoryBarrierShared(); + barrier(); + + //Cross-subgroup prefix sum: the first thread of each subgroup (subInv == 0) + // reads the per-subgroup totals and scans them in one subgroup dispatch. + // numSubgroups <= 8 <= subgroupSize, so all subgroup totals fit in one + // subgroup scan; the result is the prefix of subgroup totals before + // `subgroupId`, which every thread in subgroup subgroupId adds to its + // count. + uint subgroupPrefix = 0; + if (subInv < numSubgroups) { + uint val = subgroupSums[subInv]; + subgroupBarrier(); + subgroupSums[subInv] = subgroupExclusiveAdd(val); + } + memoryBarrierShared(); + barrier(); + + subgroupPrefix = subgroupSums[subgroupId]; + + //Combine: subgroup-exclusive + cross-subgroup-prefix + intra-uvec4 scan. + count += subgroupExclusive + subgroupPrefix; + ioCount[gid] = count; +} \ No newline at end of file diff --git a/src/main/resources/assets/voxy/shaders/util/scatter.comp b/src/main/resources/assets/voxy/shaders/util/scatter.comp index 787f2d97b..9018e0836 100644 --- a/src/main/resources/assets/voxy/shaders/util/scatter.comp +++ b/src/main/resources/assets/voxy/shaders/util/scatter.comp @@ -16,7 +16,11 @@ layout(binding = OUTPUT_BUFFER2_BINDING, std430) restrict writeonly buffer Outpu uvec4[] output2; }; +#ifdef VOXY_VULKAN +layout(push_constant) uniform PushConstants { uint count; }; +#else layout(location=0) uniform uint count; +#endif void main() { uint id = gl_GlobalInvocationID.x; diff --git a/src/main/resources/client.voxy.mixins.json b/src/main/resources/client.voxy.mixins.json index 6f7bb54a7..286aab6c8 100644 --- a/src/main/resources/client.voxy.mixins.json +++ b/src/main/resources/client.voxy.mixins.json @@ -37,7 +37,10 @@ "sodium.MixinRenderRegionManager", "sodium.MixinRenderSectionManager", "sodium.MixinSodiumWorldRenderer", - "sodium.MixinVisibleChunkCollector" + "sodium.MixinVisibleChunkCollector", + "vk.AccessorVulkanCommandEncoder", + "vk.MixinSodiumOpaqueVkFrame", + "vk.MixinVulkanDevice" ], "injectors": { "defaultRequire": 1 From 4e801b95b6eacd5543025e4eddc5aee4fb959b7b Mon Sep 17 00:00:00 2001 From: cochcoder Date: Sun, 19 Jul 2026 14:21:07 -0400 Subject: [PATCH 07/12] vk: drop stale design doc --- VULKAN.md | 106 ------------------------------------------------------ 1 file changed, 106 deletions(-) delete mode 100644 VULKAN.md diff --git a/VULKAN.md b/VULKAN.md deleted file mode 100644 index aa1ab5ea6..000000000 --- a/VULKAN.md +++ /dev/null @@ -1,106 +0,0 @@ -# Voxy Vulkan Backend - -Voxy follows Minecraft's own graphics API: when MC 26.2 runs on its Vulkan -backend, Voxy renders through Vulkan; when MC is on OpenGL, Voxy uses its -OpenGL (MDIC) backend. There is no user-facing toggle and no fallback either -way — a GL context cannot exist in a MC-on-Vulkan process, so "falling back to -OpenGL" is impossible, and forcing a cross-API split is incoherent with the -identical-experience goal. - -## Architecture - -When MC is on Vulkan, Voxy adopts MC's own `VkDevice`/queue via the Blaze3D -adapter mixin (`MixinVulkanDevice` registers a `MinecraftVkHostAdapter` at -device init) and records all of its GPU work into MC's frame command buffer -from `MixinSodiumOpaqueVkFrame` (TAIL of `SodiumWorldRenderer.drawChunkLayer` -for the OPAQUE group) — right after Sodium's opaque terrain, render pass -closed, frame command buffer recording. Per frame: - -1. **SETUP** (`VkCompositor`): clear Voxy's offscreen colour + D32S8 - depth-stencil (stencil=1); fullscreen pass copies MC's depth in - (projection-transformed) writing stencil=0 where vanilla terrain exists. -2. **Opaque LOD terrain**: `vkCmdDrawIndexedIndirectCount`, stencil==1 test - (draw calls generated last frame — same latency model as GL). -3. **HiZ pyramid** (`VkHiZ`, R32F mips, conservative REDUCTION) + - `AsyncNodeManager` sync (`VkNodeGpuOps` scatter/multi-memcpy computes) + - `VkNodeCleaner` + hierarchical traversal (`VkTraversal`: 12 flip-flop indirect - dispatches, HiZ-tested, request readback via `VkDownloadStream`). -4. **Draw-call build** (`VkTerrainRenderer`): prep -> raster box cull - (depth-only, early-fragment-test visibility writes) -> cmdgen (indirect - dispatch) -> translucency prefix-sort + build. -5. Temporal + translucent draws. -6. **COMPOSITE**: alpha-blend into MC's colour/depth attachments (dynamic - rendering, LOAD/STORE), fragment emits vanilla-space depth + env fog (fog - params sourced from MC's `CameraRenderState.fogData`). -7. Streams tick (staging recycled on VkEvent frame retirement), model bakery - tick (atlas mips via `vkCmdCopyBufferToImage`), render-distance tracking. - -Shared with GL (zero duplication): `NodeManager`/`AsyncNodeManager`, mesh -generation, model bakery CPU pipeline, render-distance tracker, viewport -math, and all shader sources — the GLSL is single-source with -`#ifdef VOXY_VULKAN` guards (push constants replace default-block uniforms, -sampler bindings remapped to the unified VK namespace, -`gl_VertexID`/`gl_InstanceID`/`gl_BaseInstance` aliased, u16 cube indices -replace u8). Seams: `IDeviceBuffer`, `AbstractUploadStream`/ -`AbstractDownloadStream` (backend-settable singletons), `INodeGpuOps`, -`INodeCleaner`, `IBasicGeometryData`, `IModelStore`, `IAtlasTextureReader`. - -## MoltenVK / macOS - -- `drawIndirectCount` is a 1.2 *feature*, not implied by `apiVersion` — - MoltenVK reports 1.2 without it. `VulkanContext.hasDrawIndirectCount` is - queried and `VkTerrainRenderer.renderTerrain` branches on it: the - fixed-count fallback issues `vkCmdDrawIndexedIndirect` with a clamped - `maxDrawCount` and zeroes the three `drawCallBuffer` slices (opaque / - temporal / translucent) before cmdgen so stale trailing slots never read as - ghost draws. -- The fixed-count budget tracks the last-read real per-pass draw count (async - readback via `VkDownloadStream`, no stall), so looking at the sky actually - reduces encoded Metal draws (MoltenVK emulates multi-draw-indirect as one - Metal draw per slot — a view-independent cap encoded ~sectionCount*6.4 - no-op draws per frame and hid all culling wins). -- `VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT` + a `VkImageFormatListCreateInfo` is - used on the D32S8 offscreen depth so a depth-only aspect view can alias the - packed image (without it MoltenVK may synthesise a separate staging texture - for depth-only sampling). -- macOS natives for `lwjgl-lmdb` and `lwjgl-zstd` must be bundled or section - storage throws `UnsatisfiedLinkError` on save and no geometry is persisted - (the bound-renderer AABBs leak through as the only visible geometry). - -## Invariants - -- GL/MDIC byte-identical when MC is on GL (seam refactors are lazy-init only). -- VK strictly follows MC's own API; never falls back to GL under - MC-on-Vulkan; gates no mod off as "GL-only". -- No GL classloads can occur on the VK path — `VoxyClient.initVoxyClient` - branches on `MinecraftVkHost.isMinecraftOnVulkan()` before the first GL - touch (`Capabilities`'s `` runs `GL.getCapabilities()` and throws - with no GL context). The streams, the shared index buffer, and the atlas - readback are lazily backend-selected, so their GL implementations never - classload on the VK path. -- `VkRenderCore.shutdown()` is idempotent and device-alive-gated - (`MinecraftVkHost.get() != null` skips GPU teardown if MC already tore its - device down on full-game exit). CPU stop (node/gen thread joins, callback - detach, world `releaseRef`) always runs. `modelService.shutdown()` owns the - `VkModelStore` lifetime (single `vkDestroySampler`) and runs after - `frameCtx.waitIdleRetireAll()`. - -## Feature parity - -VK visual output matches GL: depth-space transform in setup/composite, -stencil mask correctness, fog ramp, lightmap sampling (vertex-stage), model -atlas mips, SSAO (`VkSSAO`), depth-bound culling (`VkBoundRenderer`), and -view-bob tracking (the frame hook feeds Sodium's bobbed `ChunkRenderMatrices` -+ camera offset into `renderFrame`). - -No-op by design on the VK path: FREX integration, shader printf debugging, -GPU-timing markers (all GL-debug-only paths). - -## A/B comparison logging - -`-Dvoxy.cmplog=` makes both backends emit the same per-frame semantic -quantities (section/geometry counts, traversal request counts) as -tab-separated records via `CmpLog`. For a stationary camera the values -converge to identical integers when the VK translation is faithful, so a -script can flag any divergence without the rounding noise of a pixel diff. -Every method is a no-op when the property is unset. \ No newline at end of file From 0dd28b15704a09f20b9dc6d0e86b03f0404b7986 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Sun, 19 Jul 2026 14:32:12 -0400 Subject: [PATCH 08/12] vk: drop A/B parity logging (CmpLog) and one-shot diagnostic logs --- .../client/core/AbstractRenderPipeline.java | 3 - .../hierachical/AsyncNodeManager.java | 14 ---- .../HierarchicalOcclusionTraverser.java | 3 - .../client/core/vk/render/VkRenderCore.java | 19 ----- .../voxy/client/core/vk/render/VkSSAO.java | 2 - .../client/core/vk/render/VkTraversal.java | 3 - .../mixin/vk/MixinSodiumOpaqueVkFrame.java | 5 -- .../java/me/cortex/voxy/common/CmpLog.java | 80 ------------------- 8 files changed, 129 deletions(-) delete mode 100644 src/main/java/me/cortex/voxy/common/CmpLog.java diff --git a/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java b/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java index 2a6ffd0db..c8d593d48 100644 --- a/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java +++ b/src/main/java/me/cortex/voxy/client/core/AbstractRenderPipeline.java @@ -14,7 +14,6 @@ import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer; import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.util.GPUTiming; -import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.util.TrackedObject; import org.joml.Matrix4f; import org.lwjgl.opengl.GL30; @@ -77,7 +76,6 @@ public abstract class AbstractRenderPipeline extends TrackedObject { } protected AbstractRenderPipeline(RenderProperties properties, AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier, boolean deferTranslucency) { - CmpLog.backend = "opengl"; this.properties = properties; this.frexStillHasWork = frexSupplier; this.nodeManager = nodeManager; @@ -223,7 +221,6 @@ protected void innerPrimaryWork(Viewport viewport, int depthBuffer) { glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT | GL_PIXEL_BUFFER_BARRIER_BIT); - this.nodeManager.logCompareStats(); TimingStatistics.F.start(); this.traversal.doTraversal(viewport); TimingStatistics.F.stop(); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java index 3c8bd0e41..58d1c74c8 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java @@ -17,7 +17,6 @@ import me.cortex.voxy.client.core.rendering.section.geometry.IBasicGeometryData; import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData; import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; -import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.AllocationArena; import me.cortex.voxy.common.util.MemoryBuffer; @@ -742,19 +741,6 @@ public void stop() { this.geometryCache.free(); } - //Emit the per-frame "same scene?" gate metrics for A/B log comparison. The - // OpenGL and Vulkan paths log byte-identical values by construction — if - // they don't match, the two captures weren't the same viewpoint/world state - // and any downstream (GPU-computed) comparison is moot. Also advances - // CmpLog's frame counter (call once per rendered frame). - public void logCompareStats() { - if (!CmpLog.ENABLED) return; - CmpLog.nextFrame(); - CmpLog.rec("nodeManager", "sectionCount", this.geometryData.getSectionCount()); - CmpLog.rec("nodeManager", "usedGeometry", this.getUsedGeometryCapacity()); - CmpLog.rec("nodeManager", "geometryCapacity", this.getGeometryCapacity()); - } - public void addDebug(List debug) { debug.add("UC/GC,#N: " + (this.getUsedGeometryCapacity()/(1<<20))+"/"+(this.getGeometryCapacity()/(1<<20)) + "," + (this.geometryData.getSectionCount())); //debug.add("GUQ/NRC: " + this.geometryUpdateQueue.size()+"/"+this.removeBatchQueue.size()); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 2abe26b7f..99cad4ef5 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -14,7 +14,6 @@ import me.cortex.voxy.client.core.rendering.util.AbstractDownloadStream; import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil; import me.cortex.voxy.client.core.rendering.util.AbstractUploadStream; -import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; @@ -372,8 +371,6 @@ private void forwardDownloadResult(long ptr, long size) { //if (count > REQUEST_QUEUE_SIZE) { // Logger.warn("Count larger than 'maxRequestCount', overflow captured. Overflowed by " + (count-REQUEST_QUEUE_SIZE)); //} - CmpLog.rec("traversal", "requestCount", count); - CmpLog.rec("traversal", "topNodeCount", this.topNodeCount); if (count != 0) { var buffer = new MemoryBuffer(count*8L+8).cpyFrom(ptr-8); //Write back the exact count into the new memory buffer (not the download stream buffer) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java index 71d9c47fb..6b25db18c 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java @@ -21,7 +21,6 @@ import me.cortex.voxy.client.core.vk.VkFrameCtx; import me.cortex.voxy.client.core.vk.VkUploadStream; import me.cortex.voxy.client.core.vk.VulkanBackend; -import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.thread.ServiceManager; import me.cortex.voxy.common.world.WorldEngine; @@ -61,13 +60,11 @@ public class VkRenderCore { private final VkBoundRenderer boundRenderer; private final StreamedBoundStore visibleSectionStream; private boolean shutDown = false; - private boolean firstFrameLogged = false; private final RenderDistanceTracker renderDistanceTracker; private final ViewportSelector viewportSelector; public VkRenderCore(WorldEngine world, ServiceManager sm) { world.acquireRef(); - CmpLog.backend = "vulkan"; Logger.info("Creating Voxy pure-Vulkan render core"); try { this.worldIn = world; @@ -127,12 +124,6 @@ public VkRenderCore(WorldEngine world, ServiceManager sm) { this.frameCtx.flushImmediate(); Logger.info("Voxy pure-Vulkan render core created with " + this.geometryData.getMaxCapacity() + " geometry capacity"); - Logger.info("Voxy VK diagnostics: isZero2One=" + this.properties.isZero2One() - + " isReverseZ=" + this.properties.isReverseZ() - + " clearDepth=" + this.properties.clearDepth() - + " inverseClearDepth=" + this.properties.inverseClearDepth() - + " drawIndirectCount=" + vctx.hasDrawIndirectCount - + " device=" + vctx.deviceName); } catch (RuntimeException e) { world.releaseRef(); throw e; @@ -164,15 +155,6 @@ public void renderFrame(RenderTarget target, MinecraftVkHostAdapter adapter, Chu var crs = Minecraft.getInstance().gameRenderer.gameRenderState().levelRenderState.cameraRenderState; if (crs == null || !crs.initialized) return; - if (!this.firstFrameLogged) { - this.firstFrameLogged = true; - Logger.info("Voxy VK first frame: target=" + target.width + "x" + target.height - + " colorFormat=" + VkFrameHost.vkFormat(target.getColorTextureView()) - + " depthFormat=" + VkFrameHost.vkFormat(target.getDepthTextureView()) - + " sectionCount=" + this.geometryData.getSectionCount() - + " hasMatrices=" + (matrices != null)); - } - this.frameCtx.flushImmediate(); this.frameCtx.beginFrame(frameCmd); try { @@ -220,7 +202,6 @@ public void renderFrame(RenderTarget target, MinecraftVkHostAdapter adapter, Chu this.downloadStream.tick(); this.nodeManager.tick(this.traversal.getNodeBuffer(), this.nodeCleaner); this.nodeCleaner.tick(this.traversal.getNodeBuffer()); - this.nodeManager.logCompareStats(); this.traversal.doTraversal(viewport); //4. build the draw commands for this frame (prep, raster cull, cmdgen, translucency sort) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java index e4508aea0..5a983db84 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkSSAO.java @@ -8,7 +8,6 @@ import me.cortex.voxy.client.core.vk.VkShaderPipeline; import me.cortex.voxy.client.core.vk.VkShaderSource; import me.cortex.voxy.client.core.vk.VkUploadStream; -import me.cortex.voxy.common.Logger; import org.joml.Matrix4f; import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; @@ -51,7 +50,6 @@ public VkSSAO(VkFrameCtx ctx, VkUploadStream uploadStream, RenderProperties prop case BEST -> 24; default -> 0; }; - Logger.info("Voxy VK SSAO mode: " + mode); var defs = VkShaderSource.defs().props(properties); if (this.isBetterSSAO) { diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java index 047b4383a..90b446b38 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java @@ -13,7 +13,6 @@ import me.cortex.voxy.client.core.vk.VkShaderPipeline; import me.cortex.voxy.client.core.vk.VkShaderSource; import me.cortex.voxy.client.core.vk.VkUploadStream; -import me.cortex.voxy.common.CmpLog; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; @@ -261,8 +260,6 @@ private void forwardDownloadResult(long ptr, long size) { if (count > (this.requestBuffer.size() >> 3) - 1) { count = (int) ((this.requestBuffer.size() >> 3) - 1); } - CmpLog.rec("traversal", "requestCount", count); - CmpLog.rec("traversal", "topNodeCount", this.topNodeCount); if (count != 0) { var buffer = new MemoryBuffer(count * 8L + 8).cpyFrom(ptr - 8); MemoryUtil.memPutInt(buffer.address, count); diff --git a/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java index 7277b54f7..9918b1f72 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java +++ b/src/main/java/me/cortex/voxy/client/mixin/vk/MixinSodiumOpaqueVkFrame.java @@ -26,7 +26,6 @@ // holds vanilla terrain — exactly the state Voxy's VK frame needs. @Mixin(value = SodiumWorldRenderer.class, remap = false) public class MixinSodiumOpaqueVkFrame { - private static boolean voxy$loggedFirstFrame = false; @Inject(method = "drawChunkLayer", at = @At("TAIL"), remap = false) private void voxy$renderVkFrame(ChunkSectionLayerGroup group, ChunkRenderMatrices matrices, @@ -37,10 +36,6 @@ public class MixinSodiumOpaqueVkFrame { var renderer = IVoxyRenderSystemHolder.getNullable(); if (renderer == null || renderer.vkCore == null) return; - if (!voxy$loggedFirstFrame) { - voxy$loggedFirstFrame = true; - Logger.info("Voxy VK frame hook fired (SodiumWorldRenderer.drawChunkLayer OPAQUE TAIL) - rendering LODs"); - } try { renderer.vkCore.renderFrame(group.outputTarget(), adapter, matrices, x, y, z); } catch (Throwable t) { diff --git a/src/main/java/me/cortex/voxy/common/CmpLog.java b/src/main/java/me/cortex/voxy/common/CmpLog.java deleted file mode 100644 index d9ebd046b..000000000 --- a/src/main/java/me/cortex/voxy/common/CmpLog.java +++ /dev/null @@ -1,80 +0,0 @@ -package me.cortex.voxy.common; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -//Opt-in structured logging for GL<-->VK A/B parity comparison. -// -//Enable with -Dvoxy.cmplog=. Both the OpenGL and Vulkan render paths then -// emit the SAME semantic per-frame quantities (section/geometry counts, traversal -// request counts, ...) as tab-separated records. For a stationary camera at a -// fixed viewpoint these values converge to identical integers when the VK -// translation is faithful, so ab_compare.py compare-logs can flag any divergence -// exactly, without the rounding noise that muddies a pixel diff. -// -//When the property is unset every method is a cheap no-op (one static-field -// read), so this stays compiled into normal builds at negligible cost. -// -//Record format (one per line): VOXYCMP\t\t\t\t -public final class CmpLog { - private static final String PATH = System.getProperty("voxy.cmplog"); - /** True when {@code -Dvoxy.cmplog=} was supplied. */ - public static final boolean ENABLED = PATH != null && !PATH.isEmpty(); - - /** Set by each render core so the log header records which backend produced it. */ - public static volatile String backend = "unknown"; - - private static BufferedWriter writer; - private static boolean broken = false; - private static int frame = 0; - - private CmpLog() {} - - private static BufferedWriter writer() { - if (writer == null && !broken) { - try { - Path p = Path.of(PATH).toAbsolutePath(); - if (p.getParent() != null) { - Files.createDirectories(p.getParent()); - } - writer = Files.newBufferedWriter(p, StandardCharsets.UTF_8); - writer.write("#VOXYCMP backend=" + backend + "\n"); - writer.flush(); - Logger.info("CmpLog: writing A/B comparison log to " + PATH - + " (backend=" + backend + ")"); - } catch (IOException e) { - broken = true; - Logger.error("CmpLog: failed to open " + PATH, e); - } - } - return writer; - } - - /** Advance to the next frame and flush; call once per rendered frame. */ - public static void nextFrame() { - if (!ENABLED) return; - synchronized (CmpLog.class) { - frame++; - if (writer != null) { - try { writer.flush(); } catch (IOException e) { broken = true; } - } - } - } - - /** Record a semantic quantity for the current frame. */ - public static void rec(String phase, String key, long value) { - if (!ENABLED) return; - synchronized (CmpLog.class) { - var w = writer(); - if (w == null) return; - try { - w.write("VOXYCMP\t" + frame + "\t" + phase + "\t" + key + "\t" + value + "\n"); - } catch (IOException e) { - broken = true; - } - } - } -} From 7e1ccb83f6cb98c178a52e6e63b3ba25f84915b5 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Wed, 22 Jul 2026 13:17:09 -0400 Subject: [PATCH 09/12] vk/mac: MoltenVK correctness + first-frame loading glitch fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six MoltenVK-specific fixes that resolve the macOS '2D floating blocks' loading glitch and several latent Vulkan correctness issues: 1. VkTerrainRenderer: gate fixedCountBudget on hasAnyReadback so the first frame with geometry issues 0 draws (not 1024+ no-op Metal draws from the zeroed drawCallBuffer). renderOpaque runs BEFORE buildDrawCalls and uses last frame's commands; on MoltenVK each no-op slot becomes a real Metal drawIndexedPrimitives call, causing the glitch. Desktop Vulkan is unaffected (vkCmdDrawIndexedIndirectCount reads count=0). 2. VkTerrainRenderer: init fbOpaqueDraws/fbTranslucentDraws/fbTemporalDraws to 0 instead of MAX caps (reduces no-op count on subsequent frames too). 3. VkImage2D: UNDEFINED-layout images get TOP_OF_PIPE/0 in transition() and transitionBatch() (no prior producer — contents discarded). Fixes MoltenVK strictness on first-use image barriers. 4. VulkanContext: fix subgroup properties query to use vkGetPhysicalDeviceProperties2 (not Features2 — was silently returning all-zeroes on every device). Gate subgroup paths off via ENABLE_SUBGROUP_PATHS=false until validated (hiz_subgroup.comp has a suspect subgroupBarrier with 4/256 active lanes). 5. VkFrameHost: transition DEPTH|STENCIL aspect together (not DEPTH only) since separateDepthStencilLayouts is never enabled. MoltenVK requires both aspects in the barrier. 6. VkTerrainRenderer: prep->cmdgen barrier now includes COMPUTE in dst stages (cmdgen's atomics are RMW reads of prep's zeroes). Without it, cmdgen could see stale per-frame counters on MoltenVK, placing draw commands at wrong offsets. 7. VkTraversal: WAR barrier between download copy and requestBuffer fill (Metal blit encoders don't serialise like desktop transfer queues). Zero impact on desktop Vulkan: hasDrawIndirectCount=true skips the fixed-count path entirely, and the barrier fixes are strict supersets of the original scopes. --- .../cortex/voxy/client/core/vk/VkImage2D.java | 21 ++++++---- .../voxy/client/core/vk/VulkanContext.java | 42 +++++++++++++++++-- .../client/core/vk/render/VkFrameHost.java | 13 +++--- .../core/vk/render/VkTerrainRenderer.java | 39 ++++++++++++----- .../client/core/vk/render/VkTraversal.java | 8 ++++ 5 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java index ce5e1a879..c584aed43 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java @@ -100,8 +100,13 @@ private long createView(MemoryStack stack, VulkanContext vctx, int baseMip, int return pView.get(0); } - /** Whole-image layout transition recorded into the current frame commands. */ + /** Whole-image layout transition. UNDEFINED-layout images get TOP_OF_PIPE/0 + * (no prior producer to synchronize — contents are discarded). */ public void transition(int newLayout, int srcStage, int srcAccess, int dstStage, int dstAccess) { + if (this.currentLayout == VK_IMAGE_LAYOUT_UNDEFINED) { + srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + srcAccess = 0; + } try (MemoryStack stack = stackPush()) { var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() .srcAccessMask(srcAccess).dstAccessMask(dstAccess) @@ -115,27 +120,27 @@ public void transition(int newLayout, int srcStage, int srcAccess, int dstStage, } //Batched transition: records multiple images' layout changes in a single - // vkCmdPipelineBarrier (one call instead of N). Each image's transition is - // described by its own (newLayout, srcStage, srcAccess, dstStage, dstAccess) - // tuple; the call uses the union of all src stages -> union of all dst stages - // so the single barrier covers every image's dependency. + // vkCmdPipelineBarrier. UNDEFINED-layout images get TOP_OF_PIPE/0 (see transition()). public record BatchEntry(VkImage2D image, int newLayout, int srcAccess, int dstAccess) {} public static void transitionBatch(java.util.List entries, int unionSrcStage, int unionDstStage) { if (entries.isEmpty()) return; + boolean anyUndefined = false; try (MemoryStack stack = stackPush()) { var imbs = VkImageMemoryBarrier.calloc(entries.size(), stack); for (int i = 0; i < entries.size(); i++) { var e = entries.get(i); + boolean undef = e.image.currentLayout == VK_IMAGE_LAYOUT_UNDEFINED; + if (undef) anyUndefined = true; imbs.get(i).sType$Default() - .srcAccessMask(e.srcAccess).dstAccessMask(e.dstAccess) + .srcAccessMask(undef ? 0 : e.srcAccess).dstAccessMask(e.dstAccess) .oldLayout(e.image.currentLayout).newLayout(e.newLayout) .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED).dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .image(e.image.image) .subresourceRange().aspectMask(e.image.aspect).levelCount(e.image.mipLevels).layerCount(1); } - //Use the first image's ctx (all images share the same VkFrameCtx in Voxy). var cmd = entries.get(0).image.ctx.cmd(); - vkCmdPipelineBarrier(cmd, unionSrcStage, unionDstStage, 0, null, null, imbs); + int actualSrcStage = anyUndefined ? unionSrcStage | VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : unionSrcStage; + vkCmdPipelineBarrier(cmd, actualSrcStage, unionDstStage, 0, null, null, imbs); for (var e : entries) e.image.currentLayout = e.newLayout; } } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java index 41dec05d9..121931e64 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java @@ -10,6 +10,7 @@ import org.lwjgl.vulkan.VkPhysicalDeviceFeatures2; import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; import org.lwjgl.vulkan.VkPhysicalDeviceProperties; +import org.lwjgl.vulkan.VkPhysicalDeviceProperties2; import org.lwjgl.vulkan.VkPhysicalDeviceSubgroupProperties; import org.lwjgl.vulkan.VkPhysicalDeviceVulkan12Features; import org.lwjgl.vulkan.VkQueue; @@ -31,6 +32,29 @@ public final class VulkanContext { public final VkQueue queue; public final int queueFamily; public final boolean hasDrawIndirectCount; + //MASTER SWITCH for every subgroup-dependent VK path. Keep FALSE. + // + //querySubgroupProperties() used to chain a properties struct into + // vkGetPhysicalDeviceFeatures2, which silently returned all-zeroes, so + // subgroupArithmetic was false on EVERY device and the subgroup paths were + // dead code that had never once executed. Correcting the query turned all + // three of them on at once: + // + // VkHiZ - hiz_subgroup.comp builds the HiZ pyramid + // VkTraversal - traversal workgroup size 32 -> 64 + // VkTerrainRenderer- subgroup prefix-sum variant for translucent sorting + // + //That regressed rendering on both desktop and macOS: terrain flickered with + // only far LODs surviving, consistent with a bad HiZ pyramid feeding the + // traversal's occlusion test. hiz_subgroup.comp is also independently + // suspect — its comments assume a 64-wide subgroup, and its subgroupBarrier() + // at the mip_5/mip_6 tail runs with only 4 of 256 lanes still active, which + // SPIR-V requires to be subgroup-uniform. + // + //Flip to true only after validating each of the three paths on its own + // against the non-subgroup path (ab_compare.py) at several subgroup widths. + private static final boolean ENABLE_SUBGROUP_PATHS = false; + public final boolean subgroupArithmetic; public final int subgroupSize; public final String deviceName; @@ -48,8 +72,14 @@ private VulkanContext(IVkHost host) { this.hasDrawIndirectCount = queryDrawIndirectCount(this.physicalDevice); var subgroup = querySubgroupProperties(this.physicalDevice); this.subgroupProps = subgroup; - this.subgroupArithmetic = subgroup != null && (subgroup.supportedOperations() & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0; this.subgroupSize = subgroup != null ? subgroup.subgroupSize() : 1; + int ops = subgroup != null ? subgroup.supportedOperations() : 0; + int stages = subgroup != null ? subgroup.supportedStages() : 0; + int needOps = VK_SUBGROUP_FEATURE_ARITHMETIC_BIT | VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_CLUSTERED_BIT; + boolean deviceSupportsSubgroups = (ops & needOps) == needOps + && (stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 + && this.subgroupSize >= 16; + this.subgroupArithmetic = ENABLE_SUBGROUP_PATHS && deviceSupportsSubgroups; String name; try (MemoryStack stack = stackPush()) { var props = VkPhysicalDeviceProperties.calloc(stack); @@ -66,6 +96,7 @@ private VulkanContext(IVkHost host) { Logger.info("Voxy Vulkan context adopted Minecraft device: " + this.deviceName + " (drawIndirectCount=" + this.hasDrawIndirectCount + ", subgroupArithmetic=" + this.subgroupArithmetic + + " (deviceCapable=" + deviceSupportsSubgroups + ", gate=" + ENABLE_SUBGROUP_PATHS + ")" + ", subgroupSize=" + this.subgroupSize + ")"); } @@ -80,9 +111,14 @@ private static boolean queryDrawIndirectCount(VkPhysicalDevice pd) { private static VkPhysicalDeviceSubgroupProperties querySubgroupProperties(VkPhysicalDevice pd) { try (MemoryStack stack = stackPush()) { + //VkPhysicalDeviceSubgroupProperties is a PROPERTIES struct: it extends + // VkPhysicalDeviceProperties2, not VkPhysicalDeviceFeatures2. Chaining it + // into vkGetPhysicalDeviceFeatures2 is invalid usage (VUID-VkPhysicalDeviceFeatures2-pNext-pNext) + // and leaves the struct at its calloc'd zeroes, so supportedOperations + // read back 0 and subgroup support was reported absent on EVERY device. var sg = VkPhysicalDeviceSubgroupProperties.calloc(stack).sType$Default(); - var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(sg.address()); - VK11.vkGetPhysicalDeviceFeatures2(pd, f2); + var p2 = VkPhysicalDeviceProperties2.calloc(stack).sType$Default().pNext(sg.address()); + VK11.vkGetPhysicalDeviceProperties2(pd, p2); // Return a malloc'd copy so the caller can read it past the stack frame. var copy = VkPhysicalDeviceSubgroupProperties.malloc(); copy.set(sg); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java index 8e80db0f7..4350f8eb4 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java @@ -32,12 +32,9 @@ public static int vkFormat(GpuTextureView view) { } //Layout-transition one of MC's own images (colour/depth attachment) with - // scoped stage/access masks matching the actual producer/consumer — MC just - // rendered into the depth attachment (LATE_FRAGMENT_TESTS write), and Voxy - // samples it (FRAGMENT_SHADER read), so the previous ALL_COMMANDS masks - // (which serialised the transition with unrelated compute) are narrowed. - // Used to bracket sampling of MC's attachments mid-frame (they live in - // ATTACHMENT_OPTIMAL otherwise). + // scoped stage/access masks matching the actual producer/consumer. + // Depth-stencil images transition both aspects together (DEPTH|STENCIL) + // since Voxy never enables separateDepthStencilLayouts. public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, boolean depth, int oldLayout, int newLayout) { try (MemoryStack stack = stackPush()) { @@ -73,6 +70,8 @@ public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, dstAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; } } + //Depth-stencil: transition both aspects together. + int aspectMask = depth ? (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) : VK_IMAGE_ASPECT_COLOR_BIT; var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() .srcAccessMask(srcAccess).dstAccessMask(dstAccess) .oldLayout(oldLayout).newLayout(newLayout) @@ -80,7 +79,7 @@ public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .image(image); imb.subresourceRange() - .aspectMask(depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT) + .aspectMask(aspectMask) .levelCount(VK_REMAINING_MIP_LEVELS) .layerCount(VK_REMAINING_ARRAY_LAYERS); vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, null, null, imb); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java index b4d387253..06442de0f 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java @@ -46,13 +46,18 @@ public class VkTerrainRenderer { //MoltenVK fixed-count fallback state: the last-known REAL per-pass draw counts, // read back from drawCountCallBuffer each frame. On desktop the GPU sources - // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to the - // per-pass caps so the first few frames — before the first readback lands — - // behave like the old section-count fallback, then converge to the true - // visible counts. - private int fbOpaqueDraws = VkViewport.OPAQUE_DRAW_COUNT; - private int fbTranslucentDraws = VkViewport.TRANSLUCENT_DRAW_COUNT; - private int fbTemporalDraws = VkViewport.TEMPORAL_DRAW_COUNT; + // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to 0 and + // gated by hasAnyReadback: before the first async readback lands, fixedCountBudget + // returns 0 — no draws at all — so renderOpaque (which runs BEFORE buildDrawCalls + // and uses last frame's commands) skips entirely on the first frame with geometry. + // Without this, the headroom floor (1024/256/256) would issue 1024+ no-op Metal + // draws/frame from the zeroed drawCallBuffer, causing the "2D floating blocks" + // loading glitch on macOS. Once the first readback lands, hasAnyReadback flips + // true and fixedCountBudget operates normally (lastKnown*1.5 + headroom). + private int fbOpaqueDraws = 0; + private int fbTranslucentDraws = 0; + private int fbTemporalDraws = 0; + private boolean hasAnyReadback = false; private final VkBuffer uniform; private final VkBuffer distanceCountBuffer; @@ -253,9 +258,21 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prep (compute) wrote the cull indirect args; the raster-cull draw - // reads them. Scope to COMPUTE -> DRAW_INDIRECT|VERTEX_INPUT. - this.ctx.computeToDrawBarrier(); + //prep (compute) wrote drawCountCallBuffer, which has TWO consumers: + // the raster-cull draw below reads cullDrawIndirectCommand (@24) from + // DRAW_INDIRECT, and cmdgen atomicAdds opaque/translucent/temporal + // DrawCount (@12/@16/@20) from COMPUTE. computeToDrawBarrier() scopes + // the destination to DRAW_INDIRECT|VERTEX only, so prep -> cmdgen had + // no memory dependency at all: cmdgen's atomics could observe the + // PREVIOUS frame's counters instead of prep's zeroes, placing draw + // commands past their pass region with a baseInstance that no longer + // matches the positionBuffer slot written for that section. Desktop + // drivers happened to make the writes visible anyway; MoltenVK does + // not. Include COMPUTE (read+write, the atomics are RMW) in the dst. + this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT + | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); } {//raster occlusion test into the visibility buffer (depth-tested box draw, no writes) @@ -343,6 +360,7 @@ public void buildDrawCalls(VkViewport viewport) { this.fbOpaqueDraws = clampCount(MemoryUtil.memGetInt(ptr), VkViewport.OPAQUE_DRAW_COUNT); this.fbTranslucentDraws = clampCount(MemoryUtil.memGetInt(ptr + 4), VkViewport.TRANSLUCENT_DRAW_COUNT); this.fbTemporalDraws = clampCount(MemoryUtil.memGetInt(ptr + 8), VkViewport.TEMPORAL_DRAW_COUNT); + this.hasAnyReadback = true; }); } } @@ -367,6 +385,7 @@ private static int clampCount(int value, int cap) { */ private int fixedCountBudget(int lastKnownCount, int headroom, int cap) { if (this.ctx.vk().hasDrawIndirectCount) return cap; + if (!this.hasAnyReadback) return 0;//first frame: no draw calls generated yet int budget = (int) (lastKnownCount * 1.5f) + headroom; return Math.min(cap, Math.max(0, budget)); } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java index 90b446b38..2563231e0 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java @@ -239,6 +239,14 @@ public void doTraversal(VkViewport viewport) { //Download + reset the request queue this.downloadStream.download(this.requestBuffer, this::forwardDownloadResult); + //WAR: download() already recorded the vkCmdCopyBuffer that READS + // requestBuffer, and the fill below WRITES it. Two transfer commands in + // one command buffer are not implicitly ordered, so without this barrier + // the reset could land before/while the copy reads — zeroing the request + // count and silently dropping a frame of node requests. Desktop drivers + // serialise blits in practice; Metal's blit encoders need not. + this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); vkCmdFillBuffer(this.ctx.cmd(), this.requestBuffer.buffer, 0, 4, 0); //The download copy read the requestBuffer (TRANSFER read); the fill // resets it (TRANSFER write). The next reader is next frame's traversal From 00939a93e4b435b4f942f9a4cf1b7abfc6c2dff6 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Wed, 22 Jul 2026 18:48:45 -0400 Subject: [PATCH 10/12] Fix MoltenVK terrain rendering: replace readback/ratchet logic with section-derived caps and zeroed trailing slots - Remove unused fbDraws/fbMax/hasAnyReadback ratchet fields and VkDownloadStream import - Remove fixedCountBudget() wrapper (was identity), inline cap as maxDraw - MoltenVK fallback: zero only cap-sized regions of drawCallBuffer per frame - Include COMPUTE stage in prep->cmdgen barrier for MoltenVK atomic visibility - Compact all comments to single-line annotations --- .../client/core/vk/render/VkRenderCore.java | 2 +- .../core/vk/render/VkTerrainRenderer.java | 182 ++++-------------- 2 files changed, 37 insertions(+), 147 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java index 6b25db18c..600f8799d 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java @@ -96,7 +96,7 @@ public VkRenderCore(WorldEngine world, ServiceManager sm) { this.nodeCleaner = new VkNodeCleaner(this.frameCtx, this.uploadStream, this.downloadStream, this.nodeManager); this.traversal = new VkTraversal(this.frameCtx, this.uploadStream, this.downloadStream, this.properties, this.nodeManager, this.nodeCleaner, this.renderGen); - this.terrainRenderer = new VkTerrainRenderer(this.frameCtx, this.uploadStream, this.downloadStream, + this.terrainRenderer = new VkTerrainRenderer(this.frameCtx, this.uploadStream, this.properties, this.geometryData, this.modelStore); this.compositor = new VkCompositor(this.frameCtx, this.uploadStream, this.properties, VoxyConfig.CONFIG.useEnvironmentalFog); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java index 06442de0f..2f56c37a8 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java @@ -4,7 +4,6 @@ import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; import me.cortex.voxy.client.core.vk.VkBuffer; import me.cortex.voxy.client.core.vk.VkCmd; -import me.cortex.voxy.client.core.vk.VkDownloadStream; import me.cortex.voxy.client.core.vk.VkFrameCtx; import me.cortex.voxy.client.core.vk.VkImage2D; import me.cortex.voxy.client.core.vk.VkShaderPipeline; @@ -27,37 +26,19 @@ import static org.lwjgl.vulkan.VK10.*; import static org.lwjgl.vulkan.VK12.vkCmdDrawIndexedIndirectCount; -//Pure-VK mirror of MDICSectionRenderer: the same six GPU passes (prep, -// raster-cull, command generation, translucency prefix sort + build, then -// opaque / temporal / translucent indexed-indirect-count draws) against the -// same buffer layouts, recorded into MC's frame command buffer with dynamic -// rendering over Voxy's offscreen colour + depth-stencil targets. +//Pure-VK mirror of MDICSectionRenderer. public class VkTerrainRenderer { - //Draw-command offsets shared with the cmdgen/translucent shader defines. + //Draw-command offsets shared with cmdgen/translucent shaders. private static final int TRANSLUCENT_OFFSET = VkViewport.OPAQUE_DRAW_COUNT; private static final int TEMPORAL_OFFSET = TRANSLUCENT_OFFSET + VkViewport.TRANSLUCENT_DRAW_COUNT; private final VkFrameCtx ctx; private final VkUploadStream uploadStream; - private final VkDownloadStream downloadStream; private final RenderProperties properties; private final VkSectionGeometryData geometry; private final VkModelStore modelStore; - //MoltenVK fixed-count fallback state: the last-known REAL per-pass draw counts, - // read back from drawCountCallBuffer each frame. On desktop the GPU sources - // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to 0 and - // gated by hasAnyReadback: before the first async readback lands, fixedCountBudget - // returns 0 — no draws at all — so renderOpaque (which runs BEFORE buildDrawCalls - // and uses last frame's commands) skips entirely on the first frame with geometry. - // Without this, the headroom floor (1024/256/256) would issue 1024+ no-op Metal - // draws/frame from the zeroed drawCallBuffer, causing the "2D floating blocks" - // loading glitch on macOS. Once the first readback lands, hasAnyReadback flips - // true and fixedCountBudget operates normally (lastKnown*1.5 + headroom). - private int fbOpaqueDraws = 0; - private int fbTranslucentDraws = 0; - private int fbTemporalDraws = 0; - private boolean hasAnyReadback = false; + //MoltenVK fallback: uses fixed drawCount with zeroed trailing slots as no-ops. private final VkBuffer uniform; private final VkBuffer distanceCountBuffer; @@ -74,11 +55,10 @@ public class VkTerrainRenderer { private VkShaderPipeline terrainTranslucent; private int pipelineColorFormat = -1, pipelineDepthFormat = -1; - public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownloadStream downloadStream, + public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, RenderProperties properties, VkSectionGeometryData geometry, VkModelStore modelStore) { this.ctx = ctx; this.uploadStream = uploadStream; - this.downloadStream = downloadStream; this.properties = properties; this.geometry = geometry; this.modelStore = modelStore; @@ -86,7 +66,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownload this.uniform = new VkBuffer(ctx, 1024).zero(); this.distanceCountBuffer = new VkBuffer(ctx, 1024L * 4 + VkViewport.TRANSLUCENT_DRAW_COUNT * 4L).zero(); - //Shared index buffer: u16 quad pattern + u16 cube indices at CUBE_INDEX_OFFSET + //Shared index buffer: u16 quad + cube indices. this.indexBuffer = new VkBuffer(ctx, SharedIndexBuffer.CUBE_INDEX_OFFSET + 6 * 2 * 3 * 2L); { var quads = SharedIndexBuffer.generateQuadIndicesShort(16380); @@ -100,7 +80,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownload this.depthBoundSampler = VkImage2D.createSampler(ctx.vk(), false, false); this.lightmapSampler = VkImage2D.createSampler(ctx.vk(), false, true); - //================= compute pipelines ================= + //Compute pipelines. this.prep = new VkShaderPipeline(ctx, "prep.comp", VkShaderSource.load("voxy:lod/gl46/prep.comp", VkShaderSource.defs().build()), 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2))); @@ -115,9 +95,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownload VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5), VkShaderPipeline.ssbo(6), VkShaderPipeline.ssbo(7))); - //Subgroup prefix sum on VK when the device advertises subgroup arithmetic - // (MoltenVK/Metal simdgroups, NVIDIA, AMD, Intel — virtually every VK 1.1+ - // device). Falls back to the shared-memory Hillis-Steele scan otherwise. + //Subgroup prefix sum if available, else shared-memory fallback. boolean useSubgroup = ctx.vk().subgroupArithmetic; this.prefixSum = new VkShaderPipeline(ctx, "prefixsum.comp", VkShaderSource.load(useSubgroup ? "voxy:util/prefixsum/inital3_vk.comp" : "voxy:util/prefixsum/simple.comp", @@ -133,7 +111,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownload 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5))); - //================= raster cull pipeline (depth-only, no writes) ================= + //Raster cull pipeline (depth-only). var cullDesc = new VkShaderPipeline.GfxDesc(); cullDesc.name = "cullraster"; cullDesc.vertGlsl = VkShaderSource.load("voxy:lod/gl46/cull/raster.vert", VkShaderSource.defs().props(properties).build()); @@ -182,7 +160,7 @@ private void ensureTerrainPipelines(VkViewport viewport) { opaque.depthWrite = true; opaque.depthCompare = VkCmd.closerEqual(this.properties); opaque.blend = false; - opaque.stencilTestEqual1 = true;//only render where vanilla terrain is absent + opaque.stencilTestEqual1 = true; opaque.bindings = bindings; this.terrainOpaque = new VkShaderPipeline(this.ctx, opaque); @@ -206,7 +184,7 @@ private void ensureTerrainPipelines(VkViewport viewport) { this.pipelineDepthFormat = df; } - //================================================================================== + //--- private final Matrix4f uniformScratch = new Matrix4f(); public void uploadUniform(VkViewport viewport) { @@ -224,27 +202,21 @@ public void uploadUniform(VkViewport viewport) { this.uploadStream.commit(); } - /** Mirrors MDIC buildDrawCalls: prep -> raster cull -> cmdgen -> translucency sort. */ + /** Mirrors MDIC buildDrawCalls. */ public void buildDrawCalls(VkViewport viewport) { if (this.geometry.getSectionCount() == 0) return; var cmd = this.ctx.cmd(); this.uploadUniform(viewport); if (!this.ctx.vk().hasDrawIndirectCount) { - //Fixed-count fallback (MoltenVK): renderTerrain issues - // vkCmdDrawIndexedIndirect with maxDrawCount rather than a GPU-sourced - // count, so trailing slots past the actual command count would be read - // as stale draw commands from the previous frame. Zero the three - // drawCallBuffer slices (opaque / temporal / translucent) here so any - // un-overwritten slot reads as instanceCount=0 (a no-op draw). The - // cmdgen compute below then writes the real commands on top; the fill - // completes (with a barrier) before the cmdgen dispatch reads the buffer. - // Gated to the fallback path only — the tight-count path (desktop Vulkan) - // never reads past the actual count, so it pays nothing here. + //MoltenVK fallback: zero trailing slots so stale draws read as no-ops. long stride = 5L * 4; - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, 0L, VkViewport.OPAQUE_DRAW_COUNT * stride, 0); - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TEMPORAL_OFFSET * stride, VkViewport.TEMPORAL_DRAW_COUNT * stride, 0); - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TRANSLUCENT_OFFSET * stride, VkViewport.TRANSLUCENT_DRAW_COUNT * stride, 0); + int opaqueCap = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); + int temporalCap = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); + int translucentCap = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, 0L, opaqueCap * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TEMPORAL_OFFSET * stride, temporalCap * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TRANSLUCENT_OFFSET * stride, translucentCap * stride, 0); this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); } @@ -258,25 +230,15 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prep (compute) wrote drawCountCallBuffer, which has TWO consumers: - // the raster-cull draw below reads cullDrawIndirectCommand (@24) from - // DRAW_INDIRECT, and cmdgen atomicAdds opaque/translucent/temporal - // DrawCount (@12/@16/@20) from COMPUTE. computeToDrawBarrier() scopes - // the destination to DRAW_INDIRECT|VERTEX only, so prep -> cmdgen had - // no memory dependency at all: cmdgen's atomics could observe the - // PREVIOUS frame's counters instead of prep's zeroes, placing draw - // commands past their pass region with a baseInstance that no longer - // matches the positionBuffer slot written for that section. Desktop - // drivers happened to make the writes visible anyway; MoltenVK does - // not. Include COMPUTE (read+write, the atomics are RMW) in the dst. + //prep -> cmdgen: include COMPUTE in dst barrier (atomics need it on MoltenVK). this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); } - {//raster occlusion test into the visibility buffer (depth-tested box draw, no writes) - this.beginRendering(cmd, viewport, 0L, true);//depth-only + {//raster occlusion cull (depth-only) + this.beginRendering(cmd, viewport, 0L, true); this.cullRaster.bind(cmd); VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); try (var b = this.cullRaster.binder()) { @@ -289,16 +251,13 @@ public void buildDrawCalls(VkViewport viewport) { vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); vkCmdDrawIndexedIndirect(cmd, viewport.drawCountCallBuffer.buffer, 6 * 4, 1, 20); vkCmdEndRenderingKHR(cmd); - //The raster-cull draw wrote visibilityData (SSBO) from the fragment - // shader; the consumer is the cmdgen compute. Scope to those stages - // instead of the previous fullBarrier (ALL_COMMANDS -> ALL_COMMANDS) - // which forced a full pipeline stall on every frame. + //Cull fragment -> cmdgen compute: scoped barrier. this.ctx.barrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); } - {//command generation (indirect dispatch sized by prep) + {//command generation vkCmdFillBuffer(cmd, this.distanceCountBuffer.buffer, 0, 1024L * 4, 0); this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); @@ -315,8 +274,7 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); - //cmdgen -> prefixsum: both compute. The draw that consumes these - // indirect commands is guarded by renderTerrain's own barrier. + //cmdgen -> prefixsum: compute barrier. this.ctx.computeToComputeBarrier(); } @@ -326,7 +284,7 @@ public void buildDrawCalls(VkViewport viewport) { b.ssbo(0, this.distanceCountBuffer).push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prefixsum -> translucentGen: both compute. + //prefixsum -> translucentGen: compute barrier. this.ctx.computeToComputeBarrier(); this.translucentGen.bind(cmd); @@ -340,84 +298,32 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); - //No trailing barrier here: the translucent commands written into - // drawCallBuffer are read by renderTranslucent's renderTerrain, which - // issues its own COMPUTE|TRANSFER -> DRAW_INDIRECT|VERTEX|FRAGMENT - // barrier (line ~355) before drawing. The previous computeToAllBarrier - // here was redundant with that draw barrier and serialised the GPU. + //No trailing barrier: renderTerrain issues its own. } - if (!this.ctx.vk().hasDrawIndirectCount) { - //Read the three real per-pass draw counts back to the CPU so next frame's - // fixed-count multi-draws track them instead of the worst-case section - // cap (see fixedCountBudget). The counts live at opaque@12 / translucent@16 - // / temporal@20 in drawCountCallBuffer; download those 12 bytes on the same - // async, event-retired path the traversal request readback already uses - // (commit() scopes its own COMPUTE->TRANSFER->HOST barriers). The callback - // runs on the render thread from pollRetired, so the plain field writes are - // race-free. Desktop (drawIndirectCount) neither needs nor issues this. - this.downloadStream.download(viewport.drawCountCallBuffer, 12, 12, (ptr, size) -> { - this.fbOpaqueDraws = clampCount(MemoryUtil.memGetInt(ptr), VkViewport.OPAQUE_DRAW_COUNT); - this.fbTranslucentDraws = clampCount(MemoryUtil.memGetInt(ptr + 4), VkViewport.TRANSLUCENT_DRAW_COUNT); - this.fbTemporalDraws = clampCount(MemoryUtil.memGetInt(ptr + 8), VkViewport.TEMPORAL_DRAW_COUNT); - this.hasAnyReadback = true; - }); - } - } - - private static int clampCount(int value, int cap) { - return value < 0 ? 0 : Math.min(value, cap); - } - - /** - * Draw-count upper bound for one terrain pass. On desktop Vulkan the GPU sources - * the real count from drawCountCallBuffer (vkCmdDrawIndexedIndirectCount), so the - * section-derived {@code cap} is only a ceiling and is returned unchanged. On - * MoltenVK — which lacks drawIndirectCount — the fixed-count multi-draw instead - * iterates whatever count it is handed, encoding one Metal draw per slot; handing - * it the section-count ceiling means tens of thousands of no-op draws every frame, - * invariant to where the camera looks (this is why sky/occlusion culling produced - * no Mac speed-up). Bounding it to the last-known real count plus headroom lets the - * culling actually reduce Mac draw cost. The full drawCallBuffer is still zeroed per - * frame, so every slot within the ceiling reads either a real command or a no-op — - * a transient under-estimate during fast camera motion only drops a few LOD draws - * for a frame or two, never reads stale geometry. - */ - private int fixedCountBudget(int lastKnownCount, int headroom, int cap) { - if (this.ctx.vk().hasDrawIndirectCount) return cap; - if (!this.hasAnyReadback) return 0;//first frame: no draw calls generated yet - int budget = (int) (lastKnownCount * 1.5f) + headroom; - return Math.min(cap, Math.max(0, budget)); } public void renderOpaque(VkViewport viewport, boolean clearTargets) { - //Build pipelines BEFORE the section-count guard: within a frame geometry is - // uploaded after this call (nodeManager.tick/buildDrawCalls), so renderTemporal/ - // renderTranslucent can see sectionCount>0 later this same frame. If we skipped - // ensure here when the count is momentarily 0, those calls would bind a null - // pipeline. ensureTerrainPipelines is idempotent (no-op once built for the format). + //Ensure pipelines before section-count guard (idempotent). this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; this.uploadUniform(viewport); - int cap = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); - int maxDraw = this.fixedCountBudget(this.fbOpaqueDraws, 1024, cap); + int maxDraw = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, 0, 4 * 3, maxDraw, clearTargets); } public void renderTemporal(VkViewport viewport) { this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; - int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); - int maxDraw = this.fixedCountBudget(this.fbTemporalDraws, 256, cap); + int maxDraw = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, TEMPORAL_OFFSET * 5L * 4, 4 * 5, maxDraw, false); } - /** Translucents draw onto the SSAO output (mirrors the GL fbSSAO target). */ + /** Translucents draw onto SSAO output. */ public void renderTranslucent(VkViewport viewport) { this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; - int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); - int maxDraw = this.fixedCountBudget(this.fbTranslucentDraws, 256, cap); + int maxDraw = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); this.renderTerrain(viewport, viewport.colourSSAO.view, this.terrainTranslucent, TRANSLUCENT_OFFSET * 5L * 4, 4 * 4, maxDraw, false); } @@ -429,7 +335,7 @@ private void renderTerrain(VkViewport viewport, long colorView, VkShaderPipeline VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDEX_READ_BIT); - this.beginRendering(cmd, viewport, colorView, !clear);//LOAD unless first pass (which cleared via compositor setup) + this.beginRendering(cmd, viewport, colorView, !clear); //LOAD unless first pass pipeline.bind(cmd); VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); long lightmapView = VkFrameHost.lightmapView(); @@ -446,33 +352,19 @@ private void renderTerrain(VkViewport viewport, long colorView, VkShaderPipeline } vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); if (this.ctx.vk().hasDrawIndirectCount) { - //Tight-count path: the GPU reads the actual draw count from the count - // buffer each frame. Requires the drawIndirectCount Vulkan 1.2 feature - // to be enabled on the device — desktop Vulkan (NVIDIA/AMD/Intel) enables - // it; MoltenVK does not (see the else branch). + //Desktop: tight count from GPU buffer. vkCmdDrawIndexedIndirectCount(cmd, viewport.drawCallBuffer.buffer, indirectOffset, viewport.drawCountCallBuffer.buffer, drawCountOffset, maxDrawCount, 5 * 4); } else { - //MoltenVK/macOS fixed-count fallback: drawIndirectCount is not enabled - // on MC's adopted Vulkan device (MC's DeviceFeatures record does not - // even track it), and calling the function without the feature enabled - // is invalid usage that MoltenVK degenerates. The drawCallBuffer slice - // for this pass is zeroed per frame in buildDrawCalls (gated to this - // fallback path) so trailing slots past the actual command count read - // instanceCount=0 (no-op draws); a fixed-count multi-draw over the - // clamped maxDrawCount is therefore correct, just less tight. + //MoltenVK: fixed count, trailing slots zeroed as no-ops. vkCmdDrawIndexedIndirect(cmd, viewport.drawCallBuffer.buffer, indirectOffset, maxDrawCount, 5 * 4); } vkCmdEndRenderingKHR(cmd); } - /** - * Begin dynamic rendering over the offscreen targets (always LOAD; clears - * happen in the depth-setup pass). {@code colorView} selects the colour - * attachment (main colour vs SSAO output); 0 = depth-only. - */ + /** Begin dynamic rendering. colorView=0 for depth-only. */ private void beginRendering(VkCommandBuffer cmd, VkViewport viewport, long colorView, boolean load) { try (MemoryStack stack = stackPush()) { @@ -513,9 +405,7 @@ public void free() { this.terrainOpaque.free(); this.terrainTranslucent.free(); } - //depthBoundSampler/lightmapSampler come from VkImage2D.createSampler's - // device-lifetime cache (shared handles); never destroy them per-object - // (multi-free vkDestroySampler -> SIGSEGV on world unload). + //Samplers are device-lifetime cached; don't destroy per-object. this.uniform.free(); this.distanceCountBuffer.free(); this.indexBuffer.free(); From bee1ed7dd04c121511e401000c1f22a41a256930 Mon Sep 17 00:00:00 2001 From: cochcoder Date: Wed, 22 Jul 2026 21:28:44 -0400 Subject: [PATCH 11/12] Revert "Fix MoltenVK terrain rendering: replace readback/ratchet logic with section-derived caps and zeroed trailing slots" This reverts commit 00939a93e4b435b4f942f9a4cf1b7abfc6c2dff6. --- .../client/core/vk/render/VkRenderCore.java | 2 +- .../core/vk/render/VkTerrainRenderer.java | 182 ++++++++++++++---- 2 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java index 600f8799d..6b25db18c 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkRenderCore.java @@ -96,7 +96,7 @@ public VkRenderCore(WorldEngine world, ServiceManager sm) { this.nodeCleaner = new VkNodeCleaner(this.frameCtx, this.uploadStream, this.downloadStream, this.nodeManager); this.traversal = new VkTraversal(this.frameCtx, this.uploadStream, this.downloadStream, this.properties, this.nodeManager, this.nodeCleaner, this.renderGen); - this.terrainRenderer = new VkTerrainRenderer(this.frameCtx, this.uploadStream, + this.terrainRenderer = new VkTerrainRenderer(this.frameCtx, this.uploadStream, this.downloadStream, this.properties, this.geometryData, this.modelStore); this.compositor = new VkCompositor(this.frameCtx, this.uploadStream, this.properties, VoxyConfig.CONFIG.useEnvironmentalFog); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java index 2f56c37a8..06442de0f 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java @@ -4,6 +4,7 @@ import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer; import me.cortex.voxy.client.core.vk.VkBuffer; import me.cortex.voxy.client.core.vk.VkCmd; +import me.cortex.voxy.client.core.vk.VkDownloadStream; import me.cortex.voxy.client.core.vk.VkFrameCtx; import me.cortex.voxy.client.core.vk.VkImage2D; import me.cortex.voxy.client.core.vk.VkShaderPipeline; @@ -26,19 +27,37 @@ import static org.lwjgl.vulkan.VK10.*; import static org.lwjgl.vulkan.VK12.vkCmdDrawIndexedIndirectCount; -//Pure-VK mirror of MDICSectionRenderer. +//Pure-VK mirror of MDICSectionRenderer: the same six GPU passes (prep, +// raster-cull, command generation, translucency prefix sort + build, then +// opaque / temporal / translucent indexed-indirect-count draws) against the +// same buffer layouts, recorded into MC's frame command buffer with dynamic +// rendering over Voxy's offscreen colour + depth-stencil targets. public class VkTerrainRenderer { - //Draw-command offsets shared with cmdgen/translucent shaders. + //Draw-command offsets shared with the cmdgen/translucent shader defines. private static final int TRANSLUCENT_OFFSET = VkViewport.OPAQUE_DRAW_COUNT; private static final int TEMPORAL_OFFSET = TRANSLUCENT_OFFSET + VkViewport.TRANSLUCENT_DRAW_COUNT; private final VkFrameCtx ctx; private final VkUploadStream uploadStream; + private final VkDownloadStream downloadStream; private final RenderProperties properties; private final VkSectionGeometryData geometry; private final VkModelStore modelStore; - //MoltenVK fallback: uses fixed drawCount with zeroed trailing slots as no-ops. + //MoltenVK fixed-count fallback state: the last-known REAL per-pass draw counts, + // read back from drawCountCallBuffer each frame. On desktop the GPU sources + // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to 0 and + // gated by hasAnyReadback: before the first async readback lands, fixedCountBudget + // returns 0 — no draws at all — so renderOpaque (which runs BEFORE buildDrawCalls + // and uses last frame's commands) skips entirely on the first frame with geometry. + // Without this, the headroom floor (1024/256/256) would issue 1024+ no-op Metal + // draws/frame from the zeroed drawCallBuffer, causing the "2D floating blocks" + // loading glitch on macOS. Once the first readback lands, hasAnyReadback flips + // true and fixedCountBudget operates normally (lastKnown*1.5 + headroom). + private int fbOpaqueDraws = 0; + private int fbTranslucentDraws = 0; + private int fbTemporalDraws = 0; + private boolean hasAnyReadback = false; private final VkBuffer uniform; private final VkBuffer distanceCountBuffer; @@ -55,10 +74,11 @@ public class VkTerrainRenderer { private VkShaderPipeline terrainTranslucent; private int pipelineColorFormat = -1, pipelineDepthFormat = -1; - public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, + public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkDownloadStream downloadStream, RenderProperties properties, VkSectionGeometryData geometry, VkModelStore modelStore) { this.ctx = ctx; this.uploadStream = uploadStream; + this.downloadStream = downloadStream; this.properties = properties; this.geometry = geometry; this.modelStore = modelStore; @@ -66,7 +86,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, this.uniform = new VkBuffer(ctx, 1024).zero(); this.distanceCountBuffer = new VkBuffer(ctx, 1024L * 4 + VkViewport.TRANSLUCENT_DRAW_COUNT * 4L).zero(); - //Shared index buffer: u16 quad + cube indices. + //Shared index buffer: u16 quad pattern + u16 cube indices at CUBE_INDEX_OFFSET this.indexBuffer = new VkBuffer(ctx, SharedIndexBuffer.CUBE_INDEX_OFFSET + 6 * 2 * 3 * 2L); { var quads = SharedIndexBuffer.generateQuadIndicesShort(16380); @@ -80,7 +100,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, this.depthBoundSampler = VkImage2D.createSampler(ctx.vk(), false, false); this.lightmapSampler = VkImage2D.createSampler(ctx.vk(), false, true); - //Compute pipelines. + //================= compute pipelines ================= this.prep = new VkShaderPipeline(ctx, "prep.comp", VkShaderSource.load("voxy:lod/gl46/prep.comp", VkShaderSource.defs().build()), 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2))); @@ -95,7 +115,9 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5), VkShaderPipeline.ssbo(6), VkShaderPipeline.ssbo(7))); - //Subgroup prefix sum if available, else shared-memory fallback. + //Subgroup prefix sum on VK when the device advertises subgroup arithmetic + // (MoltenVK/Metal simdgroups, NVIDIA, AMD, Intel — virtually every VK 1.1+ + // device). Falls back to the shared-memory Hillis-Steele scan otherwise. boolean useSubgroup = ctx.vk().subgroupArithmetic; this.prefixSum = new VkShaderPipeline(ctx, "prefixsum.comp", VkShaderSource.load(useSubgroup ? "voxy:util/prefixsum/inital3_vk.comp" : "voxy:util/prefixsum/simple.comp", @@ -111,7 +133,7 @@ public VkTerrainRenderer(VkFrameCtx ctx, VkUploadStream uploadStream, 0, List.of(VkShaderPipeline.ubo(0), VkShaderPipeline.ssbo(1), VkShaderPipeline.ssbo(2), VkShaderPipeline.ssbo(3), VkShaderPipeline.ssbo(4), VkShaderPipeline.ssbo(5))); - //Raster cull pipeline (depth-only). + //================= raster cull pipeline (depth-only, no writes) ================= var cullDesc = new VkShaderPipeline.GfxDesc(); cullDesc.name = "cullraster"; cullDesc.vertGlsl = VkShaderSource.load("voxy:lod/gl46/cull/raster.vert", VkShaderSource.defs().props(properties).build()); @@ -160,7 +182,7 @@ private void ensureTerrainPipelines(VkViewport viewport) { opaque.depthWrite = true; opaque.depthCompare = VkCmd.closerEqual(this.properties); opaque.blend = false; - opaque.stencilTestEqual1 = true; + opaque.stencilTestEqual1 = true;//only render where vanilla terrain is absent opaque.bindings = bindings; this.terrainOpaque = new VkShaderPipeline(this.ctx, opaque); @@ -184,7 +206,7 @@ private void ensureTerrainPipelines(VkViewport viewport) { this.pipelineDepthFormat = df; } - //--- + //================================================================================== private final Matrix4f uniformScratch = new Matrix4f(); public void uploadUniform(VkViewport viewport) { @@ -202,21 +224,27 @@ public void uploadUniform(VkViewport viewport) { this.uploadStream.commit(); } - /** Mirrors MDIC buildDrawCalls. */ + /** Mirrors MDIC buildDrawCalls: prep -> raster cull -> cmdgen -> translucency sort. */ public void buildDrawCalls(VkViewport viewport) { if (this.geometry.getSectionCount() == 0) return; var cmd = this.ctx.cmd(); this.uploadUniform(viewport); if (!this.ctx.vk().hasDrawIndirectCount) { - //MoltenVK fallback: zero trailing slots so stale draws read as no-ops. + //Fixed-count fallback (MoltenVK): renderTerrain issues + // vkCmdDrawIndexedIndirect with maxDrawCount rather than a GPU-sourced + // count, so trailing slots past the actual command count would be read + // as stale draw commands from the previous frame. Zero the three + // drawCallBuffer slices (opaque / temporal / translucent) here so any + // un-overwritten slot reads as instanceCount=0 (a no-op draw). The + // cmdgen compute below then writes the real commands on top; the fill + // completes (with a barrier) before the cmdgen dispatch reads the buffer. + // Gated to the fallback path only — the tight-count path (desktop Vulkan) + // never reads past the actual count, so it pays nothing here. long stride = 5L * 4; - int opaqueCap = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); - int temporalCap = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); - int translucentCap = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, 0L, opaqueCap * stride, 0); - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TEMPORAL_OFFSET * stride, temporalCap * stride, 0); - vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TRANSLUCENT_OFFSET * stride, translucentCap * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, 0L, VkViewport.OPAQUE_DRAW_COUNT * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TEMPORAL_OFFSET * stride, VkViewport.TEMPORAL_DRAW_COUNT * stride, 0); + vkCmdFillBuffer(cmd, viewport.drawCallBuffer.buffer, TRANSLUCENT_OFFSET * stride, VkViewport.TRANSLUCENT_DRAW_COUNT * stride, 0); this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT); } @@ -230,15 +258,25 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prep -> cmdgen: include COMPUTE in dst barrier (atomics need it on MoltenVK). + //prep (compute) wrote drawCountCallBuffer, which has TWO consumers: + // the raster-cull draw below reads cullDrawIndirectCommand (@24) from + // DRAW_INDIRECT, and cmdgen atomicAdds opaque/translucent/temporal + // DrawCount (@12/@16/@20) from COMPUTE. computeToDrawBarrier() scopes + // the destination to DRAW_INDIRECT|VERTEX only, so prep -> cmdgen had + // no memory dependency at all: cmdgen's atomics could observe the + // PREVIOUS frame's counters instead of prep's zeroes, placing draw + // commands past their pass region with a baseInstance that no longer + // matches the positionBuffer slot written for that section. Desktop + // drivers happened to make the writes visible anyway; MoltenVK does + // not. Include COMPUTE (read+write, the atomics are RMW) in the dst. this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); } - {//raster occlusion cull (depth-only) - this.beginRendering(cmd, viewport, 0L, true); + {//raster occlusion test into the visibility buffer (depth-tested box draw, no writes) + this.beginRendering(cmd, viewport, 0L, true);//depth-only this.cullRaster.bind(cmd); VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); try (var b = this.cullRaster.binder()) { @@ -251,13 +289,16 @@ public void buildDrawCalls(VkViewport viewport) { vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); vkCmdDrawIndexedIndirect(cmd, viewport.drawCountCallBuffer.buffer, 6 * 4, 1, 20); vkCmdEndRenderingKHR(cmd); - //Cull fragment -> cmdgen compute: scoped barrier. + //The raster-cull draw wrote visibilityData (SSBO) from the fragment + // shader; the consumer is the cmdgen compute. Scope to those stages + // instead of the previous fullBarrier (ALL_COMMANDS -> ALL_COMMANDS) + // which forced a full pipeline stall on every frame. this.ctx.barrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); } - {//command generation + {//command generation (indirect dispatch sized by prep) vkCmdFillBuffer(cmd, this.distanceCountBuffer.buffer, 0, 1024L * 4, 0); this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); @@ -274,7 +315,8 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); - //cmdgen -> prefixsum: compute barrier. + //cmdgen -> prefixsum: both compute. The draw that consumes these + // indirect commands is guarded by renderTerrain's own barrier. this.ctx.computeToComputeBarrier(); } @@ -284,7 +326,7 @@ public void buildDrawCalls(VkViewport viewport) { b.ssbo(0, this.distanceCountBuffer).push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prefixsum -> translucentGen: compute barrier. + //prefixsum -> translucentGen: both compute. this.ctx.computeToComputeBarrier(); this.translucentGen.bind(cmd); @@ -298,32 +340,84 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatchIndirect(cmd, viewport.drawCountCallBuffer.buffer, 0); - //No trailing barrier: renderTerrain issues its own. + //No trailing barrier here: the translucent commands written into + // drawCallBuffer are read by renderTranslucent's renderTerrain, which + // issues its own COMPUTE|TRANSFER -> DRAW_INDIRECT|VERTEX|FRAGMENT + // barrier (line ~355) before drawing. The previous computeToAllBarrier + // here was redundant with that draw barrier and serialised the GPU. } + if (!this.ctx.vk().hasDrawIndirectCount) { + //Read the three real per-pass draw counts back to the CPU so next frame's + // fixed-count multi-draws track them instead of the worst-case section + // cap (see fixedCountBudget). The counts live at opaque@12 / translucent@16 + // / temporal@20 in drawCountCallBuffer; download those 12 bytes on the same + // async, event-retired path the traversal request readback already uses + // (commit() scopes its own COMPUTE->TRANSFER->HOST barriers). The callback + // runs on the render thread from pollRetired, so the plain field writes are + // race-free. Desktop (drawIndirectCount) neither needs nor issues this. + this.downloadStream.download(viewport.drawCountCallBuffer, 12, 12, (ptr, size) -> { + this.fbOpaqueDraws = clampCount(MemoryUtil.memGetInt(ptr), VkViewport.OPAQUE_DRAW_COUNT); + this.fbTranslucentDraws = clampCount(MemoryUtil.memGetInt(ptr + 4), VkViewport.TRANSLUCENT_DRAW_COUNT); + this.fbTemporalDraws = clampCount(MemoryUtil.memGetInt(ptr + 8), VkViewport.TEMPORAL_DRAW_COUNT); + this.hasAnyReadback = true; + }); + } + } + + private static int clampCount(int value, int cap) { + return value < 0 ? 0 : Math.min(value, cap); + } + + /** + * Draw-count upper bound for one terrain pass. On desktop Vulkan the GPU sources + * the real count from drawCountCallBuffer (vkCmdDrawIndexedIndirectCount), so the + * section-derived {@code cap} is only a ceiling and is returned unchanged. On + * MoltenVK — which lacks drawIndirectCount — the fixed-count multi-draw instead + * iterates whatever count it is handed, encoding one Metal draw per slot; handing + * it the section-count ceiling means tens of thousands of no-op draws every frame, + * invariant to where the camera looks (this is why sky/occlusion culling produced + * no Mac speed-up). Bounding it to the last-known real count plus headroom lets the + * culling actually reduce Mac draw cost. The full drawCallBuffer is still zeroed per + * frame, so every slot within the ceiling reads either a real command or a no-op — + * a transient under-estimate during fast camera motion only drops a few LOD draws + * for a frame or two, never reads stale geometry. + */ + private int fixedCountBudget(int lastKnownCount, int headroom, int cap) { + if (this.ctx.vk().hasDrawIndirectCount) return cap; + if (!this.hasAnyReadback) return 0;//first frame: no draw calls generated yet + int budget = (int) (lastKnownCount * 1.5f) + headroom; + return Math.min(cap, Math.max(0, budget)); } public void renderOpaque(VkViewport viewport, boolean clearTargets) { - //Ensure pipelines before section-count guard (idempotent). + //Build pipelines BEFORE the section-count guard: within a frame geometry is + // uploaded after this call (nodeManager.tick/buildDrawCalls), so renderTemporal/ + // renderTranslucent can see sectionCount>0 later this same frame. If we skipped + // ensure here when the count is momentarily 0, those calls would bind a null + // pipeline. ensureTerrainPipelines is idempotent (no-op once built for the format). this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; this.uploadUniform(viewport); - int maxDraw = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); + int cap = Math.min((int) (this.geometry.getSectionCount() * 4.4 + 128), VkViewport.OPAQUE_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbOpaqueDraws, 1024, cap); this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, 0, 4 * 3, maxDraw, clearTargets); } public void renderTemporal(VkViewport viewport) { this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; - int maxDraw = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); + int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TEMPORAL_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbTemporalDraws, 256, cap); this.renderTerrain(viewport, viewport.colour.view, this.terrainOpaque, TEMPORAL_OFFSET * 5L * 4, 4 * 5, maxDraw, false); } - /** Translucents draw onto SSAO output. */ + /** Translucents draw onto the SSAO output (mirrors the GL fbSSAO target). */ public void renderTranslucent(VkViewport viewport) { this.ensureTerrainPipelines(viewport); if (this.geometry.getSectionCount() == 0) return; - int maxDraw = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); + int cap = Math.min(this.geometry.getSectionCount(), VkViewport.TRANSLUCENT_DRAW_COUNT); + int maxDraw = this.fixedCountBudget(this.fbTranslucentDraws, 256, cap); this.renderTerrain(viewport, viewport.colourSSAO.view, this.terrainTranslucent, TRANSLUCENT_OFFSET * 5L * 4, 4 * 4, maxDraw, false); } @@ -335,7 +429,7 @@ private void renderTerrain(VkViewport viewport, long colorView, VkShaderPipeline VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDEX_READ_BIT); - this.beginRendering(cmd, viewport, colorView, !clear); //LOAD unless first pass + this.beginRendering(cmd, viewport, colorView, !clear);//LOAD unless first pass (which cleared via compositor setup) pipeline.bind(cmd); VkCmd.setViewportScissor(cmd, viewport.width, viewport.height); long lightmapView = VkFrameHost.lightmapView(); @@ -352,19 +446,33 @@ private void renderTerrain(VkViewport viewport, long colorView, VkShaderPipeline } vkCmdBindIndexBuffer(cmd, this.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT16); if (this.ctx.vk().hasDrawIndirectCount) { - //Desktop: tight count from GPU buffer. + //Tight-count path: the GPU reads the actual draw count from the count + // buffer each frame. Requires the drawIndirectCount Vulkan 1.2 feature + // to be enabled on the device — desktop Vulkan (NVIDIA/AMD/Intel) enables + // it; MoltenVK does not (see the else branch). vkCmdDrawIndexedIndirectCount(cmd, viewport.drawCallBuffer.buffer, indirectOffset, viewport.drawCountCallBuffer.buffer, drawCountOffset, maxDrawCount, 5 * 4); } else { - //MoltenVK: fixed count, trailing slots zeroed as no-ops. + //MoltenVK/macOS fixed-count fallback: drawIndirectCount is not enabled + // on MC's adopted Vulkan device (MC's DeviceFeatures record does not + // even track it), and calling the function without the feature enabled + // is invalid usage that MoltenVK degenerates. The drawCallBuffer slice + // for this pass is zeroed per frame in buildDrawCalls (gated to this + // fallback path) so trailing slots past the actual command count read + // instanceCount=0 (no-op draws); a fixed-count multi-draw over the + // clamped maxDrawCount is therefore correct, just less tight. vkCmdDrawIndexedIndirect(cmd, viewport.drawCallBuffer.buffer, indirectOffset, maxDrawCount, 5 * 4); } vkCmdEndRenderingKHR(cmd); } - /** Begin dynamic rendering. colorView=0 for depth-only. */ + /** + * Begin dynamic rendering over the offscreen targets (always LOAD; clears + * happen in the depth-setup pass). {@code colorView} selects the colour + * attachment (main colour vs SSAO output); 0 = depth-only. + */ private void beginRendering(VkCommandBuffer cmd, VkViewport viewport, long colorView, boolean load) { try (MemoryStack stack = stackPush()) { @@ -405,7 +513,9 @@ public void free() { this.terrainOpaque.free(); this.terrainTranslucent.free(); } - //Samplers are device-lifetime cached; don't destroy per-object. + //depthBoundSampler/lightmapSampler come from VkImage2D.createSampler's + // device-lifetime cache (shared handles); never destroy them per-object + // (multi-free vkDestroySampler -> SIGSEGV on world unload). this.uniform.free(); this.distanceCountBuffer.free(); this.indexBuffer.free(); From 4886c5a7917f8a53cc4db05ba0472578515b4e4b Mon Sep 17 00:00:00 2001 From: cochcoder Date: Wed, 22 Jul 2026 21:28:44 -0400 Subject: [PATCH 12/12] Revert "vk/mac: MoltenVK correctness + first-frame loading glitch fix" This reverts commit 7e1ccb83f6cb98c178a52e6e63b3ba25f84915b5. --- .../cortex/voxy/client/core/vk/VkImage2D.java | 21 ++++------ .../voxy/client/core/vk/VulkanContext.java | 42 ++----------------- .../client/core/vk/render/VkFrameHost.java | 13 +++--- .../core/vk/render/VkTerrainRenderer.java | 39 +++++------------ .../client/core/vk/render/VkTraversal.java | 8 ---- 5 files changed, 28 insertions(+), 95 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java index c584aed43..ce5e1a879 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VkImage2D.java @@ -100,13 +100,8 @@ private long createView(MemoryStack stack, VulkanContext vctx, int baseMip, int return pView.get(0); } - /** Whole-image layout transition. UNDEFINED-layout images get TOP_OF_PIPE/0 - * (no prior producer to synchronize — contents are discarded). */ + /** Whole-image layout transition recorded into the current frame commands. */ public void transition(int newLayout, int srcStage, int srcAccess, int dstStage, int dstAccess) { - if (this.currentLayout == VK_IMAGE_LAYOUT_UNDEFINED) { - srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - srcAccess = 0; - } try (MemoryStack stack = stackPush()) { var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() .srcAccessMask(srcAccess).dstAccessMask(dstAccess) @@ -120,27 +115,27 @@ public void transition(int newLayout, int srcStage, int srcAccess, int dstStage, } //Batched transition: records multiple images' layout changes in a single - // vkCmdPipelineBarrier. UNDEFINED-layout images get TOP_OF_PIPE/0 (see transition()). + // vkCmdPipelineBarrier (one call instead of N). Each image's transition is + // described by its own (newLayout, srcStage, srcAccess, dstStage, dstAccess) + // tuple; the call uses the union of all src stages -> union of all dst stages + // so the single barrier covers every image's dependency. public record BatchEntry(VkImage2D image, int newLayout, int srcAccess, int dstAccess) {} public static void transitionBatch(java.util.List entries, int unionSrcStage, int unionDstStage) { if (entries.isEmpty()) return; - boolean anyUndefined = false; try (MemoryStack stack = stackPush()) { var imbs = VkImageMemoryBarrier.calloc(entries.size(), stack); for (int i = 0; i < entries.size(); i++) { var e = entries.get(i); - boolean undef = e.image.currentLayout == VK_IMAGE_LAYOUT_UNDEFINED; - if (undef) anyUndefined = true; imbs.get(i).sType$Default() - .srcAccessMask(undef ? 0 : e.srcAccess).dstAccessMask(e.dstAccess) + .srcAccessMask(e.srcAccess).dstAccessMask(e.dstAccess) .oldLayout(e.image.currentLayout).newLayout(e.newLayout) .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED).dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .image(e.image.image) .subresourceRange().aspectMask(e.image.aspect).levelCount(e.image.mipLevels).layerCount(1); } + //Use the first image's ctx (all images share the same VkFrameCtx in Voxy). var cmd = entries.get(0).image.ctx.cmd(); - int actualSrcStage = anyUndefined ? unionSrcStage | VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : unionSrcStage; - vkCmdPipelineBarrier(cmd, actualSrcStage, unionDstStage, 0, null, null, imbs); + vkCmdPipelineBarrier(cmd, unionSrcStage, unionDstStage, 0, null, null, imbs); for (var e : entries) e.image.currentLayout = e.newLayout; } } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java index 121931e64..41dec05d9 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/VulkanContext.java @@ -10,7 +10,6 @@ import org.lwjgl.vulkan.VkPhysicalDeviceFeatures2; import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; import org.lwjgl.vulkan.VkPhysicalDeviceProperties; -import org.lwjgl.vulkan.VkPhysicalDeviceProperties2; import org.lwjgl.vulkan.VkPhysicalDeviceSubgroupProperties; import org.lwjgl.vulkan.VkPhysicalDeviceVulkan12Features; import org.lwjgl.vulkan.VkQueue; @@ -32,29 +31,6 @@ public final class VulkanContext { public final VkQueue queue; public final int queueFamily; public final boolean hasDrawIndirectCount; - //MASTER SWITCH for every subgroup-dependent VK path. Keep FALSE. - // - //querySubgroupProperties() used to chain a properties struct into - // vkGetPhysicalDeviceFeatures2, which silently returned all-zeroes, so - // subgroupArithmetic was false on EVERY device and the subgroup paths were - // dead code that had never once executed. Correcting the query turned all - // three of them on at once: - // - // VkHiZ - hiz_subgroup.comp builds the HiZ pyramid - // VkTraversal - traversal workgroup size 32 -> 64 - // VkTerrainRenderer- subgroup prefix-sum variant for translucent sorting - // - //That regressed rendering on both desktop and macOS: terrain flickered with - // only far LODs surviving, consistent with a bad HiZ pyramid feeding the - // traversal's occlusion test. hiz_subgroup.comp is also independently - // suspect — its comments assume a 64-wide subgroup, and its subgroupBarrier() - // at the mip_5/mip_6 tail runs with only 4 of 256 lanes still active, which - // SPIR-V requires to be subgroup-uniform. - // - //Flip to true only after validating each of the three paths on its own - // against the non-subgroup path (ab_compare.py) at several subgroup widths. - private static final boolean ENABLE_SUBGROUP_PATHS = false; - public final boolean subgroupArithmetic; public final int subgroupSize; public final String deviceName; @@ -72,14 +48,8 @@ private VulkanContext(IVkHost host) { this.hasDrawIndirectCount = queryDrawIndirectCount(this.physicalDevice); var subgroup = querySubgroupProperties(this.physicalDevice); this.subgroupProps = subgroup; + this.subgroupArithmetic = subgroup != null && (subgroup.supportedOperations() & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0; this.subgroupSize = subgroup != null ? subgroup.subgroupSize() : 1; - int ops = subgroup != null ? subgroup.supportedOperations() : 0; - int stages = subgroup != null ? subgroup.supportedStages() : 0; - int needOps = VK_SUBGROUP_FEATURE_ARITHMETIC_BIT | VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_CLUSTERED_BIT; - boolean deviceSupportsSubgroups = (ops & needOps) == needOps - && (stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 - && this.subgroupSize >= 16; - this.subgroupArithmetic = ENABLE_SUBGROUP_PATHS && deviceSupportsSubgroups; String name; try (MemoryStack stack = stackPush()) { var props = VkPhysicalDeviceProperties.calloc(stack); @@ -96,7 +66,6 @@ private VulkanContext(IVkHost host) { Logger.info("Voxy Vulkan context adopted Minecraft device: " + this.deviceName + " (drawIndirectCount=" + this.hasDrawIndirectCount + ", subgroupArithmetic=" + this.subgroupArithmetic - + " (deviceCapable=" + deviceSupportsSubgroups + ", gate=" + ENABLE_SUBGROUP_PATHS + ")" + ", subgroupSize=" + this.subgroupSize + ")"); } @@ -111,14 +80,9 @@ private static boolean queryDrawIndirectCount(VkPhysicalDevice pd) { private static VkPhysicalDeviceSubgroupProperties querySubgroupProperties(VkPhysicalDevice pd) { try (MemoryStack stack = stackPush()) { - //VkPhysicalDeviceSubgroupProperties is a PROPERTIES struct: it extends - // VkPhysicalDeviceProperties2, not VkPhysicalDeviceFeatures2. Chaining it - // into vkGetPhysicalDeviceFeatures2 is invalid usage (VUID-VkPhysicalDeviceFeatures2-pNext-pNext) - // and leaves the struct at its calloc'd zeroes, so supportedOperations - // read back 0 and subgroup support was reported absent on EVERY device. var sg = VkPhysicalDeviceSubgroupProperties.calloc(stack).sType$Default(); - var p2 = VkPhysicalDeviceProperties2.calloc(stack).sType$Default().pNext(sg.address()); - VK11.vkGetPhysicalDeviceProperties2(pd, p2); + var f2 = VkPhysicalDeviceFeatures2.calloc(stack).sType$Default().pNext(sg.address()); + VK11.vkGetPhysicalDeviceFeatures2(pd, f2); // Return a malloc'd copy so the caller can read it past the stack frame. var copy = VkPhysicalDeviceSubgroupProperties.malloc(); copy.set(sg); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java index 4350f8eb4..8e80db0f7 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkFrameHost.java @@ -32,9 +32,12 @@ public static int vkFormat(GpuTextureView view) { } //Layout-transition one of MC's own images (colour/depth attachment) with - // scoped stage/access masks matching the actual producer/consumer. - // Depth-stencil images transition both aspects together (DEPTH|STENCIL) - // since Voxy never enables separateDepthStencilLayouts. + // scoped stage/access masks matching the actual producer/consumer — MC just + // rendered into the depth attachment (LATE_FRAGMENT_TESTS write), and Voxy + // samples it (FRAGMENT_SHADER read), so the previous ALL_COMMANDS masks + // (which serialised the transition with unrelated compute) are narrowed. + // Used to bracket sampling of MC's attachments mid-frame (they live in + // ATTACHMENT_OPTIMAL otherwise). public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, boolean depth, int oldLayout, int newLayout) { try (MemoryStack stack = stackPush()) { @@ -70,8 +73,6 @@ public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, dstAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; } } - //Depth-stencil: transition both aspects together. - int aspectMask = depth ? (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) : VK_IMAGE_ASPECT_COLOR_BIT; var imb = VkImageMemoryBarrier.calloc(1, stack).sType$Default() .srcAccessMask(srcAccess).dstAccessMask(dstAccess) .oldLayout(oldLayout).newLayout(newLayout) @@ -79,7 +80,7 @@ public static void transitionMcImage(VkCommandBuffer cmd, GpuTextureView view, .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .image(image); imb.subresourceRange() - .aspectMask(aspectMask) + .aspectMask(depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT) .levelCount(VK_REMAINING_MIP_LEVELS) .layerCount(VK_REMAINING_ARRAY_LAYERS); vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, null, null, imb); diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java index 06442de0f..b4d387253 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTerrainRenderer.java @@ -46,18 +46,13 @@ public class VkTerrainRenderer { //MoltenVK fixed-count fallback state: the last-known REAL per-pass draw counts, // read back from drawCountCallBuffer each frame. On desktop the GPU sources - // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to 0 and - // gated by hasAnyReadback: before the first async readback lands, fixedCountBudget - // returns 0 — no draws at all — so renderOpaque (which runs BEFORE buildDrawCalls - // and uses last frame's commands) skips entirely on the first frame with geometry. - // Without this, the headroom floor (1024/256/256) would issue 1024+ no-op Metal - // draws/frame from the zeroed drawCallBuffer, causing the "2D floating blocks" - // loading glitch on macOS. Once the first readback lands, hasAnyReadback flips - // true and fixedCountBudget operates normally (lastKnown*1.5 + headroom). - private int fbOpaqueDraws = 0; - private int fbTranslucentDraws = 0; - private int fbTemporalDraws = 0; - private boolean hasAnyReadback = false; + // these counts itself via vkCmdDrawIndexedIndirectCount. Initialised to the + // per-pass caps so the first few frames — before the first readback lands — + // behave like the old section-count fallback, then converge to the true + // visible counts. + private int fbOpaqueDraws = VkViewport.OPAQUE_DRAW_COUNT; + private int fbTranslucentDraws = VkViewport.TRANSLUCENT_DRAW_COUNT; + private int fbTemporalDraws = VkViewport.TEMPORAL_DRAW_COUNT; private final VkBuffer uniform; private final VkBuffer distanceCountBuffer; @@ -258,21 +253,9 @@ public void buildDrawCalls(VkViewport viewport) { .push(cmd); } vkCmdDispatch(cmd, 1, 1, 1); - //prep (compute) wrote drawCountCallBuffer, which has TWO consumers: - // the raster-cull draw below reads cullDrawIndirectCommand (@24) from - // DRAW_INDIRECT, and cmdgen atomicAdds opaque/translucent/temporal - // DrawCount (@12/@16/@20) from COMPUTE. computeToDrawBarrier() scopes - // the destination to DRAW_INDIRECT|VERTEX only, so prep -> cmdgen had - // no memory dependency at all: cmdgen's atomics could observe the - // PREVIOUS frame's counters instead of prep's zeroes, placing draw - // commands past their pass region with a baseInstance that no longer - // matches the positionBuffer slot written for that section. Desktop - // drivers happened to make the writes visible anyway; MoltenVK does - // not. Include COMPUTE (read+write, the atomics are RMW) in the dst. - this.ctx.barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, - VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT - | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + //prep (compute) wrote the cull indirect args; the raster-cull draw + // reads them. Scope to COMPUTE -> DRAW_INDIRECT|VERTEX_INPUT. + this.ctx.computeToDrawBarrier(); } {//raster occlusion test into the visibility buffer (depth-tested box draw, no writes) @@ -360,7 +343,6 @@ public void buildDrawCalls(VkViewport viewport) { this.fbOpaqueDraws = clampCount(MemoryUtil.memGetInt(ptr), VkViewport.OPAQUE_DRAW_COUNT); this.fbTranslucentDraws = clampCount(MemoryUtil.memGetInt(ptr + 4), VkViewport.TRANSLUCENT_DRAW_COUNT); this.fbTemporalDraws = clampCount(MemoryUtil.memGetInt(ptr + 8), VkViewport.TEMPORAL_DRAW_COUNT); - this.hasAnyReadback = true; }); } } @@ -385,7 +367,6 @@ private static int clampCount(int value, int cap) { */ private int fixedCountBudget(int lastKnownCount, int headroom, int cap) { if (this.ctx.vk().hasDrawIndirectCount) return cap; - if (!this.hasAnyReadback) return 0;//first frame: no draw calls generated yet int budget = (int) (lastKnownCount * 1.5f) + headroom; return Math.min(cap, Math.max(0, budget)); } diff --git a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java index 2563231e0..90b446b38 100644 --- a/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java +++ b/src/main/java/me/cortex/voxy/client/core/vk/render/VkTraversal.java @@ -239,14 +239,6 @@ public void doTraversal(VkViewport viewport) { //Download + reset the request queue this.downloadStream.download(this.requestBuffer, this::forwardDownloadResult); - //WAR: download() already recorded the vkCmdCopyBuffer that READS - // requestBuffer, and the fill below WRITES it. Two transfer commands in - // one command buffer are not implicitly ordered, so without this barrier - // the reset could land before/while the copy reads — zeroing the request - // count and silently dropping a frame of node requests. Desktop drivers - // serialise blits in practice; Metal's blit encoders need not. - this.ctx.barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); vkCmdFillBuffer(this.ctx.cmd(), this.requestBuffer.buffer, 0, 4, 0); //The download copy read the requestBuffer (TRANSFER read); the fill // resets it (TRANSFER write). The next reader is next frame's traversal