From e9eff2ebfe9aba61c0b8b25ba6356520069dda18 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:04:57 +0000 Subject: [PATCH 1/6] fix Vulkan-side bugs and comments that disagree with the code Bloom's eight-bit fallback never took effect, the startup readback used an image already handed to OpenGL, frame uniforms leaked when no lightmap arrived, ray-traced chunk structures were rebuilt every frame, a stale top-level structure could be traced after its chunks were freed, the material fill raced its copy, and the opaque submit waited too late for the depth clear. Stale and misplaced comments fixed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../net/vulkanmodnext/vkimpl/Interop.java | 31 ++-- .../vulkanmodnext/vkimpl/VertexLayout.java | 28 +-- .../vulkanmodnext/vkimpl/VkChunkMirror.java | 47 +++-- .../vulkanmodnext/vkimpl/VkDemoRenderer.java | 32 +++- .../vkimpl/VkInteropRenderer.java | 26 ++- .../vulkanmodnext/vkimpl/VkRayTracing.java | 28 ++- .../vkimpl/VkTerrainRenderer.java | 160 +++++++++++------- .../vkimpl/VulkanContextImpl.java | 2 +- 8 files changed, 241 insertions(+), 113 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/vkimpl/Interop.java b/src/main/java/net/vulkanmodnext/vkimpl/Interop.java index 558419b..f2b3be2 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/Interop.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/Interop.java @@ -114,17 +114,8 @@ static int semaphoreHandleType() { private static final int GENERIC_ALL = 0x10000000; /** - * Puts the Win32 access rights in front of an export structure, and hands - * back whatever should now start the chain. - * - * On anything but Windows this is the export structure unchanged, because - * a file descriptor carries no rights to ask for. On Windows the rights are - * only defaulted when the structure is missing, and a driver may default - * them to nothing: the handle then imports cleanly and never signals, which - * is indistinguishable from a hang until the card is reset. - */ - /** - * The same rights, for an exported memory handle. + * The access rights of {@link #appendWin32SemaphoreRights}, for an exported + * memory handle. * * Separate structure, identical reasoning: without it the rights on the * handle are whatever the driver defaults to, and a handle imported without @@ -145,6 +136,16 @@ static long appendWin32MemoryRights(MemoryStack stack, long exportInfo) { return rights.address(); } + /** + * Puts the Win32 access rights in front of an export structure, and hands + * back whatever should now start the chain. + * + * On anything but Windows this is the export structure unchanged, because + * a file descriptor carries no rights to ask for. On Windows the rights are + * only defaulted when the structure is missing, and a driver may default + * them to nothing: the handle then imports cleanly and never signals, which + * is indistinguishable from a hang until the card is reset. + */ static long appendWin32SemaphoreRights(MemoryStack stack, long exportInfo) { if (!WINDOWS) { return exportInfo; @@ -532,6 +533,12 @@ static int importSemaphoreToGL(MemoryStack stack, VkDevice device, long semaphor int glHandleType = D3D12_FENCE_SEMAPHORES ? EXTSemaphoreWin32.GL_HANDLE_TYPE_D3D12_FENCE_EXT : EXTSemaphoreWin32.GL_HANDLE_TYPE_OPAQUE_WIN32_EXT; + // Drained first: an error some earlier GL call left in the queue + // would otherwise be read below as this import being refused. + for (int drained = 0; drained < 32 + && GL11C.glGetError() != GL11C.GL_NO_ERROR; drained++) { + // nothing: only emptying the queue + } EXTSemaphoreWin32.glImportSemaphoreWin32HandleEXT(glSem, glHandleType, pHandle.get(0)); // Checked here and nowhere else, because a refused import has no // other symptom: the semaphore object exists, the wait on it is @@ -539,6 +546,8 @@ static int importSemaphoreToGL(MemoryStack stack, VkDevice device, long semaphor // startup and fall back to vanilla than to hang the card. int error = org.lwjgl.opengl.GL11C.glGetError(); if (error != 0) { + // Refused, so nothing on the GL side holds it: ours to close. + closeHandle(pHandle.get(0)); throw new IllegalStateException("glImportSemaphoreWin32HandleEXT refused the " + "exported semaphore (GL error 0x" + Integer.toHexString(error) + "). Waiting on it from OpenGL would never return."); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VertexLayout.java b/src/main/java/net/vulkanmodnext/vkimpl/VertexLayout.java index 8d985ed..8b83138 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VertexLayout.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VertexLayout.java @@ -361,10 +361,13 @@ protected int[] initialValue() { * both ends of the range be trimmed in the same frame — the quads under the * camera and the ones over it are different quads. * - * @param quadGroup scratch, one byte per quad, at least {@code sourceBytes / 112} long + * @param quadShelf scratch, one byte per quad, at least {@code sourceBytes / 112} long + * @param quadTarget filled with each source quad's position in the sorted + * output, at least {@code sourceBytes / 112} long * @param counts filled with the down and up vertex counts, then 17 running - * totals of down-facing quads below each block level and 17 - * of up-facing quads at or above it + * totals of down-facing quads below each block level, 17 + * of up-facing quads at or above it, and the five side-shelf + * quad offsets at {@link #SIDE_TABLE} * @return false if the geometry could not be grouped and was left alone */ public static boolean copyGrouped(long source, long destination, int sourceBytes, @@ -381,10 +384,11 @@ public static boolean copyGrouped(long source, long destination, int sourceBytes // low to high, so the ones under a camera are a prefix of the range. // Shelf 16 holds the down-facing quads that sit outside those levels // and may never be skipped, and it comes after them for that reason: - // put first, as it was at first, the prefix skip eats it. Shelf 17 is - // everything facing neither way. Shelf 18 is the up-facing quads - // outside the levels, before 19..34, which are the levels low to high, - // so the ones over a camera are a suffix. + // put first, as it was at first, the prefix skip eats it. Shelves + // 17..20 are the four sideways facings and shelf 21 is everything + // facing no axis. Shelf 22 is the up-facing quads outside the levels, + // before 23..38, which are the levels low to high, so the ones over a + // camera are a suffix. int[] tally = SHELF_TALLY.get(); java.util.Arrays.fill(tally, 0); for (int q = 0; q < quads; q++) { @@ -545,11 +549,6 @@ static short texture(float value) { return (short) units; } - /** - * Reads and resets the three counts that say whether the packing is honest. - * All three are meant to stay at zero, and a report that never shows them - * is a report that cannot tell anybody it went wrong. - */ /** * Counts one chunk layer's quads by which way they face. * @@ -608,6 +607,11 @@ private static void tallyFacings(long source, int sourceBytes) { } } + /** + * Reads and resets the three counts that say whether the packing is honest. + * All three are meant to stay at zero, and a report that never shows them + * is a report that cannot tell anybody it went wrong. + */ public static String stats() { String line = "vertex layout: " + reason + (atlasPixels > 0 ? ", atlas " + atlasPixels + " px" : ""); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java b/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java index 8e4568d..5ea486a 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java @@ -57,13 +57,6 @@ static final class Entry { long offset; int capacity; int size; - /** - * How many bytes at the start of this range face straight down, and how - * many at the end face straight up. - * - * Zero for geometry that was not grouped, which is the safe reading: - * the draw then covers the whole range exactly as it always did. - */ /** * How many down-facing quads of this range lie below each of the * section's seventeen level boundaries, and how many up-facing ones lie @@ -340,7 +333,7 @@ synchronized long geometryBytes() { private long geometryMemory; private long geometryCapacity; private long nextGeometryOffset; - /** Once a session: the second complaint says nothing the first did not. */ + /** Holes in the geometry buffer, sorted by offset; see {@link FreeRange}. */ private final List freeRanges = new ArrayList(); /** * How much VRAM the geometry buffer is allowed to take before growth turns @@ -419,6 +412,7 @@ synchronized long geometryBytes() { * the recorded copies are submitted and waited on before the head returns * to zero, so nothing is overwritten while the GPU is still reading it. */ + private long stagingBuffer; /** * Sized against the wrap, not against a single upload. Wrapping blocks the * render thread until the GPU has drained the ring, so the interval between @@ -428,7 +422,6 @@ synchronized long geometryBytes() { * be — this replaced a pinned copy per chunk, not nothing. */ private static final long STAGING_RING_MIN = 96L * 1024L * 1024L; - private long stagingBuffer; private long stagingMemory; private long stagingMappedAddress; private long stagingCapacity; @@ -504,7 +497,6 @@ private void queueMaterialCopy(long srcOffset, long dstOffset, int size) { pendingMaterialCopyCount++; } - /** Called by the terrain renderer at the start of each frame. */ /** * The thread that stamps frames, remembered so the assumption below can be * checked instead of believed. @@ -527,6 +519,7 @@ private void queueMaterialCopy(long srcOffset, long dstOffset, int size) { private Thread stampingThread; private boolean threadWarned; + /** Called by the terrain renderer at the start of each frame. */ synchronized void setFrameStamp(long stamp) { this.stampingThread = Thread.currentThread(); this.frameStamp = stamp; @@ -998,12 +991,14 @@ private boolean hasStagedMaterials(int slot) { } } - /** The staged copy for this slot if it still matches, else -1. */ - /** {@link #takeStaged} plus the two group sizes the copy worked out. */ /** Materials that came with the staged geometry, for this upload only. */ private byte[] carriedMaterial = new byte[0]; private int carriedVertices; + /** + * {@link #takeStaged} plus what the copy worked out on the way: the facing + * shelves and the materials it carried. + */ private long takeStagedGrouped(int slot, int size, Entry entry) { carriedVertices = 0; synchronized (workerLock) { @@ -1027,6 +1022,7 @@ private long takeStagedGrouped(int slot, int size, Entry entry) { return takeStaged(slot, size); } + /** The staged copy for this slot if it still matches, else -1. */ private long takeStaged(int slot, int size) { synchronized (workerLock) { Staged entry = staged.remove(slot); @@ -1296,13 +1292,19 @@ private Entry createEntry(int capacity) { /** Chunks turned away because the geometry buffer could not grow. */ private long refusedRanges; - /** Grows the staging ring if a single upload would not fit in it. */ + /** + * Grows the staging ring if a single upload would not fit in it. + * + * Measured against the render thread's half, not the whole ring: that half + * is all {@link #allocateStagingRange} ever hands out, and an upload larger + * than it would run on into the builder region. + */ private void ensureStagingRing(int needed) { - if (stagingCapacity >= needed && stagingBuffer != 0) { + if (stagingCapacity - stagingCapacity / 2 >= needed && stagingBuffer != 0) { return; } long capacity = Math.max(STAGING_RING_MIN, stagingCapacity == 0 ? STAGING_RING_MIN : stagingCapacity); - while (capacity < needed) { + while (capacity - capacity / 2 < needed) { capacity *= 2; } if (stagingBuffer != 0) { @@ -1968,7 +1970,11 @@ private void growGeometryBuffer(long required) { // that the allocation this growth was for lands // somewhere real. int dropped = dropEntriesPast(oldCapacity); - nextGeometryOffset = oldCapacity; + // Rounded down to a vertex: every range has to start on + // one, and the capacity is a MiB multiple, not a stride + // multiple. Nothing kept ends past this point, because + // every kept range ends on a vertex boundary too. + nextGeometryOffset = oldCapacity - oldCapacity % VertexLayout.stride(); LOGGER.error("Geometry mark {} is past the buffer it indexes ({}); the {} " + "bytes beyond it were never uploaded, so {} chunk(s) have " + "been dropped and will return when the game rebuilds them", @@ -2055,6 +2061,15 @@ private void ensureMaterialBuffer() { vkCmdFillBuffer(uploadCommandBuffer, materialBuffer, 0, VK_WHOLE_SIZE, 0); if (oldBuffer != 0 && keep > 0) { try (MemoryStack stack = stackPush()) { + // Two transfer writes to the same bytes are unordered without + // this: the fill could land after the copy and wipe it. + VkMemoryBarrier.Buffer fillDone = VkMemoryBarrier.calloc(1, stack); + fillDone.get(0) + .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER) + .srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT); + vkCmdPipelineBarrier(uploadCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, fillDone, null, null); VkBufferCopy.Buffer copy = VkBufferCopy.calloc(1, stack); copy.get(0).srcOffset(0).dstOffset(0).size(keep); vkCmdCopyBuffer(uploadCommandBuffer, oldBuffer, materialBuffer, copy); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkDemoRenderer.java b/src/main/java/net/vulkanmodnext/vkimpl/VkDemoRenderer.java index 9c0d83c..ca9938a 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkDemoRenderer.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkDemoRenderer.java @@ -7,6 +7,7 @@ import org.lwjgl.system.MemoryUtil; import org.lwjgl.vulkan.VkBufferCreateInfo; import org.lwjgl.vulkan.VkBufferImageCopy; +import org.lwjgl.vulkan.VkBufferMemoryBarrier; import org.lwjgl.vulkan.VkClearValue; import org.lwjgl.vulkan.VkCommandBuffer; import org.lwjgl.vulkan.VkCommandBufferAllocateInfo; @@ -37,6 +38,7 @@ import org.lwjgl.vulkan.VkRenderPassCreateInfo; import org.lwjgl.vulkan.VkShaderModuleCreateInfo; import org.lwjgl.vulkan.VkSubmitInfo; +import org.lwjgl.vulkan.VkSubpassDependency; import org.lwjgl.vulkan.VkSubpassDescription; import org.lwjgl.vulkan.VkViewport; import org.lwjgl.vulkan.VkAttachmentDescription; @@ -173,6 +175,21 @@ private void recordCommandBuffer(MemoryStack stack) { .layerCount(1); vkCmdCopyImageToBuffer(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer, region); + // A fence wait alone does not make device writes visible to the host; + // the copy has to be made available to HOST_READ explicitly. + VkBufferMemoryBarrier.Buffer toHost = VkBufferMemoryBarrier.calloc(1, stack); + toHost.get(0) + .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER) + .srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_HOST_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .buffer(stagingBuffer) + .offset(0) + .size(VK_WHOLE_SIZE); + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, + 0, null, toHost, null); + check(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer"); } @@ -238,10 +255,23 @@ private void createRenderPass() { .colorAttachmentCount(1) .pColorAttachments(colorRef); + // The implicit external dependency after the pass waits on nothing + // (BOTTOM_OF_PIPE, no access), so the copy that reads the image + // right after it needs an explicit one against the colour writes. + VkSubpassDependency.Buffer dependency = VkSubpassDependency.calloc(1, stack); + dependency.get(0) + .srcSubpass(0) + .dstSubpass(VK_SUBPASS_EXTERNAL) + .srcStageMask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) + .srcAccessMask(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) + .dstStageMask(VK_PIPELINE_STAGE_TRANSFER_BIT) + .dstAccessMask(VK_ACCESS_TRANSFER_READ_BIT); + VkRenderPassCreateInfo rpInfo = VkRenderPassCreateInfo.calloc(stack) .sType(VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO) .pAttachments(attachment) - .pSubpasses(subpass); + .pSubpasses(subpass) + .pDependencies(dependency); LongBuffer pRenderPass = stack.mallocLong(1); check(vkCreateRenderPass(device(), rpInfo, null, pRenderPass), "vkCreateRenderPass"); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkInteropRenderer.java b/src/main/java/net/vulkanmodnext/vkimpl/VkInteropRenderer.java index 9404235..43451a5 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkInteropRenderer.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkInteropRenderer.java @@ -47,6 +47,7 @@ import org.lwjgl.vulkan.VkSemaphoreCreateInfo; import org.lwjgl.vulkan.VkShaderModuleCreateInfo; import org.lwjgl.vulkan.VkSubmitInfo; +import org.lwjgl.vulkan.VkSubpassDependency; import org.lwjgl.vulkan.VkSubpassDescription; import org.lwjgl.vulkan.VkViewport; @@ -225,7 +226,7 @@ private long createExportedImage() { VkMemoryAllocateInfo alloc = VkMemoryAllocateInfo.calloc(stack) .sType(VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO) - .pNext(export.address()) + .pNext(Interop.appendWin32MemoryRights(stack, export.address())) .allocationSize(req.size()) .memoryTypeIndex(findMemoryType(stack, req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); LongBuffer pMemory = stack.mallocLong(1); @@ -282,7 +283,7 @@ private void createSemaphores() { .handleTypes(Interop.semaphoreHandleType()); VkSemaphoreCreateInfo semInfo = VkSemaphoreCreateInfo.calloc(stack) .sType(VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO) - .pNext(export.address()); + .pNext(Interop.appendWin32SemaphoreRights(stack, export.address())); LongBuffer pSem = stack.mallocLong(1); check(vkCreateSemaphore(device(), semInfo, null, pSem), "vkCreateSemaphore(signal)"); @@ -321,10 +322,24 @@ private void createRenderPassAndPipeline() { .colorAttachmentCount(1) .pColorAttachments(colorRef); + // The frame waits on GL's semaphore at COLOR_ATTACHMENT_OUTPUT, but + // the implicit external dependency lets the UNDEFINED -> attachment + // layout transition run at TOP_OF_PIPE, before that wait, while GL + // may still be sampling. Tie it to the waited stage instead. + VkSubpassDependency.Buffer dependency = VkSubpassDependency.calloc(1, stack); + dependency.get(0) + .srcSubpass(VK_SUBPASS_EXTERNAL) + .dstSubpass(0) + .srcStageMask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) + .srcAccessMask(0) + .dstStageMask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) + .dstAccessMask(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + VkRenderPassCreateInfo rpInfo = VkRenderPassCreateInfo.calloc(stack) .sType(VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO) .pAttachments(attachment) - .pSubpasses(subpass); + .pSubpasses(subpass) + .pDependencies(dependency); LongBuffer pRenderPass = stack.mallocLong(1); check(vkCreateRenderPass(device(), rpInfo, null, pRenderPass), "vkCreateRenderPass"); renderPass = pRenderPass.get(0); @@ -568,6 +583,11 @@ synchronized void destroy() { ready = false; return; } + // The last submitted frame may still be executing; its command buffer + // and everything it references must outlive it. + if (fence != 0) { + vkWaitForFences(device, fence, true, 1_000_000_000L); + } vkDestroyFence(device, fence, null); fence = 0; vkDestroyCommandPool(device, commandPool, null); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkRayTracing.java b/src/main/java/net/vulkanmodnext/vkimpl/VkRayTracing.java index b61044a..e834afa 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkRayTracing.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkRayTracing.java @@ -77,7 +77,6 @@ final class VkRayTracing { private static final Logger LOGGER = LogManager.getLogger("VulkanModNext/RayTracing"); - /** Vertex stride of the mirrored chunk geometry, positions first. */ /** * Creature geometry, which is captured whole and never packed. * @@ -402,7 +401,10 @@ private void buildFrame(int[] chunks, int chunkCount, VkChunkMirror mirror, long buildOne(stack, creatureBlas, creatureAddress); } int instances = writeInstances(stack); - if (instances > 0) { + // Rebuilt even when empty once this slot has a structure: left + // alone, it would go on naming bottom-level structures that are + // retired and then freed while the shader still traces through it. + if (instances > 0 || topLevel(activeSlot) != 0) { buildTopLevel(stack, instances); } lastInstances = instances; @@ -1036,10 +1038,19 @@ private void ensureTopLevelSlots() { if (tlas != null && tlas.length >= slotCount) { return; } - tlas = new long[slotCount]; - tlasBuffer = new long[slotCount]; - tlasMemory = new long[slotCount]; - tlasCapacity = new long[slotCount]; + // Grown rather than replaced: the structures already in the old + // arrays are still live, and dropping them would leak them. + if (tlas == null) { + tlas = new long[slotCount]; + tlasBuffer = new long[slotCount]; + tlasMemory = new long[slotCount]; + tlasCapacity = new long[slotCount]; + return; + } + tlas = java.util.Arrays.copyOf(tlas, slotCount); + tlasBuffer = java.util.Arrays.copyOf(tlasBuffer, slotCount); + tlasMemory = java.util.Arrays.copyOf(tlasMemory, slotCount); + tlasCapacity = java.util.Arrays.copyOf(tlasCapacity, slotCount); } private void retireTopLevel(int slot) { @@ -1069,12 +1080,15 @@ private void retire(Blas blas, long frameIndex) { entry.bytes = blas.bytes; entry.frame = frameIndex; retired.add(entry); + // sourceOffset/Size/Version are left alone: buildOne retires the old + // structure after they were set for the new build, and clearing the + // size here made every chunk look stale again the following frame. + // A zero structure already marks a Blas as needing a build. blas.structure = 0; blas.buffer = 0; blas.memory = 0; blas.address = 0; blas.bytes = 0; - blas.sourceSize = 0; } /** diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java b/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java index e106c3e..aef09ae 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java @@ -481,8 +481,9 @@ private static final class TerrainPipeline { new TerrainPipeline("cutout", true, false, true), new TerrainPipeline("translucent", false, true, false), }; - /** Index into {@link #TERRAIN_PIPELINES}; also the layer ordinal vanilla uses. */ + /** Vanilla's ordinal for the translucent layer. */ private static final int LAYER_TRANSLUCENT = 3; + /** Index into {@link #TERRAIN_PIPELINES}. */ private static final int PIPELINE_TRANSLUCENT = 2; /** Vanilla's second opaque layer: leaves, and what casts a canopy's shadow. */ private static final int LAYER_CUTOUT_MIPPED = 1; @@ -1314,8 +1315,9 @@ private static boolean queueTransferWanted() { /** * Releases the shared colour and depth images to OpenGL, or takes them back. * - * Both sit in SHADER_READ_ONLY_OPTIMAL between frames, which is where the - * render pass leaves them and what the composite samples. The layout does + * Both sit in {@link #sharedLayout()} and {@link #sharedDepthLayout()} + * between frames, which is where the render pass leaves them and what the + * composite samples. The layout does * not move here — only the ownership does, and the pair has to match: a * release without its acquire leaves the next frame writing to an image it * does not hold. @@ -1903,6 +1905,11 @@ synchronized boolean renderLayer(int layerOrdinal, int[] chunks, int chunkCount, if (frameOpen || colorImage == 0) { return false; } + // Its draws come out of the same batch, which is sized from this + // on the next frame's first layer. + if (chunkCount > peakDrawsNeeded) { + peakDrawsNeeded = chunkCount; + } long t = System.nanoTime(); long waitBefore = translucentWaitNanos; boolean taken = renderTranslucent(chunks, chunkCount, mvp, viewX, viewY, viewZ, mirror); @@ -2315,8 +2322,10 @@ private void updateRayTracing(int[] chunks, int chunkCount, VkChunkMirror mirror // are the previous frame's: this runs on the first layer of the frame // and they are not drawn until the last. Their slot is not the one // being written now, so the geometry is still there to be read. + // With one frame in flight the previous slot is this one, which the + // sprite pass rewrites later this frame while the build still reads it. int previous = (activeFrameSlot + framesInFlight - 1) % framesInFlight; - if (creatureVertexCount != null && creatureVertexCount[previous] > 0 + if (framesInFlight > 1 && creatureVertexCount != null && creatureVertexCount[previous] > 0 && spriteVertexBuffers != null && spriteVertexBuffers[previous] != 0) { rayTracing.setCreatureGeometry(spriteVertexBuffers[previous], (long) creatureFirstVertex[previous] * SPRITE_VERTEX_STRIDE, @@ -2909,8 +2918,8 @@ private void drawChunks(int layerOrdinal, int[] chunks, int chunkCount, float[] // lie inside that span, and a camera above all of them sees the // underside of none — the card would work that out too, but only // after fetching every one of those vertices, and fetching is - // what this pass is bound by. Both ends can never go at once, - // and geometry that was never grouped has zero at both. + // what this pass is bound by. Geometry that was never grouped + // has zero at both ends. int quadCount = vertexCount / 4; int begin = 0; int end = quadCount; @@ -3107,12 +3116,14 @@ private void submitFrame() { frameSignalled = true; try (MemoryStack stack = stackPush()) { vkCmdEndRenderPass(commandBuffer); - // Handed to OpenGL as the last thing this frame records, so the - // composite that follows reads images Vulkan no longer owns. - transferSharedImages(stack, commandBuffer, true); + // The readback copies out of the colour image, so it has to be + // recorded while this queue still owns it. if (STARTUP_READBACK && (frameCounter == 0 || frameCounter == 119)) { recordColorReadback(stack); } + // Handed to OpenGL as the last thing this frame records, so the + // composite that follows reads images Vulkan no longer owns. + transferSharedImages(stack, commandBuffer, true); if (timestampsSupported) { vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool, activeFrameSlot * 4 + 1); @@ -3130,9 +3141,13 @@ private void submitFrame() { signalFenceValue++; } if (!firstFrame && SHARED_SEMAPHORES) { + // The depth clear runs at the early fragment tests, ahead of + // colour output: a wait at colour output alone would let it + // land while OpenGL is still reading the last frame's depth. submit.waitSemaphoreCount(1) .pWaitSemaphores(stack.longs(vkWaitSemaphore)) - .pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT)); + .pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT)); } firstFrame = false; firstFrameStage("submitting the opaque frame"); @@ -3696,7 +3711,9 @@ private int compositeVariant() { * the caller is expected to keep asking rather than to ask once. */ synchronized int sharedDepthTextureForGame(int wantedWidth, int wantedHeight) { - if (!SHARED_DEPTH_WANTED || !baseReady || glDepthTexture == -1) { + // Not without semaphores: handing depth back and forth is a semaphore + // signal each way, and with them off nothing waits on either. + if (!SHARED_DEPTH_WANTED || !SHARED_SEMAPHORES || !baseReady || glDepthTexture == -1) { return 0; } // A target of the wrong size is worse than no offer: a framebuffer whose @@ -4400,7 +4417,7 @@ private void compositeInner() { * back from a depth and the two numbers the projection is made of; the * surface's direction comes from how that position changes across the * screen, which is exact here because every face of a block is flat. Then - * eight neighbours are asked whether they stand in front of the surface, + * sixteen neighbours are asked whether they stand in front of the surface, * and how much they do is the answer. * * Run inside the composite, while the depth image is still this renderer's @@ -5018,8 +5035,8 @@ private void buildAoProgram() { // hundreds of blocks away in the sky and says nothing // about what stands between. Without this the shade of // passing clouds swept across the floor of a closed - // house. Six steps of two blocks towards the sun is - // enough to find a ceiling and cheap enough to spend: + // house. A short march towards the sun is enough to + // find a ceiling and cheap enough to spend: // it is the same march the contact shadows do, walked // further and asked a coarser question. + " vec3 up = rayStart;\n" @@ -5790,8 +5807,8 @@ private void applySceneOcclusionTimed(int sceneTexture) { GL30C.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL11C.GL_COLOR_BUFFER_BIT, GL11C.GL_NEAREST); // Asked as well, and for the harder reason: a refused colour - // copy leaves this texture black, and the grading pass writes - // what it read back over the whole frame. That is a world that + // copy leaves this texture black, and the passes that read it + // write what they read back over the whole frame. That is a world that // has gone black with the hand still on top of it — the hand // being drawn after all of this — which reads as the renderer // having failed rather than as one copy having been refused. @@ -5801,7 +5818,7 @@ private void applySceneOcclusionTimed(int sceneTexture) { if (error != GL11C.GL_NO_ERROR) { sceneColourUsable = false; LOGGER.warn("The frame's colour would not copy into a texture (GL error" - + " 0x{}), so grading and light shafts are off for this session:" + + " 0x{}), so scene occlusion and light shafts are off for this session:" + " both read that copy, and reading it empty paints the world" + " black", Integer.toHexString(error)); } @@ -6547,7 +6564,9 @@ private String probeCentre(int fbo) { org.lwjgl.opengl.GL15C.glBindBuffer( org.lwjgl.opengl.GL21C.GL_PIXEL_PACK_BUFFER, prevPack); } - if (readError != GL11C.GL_NO_ERROR || samples > 1 || prevPack != 0) { + // A pack buffer was unbound above, so it no longer spoils the read; + // it is only named in the message. + if (readError != GL11C.GL_NO_ERROR || samples > 1) { return "UNREADABLE (GL error 0x" + Integer.toHexString(readError) + ", " + samples + " samples, pack buffer " + prevPack + ")"; } @@ -6970,8 +6989,13 @@ private boolean ensureBloomTargets() { destroyBloomTargets(); bloomWidth = wantWidth; bloomHeight = wantHeight; + // A refusal from buildBloomTargets only turns the effect off once the + // eight-bit fallback has been refused too; setting bloomFailed there + // would make the fallback below build targets nothing ever used. if (!buildBloomTargets()) { if (!bloomFloat) { + LOGGER.error("Bloom targets refused; the effect is off for this session"); + bloomFailed = true; return false; } // A driver without float render targets: the reach will suffer, @@ -6982,6 +7006,7 @@ private boolean ensureBloomTargets() { bloomWidth = wantWidth; bloomHeight = wantHeight; if (!buildBloomTargets()) { + LOGGER.error("Bloom targets refused in eight bits too; the effect is off for this session"); bloomFailed = true; return false; } @@ -7009,11 +7034,10 @@ private boolean buildBloomTargets() { GL30C.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, bloomTexture[i], 0); if (GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER) != GL30C.GL_FRAMEBUFFER_COMPLETE) { - LOGGER.error("Bloom framebuffer incomplete; the effect is off for this session"); + LOGGER.error("Bloom framebuffer incomplete"); GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, prevFbo); GL11C.glBindTexture(GL11C.GL_TEXTURE_2D, prevTexture); destroyBloomTargets(); - bloomFailed = true; return false; } } @@ -7029,11 +7053,10 @@ private boolean buildBloomTargets() { GL30C.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, bloomNearTexture[i], 0); if (GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER) != GL30C.GL_FRAMEBUFFER_COMPLETE) { - LOGGER.error("Bloom framebuffer incomplete; the effect is off for this session"); + LOGGER.error("Bloom framebuffer incomplete"); GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, prevFbo); GL11C.glBindTexture(GL11C.GL_TEXTURE_2D, prevTexture); destroyBloomTargets(); - bloomFailed = true; return false; } } @@ -7047,8 +7070,10 @@ private boolean buildBloomTargets() { GL30C.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, bloomFarTexture[i], 0); if (GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER) != GL30C.GL_FRAMEBUFFER_COMPLETE) { + LOGGER.error("Bloom framebuffer incomplete"); GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, prevFbo); GL11C.glBindTexture(GL11C.GL_TEXTURE_2D, prevTexture); + destroyBloomTargets(); return false; } } @@ -7059,11 +7084,10 @@ private boolean buildBloomTargets() { GL30C.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, bloomMaskTexture, 0); if (GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER) != GL30C.GL_FRAMEBUFFER_COMPLETE) { - LOGGER.error("Bloom mask framebuffer incomplete; the effect is off for this session"); + LOGGER.error("Bloom mask framebuffer incomplete"); GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, prevFbo); GL11C.glBindTexture(GL11C.GL_TEXTURE_2D, prevTexture); destroyBloomTargets(); - bloomFailed = true; return false; } GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, prevFbo); @@ -7160,7 +7184,7 @@ private void recordColorReadback(MemoryStack stack) { .sType(VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER) .srcAccessMask(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) .dstAccessMask(VK_ACCESS_TRANSFER_READ_BIT) - .oldLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + .oldLayout(sharedLayout()) .newLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) @@ -7183,7 +7207,7 @@ private void recordColorReadback(MemoryStack stack) { .srcAccessMask(VK_ACCESS_TRANSFER_READ_BIT) .dstAccessMask(VK_ACCESS_SHADER_READ_BIT) .oldLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) - .newLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + .newLayout(sharedLayout()); vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, null, null, barrier); readbackRecorded = true; @@ -7210,8 +7234,11 @@ private void logFrameDiagnostics() { check(vkWaitForFences(device(), fence, true, 1_000_000_000L), "vkWaitForFences(readback)"); int samples = width * READBACK_ROWS; int covered = 0; + // The last byte of a pixel is the top of its alpha in both + // formats: RGBA8 and RGBA16F alike. + int bpp = colourBytesPerPixel(); for (int i = 0; i < samples; i++) { - if (MemoryUtil.memGetByte(readbackMapped + i * 4L + 3) != 0) { + if (MemoryUtil.memGetByte(readbackMapped + i * (long) bpp + bpp - 1) != 0) { covered++; } } @@ -8178,7 +8205,7 @@ private void readShaderSettings() { hdrFrame = Boolean.parseBoolean( System.getProperty("vulkanmodnext.hdrFrameActive", "false")); exposure = clampPercent(intProperty("vulkanmodnext.exposure", 50)); - sceneGamma = intProperty("vulkanmodnext.sceneGamma", 50); + sceneGamma = Math.max(0, Math.min(100, intProperty("vulkanmodnext.sceneGamma", 50))); colourVision = Math.max(0, Math.min(3, intProperty("vulkanmodnext.colourVision", 0))); skyGradient = clampPercent(intProperty("vulkanmodnext.skyGradient", 0)); sceneOcclusion = Boolean.parseBoolean( @@ -8531,10 +8558,12 @@ private void createRenderPass(MemoryStack stack) { * Two things separate it from the opaque one. Its colour attachment starts * cleared to fully transparent, because what it produces is composited over * a frame OpenGL has meanwhile drawn entities into rather than replacing - * it. And its depth attachment is loaded rather than cleared, and never - * stored: the layer is depth-tested against what is already there and - * writes nothing back, which is what vanilla does too — {@code - * depthMask(false)} right before it asks for the layer. + * it. And its depth attachment is loaded rather than cleared: the layer is + * depth-tested against what is already there and writes nothing back, + * which is what vanilla does too — {@code depthMask(false)} right before it + * asks for the layer. It is still stored, because the creature subpass + * before the layer does write depth, and with shared depth the game goes + * on using that image for rain, clouds and the hand. */ private void createTranslucentRenderPass(MemoryStack stack) { VkAttachmentDescription.Buffer attachments = VkAttachmentDescription.calloc(2, stack); @@ -8551,7 +8580,7 @@ private void createTranslucentRenderPass(MemoryStack stack) { .format(depthFormat(stack)) .samples(VK_SAMPLE_COUNT_1_BIT) .loadOp(VK_ATTACHMENT_LOAD_OP_LOAD) - .storeOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) + .storeOp(VK_ATTACHMENT_STORE_OP_STORE) .stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE) .stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE) // Not what the opaque pass left it in — what OpenGL says it is @@ -9156,11 +9185,8 @@ private void createCompositeProgram() { * clear value, and that is exactly the sky. No test on colour, which would * catch a white cloud or a snowy peak. * - * Screen height rather than a true view direction, and this is the honest - * limit of it: looking straight up puts the deepest part of the gradient - * across the middle of the screen instead of at the point overhead. That is - * what every cheap version of this does, it is a look rather than a sky - * model, and the slider is where somebody decides how much of it they want. + * It is a look rather than a sky model, and the slider is where somebody + * decides how much of it they want. */ private int buildSkyGradientProgram() { return buildQuadProgram( @@ -9756,7 +9782,7 @@ private static boolean depthBlitAllowed() { return !"false".equals(System.getProperty("vulkanmodnext.depthBlit")); } - /** Read where the sampler is built; the world has to be reloaded to change it. */ + /** Checked every frame by refreshSamplerIfNeeded; a change re-mirrors the atlas, no reload needed. */ private static boolean flatBlockColours() { return "true".equals(System.getProperty("vulkanmodnext.flatBlockColours")); } @@ -9817,6 +9843,11 @@ private void verifyImportedTargets() { GL11C.GL_TEXTURE_2D, 0, 0); GL30C.glFramebufferTexture2D(GL30C.GL_DRAW_FRAMEBUFFER, GL30C.GL_DEPTH_ATTACHMENT, GL11C.GL_TEXTURE_2D, glDepthTexture, 0); + // Depth only now: without this an older driver calls the framebuffer + // incomplete for the colour draw buffer it no longer has, and the + // refusal would be blamed on the depth image. + GL20C.glDrawBuffers(GL11C.GL_NONE); + GL11C.glReadBuffer(GL11C.GL_NONE); int depthStatus = GL30C.glCheckFramebufferStatus(GL30C.GL_DRAW_FRAMEBUFFER); GL30C.glBindFramebuffer(GL30C.GL_DRAW_FRAMEBUFFER, prevDraw); GL30C.glDeleteFramebuffers(fbo); @@ -9882,10 +9913,9 @@ private void probeImportedRead() { int scratchTexture = 0; int readFbo = 0; int drawFbo = 0; + boolean pushed = false; try (MemoryStack stack = stackPush()) { - IntBuffer viewport = stack.mallocInt(4); - GL11C.glGetIntegerv(GL11C.GL_VIEWPORT, viewport); java.nio.ByteBuffer pixel = stack.calloc(16); org.lwjgl.opengl.GL11.glPushAttrib(org.lwjgl.opengl.GL11.GL_ENABLE_BIT @@ -9895,6 +9925,7 @@ private void probeImportedRead() { | org.lwjgl.opengl.GL11.GL_CURRENT_BIT | org.lwjgl.opengl.GL11.GL_VIEWPORT_BIT | org.lwjgl.opengl.GL11.GL_POLYGON_BIT); + pushed = true; GL20C.glUseProgram(0); GL11C.glDisable(GL11C.GL_DEPTH_TEST); GL11C.glDisable(GL11C.GL_BLEND); @@ -9939,10 +9970,12 @@ private void probeImportedRead() { GL11C.glFinish(); LOGGER.info("Shared memory probe: all three steps survived — this driver can read" + " what Vulkan shared with it"); - - GL11C.glViewport(viewport.get(0), viewport.get(1), viewport.get(2), viewport.get(3)); - org.lwjgl.opengl.GL11.glPopAttrib(); } finally { + // The attribute stack is the game's too: a probe that throws must + // not leave it one deeper. GL_VIEWPORT_BIT brings the viewport back. + if (pushed) { + org.lwjgl.opengl.GL11.glPopAttrib(); + } if (scratchTexture != 0) { GL11C.glDeleteTextures(scratchTexture); } @@ -10067,14 +10100,7 @@ private void destroyDepthImportTargets() { } } - /** - * One GL pass, timed by the card itself. - * - * Two query objects used in turn, because reading the one just written - * means waiting for the card to reach it — which stops the processor dead - * and changes the very thing being measured. The other one holds last - * frame's answer and is ready by now. - */ + /** A GL pass's latest time on the card for the log, or "n/a" when the driver cannot time it. */ private static String glTimeText(GlTimer timer) { double ms = timer.millis(); return ms < 0.0 ? "n/a" : String.format("%.2f ms", ms); @@ -10201,6 +10227,9 @@ private int createDepthReadFbo() { GL30C.glBindFramebuffer(GL30C.GL_READ_FRAMEBUFFER, fbo); GL30C.glFramebufferTexture2D(GL30C.GL_READ_FRAMEBUFFER, GL30C.GL_DEPTH_ATTACHMENT, GL11C.GL_TEXTURE_2D, glDepthTexture, 0); + // Depth only, so the read buffer has to say so or an older driver + // calls the framebuffer incomplete. + GL11C.glReadBuffer(GL11C.GL_NONE); int status = GL30C.glCheckFramebufferStatus(GL30C.GL_READ_FRAMEBUFFER); GL30C.glBindFramebuffer(GL30C.GL_READ_FRAMEBUFFER, prevRead); if (status != GL30C.GL_FRAMEBUFFER_COMPLETE) { @@ -10714,6 +10743,8 @@ private void destroyTargets() { destroyToneTargets(); destroySceneOcclusionTargets(); destroyRayTargets(); + destroyMotionTargets(); + destroyMotionProgram(); GL11C.glDeleteTextures(glColorTexture); GL11C.glDeleteTextures(glDepthTexture); EXTMemoryObject.glDeleteMemoryObjectsEXT(glColorMemoryObject); @@ -10734,6 +10765,9 @@ private void destroyTargets() { compositeWaitTimer.destroy(); depthBlitTimer.destroy(); depthImportTimer.destroy(); + occlusionTimer.destroy(); + toneTimer.destroy(); + bloomTimer.destroy(); } else { gameDepthTexture = 0; glDepthWriteFbo = -1; @@ -10809,20 +10843,22 @@ synchronized void destroy() { lightmapStagingMapped[i] = 0; } } - for (int i = 0; i < framesInFlight; i++) { - if (frameUniformMemories[i] != 0) { - vkUnmapMemory(device(), frameUniformMemories[i]); - vkDestroyBuffer(device(), frameUniformBuffers[i], null); - vkFreeMemory(device(), frameUniformMemories[i], null); - frameUniformBuffers[i] = 0; - frameUniformMemories[i] = 0; - frameUniformMapped[i] = 0; - } - } MemoryUtil.memFree(lightmapReadBuffer); lightmapReadBuffer = null; lightmapImage = 0; } + // Made with the descriptor infrastructure, not with the lightmap, which + // only exists once the game has handed one over. + for (int i = 0; i < framesInFlight; i++) { + if (frameUniformMemories[i] != 0) { + vkUnmapMemory(device(), frameUniformMemories[i]); + vkDestroyBuffer(device(), frameUniformBuffers[i], null); + vkFreeMemory(device(), frameUniformMemories[i], null); + frameUniformBuffers[i] = 0; + frameUniformMemories[i] = 0; + frameUniformMapped[i] = 0; + } + } if (quadIndexBuffer != 0) { vkDestroyBuffer(device(), quadIndexBuffer, null); vkFreeMemory(device(), quadIndexMemory, null); diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VulkanContextImpl.java b/src/main/java/net/vulkanmodnext/vkimpl/VulkanContextImpl.java index 0b87d05..4b500e6 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VulkanContextImpl.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VulkanContextImpl.java @@ -393,7 +393,7 @@ private void pickPhysicalDevice() { // The extension count is here because it is what sizes the // scratch stack this very startup nearly ran out of, and a log // that reports it turns the next such report into one line - // instead of a guess. See Lwjgl3Natives.DEFAULT_STACK_SIZE_KB. + // instead of a guess. See VulkanLoader.DEFAULT_STACK_SIZE_KB. IntBuffer extensions = stack.mallocInt(1); vkEnumerateDeviceExtensionProperties(candidate, (String) null, extensions, null); LOGGER.info("GPU {}: {} ({}, Vulkan {}, {} extensions, score {}{})", From 39a65d6d952b020c0013d5a81e3ddc256b44280b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:04:57 +0000 Subject: [PATCH 2/6] fix patch and loader comments, boss fog scaling and turning pop-in Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../java/net/vulkanmodnext/VulkanBridge.java | 3 +-- .../java/net/vulkanmodnext/VulkanLoader.java | 4 ++-- .../java/net/vulkanmodnext/VulkanModNext.java | 2 +- .../net/vulkanmodnext/core/VulkanCorePlugin.java | 2 +- .../vulkanmodnext/core/VulkanPatchGroups.java | 2 +- .../vulkanmodnext/mixin/FogDistanceMixin.java | 12 ++++++++---- .../net/vulkanmodnext/mixin/ModelPartMixin.java | 16 +++++++++------- .../mixin/OwnVisibilityWalkMixin.java | 15 ++++++++++++++- .../net/vulkanmodnext/mixin/SkySunMixin.java | 4 ++-- .../mixin/TextureMapAnimationMixin.java | 5 +++-- .../vulkanmodnext/mixin/VisibilityWalkMixin.java | 6 ++++-- 11 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/VulkanBridge.java b/src/main/java/net/vulkanmodnext/VulkanBridge.java index 7dd8e0e..7333682 100644 --- a/src/main/java/net/vulkanmodnext/VulkanBridge.java +++ b/src/main/java/net/vulkanmodnext/VulkanBridge.java @@ -420,8 +420,7 @@ boolean renderTerrainLayer(int layerOrdinal, int[] chunks, int chunkCount, float * * {@code alphaCutoff} is the game's own alpha test for this batch, and the * two values differ: particles cut at one 255th, weather at a tenth. - */ - /** + * * @return false when the renderer could not take the batch, so that the * caller draws it the way the game would have. It used to return * nothing, drop what would not fit and leave the caller believing diff --git a/src/main/java/net/vulkanmodnext/VulkanLoader.java b/src/main/java/net/vulkanmodnext/VulkanLoader.java index 7d350df..3d5f3c5 100644 --- a/src/main/java/net/vulkanmodnext/VulkanLoader.java +++ b/src/main/java/net/vulkanmodnext/VulkanLoader.java @@ -29,7 +29,7 @@ public final class VulkanLoader { /** * Volatile so that reading it needs no monitor. * - * It is written once, inside the synchronized constructor below, and read + * It is written once, inside the synchronized bridge() below, and read * from the render thread several times a frame and from chunk-building * threads besides. A monitor was being taken for each of those reads, on a * field that never changes again after the one write; volatile gives the @@ -107,7 +107,7 @@ private static int maxSupportedJava() { * * LWJGL's own default of 64 KiB holds 252 entries, and current drivers list * around 270 for a single card — machines have failed to start over a - * shortfall of three kilobytes. 2 MiB is 8064 entries, twenty-nine drivers' + * shortfall of three kilobytes. 2 MiB is 8065 entries, twenty-nine drivers' * worth, which is room for whatever a multi-GPU machine with overlay layers * turns out to list. * diff --git a/src/main/java/net/vulkanmodnext/VulkanModNext.java b/src/main/java/net/vulkanmodnext/VulkanModNext.java index 037c9c2..86aafab 100644 --- a/src/main/java/net/vulkanmodnext/VulkanModNext.java +++ b/src/main/java/net/vulkanmodnext/VulkanModNext.java @@ -20,7 +20,7 @@ * loader of its own with a different LWJGL, and it is brought up later, from * the game's own client side, once there is a window to draw into. * - * @see net.vulkanmodnext.client.VulkanLoader for how the other half is reached + * @see net.vulkanmodnext.VulkanLoader for how the other half is reached * @see net.vulkanmodnext.VulkanBridge for the whole of what the two halves share */ @Mod( diff --git a/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java b/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java index 4d753a1..e2d876d 100644 --- a/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java +++ b/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java @@ -244,7 +244,7 @@ private static void writeTemplate(File config, File file) throws java.io.IOExcep out.write("# It is for renderers that came out after this build did. These are\n"); out.write("# already known and do not need a line: optifine, shadersmod,\n"); out.write("# celeritas, actinium, nothirium, vulcanizator, vintagium, relictium,\n"); - out.write("# neonium, magnesium.\n"); + out.write("# neonium.\n"); out.write("#\n"); out.write("# Anything after a # is ignored. Blank lines are ignored.\n"); out.write("# To go the other way and load this renderer anyway, start the game\n"); diff --git a/src/main/java/net/vulkanmodnext/core/VulkanPatchGroups.java b/src/main/java/net/vulkanmodnext/core/VulkanPatchGroups.java index dccd6e4..1b34270 100644 --- a/src/main/java/net/vulkanmodnext/core/VulkanPatchGroups.java +++ b/src/main/java/net/vulkanmodnext/core/VulkanPatchGroups.java @@ -16,7 +16,7 @@ * later attempt to load that class reports it as missing — so a broken patch on * {@code RenderGlobal} surfaces as {@code NoClassDefFoundError: * net/minecraft/client/renderer/RenderGlobal}, naming neither the patch nor the - * mod that wrote it. Eleven of the patches below aim at that one class, and so + * mod that wrote it. Twelve of the patches below aim at that one class, and so * do Alfheim, VintageFix and LoliASM. Whoever loses that argument, the message * on screen is the same and points at nobody. * diff --git a/src/main/java/net/vulkanmodnext/mixin/FogDistanceMixin.java b/src/main/java/net/vulkanmodnext/mixin/FogDistanceMixin.java index ae90d7d..30870a1 100644 --- a/src/main/java/net/vulkanmodnext/mixin/FogDistanceMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/FogDistanceMixin.java @@ -82,12 +82,16 @@ public abstract class FogDistanceMixin { /** * Whether the fog on screen is carrying gameplay state rather than distance. * - * No test for boss fog: it is set up elsewhere and by the time this runs it - * cannot be told apart from an ordinary view, so it is left alone by being - * out of reach rather than by being detected. + * Boss fog is linear like an ordinary view's and is set up in this same + * method, so the fog mode alone cannot tell it apart; it is asked for by + * name, the same way the game decides to draw it. */ private static boolean tellsYouSomething() { - Entity view = Minecraft.getMinecraft().getRenderViewEntity(); + Minecraft mc = Minecraft.getMinecraft(); + if (mc.ingameGUI != null && mc.ingameGUI.getBossOverlay().shouldCreateFog()) { + return true; + } + Entity view = mc.getRenderViewEntity(); if (view == null) { return false; } diff --git a/src/main/java/net/vulkanmodnext/mixin/ModelPartMixin.java b/src/main/java/net/vulkanmodnext/mixin/ModelPartMixin.java index a15d4fa..4c1886d 100644 --- a/src/main/java/net/vulkanmodnext/mixin/ModelPartMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/ModelPartMixin.java @@ -9,18 +9,20 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * Watches every model part the game draws, and changes nothing. + * Watches every model part the game draws, and takes the ones Vulkan draws. * * Both ends of the call, because the skeleton nests: a part's children are * drawn inside its frame, and without the second hook every sibling would be * placed inside its elder brother instead of beside him. * - * Neither is cancellable. This is the point where entities would eventually be - * taken into Vulkan, and it is also the busiest shared method in the client — - * every creature, every armour layer, every mod that builds its models the - * ordinary way passes through here. A hook that only looks cannot break a mod - * that does something unusual, because that mod goes on drawing exactly as it - * did. + * Only the head is cancellable, and it cancels only when + * {@code EntityGeometry.takePart} has taken the part — and with it every child + * — into Vulkan. This is the busiest shared method in the client — every + * creature, every armour layer, every mod that builds its models the ordinary + * way passes through here — so whenever the part is not taken the hook only + * looks, and a mod that does something unusual goes on drawing exactly as it + * did. The return hook is skipped along with a cancelled call, which is + * correct: the part was never begun on this side either. */ @Mixin(ModelRenderer.class) public abstract class ModelPartMixin { diff --git a/src/main/java/net/vulkanmodnext/mixin/OwnVisibilityWalkMixin.java b/src/main/java/net/vulkanmodnext/mixin/OwnVisibilityWalkMixin.java index 5303364..8d0990d 100644 --- a/src/main/java/net/vulkanmodnext/mixin/OwnVisibilityWalkMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/OwnVisibilityWalkMixin.java @@ -151,6 +151,15 @@ public abstract class OwnVisibilityWalkMixin implements SectionIndex { private int vulkanmodnext$lastWalkY = Integer.MIN_VALUE; @Unique private int vulkanmodnext$lastWalkZ = Integer.MIN_VALUE; + /** + * Which way the camera faced at the last walk. Turning changes what is on + * screen as surely as stepping does — the frustum is part of the walk — so + * it is answered at once too, exactly as vanilla's own arming does. + */ + @Unique + private float vulkanmodnext$lastWalkPitch = Float.NaN; + @Unique + private float vulkanmodnext$lastWalkYaw = Float.NaN; /** * The longest a chunk may wait to appear, in nanoseconds. @@ -182,7 +191,9 @@ public abstract class OwnVisibilityWalkMixin implements SectionIndex { int cameraZ = MathHelper.floor(viewEntity.posZ); boolean cameraMoved = cameraX != vulkanmodnext$lastWalkX || cameraY != vulkanmodnext$lastWalkY - || cameraZ != vulkanmodnext$lastWalkZ; + || cameraZ != vulkanmodnext$lastWalkZ + || viewEntity.rotationPitch != vulkanmodnext$lastWalkPitch + || viewEntity.rotationYaw != vulkanmodnext$lastWalkYaw; if (!cameraMoved && started - vulkanmodnext$lastWalkNanos < VULKANMOD112$CHURN_INTERVAL) { VanillaFrame.countOwnWalkHeld(); return false; @@ -207,6 +218,8 @@ public abstract class OwnVisibilityWalkMixin implements SectionIndex { vulkanmodnext$lastWalkX = cameraX; vulkanmodnext$lastWalkY = cameraY; vulkanmodnext$lastWalkZ = cameraZ; + vulkanmodnext$lastWalkPitch = viewEntity.rotationPitch; + vulkanmodnext$lastWalkYaw = viewEntity.rotationYaw; ((WalkTimer) self).vulkanmodnext$noteWalkRan(); return false; } diff --git a/src/main/java/net/vulkanmodnext/mixin/SkySunMixin.java b/src/main/java/net/vulkanmodnext/mixin/SkySunMixin.java index ca3b104..c47e68b 100644 --- a/src/main/java/net/vulkanmodnext/mixin/SkySunMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/SkySunMixin.java @@ -11,11 +11,11 @@ import org.spongepowered.asm.mixin.injection.Redirect; /** - * Puts a different picture on the sun, and touches nothing else. + * Puts a different picture on the sun and the moon, and touches nothing else. * * The sky is drawn by one long vanilla method that binds several textures in * turn — the sky itself, the sun, the moon. Redirecting the bind and answering - * only for the sun is the narrowest possible change: the quad, its size, its + * only for the sun and the moon is the narrowest possible change: the quad, its size, its * position, the blend, the order and the moon are all still vanilla's, and a * mod that draws its own sky never reaches this code at all. */ diff --git a/src/main/java/net/vulkanmodnext/mixin/TextureMapAnimationMixin.java b/src/main/java/net/vulkanmodnext/mixin/TextureMapAnimationMixin.java index 5e68e8b..2019004 100644 --- a/src/main/java/net/vulkanmodnext/mixin/TextureMapAnimationMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/TextureMapAnimationMixin.java @@ -22,8 +22,9 @@ * Every animated sprite in the atlas uploads a new frame to the GPU each tick, * whether or not a single one of its blocks is on screen. Vanilla has no * switch for it, and with a modpack's worth of machines and fluids the uploads - * add up to real frame time. This is the blunt version of what OptiFine calls - * Smart Animations: off means off, everywhere. + * add up to real frame time. With animations off this is the blunt version: + * off means off, everywhere. With Smart Animations on, only the sprites that + * something on screen is using are stepped (see {@code AnimatedSprites}). */ @Mixin(TextureMap.class) public abstract class TextureMapAnimationMixin { diff --git a/src/main/java/net/vulkanmodnext/mixin/VisibilityWalkMixin.java b/src/main/java/net/vulkanmodnext/mixin/VisibilityWalkMixin.java index c243e73..8387b79 100644 --- a/src/main/java/net/vulkanmodnext/mixin/VisibilityWalkMixin.java +++ b/src/main/java/net/vulkanmodnext/mixin/VisibilityWalkMixin.java @@ -54,8 +54,10 @@ * The counting. "The walk ran on 1882 frames of 2316, from 136399 requests, of * which 102198 were the camera moving" is the sentence that says where the * frame went, and nothing else in the game can say it — the flag is written - * from three places that share nothing but the field, and only two of the - * three arm it: the third is vanilla clearing it as the walk begins. + * from several places that share nothing but the field. Three are watched + * here, and only two of those arm it: the third is vanilla clearing it as the + * walk begins. The write near the top of {@code setupTerrain} that folds + * camera movement in (PUTFIELD ordinal 0) is deliberately not redirected. * * Camera movement is judged against this class's own record of the previous * frame, not vanilla's: the game overwrites its {@code lastViewEntity*} fields From 0fb33fb4bfb74bf08c1ff605b5eb36a303e1a2d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:04:57 +0000 Subject: [PATCH 3/6] fix client-side bugs and settings that disagree with their text Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../vulkanmodnext/client/AnimatedSprites.java | 33 ++++++----- .../client/BlockLightSources.java | 22 +++++++- .../vulkanmodnext/client/ChunkBuildStats.java | 6 +- .../net/vulkanmodnext/client/Diagnostics.java | 27 +++++++-- .../vulkanmodnext/client/EntityCapture.java | 26 ++++++++- .../java/net/vulkanmodnext/client/Flight.java | 24 ++++---- .../client/GuiVulkanPatches.java | 4 +- .../client/GuiVulkanProfiles.java | 20 +++++-- .../client/GuiVulkanSettings.java | 15 +++-- .../vulkanmodnext/client/MaterialRuns.java | 24 ++++---- .../vulkanmodnext/client/MaterialSprites.java | 6 +- .../net/vulkanmodnext/client/Matrices.java | 9 +++ .../vulkanmodnext/client/RenderNotice.java | 10 +++- .../net/vulkanmodnext/client/SessionLog.java | 2 + .../vulkanmodnext/client/SettingsHealth.java | 28 +++++++++- .../net/vulkanmodnext/client/SharedDepth.java | 11 +++- .../net/vulkanmodnext/client/SunSkin.java | 19 +++++++ .../vulkanmodnext/client/TerrainHooks.java | 52 ++++++++--------- .../vulkanmodnext/client/VanillaFrame.java | 5 ++ .../vulkanmodnext/client/VulkanConfig.java | 18 ++++-- .../vulkanmodnext/client/VulkanOptions.java | 56 +++++++++++++++---- .../vulkanmodnext/client/VulkanPresets.java | 24 +++----- .../vulkanmodnext/client/VulkanProfiles.java | 11 +++- .../vulkanmodnext/client/WeatherHooks.java | 6 +- .../net/vulkanmodnext/client/gui/Lang.java | 8 +++ .../assets/vulkanmodnext/lang/de_de.lang | 2 +- .../assets/vulkanmodnext/lang/en_us.lang | 28 ++++++++-- .../assets/vulkanmodnext/lang/es_es.lang | 2 +- .../assets/vulkanmodnext/lang/fr_fr.lang | 2 +- .../assets/vulkanmodnext/lang/ja_jp.lang | 2 +- .../assets/vulkanmodnext/lang/pt_br.lang | 2 +- .../assets/vulkanmodnext/lang/ru_ru.lang | 2 +- .../assets/vulkanmodnext/lang/zh_cn.lang | 2 +- 33 files changed, 369 insertions(+), 139 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/client/AnimatedSprites.java b/src/main/java/net/vulkanmodnext/client/AnimatedSprites.java index e278137..418ae3e 100644 --- a/src/main/java/net/vulkanmodnext/client/AnimatedSprites.java +++ b/src/main/java/net/vulkanmodnext/client/AnimatedSprites.java @@ -42,17 +42,19 @@ * patch that records them is disabled: the failure is "no saving", never "the * world stopped moving". * - * A sprite that no chunk has ever reported is always updated. That is what - * keeps the item in your hand, the fire on a burning creature and the texture - * in a menu moving — none of them are terrain, so none of them are in any - * chunk's record, and a rule that only knew about terrain would freeze them. - * Fluids are always updated for the same reason and one more: they are what a - * player looks at when they want to know whether the game is still running. + * Anything drawn that is not terrain is vouched for another way. An item + * model marks its sprites as it is drawn — in the hand, in a menu, in a frame + * or on the ground — and they stay marked for a second or two. Water, lava, + * fire and portals are always updated: fluids are drawn by a renderer that + * never asks a model, fire burns on creatures as well as on blocks, and they + * are what a player looks at when they want to know whether the game is still + * running. * - *

What remains, and it is why this ships switched off: a texture that is - * both a block and an item freezes in your hand while no such block is in - * sight. Holding a block of magma in a world without one placed is the shape - * of it. + *

What remains, and it is why this ships switched off: an animated block + * texture drawn by something that is neither a chunk nor an item model — a + * block entity renderer, a creature, a mod's own geometry — freezes while no + * chunk in sight uses it. There is no "never reported by a chunk" rule behind + * this; a sprite is updated only for the reasons above. */ public final class AnimatedSprites { @@ -69,8 +71,8 @@ public final class AnimatedSprites { private static int words; /** - * Sprites that must be updated whatever is on screen: the ones no chunk - * has ever used, and the fluids. + * Sprites that must be updated whatever is on screen: water, lava, fire + * and portals, matched by name. */ private static long[] always = new long[0]; @@ -188,7 +190,12 @@ public static synchronized void index(List animated) { itemsBefore = new long[words]; gathering = new long[words]; wanted = new long[words]; - BY_STATE.clear(); + // Under its own lock: build threads read and fill this map while the + // tick that reindexes runs, and they only ever hold that lock, not + // this method's. + synchronized (BY_STATE) { + BY_STATE.clear(); + } synchronized (BY_ITEM_MODEL) { BY_ITEM_MODEL.clear(); } diff --git a/src/main/java/net/vulkanmodnext/client/BlockLightSources.java b/src/main/java/net/vulkanmodnext/client/BlockLightSources.java index 897159f..566b30b 100644 --- a/src/main/java/net/vulkanmodnext/client/BlockLightSources.java +++ b/src/main/java/net/vulkanmodnext/client/BlockLightSources.java @@ -140,10 +140,13 @@ public static void update(double viewX, double viewY, double viewZ, int radius) shift(viewX, viewY, viewZ); long now = System.nanoTime(); if (!sweeping) { + // Measured from where the last sweep started, not from lastX: shift() + // above has just moved lastX to this frame's camera, so comparing + // against it measured one frame's motion and never fired. boolean moved = Double.isNaN(lastX) - || Math.abs(viewX - lastX) > MOVED - || Math.abs(viewY - lastY) > MOVED - || Math.abs(viewZ - lastZ) > MOVED; + || Math.abs(viewX - buildX) > MOVED + || Math.abs(viewY - buildY) > MOVED + || Math.abs(viewZ - buildZ) > MOVED; if (!moved && now - lastScanNanos < INTERVAL_NANOS) { return; } @@ -164,6 +167,19 @@ public static void update(double viewX, double viewY, double viewZ, int radius) stepSweep(viewX, viewY, viewZ); } + /** + * Drops everything found in the world being left. + * + * The list is positions relative to the camera, so after a change of + * dimension it would light the new world with the old one's torches until + * the next sweep finished. + */ + public static void forget() { + count = 0; + sweeping = false; + lastX = Double.NaN; + } + /** * Moves the stored positions to follow the camera between sweeps. * diff --git a/src/main/java/net/vulkanmodnext/client/ChunkBuildStats.java b/src/main/java/net/vulkanmodnext/client/ChunkBuildStats.java index 6290500..4e63dae 100644 --- a/src/main/java/net/vulkanmodnext/client/ChunkBuildStats.java +++ b/src/main/java/net/vulkanmodnext/client/ChunkBuildStats.java @@ -20,9 +20,9 @@ * side of every block, and reported 24 ns a block. Two calls to * {@code System.nanoTime} cost about that between them on their own: the * measurement was most of what it measured, and no conclusion could be drawn - * about the work. A whole chunk is forty thousand block positions, so one - * clock pair around the rebuild is four orders of magnitude cheaper per block - * and reports the same total. + * about the work. A chunk section is four thousand block positions (16 cubed), + * so one clock pair around the rebuild is three orders of magnitude cheaper + * per block and reports the same total. * * That also makes the number the useful one. Nobody wants to know what a * hashmap lookup costs; the question is whether a chunk takes longer to build diff --git a/src/main/java/net/vulkanmodnext/client/Diagnostics.java b/src/main/java/net/vulkanmodnext/client/Diagnostics.java index 21b6c1f..b820dcc 100644 --- a/src/main/java/net/vulkanmodnext/client/Diagnostics.java +++ b/src/main/java/net/vulkanmodnext/client/Diagnostics.java @@ -177,7 +177,7 @@ private static void mirror(org.apache.logging.log4j.core.LogEvent event) { return; } StringBuilder line = new StringBuilder() - .append('[').append(STAMP.format(new Date())).append("] ") + .append('[').append(stamp()).append("] ") .append(event.getLevel()).append(' ') .append(name).append(": ") .append(event.getMessage().getFormattedMessage()); @@ -250,14 +250,33 @@ public static void flushNow(String reason) { try { PrintWriter out = open(); if (out != null) { - out.println("[" + STAMP.format(new Date()) + "] EVENT: " + reason); - out.flush(); + // Under the lock, and after any folded run is written out: + // mirrored lines arrive from other threads, and an event line + // written between a run and its "repeated" summary reads as + // though it happened before the lines it followed. + synchronized (LOCK) { + flushRepeat(); + out.println("[" + stamp() + "] EVENT: " + reason); + out.flush(); + } } } catch (Throwable ignored) { // Diagnostics must never be the reason something breaks. } } + /** + * The time of day for a line. {@code SimpleDateFormat} keeps its working + * state in the instance, and mirrored log lines are formatted on whichever + * thread logged them — chunk builders included — so it is never used + * unguarded. + */ + private static String stamp() { + synchronized (STAMP) { + return STAMP.format(new Date()); + } + } + private static PrintWriter open() throws IOException { if (writer != null) { return writer; @@ -516,7 +535,7 @@ private static void emitSnapshot(PrintWriter out) { private static void writeSnapshot(PrintWriter out) { Minecraft mc = Minecraft.getMinecraft(); - out.println("[" + STAMP.format(new Date()) + "] snapshot"); + out.println("[" + stamp() + "] snapshot"); out.println(" fps: " + Minecraft.getDebugFPS() + ", world: " + (mc.world == null ? "none" : "loaded") + ", gui: " + (mc.currentScreen == null ? "none" : mc.currentScreen.getClass().getSimpleName())); diff --git a/src/main/java/net/vulkanmodnext/client/EntityCapture.java b/src/main/java/net/vulkanmodnext/client/EntityCapture.java index 243be85..3b0b45b 100644 --- a/src/main/java/net/vulkanmodnext/client/EntityCapture.java +++ b/src/main/java/net/vulkanmodnext/client/EntityCapture.java @@ -93,6 +93,16 @@ public final class EntityCapture { /** The frame of the creature being drawn, and one frame per skeleton level. */ private static final float[][] STACK = new float[MAX_DEPTH][16]; private static int depth; + /** + * Whether each {@link #beginPart} still waiting for its {@link #endPart} + * pushed a frame, one entry per nested call. + * + * A part past the depth limit, or one that threw before its push, pushes + * nothing — and its end used to pop the parent's frame regardless, so every + * sibling after it was placed one level too shallow. + */ + private static final boolean[] PUSHED = new boolean[64]; + private static int calls; private static final float[] LOCAL = new float[16]; private static final float[] COMPOSED = new float[16]; @@ -195,6 +205,7 @@ public static void disarm() { } armed = false; depth = 0; + calls = 0; frameParts = 0; frameQuads = 0; frameTextures = 0; @@ -222,6 +233,7 @@ public static void beginModel() { long start = System.nanoTime(); try { depth = 0; + calls = 0; readMatrix(STACK[0]); matrixReads++; } catch (Throwable ignored) { @@ -242,6 +254,7 @@ public static void beginPart(ModelRenderer part, float scale) { if (!armed || part == null) { return; } + int before = depth; try { if (depth >= MAX_DEPTH - 1) { return; @@ -291,6 +304,11 @@ public static void beginPart(ModelRenderer part, float scale) { } } catch (Throwable ignored) { // As above. + } finally { + if (calls < PUSHED.length) { + PUSHED[calls] = depth != before; + } + calls++; } } @@ -330,7 +348,11 @@ private static void noteTexture() { /** Leaves the part's frame, so its siblings are placed beside it and not inside it. */ public static void endPart() { - if (armed && depth > 0) { + if (!armed || calls == 0) { + return; + } + calls--; + if (calls < PUSHED.length && PUSHED[calls] && depth > 0) { depth--; } } @@ -424,7 +446,7 @@ static void localTransform(ModelRenderer part, float scale, float[] out) { * * Every matrix in this chain is a rotation and a shift, so the fourth row * is known and the fourth column of the product is the only one that needs - * the shift added. Twenty-seven multiplications instead of sixty-four, on + * the shift added. Thirty-six multiplications instead of sixty-four, on * the one piece of arithmetic that runs once per bone of every creature on * screen. */ diff --git a/src/main/java/net/vulkanmodnext/client/Flight.java b/src/main/java/net/vulkanmodnext/client/Flight.java index fc55fee..a558f1b 100644 --- a/src/main/java/net/vulkanmodnext/client/Flight.java +++ b/src/main/java/net/vulkanmodnext/client/Flight.java @@ -394,14 +394,6 @@ private static void enterWorld(Minecraft mc) { ticks = 0; } - /** - * Loads a named preset, if one was asked for. - * - * Applied after the world is there rather than at startup, because two of - * these presets read the machine they are on — how many cores there are to - * build chunks with, how much memory the card admits to — and one of them - * moves the game's own video settings. - */ /** * Resizes the window, if one was asked for, before anything is measured. * @@ -439,6 +431,14 @@ private static void applyWindow(Minecraft mc) { } } + /** + * Loads a named preset, if one was asked for. + * + * Applied after the world is there rather than at startup, because two of + * these presets read the machine they are on — how many cores there are to + * build chunks with, how much memory the card admits to — and one of them + * moves the game's own video settings. + */ private static void applyPreset(Minecraft mc) { if (PRESET.isEmpty()) { return; @@ -617,9 +617,11 @@ public static void onFrameEnd() { } String name = pendingShot; // The same frame again on the very next one, when asked. A camera - // turning at eighteen degrees a second moves a twentieth of a degree - // between two frames, so a pair that differs by more than a rounding - // error says the picture being read is not the picture just drawn — + // turning at twelve degrees a second — one turn over the default + // thirty-second spin — moves a twentieth of a degree between two + // frames at a couple of hundred a second, so a pair that differs by + // more than a rounding error says the picture being read is not the + // picture just drawn — // which is the one thing that cannot be told apart from a fault in the // renderer by looking at a single frame. if (PAIRS && !name.endsWith("b")) { diff --git a/src/main/java/net/vulkanmodnext/client/GuiVulkanPatches.java b/src/main/java/net/vulkanmodnext/client/GuiVulkanPatches.java index eeef7a5..7f32260 100644 --- a/src/main/java/net/vulkanmodnext/client/GuiVulkanPatches.java +++ b/src/main/java/net/vulkanmodnext/client/GuiVulkanPatches.java @@ -177,8 +177,8 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { } /** - * The three lines under the list: what the selected group is, what turning - * it off costs, and what it said if it failed. + * The lines under the list, two at most: what turning the selected group + * off costs, or what it said if it failed. */ private void drawFoot(int bottom) { if (this.selected == null) { diff --git a/src/main/java/net/vulkanmodnext/client/GuiVulkanProfiles.java b/src/main/java/net/vulkanmodnext/client/GuiVulkanProfiles.java index 32cca4b..6566dc0 100644 --- a/src/main/java/net/vulkanmodnext/client/GuiVulkanProfiles.java +++ b/src/main/java/net/vulkanmodnext/client/GuiVulkanProfiles.java @@ -47,8 +47,13 @@ public GuiVulkanProfiles(GuiScreen parent) { this.parent = parent; } + /** + * Twelve pixels above the name field more than the list itself needs: the + * status line goes there. At {@code height - 78} it was drawn at exactly + * the field's own top and printed over whatever was being typed. + */ private int listBottom() { - return this.height - 78; + return this.height - 90; } private int listLeft() { @@ -153,9 +158,12 @@ protected void actionPerformed(GuiButton button) throws IOException { } break; case DELETE: - VulkanProfiles.delete(this.selected); - this.status = Lang.tr(Lang.UI, "Deleted") + ": " + this.selected; - this.selected = null; + if (VulkanProfiles.delete(this.selected)) { + this.status = Lang.tr(Lang.UI, "Deleted") + ": " + this.selected; + this.selected = null; + } else { + this.status = Lang.tr(Lang.UI, "Could not write that profile"); + } break; case DONE: this.mc.displayGuiScreen(this.parent); @@ -168,6 +176,8 @@ protected void actionPerformed(GuiButton button) throws IOException { if (this.selected != null && !this.names.contains(this.selected)) { this.selected = null; } + // A deleted row can leave the list scrolled past its own end. + clampScroll(); updateButtons(); } @@ -212,7 +222,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { this.nameField.drawTextBox(); if (!this.status.isEmpty()) { - this.fontRenderer.drawString(this.status, left, bottom + 6, 0xA0A0A0); + this.fontRenderer.drawString(this.status, left, bottom + 5, 0xA0A0A0); } super.drawScreen(mouseX, mouseY, partialTicks); SixSeven.end(); diff --git a/src/main/java/net/vulkanmodnext/client/GuiVulkanSettings.java b/src/main/java/net/vulkanmodnext/client/GuiVulkanSettings.java index 6202b6b..2fc3628 100644 --- a/src/main/java/net/vulkanmodnext/client/GuiVulkanSettings.java +++ b/src/main/java/net/vulkanmodnext/client/GuiVulkanSettings.java @@ -142,13 +142,17 @@ public void initGui() { // there and greyed out the rest of the time would be a promise of news // on every screen that has none. String newer = UpdateCheck.newerVersion(); + // Done gives up a third of its width when the update button is there: + // at 150 it reached to width / 2 + 100 and the update button, starting + // at + 54, was drawn over its right end. + int doneWidth = newer != null ? 100 : 150; if (newer != null) { - this.buttonList.add(new GuiButton(UPDATE, this.width / 2 + 56, this.height - 27, 98, 20, + this.buttonList.add(new GuiButton(UPDATE, this.width / 2 + 54, this.height - 27, 98, 20, Lang.tr(Lang.UI, "Get") + " " + newer)); } this.buttonList.add(new GuiButton(RESET, this.width / 2 - 154, this.height - 27, 100, 20, Lang.tr(Lang.UI, "Reset"))); - this.buttonList.add(new GuiButton(DONE, this.width / 2 - 50, this.height - 27, 150, 20, + this.buttonList.add(new GuiButton(DONE, this.width / 2 - 50, this.height - 27, doneWidth, 20, I18n.format("gui.done"))); updateTabHighlight(); clampScroll(); @@ -159,8 +163,11 @@ private void updateTabHighlight() { GuiButton button = (GuiButton) entry; if (button.id >= PAGE_BUTTON_BASE) { // Vanilla buttons have no selected state; the current tab is - // the disabled one, which reads as "pressed". - button.enabled = button.id - PAGE_BUTTON_BASE != this.currentPage; + // the disabled one, which reads as "pressed". Not while search + // results are up, the same rule refreshSearch applies — or a + // resize with a search typed greys out a tab that is not shown. + button.enabled = this.searchPage != null + || button.id - PAGE_BUTTON_BASE != this.currentPage; } } } diff --git a/src/main/java/net/vulkanmodnext/client/MaterialRuns.java b/src/main/java/net/vulkanmodnext/client/MaterialRuns.java index 61d54fa..3476665 100644 --- a/src/main/java/net/vulkanmodnext/client/MaterialRuns.java +++ b/src/main/java/net/vulkanmodnext/client/MaterialRuns.java @@ -54,7 +54,11 @@ public final class MaterialRuns { /** Nothing special; the great majority of a world. */ public static final int PLAIN = 0; public static final int WATER = 1; - /** Leaves, grass, flowers, crops — anything that should move in wind. */ + /** + * A plant that is lit as foliage but stands still in the wind: sugar cane, + * lily pads, cocoa, chorus, and a double plant that cannot say which half + * it is. The ones that sway are PLANT, PLANT_TALL_* and LEAVES. + */ public static final int FOLIAGE = 2; public static final int GLASS = 3; public static final int LAVA = 4; @@ -110,7 +114,7 @@ public final class MaterialRuns { * fragment can see gets that wrong. */ public static final int LIGHT_SHIFT = 4; - /** The material itself is 0..6, so four bits is room to spare. */ + /** The material itself is 0..9, so four bits is enough. */ public static final int MATERIAL_MASK = 0x0F; /** @@ -361,13 +365,13 @@ private static int shapeOf(IBlockState state) { // sway. Everything about a cross model is vertical, so the top pair of // corners of every quad is its top and moving them is the whole of it. // - // Two are held back. A plant that spans more than one block — a double - // plant, sugar cane — has the top of the lower block and the bottom of - // the upper block at the same height, and only the first of the two - // would move: the stem would come apart at the seam. Doing that - // properly means knowing how far up its own plant a block is, which is - // not something a single block state can say. - // Both, and this is not tidiness. Vanilla's Material is a table of + // A plant that spans more than one block has the top of the lower + // block and the bottom of the upper block at the same height, and + // moving the top pair of each would part the stem at the seam. A double + // plant can say which half it is, so it gets PLANT_TALL_*; sugar cane + // cannot, so it stands still (see below). + // + // PLANTS and VINE both, and this is not tidiness. Vanilla's Material is a table of // physical behaviour, not of shape, and in this version ordinary grass, // ferns, dead bushes, real vines and two-block plants are all // Material.VINE together. Reading that name as "a vine" is what left @@ -476,7 +480,7 @@ public static String stats() { // work between them — the first version reported 24 ns a block and most // of that was the reading of it. What the cost of this actually is has // to be read off the whole chunk rebuild, which is what ChunkBuildStats - // times, with one clock pair per forty thousand blocks instead of two. + // times, with one clock pair per four thousand blocks instead of two. return String.format( "material tags: %d blocks recorded, %d chunk layers averaging %.1f runs, " + "%.0f%% of them one plain run; %d layers sent to the renderer, " diff --git a/src/main/java/net/vulkanmodnext/client/MaterialSprites.java b/src/main/java/net/vulkanmodnext/client/MaterialSprites.java index 9d5c7a4..5117c7b 100644 --- a/src/main/java/net/vulkanmodnext/client/MaterialSprites.java +++ b/src/main/java/net/vulkanmodnext/client/MaterialSprites.java @@ -84,10 +84,12 @@ public static void handOver(VulkanBridge bridge, TextureMap atlas) { } catch (Throwable t) { continue; } - if (sprite == null || sprite.getMinU() == sprite.getMaxU()) { + if (sprite == null || !NAMES[i].equals(sprite.getIconName()) + || sprite.getMinU() == sprite.getMaxU()) { // Missing sprites come back as the "missing texture" one, which // every unknown name shares. Sending it would paint everything - // that failed to load as water. + // that failed to load as water. It is a real rectangle in the + // atlas, so only its name gives it away. continue; } materials[found] = MATERIALS[i]; diff --git a/src/main/java/net/vulkanmodnext/client/Matrices.java b/src/main/java/net/vulkanmodnext/client/Matrices.java index ba46887..0d7c438 100644 --- a/src/main/java/net/vulkanmodnext/client/Matrices.java +++ b/src/main/java/net/vulkanmodnext/client/Matrices.java @@ -80,6 +80,12 @@ public static void toVulkanDepth(float[] mvp) { * caller has nothing useful to do with a made-up answer. */ public static float nearPlane(float[] projection) { + // A perspective matrix copies -z into w; an orthographic one leaves + // that element at zero, and its planes would divide out to a + // plausible-looking number that is not a distance at all. + if (projection[2 * 4 + 3] == 0.0f) { + return 0.0f; + } float m10 = projection[2 * 4 + 2]; float m14 = projection[3 * 4 + 2]; float denominator = m10 - 1.0f; @@ -88,6 +94,9 @@ public static float nearPlane(float[] projection) { /** The far plane of a perspective projection; see {@link #nearPlane}. */ public static float farPlane(float[] projection) { + if (projection[2 * 4 + 3] == 0.0f) { + return 0.0f; + } float m10 = projection[2 * 4 + 2]; float m14 = projection[3 * 4 + 2]; float denominator = m10 + 1.0f; diff --git a/src/main/java/net/vulkanmodnext/client/RenderNotice.java b/src/main/java/net/vulkanmodnext/client/RenderNotice.java index da9a9f8..aca645e 100644 --- a/src/main/java/net/vulkanmodnext/client/RenderNotice.java +++ b/src/main/java/net/vulkanmodnext/client/RenderNotice.java @@ -113,9 +113,15 @@ private static void say(Minecraft mc, String line) { mc.ingameGUI.getChatGUI().printChatMessage(new TextComponentString(line)); } - /** A fresh world may well succeed where the last one failed. */ + /** + * A fresh world may well succeed where the last one failed. + * + * A notice still waiting is kept. The world-load event fires before the + * player exists, so a failure queued at startup — the Vulkan side never + * coming up at all — is always still pending here, and clearing it threw + * away the one message that case has. + */ public static void reset() { told = false; - pending = null; } } diff --git a/src/main/java/net/vulkanmodnext/client/SessionLog.java b/src/main/java/net/vulkanmodnext/client/SessionLog.java index 532732e..749786b 100644 --- a/src/main/java/net/vulkanmodnext/client/SessionLog.java +++ b/src/main/java/net/vulkanmodnext/client/SessionLog.java @@ -144,6 +144,8 @@ public void onWorldLoad(WorldEvent.Load event) { // A held hour belongs to the world it was held in. Carrying it into // the next one would freeze a new world at a time it never had. WorldDisplay.forget(); + // Camera-relative positions from the world just left. + BlockLightSources.forget(); Diagnostics.flushNow("world loaded, dimension " + event.getWorld().provider.getDimension() + ", render distance " + Minecraft.getMinecraft().gameSettings.renderDistanceChunks); } diff --git a/src/main/java/net/vulkanmodnext/client/SettingsHealth.java b/src/main/java/net/vulkanmodnext/client/SettingsHealth.java index b5bd185..df40d77 100644 --- a/src/main/java/net/vulkanmodnext/client/SettingsHealth.java +++ b/src/main/java/net/vulkanmodnext/client/SettingsHealth.java @@ -144,6 +144,7 @@ public static String describeInert() { appendTerrainEffects(names); appendTracedEffects(names); if (names.length() > 0) { + separate(out); out.append(names).append(" — ").append(why); } } else if (tracingActive()) { @@ -162,6 +163,7 @@ public static String describeInert() { add(names, "water caustics", VulkanConfig.getWaterCaustics()); add(names, "water depth and shore foam", VulkanConfig.getWaterRefraction()); if (names.length() > 0) { + separate(out); out.append(names).append(" — these are left out of the traced " + "terrain shader on purpose, and ray tracing is on"); } @@ -178,6 +180,7 @@ public static String describeInert() { StringBuilder names = new StringBuilder(); appendTracedEffects(names); if (names.length() > 0) { + separate(out); out.append(names).append(" — ").append(tracingReason()); } } @@ -314,9 +317,17 @@ private static void appendTerrainEffects(StringBuilder out) { add(out, "bloom", VulkanConfig.getBloom()); add(out, "scene tone", VulkanConfig.getSceneTone()); add(out, "ambient occlusion", VulkanConfig.getAmbientOcclusion()); - add(out, "directional block light", VulkanConfig.getDirectionalLight()); + // These two are modifiers that ship above zero — fifty and sixty — and + // listing them on their own value named them in every warning of every + // install whose renderer was off. Each is only switched on in any sense + // that matters while the thing it modifies is. + add(out, "directional block light", + VulkanConfig.isDynamicLights() && VulkanConfig.getDirectionalLight() > 0); add(out, "height fog", VulkanConfig.getHeightFog()); - add(out, "frame accumulation", VulkanConfig.getTemporalAccumulation()); + StringBuilder traced = new StringBuilder(); + appendTracedEffects(traced); + add(out, "frame accumulation", + VulkanConfig.getTemporalAccumulation() > 0 && traced.length() > 0); // Not a shader effect, and it belongs here all the same: the sources // are collected inside the Vulkan draw and nowhere else, so with the // renderer off nothing is ever gathered and the entity, particle and @@ -380,11 +391,22 @@ private static String tracingReason() { */ private static void appendTracedEffects(StringBuilder out) { add(out, "sun shadows", VulkanConfig.getSunShadows()); - add(out, "traced light shadows", VulkanConfig.getTracedLights()); + // Ships at two, and traces nothing but the moving lights: without + // dynamic lights it has nothing to ask about, and counting it on its + // own value warned about it on every fresh install. + add(out, "traced light shadows", + VulkanConfig.isDynamicLights() && VulkanConfig.getTracedLights() > 0); add(out, "traced block light", VulkanConfig.getTracedBlockLight()); add(out, "light through leaves", VulkanConfig.getLeafShadows()); } + /** The same "; " every other cause in the sentence is joined with. */ + private static void separate(StringBuilder out) { + if (out.length() > 0) { + out.append("; "); + } + } + private static void add(StringBuilder out, String name, int strength) { add(out, name, strength > 0); } diff --git a/src/main/java/net/vulkanmodnext/client/SharedDepth.java b/src/main/java/net/vulkanmodnext/client/SharedDepth.java index 13dac62..fb9e533 100644 --- a/src/main/java/net/vulkanmodnext/client/SharedDepth.java +++ b/src/main/java/net/vulkanmodnext/client/SharedDepth.java @@ -97,8 +97,15 @@ static void ensure(VulkanBridge bridge, Minecraft mc) { return; } // The framebuffer was rebuilt under us: whatever was recorded about the - // old one describes an object that no longer exists. - if (frame.framebufferObject != knownFramebuffer) { + // old one describes an object that no longer exists. A new size means + // the same thing — the game resizes by deleting and regenerating the + // framebuffer and its depth renderbuffer, and the driver may hand the + // framebuffer back under the very name it had, so the name alone does + // not say it is new. Keeping the old renderbuffer's name then would + // hang a deleted object back up on the way out. + if (frame.framebufferObject != knownFramebuffer + || frame.framebufferTextureWidth != knownWidth + || frame.framebufferTextureHeight != knownHeight) { attached = false; replacedRenderbuffer = 0; } diff --git a/src/main/java/net/vulkanmodnext/client/SunSkin.java b/src/main/java/net/vulkanmodnext/client/SunSkin.java index a3638c1..869aab0 100644 --- a/src/main/java/net/vulkanmodnext/client/SunSkin.java +++ b/src/main/java/net/vulkanmodnext/client/SunSkin.java @@ -49,6 +49,7 @@ public static ResourceLocation current() { if (location != null && builtFor == wanted) { return location; } + ResourceLocation old = location; try { location = build(); builtFor = wanted; @@ -56,6 +57,7 @@ public static ResourceLocation current() { // A sun we cannot draw is a sun the game draws instead. location = null; } + release(old); return location; } @@ -195,15 +197,32 @@ public static ResourceLocation currentMoon() { if (moonLocation != null && moonBuiltFor == wanted) { return moonLocation; } + ResourceLocation old = moonLocation; try { moonLocation = moonSheet(); moonBuiltFor = wanted; } catch (Throwable t) { moonLocation = null; } + release(old); return moonLocation; } + /** + * Deletes a picture that has been replaced. Every rebuild is a new texture + * under a new name, and a slider dragged across its range builds dozens. + */ + private static void release(ResourceLocation old) { + if (old == null) { + return; + } + try { + Minecraft.getMinecraft().getTextureManager().deleteTexture(old); + } catch (Throwable ignored) { + // A texture that cannot be deleted is a leak, not a reason to fail. + } + } + private static ResourceLocation upload(String name, BufferedImage image) { return Minecraft.getMinecraft().getTextureManager() .getDynamicTextureLocation(name, new DynamicTexture(image)); diff --git a/src/main/java/net/vulkanmodnext/client/TerrainHooks.java b/src/main/java/net/vulkanmodnext/client/TerrainHooks.java index e0b1c4b..e138a3b 100644 --- a/src/main/java/net/vulkanmodnext/client/TerrainHooks.java +++ b/src/main/java/net/vulkanmodnext/client/TerrainHooks.java @@ -813,19 +813,10 @@ private static float biomeRainShare(Minecraft mc) { } } - /** - * Copies the fixed-function fog the game has already configured for this - * frame, so the Vulkan terrain fades exactly like everything OpenGL still - * draws. Underwater this is the difference between entities turning the - * colour of the water and the blocks behind them staying perfectly clear. - * - * Read from GL rather than recomputed, because the game changes fog for - * water, lava, blindness, the void and render distance, and mods add more. - */ - /** The sheet the game draws its clouds from, looked up once. */ /** The last drift handed over, so the clouds are never told to go back. */ private static float lastCloudDrift; + /** The sheet the game draws its clouds from. */ private static final net.minecraft.util.ResourceLocation CLOUD_SHEET = new net.minecraft.util.ResourceLocation("textures/environment/clouds.png"); @@ -879,6 +870,15 @@ private static void captureClouds(Minecraft mc) { bridge.updateClouds(texture, height, drift); } + /** + * Copies the fixed-function fog the game has already configured for this + * frame, so the Vulkan terrain fades exactly like everything OpenGL still + * draws. Underwater this is the difference between entities turning the + * colour of the water and the blocks behind them staying perfectly clear. + * + * Read from GL rather than recomputed, because the game changes fog for + * water, lava, blindness, the void and render distance, and mods add more. + */ private static void captureFog() { if (!VulkanConfig.isFogEnabled() || !GL11.glIsEnabled(GL11.GL_FOG)) { FOG[3] = 0.0f; // mode 0: the shader skips the blend @@ -942,22 +942,6 @@ private static void captureMatrices() { CAMERA_OFFSET[2] = -(MV[8] * MV[12] + MV[9] * MV[13] + MV[10] * MV[14]); } - /** - * Hands this tick's animation frames to the Vulkan copy of the atlas. - * - * Called when the game has finished stepping its own animations. With no - * renderer to send them to they are dropped rather than kept: a session - * that never brings Vulkan up would otherwise grow this buffer forever, and - * frames that arrive late are of no use to anyone. - */ - /** - * Adds the terrain's glow once the game has drawn the rest of the world. - * - * Late on purpose: at this point the frame holds entities, particles, - * weather and water as well as terrain, so a mob in front of a lava lake - * is inside the glow rather than pasted over it, and a torch throws light - * onto the sky, which is drawn long after this mod's own frame is finished. - */ /** * Whether the passes over the finished picture are going to run at all. * @@ -974,6 +958,14 @@ public static boolean sceneEffectsWillRun() { return bridge != null && VulkanConfig.isTerrainEnabled() && bridge.isSceneToneAvailable(); } + /** + * Adds the terrain's glow once the game has drawn the rest of the world. + * + * Late on purpose: at this point the frame holds entities, particles, + * weather and water as well as terrain, so a mob in front of a lava lake + * is inside the glow rather than pasted over it, and a torch throws light + * onto the sky, which is drawn long after this mod's own frame is finished. + */ public static void applySceneBloom() { VulkanBridge bridge = liveBridge(); if (bridge == null || !VulkanConfig.isTerrainEnabled()) { @@ -1038,6 +1030,14 @@ public static void shutdown() { private static final boolean ATLAS_ANIMATIONS_OFF = "true".equals(System.getProperty("vulkanmodnext.noAtlasAnimations")); + /** + * Hands this tick's animation frames to the Vulkan copy of the atlas. + * + * Called when the game has finished stepping its own animations. With no + * renderer to send them to they are dropped rather than kept: a session + * that never brings Vulkan up would otherwise grow this buffer forever, and + * frames that arrive late are of no use to anyone. + */ public static void flushAtlasAnimations() { VulkanBridge bridge = liveBridge(); if (bridge == null || ATLAS_ANIMATIONS_OFF || !VulkanConfig.isTerrainEnabled()) { diff --git a/src/main/java/net/vulkanmodnext/client/VanillaFrame.java b/src/main/java/net/vulkanmodnext/client/VanillaFrame.java index d8616dd..f33d550 100644 --- a/src/main/java/net/vulkanmodnext/client/VanillaFrame.java +++ b/src/main/java/net/vulkanmodnext/client/VanillaFrame.java @@ -547,6 +547,11 @@ public static String stats() { */ public static String walkStats() { if (walkAsked == 0) { + // Reset here too: every other counter in this class reads and resets, + // and leaving these standing let the next interval report walks and + // frames that belonged to this one. + walkRan = 0L; + walkFramesSeen = 0L; return "visibility walk: never reached our check — the dirty flag was already set"; } String line = String.format( diff --git a/src/main/java/net/vulkanmodnext/client/VulkanConfig.java b/src/main/java/net/vulkanmodnext/client/VulkanConfig.java index 5669468..c7fda68 100644 --- a/src/main/java/net/vulkanmodnext/client/VulkanConfig.java +++ b/src/main/java/net/vulkanmodnext/client/VulkanConfig.java @@ -991,7 +991,7 @@ private static void readAll() { net.vulkanmodnext.VulkanModNext.LOGGER.info( "Pack Chunk Vertices is now on by default and has been switched on in your " + "settings. It saves 188 MiB of video memory and some frames; " - + "Advanced turns it off again if you want it off."); + + "Optimizations turns it off again if you want it off."); } settingsRevision = 1; store(CATEGORY_ADVANCED, "settingsRevision", settingsRevision); @@ -1625,11 +1625,6 @@ public static int restoreFrom(java.util.Properties in) { return applied; } - /** - * Returns every mod-owned setting to its shipped value. Minecraft's own - * settings are left alone: they are not ours to reset, and the screen only - * borrows them. - */ /** * Puts every diagnostic view back, wherever the settings came from. * @@ -1652,6 +1647,11 @@ public static void clearDiagnosticViews() { setShowReflections(DEF_SHOW_REFLECTIONS); } + /** + * Returns every mod-owned setting to its shipped value. Minecraft's own + * settings are left alone: they are not ours to reset, and the screen only + * borrows them. + */ public static void resetToDefaults() { clearDiagnosticViews(); setContactShadows(DEF_CONTACT_SHADOWS); @@ -1730,6 +1730,12 @@ public static void resetToDefaults() { setWaterCaustics(DEF_WATER_CAUSTICS); setWetSurfaces(DEF_WET_SURFACES); setSunHaze(DEF_SUN_HAZE); + // Every preset writes these three, and until they were here a showcase + // preset followed by Reset left the sky deepened, leaf shadows at full + // and the occlusion reading the whole picture. + setSkyGradient(DEF_SKY_GRADIENT); + setSceneOcclusion(DEF_SCENE_OCCLUSION); + setLeafShadows(DEF_LEAF_SHADOWS); setCloudTint(DEF_CLOUD_TINT); setPreloadQueue(DEF_PRELOAD_QUEUE); setPreloadScan(DEF_PRELOAD_SCAN); diff --git a/src/main/java/net/vulkanmodnext/client/VulkanOptions.java b/src/main/java/net/vulkanmodnext/client/VulkanOptions.java index e2d0879..c3a72c2 100644 --- a/src/main/java/net/vulkanmodnext/client/VulkanOptions.java +++ b/src/main/java/net/vulkanmodnext/client/VulkanOptions.java @@ -162,8 +162,9 @@ public void run() { }), new VActionOption("Potato", "For a machine this game is too heavy for. Everything Performance " - + "gives up, plus smooth lighting, clouds and eight chunks " - + "of view — and a sixty frame ceiling with vsync on. " + + "gives up, plus block textures — every face drawn in one " + + "flat colour — and eight chunks of view, and a sixty " + + "frame ceiling with vsync on. " + "Mipmaps stay on, and deliberately: turning them off is " + "the obvious-looking way to make textures cheap and it " + "does the reverse, sending distant blocks to read the " @@ -292,7 +293,7 @@ public void set(int value) { new VSwitchOption("Frame Time Graph", "Draw a frame-time graph in the bottom-left corner: one bar per " + "frame over the last couple of seconds, with the best and " - + "worst single frame and the 1%% low — the frame time that " + + "worst single frame and the 1% low — the frame time that " + "only one frame in a hundred exceeds. The framerate the " + "game already shows is frames divided by seconds, and it " + "cannot tell a steady 120 from a 240 that stalls every " @@ -784,6 +785,37 @@ public void set(boolean value) { VulkanConfig.setCompactVertices(value); } }), + // The settings file, the log line that switched it on + // for existing installs and the changelog all send + // people to this row; until it existed there was no + // way to reach the setting from the game. + new VSwitchOption("Group Quad Facings", + "Sort each chunk's faces by which way they point, so the ones " + + "a camera cannot possibly see are never read. Standing " + + "above a floor you cannot see its underside, and the " + + "card knows that too — but it only finds out after " + + "reading every one of those vertices, and reading " + + "vertices is what this renderer's terrain pass is " + + "limited by. Measured at render distance 32: 36% of the " + + "reading stops happening, and the frame rate rises " + + "eleven to thirteen per cent up to 1440p and five at " + + "4K, where the frame is spending its time on pixels " + + "instead. On by default; turn it off if a face ever " + + "goes missing where the camera crosses a floor, a " + + "ceiling or a wall.", + Cost.of(Level.NONE, Level.SAVES_MEDIUM, Level.NONE), + "Applies after the game restarts.", + new VSwitchOption.Access() { + @Override + public boolean get() { + return VulkanConfig.isGroupFacings(); + } + + @Override + public void set(boolean value) { + VulkanConfig.setGroupFacings(value); + } + }), new VSwitchOption("Short Layer Filter List", "Give the step that picks which chunks contribute to a render " + "layer only the sections that hold blocks. It runs four " @@ -798,11 +830,13 @@ public void set(boolean value) { + "at all, and saying so is the point: once the entity " + "passes were shortened the frame stopped waiting on this " + "thread, so the work removed here is real and hides in " - + "time that was already spare. Worth turning on if your " + + "time that was already spare. Worth having if your " + "frames are held back by the processor rather than the " - + "graphics card. Off by default because what it could get " - + "wrong is a chunk that stops being drawn, and that looks " - + "exactly like terrain that has not finished building.", + + "graphics card. On by default now that the list has been " + + "checked against the full scan over 16 500 layer passes " + + "with nothing missing. Turn it off if a chunk ever stops " + + "being drawn, which looks exactly like terrain that has " + + "not finished building.", Cost.of(Level.NONE, Level.NONE, Level.NONE), "Needs Own Visibility Search on; the game's own search keeps no " + "index of where a section sits.", @@ -1806,7 +1840,7 @@ public void set(int value) { + "than about the surface, which is what a depth buffer " + "answers — and the depth buffer is already here, so this " + "costs no geometry and no second pass over the world. " - + "Eight neighbours are asked whether they stand in front " + + "Sixteen neighbours are asked whether they stand in front " + "of the surface, at half resolution and blurred, because " + "the answer is about corners and crevices rather than " + "about texels. This is not the only occlusion in the " @@ -2786,8 +2820,10 @@ private static VOptionPage rayTracingPage(final Minecraft mc) { new VOptionBlock("Ray Tracing", new VSwitchOption("Terrain Acceleration Structures", "Build the structures a traced ray needs over the terrain this mod " - + "draws. Nothing uses them yet, and the world looks " - + "exactly the same with this on. What it produces is a " + + "draws. On their own they change nothing on screen, " + + "but the sun's shadow, traced light shadows, traced " + + "block light and the light through a canopy need them " + + "and do nothing without them. They also produce a " + "measurement: the obstacle to ray tracing in this game " + "has always been that the structure has to be rebuilt " + "whenever a chunk is, and rebuilding chunks is already " diff --git a/src/main/java/net/vulkanmodnext/client/VulkanPresets.java b/src/main/java/net/vulkanmodnext/client/VulkanPresets.java index 8e3d4ed..8f712ba 100644 --- a/src/main/java/net/vulkanmodnext/client/VulkanPresets.java +++ b/src/main/java/net/vulkanmodnext/client/VulkanPresets.java @@ -160,18 +160,6 @@ private static final class Look { boolean smartAnimations; } - /** - * Everything on, on the assumption that the machine can afford it. - * - * The mirror image of Potato, and written second on purpose: a preset that - * only ever gives things up leaves nothing to come back to. This is what - * "come back" means. - * - * Screen reflections are set well below their maximum. They are the newest - * and least settled of the effects here, and a preset is the wrong place to - * show somebody an effect at its most demanding and least finished — the - * slider is still there for anyone who wants to push it. - */ /** * What every showcase look shares: how the machine is asked to behave. * @@ -439,8 +427,8 @@ public static void balanced(Minecraft mc) { look.animations = true; look.flatBlockColours = false; look.framesInFlight = 2; - // Same reasoning as in Beautiful: free of pixels, and this preset caps - // the render distance at the same thirty-two. + // Same reasoning as in Beautiful: free of pixels, and this preset + // leaves up to thirty-two chunks of render distance to build. look.chunkBuildThreads = VulkanConfig.coresForChunkBuilding(); look.particles = 1; look.fancy = true; @@ -637,7 +625,13 @@ private static void apply(Minecraft mc, Look look) { settings.clouds = look.clouds; settings.entityShadows = look.entityShadows; settings.limitFramerate = look.fpsLimit; - settings.enableVsync = look.vsync; + if (settings.enableVsync != look.vsync) { + settings.enableVsync = look.vsync; + // What vanilla's own toggle does, and what the VSync row does. The + // field alone is read only when the display is created, so without + // this Potato's vsync did nothing until the next start. + org.lwjgl.opengl.Display.setVSyncEnabled(look.vsync); + } if (look.renderDistanceExact > 0) { settings.renderDistanceChunks = look.renderDistanceExact; } else if (settings.renderDistanceChunks > look.renderDistanceCap) { diff --git a/src/main/java/net/vulkanmodnext/client/VulkanProfiles.java b/src/main/java/net/vulkanmodnext/client/VulkanProfiles.java index 3d57515..089b246 100644 --- a/src/main/java/net/vulkanmodnext/client/VulkanProfiles.java +++ b/src/main/java/net/vulkanmodnext/client/VulkanProfiles.java @@ -161,7 +161,16 @@ public static boolean load(String name, Minecraft mc) { mc.gameSettings.clouds = number(values, "mc.clouds", mc.gameSettings.clouds); mc.gameSettings.entityShadows = bool(values, "mc.shadows", mc.gameSettings.entityShadows); mc.gameSettings.limitFramerate = number(values, "mc.fpsLimit", mc.gameSettings.limitFramerate); - mc.gameSettings.enableVsync = bool(values, "mc.vsync", mc.gameSettings.enableVsync); + boolean vsync = bool(values, "mc.vsync", mc.gameSettings.enableVsync); + if (vsync != mc.gameSettings.enableVsync) { + mc.gameSettings.enableVsync = vsync; + // The field alone is read only when the display is created. + org.lwjgl.opengl.Display.setVSyncEnabled(vsync); + } + // The profile may have brought Extreme Render Distance with it, on or + // off, and the slider's ceiling follows that switch only through here; + // this also pulls a restored distance back inside the limit. + RenderDistanceLimit.apply(); int mipmap = number(values, "mc.mipmap", wasMipmap); if (mipmap != wasMipmap) { diff --git a/src/main/java/net/vulkanmodnext/client/WeatherHooks.java b/src/main/java/net/vulkanmodnext/client/WeatherHooks.java index 30aa555..8db90f8 100644 --- a/src/main/java/net/vulkanmodnext/client/WeatherHooks.java +++ b/src/main/java/net/vulkanmodnext/client/WeatherHooks.java @@ -42,9 +42,6 @@ public static void noteTexture(ResourceLocation location) { } } - /** - * @return true when Vulkan took this batch and the game must not draw it - */ /** * What the weather pass actually handed over, counted where it happens. * @@ -73,6 +70,9 @@ public static String stats() { return line; } + /** + * @return true when Vulkan took this batch and the game must not draw it + */ public static boolean take(Tessellator tessellator) { passes++; vertices += tessellator.getBuffer().getVertexCount(); diff --git a/src/main/java/net/vulkanmodnext/client/gui/Lang.java b/src/main/java/net/vulkanmodnext/client/gui/Lang.java index 57ff57d..800b0bb 100644 --- a/src/main/java/net/vulkanmodnext/client/gui/Lang.java +++ b/src/main/java/net/vulkanmodnext/client/gui/Lang.java @@ -135,6 +135,14 @@ public static String slug(String text) { "No profiles yet — type a name below and press Save As", "Everything this mod owns, plus the game's own settings that decide the frame rate", "Search settings", + // Asked for by the profile, class patch and settings screens and + // missing here, so the generated file never carried them and no + // language could translate them. + "Deleted", "Get", "Version", "is available", + "Class Patches", + "What this mod is allowed to rewrite. Changes apply the next time the game starts.", + "Turn Everything Back On", "Do Not Install", "Install Again", + "It stood down on its own", "always", "stood down", "off", "on", "on after restart", }; /** diff --git a/src/main/resources/assets/vulkanmodnext/lang/de_de.lang b/src/main/resources/assets/vulkanmodnext/lang/de_de.lang index 2eb211c..f3b9962 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/de_de.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/de_de.lang @@ -109,7 +109,7 @@ vulkanmodnext.option.screen_reflections=Bildschirmreflexionen vulkanmodnext.tooltip.screen_reflections=Wie viel einer Wasserspiegelung tatsächlich die dort stehende Welt ist, statt der schlichten Nebelfarbe, die die Zeile darüber darauf legt. Der reflektierte Strahl wird über das bereits gezeichnete Bild verfolgt — was überhaupt nur möglich ist, weil Wasser in einem eigenen Durchgang gezeichnet wird, nachdem die undurchsichtige Welt fertig ist und zurückgegeben wurde, sodass Farbe und Tiefe von allem hinter der Oberfläche bereits existieren, wenn ein Wasserpixel schattiert wird. Es wird nichts gegen die Welt selbst getraced, weshalb dies einen Loop statt einer zweiten Kopie der Welt im Speicher kostet. Was er finden kann, ist genau das, was auf dem Bildschirm ist, und nicht mehr: Ein Strahl, der den Bildrand verlässt, oder der dorthin zurückkehrt, wo nie etwas gezeichnet wurde, hat keine Antwort, und die Nebelfarbe beendet ihn — was kein Notbehelf ist, denn die Nebelfarbe ist der Horizont, und der Horizont ist ohnehin das, was flaches Wasser unter diesem Winkel zeigt. Kreaturen fehlen darin aus demselben Grund, aus dem sie bei allem anderen hier fehlen: Das Spiel zeichnet sie, nachdem dieser Renderer fertig ist. Um das zu spiegeln, was außerhalb des Bildschirms liegt, braucht es Strahlen in die Welt selbst, was etwas völlig anderes ist und nicht das hier. vulkanmodnext.applies.screen_reflections=Experimentell. Braucht eingeschaltetes Vulkan-Wasser und -Glas sowie Wasserreflexion über null. vulkanmodnext.option.ambient_occlusion=Ambient Occlusion -vulkanmodnext.tooltip.ambient_occlusion=Wie stark ein Punkt abgedunkelt wird, je nachdem wie wenig er von seiner Umgebung sehen kann. Das Spiel schattiert eine Fläche nur danach, wohin sie zeigt, und sonst nach nichts, sodass eine Innenecke genauso hell ist wie eine offene Wand und ein Raum keine Form hat. Was fehlt, ist eine Frage nach der Nachbarschaft statt nach der Oberfläche, und genau das beantwortet ein Tiefenpuffer — und der Tiefenpuffer ist bereits vorhanden, sodass dies keine Geometrie und keinen zweiten Durchgang über die Welt kostet. Acht Nachbarn werden gefragt, ob sie vor der Oberfläche liegen, in halber Auflösung und geglättet, weil die Antwort eine Frage nach Ecken und Ritzen ist und keine nach Texeln. Das ist nicht die einzige Verdeckung im Bild, und die Skalierung weiß das: Das Spiel backt seine eigene bereits beim Bau des Chunks in die Ecken jedes Blocks ein, und was hier hinzukommt, legt sich darüber statt an ihre Stelle, sodass eine doppelt abgedunkelte Naht schwärzer wird als alles andere im Raum. Der gesamte Reglerbereich soll nutzbar sein. Nur Terrain: Entitäten werden vom Spiel gezeichnet, nachdem dieser Renderer fertig ist, sodass eine Kreatur keinen Schatten in die Ecke wirft, in der sie steht. +vulkanmodnext.tooltip.ambient_occlusion=Wie stark ein Punkt abgedunkelt wird, je nachdem wie wenig er von seiner Umgebung sehen kann. Das Spiel schattiert eine Fläche nur danach, wohin sie zeigt, und sonst nach nichts, sodass eine Innenecke genauso hell ist wie eine offene Wand und ein Raum keine Form hat. Was fehlt, ist eine Frage nach der Nachbarschaft statt nach der Oberfläche, und genau das beantwortet ein Tiefenpuffer — und der Tiefenpuffer ist bereits vorhanden, sodass dies keine Geometrie und keinen zweiten Durchgang über die Welt kostet. Sechzehn Nachbarn werden gefragt, ob sie vor der Oberfläche liegen, in halber Auflösung und geglättet, weil die Antwort eine Frage nach Ecken und Ritzen ist und keine nach Texeln. Das ist nicht die einzige Verdeckung im Bild, und die Skalierung weiß das: Das Spiel backt seine eigene bereits beim Bau des Chunks in die Ecken jedes Blocks ein, und was hier hinzukommt, legt sich darüber statt an ihre Stelle, sodass eine doppelt abgedunkelte Naht schwärzer wird als alles andere im Raum. Der gesamte Reglerbereich soll nutzbar sein. Nur Terrain: Entitäten werden vom Spiel gezeichnet, nachdem dieser Renderer fertig ist, sodass eine Kreatur keinen Schatten in die Ecke wirft, in der sie steht. vulkanmodnext.applies.ambient_occlusion=Braucht eingeschaltetes Vulkan-Terrain. vulkanmodnext.option.ambient_occlusion_reach=AO-Reichweite vulkanmodnext.tooltip.ambient_occlusion_reach=Wie weit der Schatten einer Ecke reicht, in Blöcken. Die Zeile darüber sagt, wie dunkel, diese sagt, wie weit, und Letzteres entscheidet, ob es überhaupt als Schatten gelesen wird: Eine Reichweite unter einem Block zeichnet eine dunkle Linie entlang der Naht, wo eine Wand auf eine Decke trifft, statt eines Schattens, der aus ihr heraus verblasst, weil alles, was der Effekt zu sagen hat, dann innerhalb weniger Pixel gesagt ist. Zwei Blöcke sind etwas weniger als die Höhe eines Türrahmens, das ist die Größenordnung, auf der die Ecken eines Raums gelesen werden. Größer ist weicher und reicht weiter; es kostet nichts zusätzlich, dieselben sechzehn Nachbarn werden befragt, nur weiter auseinander — je weiter, desto gröber also die Antwort, und jenseits von vier Blöcken wird eine kleine Nische von einem über den ganzen Raum verteilten Satz von Samples vollständig übersehen. diff --git a/src/main/resources/assets/vulkanmodnext/lang/en_us.lang b/src/main/resources/assets/vulkanmodnext/lang/en_us.lang index 39c4d32..da70054 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/en_us.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/en_us.lang @@ -26,6 +26,21 @@ vulkanmodnext.ui.could_not_read_that_profile=Could not read that profile vulkanmodnext.ui.no_profiles_yet_type_a_name_below_and_press_save_as=No profiles yet — type a name below and press Save As vulkanmodnext.ui.everything_this_mod_owns_plus_the_game_s_own_settings_that_decide_the_frame_rate=Everything this mod owns, plus the game's own settings that decide the frame rate vulkanmodnext.ui.search_settings=Search settings +vulkanmodnext.ui.deleted=Deleted +vulkanmodnext.ui.get=Get +vulkanmodnext.ui.version=Version +vulkanmodnext.ui.is_available=is available +vulkanmodnext.ui.class_patches=Class Patches +vulkanmodnext.ui.what_this_mod_is_allowed_to_rewrite_changes_apply_the_next_time_the_game_starts=What this mod is allowed to rewrite. Changes apply the next time the game starts. +vulkanmodnext.ui.turn_everything_back_on=Turn Everything Back On +vulkanmodnext.ui.do_not_install=Do Not Install +vulkanmodnext.ui.install_again=Install Again +vulkanmodnext.ui.it_stood_down_on_its_own=It stood down on its own +vulkanmodnext.ui.always=always +vulkanmodnext.ui.stood_down=stood down +vulkanmodnext.ui.off=off +vulkanmodnext.ui.on=on +vulkanmodnext.ui.on_after_restart=on after restart vulkanmodnext.page.search=Search vulkanmodnext.group.search_results=Search results vulkanmodnext.group.nothing_found=Nothing found @@ -55,7 +70,7 @@ vulkanmodnext.tooltip.balanced=Stable, and then it also reaches into Minecraft's vulkanmodnext.option.performance=Performance vulkanmodnext.tooltip.performance=Trades looks for frames: short entity distances, no texture animation, minimal particles, fast graphics. The world will visibly lose detail — this is the one to pick when the framerate matters more than the view. vulkanmodnext.option.potato=Potato -vulkanmodnext.tooltip.potato=For a machine this game is too heavy for. Everything Performance gives up, plus smooth lighting, clouds and eight chunks of view — and a sixty frame ceiling with vsync on. Mipmaps stay on, and deliberately: turning them off is the obvious-looking way to make textures cheap and it does the reverse, sending distant blocks to read the full-size atlas at random. The ceiling is the point: above the refresh rate of the screen, extra frames are heat and fan noise for pictures nobody sees. The world will look plainly worse. Pick this one only if the game is currently unplayable. +vulkanmodnext.tooltip.potato=For a machine this game is too heavy for. Everything Performance gives up, plus block textures — every face drawn in one flat colour — and eight chunks of view, and a sixty frame ceiling with vsync on. Mipmaps stay on, and deliberately: turning them off is the obvious-looking way to make textures cheap and it does the reverse, sending distant blocks to read the full-size atlas at random. The ceiling is the point: above the refresh rate of the screen, extra frames are heat and fan noise for pictures nobody sees. The world will look plainly worse. Pick this one only if the game is currently unplayable. vulkanmodnext.group.profiles=Profiles vulkanmodnext.option.saved_configurations=Saved Configurations vulkanmodnext.tooltip.saved_configurations=Open the list of saved configurations. A profile is your own settings kept whole and given a name — everything this mod owns, plus the game's own render distance, graphics quality, particles and mipmaps, because those cost more frames than anything here does. The point is going back to a configuration without having to remember what was in it. @@ -74,7 +89,7 @@ vulkanmodnext.option.max_framerate=Max Framerate vulkanmodnext.tooltip.max_framerate=Frame cap. 260 means unlimited. A cap below your display's refresh rate lowers load, heat and fan noise without costing you anything you could see. vulkanmodnext.unit.fps= fps vulkanmodnext.option.frame_time_graph=Frame Time Graph -vulkanmodnext.tooltip.frame_time_graph=Draw a frame-time graph in the bottom-left corner: one bar per frame over the last couple of seconds, with the best and worst single frame and the 1%%%% low — the frame time that only one frame in a hundred exceeds. The framerate the game already shows is frames divided by seconds, and it cannot tell a steady 120 from a 240 that stalls every tenth frame; those two average out the same and only one of them is pleasant to play. Nothing is recorded at all while this is off, and with it on the whole graph is one draw call. The diagnostics log states what drawing it actually cost, rather than leaving that to be believed. +vulkanmodnext.tooltip.frame_time_graph=Draw a frame-time graph in the bottom-left corner: one bar per frame over the last couple of seconds, with the best and worst single frame and the 1%% low — the frame time that only one frame in a hundred exceeds. The framerate the game already shows is frames divided by seconds, and it cannot tell a steady 120 from a 240 that stalls every tenth frame; those two average out the same and only one of them is pleasant to play. Nothing is recorded at all while this is off, and with it on the whole graph is one draw call. The diagnostics log states what drawing it actually cost, rather than leaving that to be believed. vulkanmodnext.option.graph_corner=Graph Corner vulkanmodnext.tooltip.graph_corner=Which corner the frame time graph sits in. The default is the bottom left, and so is the chat window — a readout over what you are reading is a tool nobody leaves on. The two lines of numbers move to the other side of the graph in the top corners, so nothing runs off the screen. vulkanmodnext.value.bottom_left=Bottom Left @@ -115,7 +130,7 @@ vulkanmodnext.option.screen_reflections=Screen Reflections vulkanmodnext.tooltip.screen_reflections=How much of a water reflection is the world that is actually standing there, rather than the flat fog colour the row above puts on it. The reflected ray is followed across the picture that has already been drawn — which is possible at all only because water is drawn in a pass of its own, after the opaque world is finished and handed back, so the colour and depth of everything behind the surface exist by the time a water pixel is being shaded. Nothing is traced against the world itself, which is what makes this cost a loop rather than a second copy of the world in memory. What it can find is exactly what is on screen and no more: a ray leaving the edge of the frame, or turning back towards you where nothing was ever drawn, has no answer, and the fog colour finishes it — which is not a patch, since the fog colour is the horizon and the horizon is what flat water shows at that angle anyway. Creatures are missing from it for the same reason they are missing from everything else here: the game draws them after this renderer has finished. Reflecting what is off screen needs rays into the world itself, which is a different thing entirely and is not this. vulkanmodnext.applies.screen_reflections=Experimental. Needs Vulkan Water and Glass on, and Water Reflection above zero. vulkanmodnext.option.ambient_occlusion=Ambient Occlusion -vulkanmodnext.tooltip.ambient_occlusion=How much a point is darkened by how little of its surroundings it can see. The game shades a face by which way it points and by nothing else, so an inside corner is lit exactly like an open wall and a room has no shape to it. What is missing is a question about the neighbourhood rather than about the surface, which is what a depth buffer answers — and the depth buffer is already here, so this costs no geometry and no second pass over the world. Eight neighbours are asked whether they stand in front of the surface, at half resolution and blurred, because the answer is about corners and crevices rather than about texels. This is not the only occlusion in the picture and is scaled knowing it: the game bakes its own into the corners of every block while the chunk is built, and what this adds lands on top of that rather than instead of it, so a seam darkened twice comes out blacker than anything else in a room. The whole length of the slider is meant to be usable. Terrain only: entities are drawn by the game after this renderer has finished, so a creature casts no shadow into the corner it stands in. +vulkanmodnext.tooltip.ambient_occlusion=How much a point is darkened by how little of its surroundings it can see. The game shades a face by which way it points and by nothing else, so an inside corner is lit exactly like an open wall and a room has no shape to it. What is missing is a question about the neighbourhood rather than about the surface, which is what a depth buffer answers — and the depth buffer is already here, so this costs no geometry and no second pass over the world. Sixteen neighbours are asked whether they stand in front of the surface, at half resolution and blurred, because the answer is about corners and crevices rather than about texels. This is not the only occlusion in the picture and is scaled knowing it: the game bakes its own into the corners of every block while the chunk is built, and what this adds lands on top of that rather than instead of it, so a seam darkened twice comes out blacker than anything else in a room. The whole length of the slider is meant to be usable. Terrain only: entities are drawn by the game after this renderer has finished, so a creature casts no shadow into the corner it stands in. vulkanmodnext.applies.ambient_occlusion=Needs Vulkan Terrain on. vulkanmodnext.option.ambient_occlusion_reach=Ambient Occlusion Reach vulkanmodnext.tooltip.ambient_occlusion_reach=How far a corner's shadow reaches, in blocks. The row above says how dark, this one says how far, and the second is what decides whether it reads as shadow at all: a reach under a block draws a dark line along the seam where a wall meets a ceiling rather than a shadow fading out of it, because everything the effect has to say is then said within a few pixels. Two blocks is a little under the height of a doorway, which is the scale a room's corners are read at. Larger is softer and reaches further; it costs nothing extra, the same sixteen neighbours are asked, only further apart — so the wider it goes the coarser the answer, and past four blocks a small alcove is missed entirely by a set of samples spread across a room. @@ -235,7 +250,7 @@ vulkanmodnext.tooltip.sky_pictures_from_pack=Take the sun and the moon from a sh vulkanmodnext.page.ray_tracing=Ray Tracing vulkanmodnext.group.ray_tracing=Ray Tracing vulkanmodnext.option.terrain_acceleration_structures=Terrain Acceleration Structures -vulkanmodnext.tooltip.terrain_acceleration_structures=Build the structures a traced ray needs over the terrain this mod draws. Nothing uses them yet, and the world looks exactly the same with this on. What it produces is a measurement: the obstacle to ray tracing in this game has always been that the structure has to be rebuilt whenever a chunk is, and rebuilding chunks is already the largest cost in a moving frame — this turns that sentence into a number in the diagnostics report. Needs Vulkan 1.2 and the acceleration-structure extension; if the card cannot, the report says which part is missing. +vulkanmodnext.tooltip.terrain_acceleration_structures=Build the structures a traced ray needs over the terrain this mod draws. On their own they change nothing on screen, but the sun's shadow, traced light shadows, traced block light and the light through a canopy need them and do nothing without them. They also produce a measurement: the obstacle to ray tracing in this game has always been that the structure has to be rebuilt whenever a chunk is, and rebuilding chunks is already the largest cost in a moving frame — this turns that sentence into a number in the diagnostics report. Needs Vulkan 1.2 and the acceleration-structure extension; if the card cannot, the report says which part is missing. vulkanmodnext.applies.terrain_acceleration_structures=Applies after the game restarts. vulkanmodnext.option.ray_traced_sun_shadows=Ray Traced Sun Shadows vulkanmodnext.tooltip.ray_traced_sun_shadows=Shadows cast by the world onto itself, traced against the terrain rather than guessed from the screen. Needs Terrain Acceleration Structures on and a card that can trace from a shader; without either it does nothing and says so in the diagnostics report. The shadow lowers how much sky light a surface receives instead of darkening the finished picture — that is how this game shades, and it is why a cave lit by a torch is left alone and why night changes nothing. Creatures do not cast one yet: the mod does not own them. @@ -302,8 +317,11 @@ vulkanmodnext.tooltip.short_entity_section_lists=Hand the two passes that draw c vulkanmodnext.applies.short_entity_section_lists=Needs Own Visibility Search on; the game's own search keeps no index of where a section sits. vulkanmodnext.option.pack_chunk_vertices=Pack Chunk Vertices vulkanmodnext.tooltip.pack_chunk_vertices=Store each vertex of the world in 16 bytes instead of the 28 the game uses. The position keeps a two-thousand-and-forty-eighth of a block, which is 128 times finer than one pixel of a block texture; the light is exact to the value the game wrote; the colour, which carries the shading in corners, is untouched. Measured at render distance 32: the card spends 0.42 ms a frame on terrain instead of 0.49, the route runs at 547 frames a second instead of 521, and the buffer holding the world drops from 428 MiB of video memory to 240. That second number is the bigger one — this mod keeps its own copy of the world's geometry, and how much of it fits is what decides whether a long render distance is possible at all. Takes effect the next time the game starts. +vulkanmodnext.option.group_quad_facings=Group Quad Facings +vulkanmodnext.tooltip.group_quad_facings=Sort each chunk's faces by which way they point, so the ones a camera cannot possibly see are never read. Standing above a floor you cannot see its underside, and the card knows that too — but it only finds out after reading every one of those vertices, and reading vertices is what this renderer's terrain pass is limited by. Measured at render distance 32: 36%% of the reading stops happening, and the frame rate rises eleven to thirteen per cent up to 1440p and five at 4K, where the frame is spending its time on pixels instead. On by default; turn it off if a face ever goes missing where the camera crosses a floor, a ceiling or a wall. +vulkanmodnext.applies.group_quad_facings=Applies after the game restarts. vulkanmodnext.option.short_layer_filter_list=Short Layer Filter List -vulkanmodnext.tooltip.short_layer_filter_list=Give the step that picks which chunks contribute to a render layer only the sections that hold blocks. It runs four times a frame, once per layer, and walks every section on screen to do it — around 17 700 at render distance 32, of which under 2 700 hold anything. Whether a section is empty is one bit on the same object the visibility search already reads for it, so the list is kept while that search walks and costs nothing extra to have. It takes that step from 0.72 ms a frame to 0.45. On the machine it was measured on that bought no frames at all, and saying so is the point: once the entity passes were shortened the frame stopped waiting on this thread, so the work removed here is real and hides in time that was already spare. Worth turning on if your frames are held back by the processor rather than the graphics card. Off by default because what it could get wrong is a chunk that stops being drawn, and that looks exactly like terrain that has not finished building. +vulkanmodnext.tooltip.short_layer_filter_list=Give the step that picks which chunks contribute to a render layer only the sections that hold blocks. It runs four times a frame, once per layer, and walks every section on screen to do it — around 17 700 at render distance 32, of which under 2 700 hold anything. Whether a section is empty is one bit on the same object the visibility search already reads for it, so the list is kept while that search walks and costs nothing extra to have. It takes that step from 0.72 ms a frame to 0.45. On the machine it was measured on that bought no frames at all, and saying so is the point: once the entity passes were shortened the frame stopped waiting on this thread, so the work removed here is real and hides in time that was already spare. Worth having if your frames are held back by the processor rather than the graphics card. On by default now that the list has been checked against the full scan over 16 500 layer passes with nothing missing. Turn it off if a chunk ever stops being drawn, which looks exactly like terrain that has not finished building. vulkanmodnext.applies.short_layer_filter_list=Needs Own Visibility Search on; the game's own search keeps no index of where a section sits. vulkanmodnext.option.fast_rebuild_scan=Fast Rebuild Scan vulkanmodnext.tooltip.fast_rebuild_scan=Hand the last step of the terrain setup only the chunks it can do anything with. That step walks every chunk on screen every frame — around 8 600 of them at render distance 64, and the game's own profiler puts it at 19%% of the frame — to find the handful somebody just broke a block in. The answer for each chunk is one bit, and what makes it expensive is that the bit lives inside a chunk object somewhere else in memory; it is kept in a flat array beside the grid now, small enough to sit in the processor's own cache. Off by default because it replaces the game's own logic, and the way that goes wrong is that a chunk stops being rebuilt. diff --git a/src/main/resources/assets/vulkanmodnext/lang/es_es.lang b/src/main/resources/assets/vulkanmodnext/lang/es_es.lang index f954ca9..4327052 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/es_es.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/es_es.lang @@ -107,7 +107,7 @@ vulkanmodnext.option.screen_reflections=Reflejos en Pantalla vulkanmodnext.tooltip.screen_reflections=Cuánto de un reflejo en el agua es el mundo que realmente está ahí, en lugar del color de niebla plano que la fila de arriba le pone. El rayo reflejado se sigue a través de la imagen que ya se ha dibujado — algo que solo es posible porque el agua se dibuja en una pasada propia, después de que el mundo opaco está terminado y devuelto, así que el color y la profundidad de todo lo que hay detrás de la superficie ya existen para cuando se sombrea un píxel de agua. No se traza nada contra el mundo en sí, que es lo que hace que esto cueste un bucle en lugar de una segunda copia del mundo en memoria. Lo que puede encontrar es exactamente lo que hay en pantalla y nada más: un rayo que sale por el borde del fotograma, o que se vuelve hacia ti donde nunca se dibujó nada, no tiene respuesta, y el color de niebla lo termina — lo cual no es un parche, ya que el color de niebla es el horizonte y el horizonte es lo que el agua plana muestra de todos modos a ese ángulo. Las criaturas faltan en él por la misma razón por la que faltan en todo lo demás aquí: el juego las dibuja después de que este renderizador ha terminado. Reflejar lo que está fuera de pantalla necesita rayos hacia el mundo en sí, que es algo completamente distinto y no es esto. vulkanmodnext.applies.screen_reflections=Experimental. Necesita Agua y Cristal en Vulkan activado, y Reflejo del Agua superior a 0. vulkanmodnext.option.ambient_occlusion=Oclusión Ambiental -vulkanmodnext.tooltip.ambient_occlusion=Cuánto se oscurece un punto según lo poco que puede ver de su entorno. El juego sombrea una cara según hacia dónde apunta y nada más, así que una esquina interior queda iluminada exactamente igual que una pared abierta y una habitación no tiene forma propia. Lo que falta es una pregunta sobre el entorno más que sobre la superficie, que es lo que responde un búfer de profundidad — y el búfer de profundidad ya está aquí, así que esto no cuesta geometría ni una segunda pasada sobre el mundo. Se pregunta a ocho vecinos si están delante de la superficie, a media resolución y difuminado, porque la respuesta trata de esquinas y grietas más que de texels. Esta no es la única oclusión presente en la imagen, y se calibra sabiéndolo: el juego hornea la suya propia en las esquinas de cada bloque mientras se construye el chunk, y lo que esto añade se suma a eso en lugar de sustituirlo, así que una costura oscurecida dos veces sale más negra que cualquier otra cosa en una habitación. Todo el recorrido del control deslizante está pensado para ser útil. Solo terreno: las entidades las dibuja el juego después de que este renderizador ha terminado, así que una criatura no proyecta ninguna sombra en la esquina donde se encuentra. +vulkanmodnext.tooltip.ambient_occlusion=Cuánto se oscurece un punto según lo poco que puede ver de su entorno. El juego sombrea una cara según hacia dónde apunta y nada más, así que una esquina interior queda iluminada exactamente igual que una pared abierta y una habitación no tiene forma propia. Lo que falta es una pregunta sobre el entorno más que sobre la superficie, que es lo que responde un búfer de profundidad — y el búfer de profundidad ya está aquí, así que esto no cuesta geometría ni una segunda pasada sobre el mundo. Se pregunta a dieciséis vecinos si están delante de la superficie, a media resolución y difuminado, porque la respuesta trata de esquinas y grietas más que de texels. Esta no es la única oclusión presente en la imagen, y se calibra sabiéndolo: el juego hornea la suya propia en las esquinas de cada bloque mientras se construye el chunk, y lo que esto añade se suma a eso en lugar de sustituirlo, así que una costura oscurecida dos veces sale más negra que cualquier otra cosa en una habitación. Todo el recorrido del control deslizante está pensado para ser útil. Solo terreno: las entidades las dibuja el juego después de que este renderizador ha terminado, así que una criatura no proyecta ninguna sombra en la esquina donde se encuentra. vulkanmodnext.applies.ambient_occlusion=Necesita Terreno Vulkan activado. vulkanmodnext.option.ambient_occlusion_reach=Alcance de la Oclusión Ambiental vulkanmodnext.tooltip.ambient_occlusion_reach=Hasta dónde llega la sombra de una esquina, en bloques. La fila de arriba dice cuán oscura, esta dice hasta dónde, y es esta segunda la que decide si se lee como sombra siquiera: un alcance menor a un bloque dibuja una línea oscura a lo largo de la costura donde una pared se une con un techo en lugar de una sombra que se desvanece desde ahí, porque todo lo que el efecto tiene que decir se dice entonces en unos pocos píxeles. Dos bloques es un poco menos que la altura de una puerta, que es la escala a la que se leen las esquinas de una habitación. Más grande es más suave y llega más lejos; no cuesta nada extra, se pregunta a los mismos dieciséis vecinos, solo que más separados — así que cuanto más se amplía, más basta es la respuesta, y pasados los cuatro bloques una pequeña alcoba pasa completamente desapercibida para un conjunto de muestras repartidas por toda una habitación. diff --git a/src/main/resources/assets/vulkanmodnext/lang/fr_fr.lang b/src/main/resources/assets/vulkanmodnext/lang/fr_fr.lang index e5964a2..283b2a8 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/fr_fr.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/fr_fr.lang @@ -107,7 +107,7 @@ vulkanmodnext.option.screen_reflections=Reflets à l'écran vulkanmodnext.tooltip.screen_reflections=Quelle part d'un reflet sur l'eau est réellement le monde qui se trouve là, plutôt que la couleur de brouillard plate que la ligne au-dessus y applique. Le rayon réfléchi est suivi à travers l'image déjà dessinée — ce qui n'est possible que parce que l'eau est dessinée dans une passe qui lui est propre, après que le monde opaque est terminé et rendu, si bien que la couleur et la profondeur de tout ce qui se trouve derrière la surface existent déjà au moment où un pixel d'eau est ombré. Rien n'est tracé contre le monde lui-même, ce qui fait que cela coûte une boucle plutôt qu'une seconde copie du monde en mémoire. Ce qu'il peut trouver, c'est exactement ce qui est à l'écran et rien de plus : un rayon qui sort du bord de l'image, ou qui se retourne vers vous là où rien n'a jamais été dessiné, n'a pas de réponse, et la couleur de brouillard le termine — ce qui n'est pas une rustine, puisque la couleur de brouillard est l'horizon et que l'horizon est ce que l'eau plate montre de toute façon sous cet angle. Les créatures en sont absentes pour la même raison qu'elles sont absentes de tout le reste ici : le jeu les dessine une fois que ce moteur de rendu a terminé. Refléter ce qui est hors écran demande des rayons lancés dans le monde lui-même, ce qui est une tout autre chose et n'est pas ceci. vulkanmodnext.applies.screen_reflections=Expérimental. Nécessite Eau et verre Vulkan activé, et Reflet de l'eau supérieur à 0. vulkanmodnext.option.ambient_occlusion=Occlusion ambiante -vulkanmodnext.tooltip.ambient_occlusion=Jusqu'à quel point un point est assombri selon la faible part de son environnement qu'il peut voir. Le jeu ombre une face selon la direction qu'elle pointe et rien d'autre, si bien qu'un coin intérieur est éclairé exactement comme un mur ouvert et qu'une pièce n'a aucune forme propre. Ce qui manque, c'est une question sur le voisinage plutôt que sur la surface, à laquelle répond un tampon de profondeur — et le tampon de profondeur est déjà là, donc cela ne coûte aucune géométrie ni aucune seconde passe sur le monde. Huit voisins sont interrogés pour savoir s'ils se trouvent devant la surface, à demi-résolution et flous, parce que la réponse porte sur les coins et les fissures plutôt que sur les texels. Ce n'est pas la seule occlusion présente dans l'image, et l'effet est calibré en le sachant : le jeu cuit la sienne dans les coins de chaque bloc pendant la construction du chunk, et ce que cela ajoute vient s'ajouter à cela plutôt que le remplacer, si bien qu'une jointure assombrie deux fois ressort plus noire que tout le reste d'une pièce. Toute la longueur du curseur est censée être utilisable. Terrain uniquement : les entités sont dessinées par le jeu une fois que ce moteur de rendu a terminé, si bien qu'une créature ne projette aucune ombre dans le coin où elle se tient. +vulkanmodnext.tooltip.ambient_occlusion=Jusqu'à quel point un point est assombri selon la faible part de son environnement qu'il peut voir. Le jeu ombre une face selon la direction qu'elle pointe et rien d'autre, si bien qu'un coin intérieur est éclairé exactement comme un mur ouvert et qu'une pièce n'a aucune forme propre. Ce qui manque, c'est une question sur le voisinage plutôt que sur la surface, à laquelle répond un tampon de profondeur — et le tampon de profondeur est déjà là, donc cela ne coûte aucune géométrie ni aucune seconde passe sur le monde. Seize voisins sont interrogés pour savoir s'ils se trouvent devant la surface, à demi-résolution et flous, parce que la réponse porte sur les coins et les fissures plutôt que sur les texels. Ce n'est pas la seule occlusion présente dans l'image, et l'effet est calibré en le sachant : le jeu cuit la sienne dans les coins de chaque bloc pendant la construction du chunk, et ce que cela ajoute vient s'ajouter à cela plutôt que le remplacer, si bien qu'une jointure assombrie deux fois ressort plus noire que tout le reste d'une pièce. Toute la longueur du curseur est censée être utilisable. Terrain uniquement : les entités sont dessinées par le jeu une fois que ce moteur de rendu a terminé, si bien qu'une créature ne projette aucune ombre dans le coin où elle se tient. vulkanmodnext.applies.ambient_occlusion=Nécessite Terrain Vulkan activé. vulkanmodnext.option.ambient_occlusion_reach=Portée de l'occlusion ambiante vulkanmodnext.tooltip.ambient_occlusion_reach=Jusqu'à quelle distance l'ombre d'un coin s'étend, en blocs. La ligne au-dessus dit à quel point c'est sombre, celle-ci dit jusqu'où, et c'est la seconde qui décide si cela se lit comme une ombre ou non : une portée inférieure à un bloc trace une ligne sombre le long de la jointure où un mur rencontre un plafond plutôt qu'une ombre qui s'en éloigne en s'estompant, car tout ce que l'effet a à dire se dit alors en quelques pixels. Deux blocs, c'est un peu moins que la hauteur d'une porte, ce qui est l'échelle à laquelle les coins d'une pièce sont lus. Plus grand est plus doux et porte plus loin ; cela ne coûte rien de plus, les mêmes seize voisins sont interrogés, simplement plus espacés — donc plus la portée s'élargit, plus la réponse est grossière, et au-delà de quatre blocs, une petite alcôve est totalement manquée par un ensemble d'échantillons répartis sur toute une pièce. diff --git a/src/main/resources/assets/vulkanmodnext/lang/ja_jp.lang b/src/main/resources/assets/vulkanmodnext/lang/ja_jp.lang index f03294e..79f29fe 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/ja_jp.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/ja_jp.lang @@ -98,7 +98,7 @@ vulkanmodnext.option.screen_reflections=スクリーン反射 vulkanmodnext.tooltip.screen_reflections=水面の反射のうち、上の行が塗る平坦な霧の色ではなく、実際にそこに立っているワールドがどれだけ映っているか。反射光線は、すでに描画済みの画面上をたどっていく——これがそもそも可能なのは、水が不透明なワールドが描き終わって渡された後、独自のパスで描画されるからであり、そのため水のピクセルがシェーディングされる時点で、その奥にあるすべてのものの色と深度がすでに存在している。ワールド自体に対してレイトレースを行うことは一切なく、そのためこの機能のコストはメモリ上にワールドをもう1つ複製することではなく、単なるループで済んでいる。見つけられるのは画面に映っているものだけであり、それ以上ではない:フレームの端から出ていく光線や、何も描画されていない場所へ折り返してくる光線には答えがなく、そこは霧の色で締めくくられる——これは埋め合わせではない。霧の色はそもそも地平線であり、地平線こそがその角度で平らな水面が本来映すものだからだ。生物がここに映らないのも、他のすべてで生物が抜けているのと同じ理由による:ゲームはこのレンダラーが処理を終えた後にそれらを描画する。画面外にあるものを反射するにはワールド自体へのレイが必要であり、それはまったく別の仕組みであって、これではない。 vulkanmodnext.applies.screen_reflections=実験的機能。Vulkanの水とガラスがオンであり、かつ水面の反射が0より大きい必要がある。 vulkanmodnext.option.ambient_occlusion=アンビエントオクルージョン -vulkanmodnext.tooltip.ambient_occlusion=ある点が、周囲をどれだけ見渡せないかに応じてどれだけ暗くなるか。ゲームは面をどちらに向いているかだけでシェーディングしており、それ以外は何も見ていない。そのため内側の角も開けた壁も同じ明るさで照らされ、部屋には立体感がない。欠けているのは表面についての問いではなく周囲についての問いであり、それに答えるのが深度バッファである——そして深度バッファはすでに存在しているため、この機能に追加のジオメトリもワールドへの2回目のパスも必要ない。8つの近傍点がその表面より手前にあるかどうかを尋ねられる。解像度は半分でぼかしがかかっている。答えが問題にしているのは角や隙間であって、テクセル単位の話ではないからだ。これは画面に映る唯一のオクルージョンではなく、そのことを踏まえてスケーリングされている:ゲームはチャンク構築中に、それ自身のオクルージョンをすべてのブロックの角に焼き込んでおり、この機能が加えるものはそれを置き換えるのではなく上乗せする。そのため二重に暗くなった継ぎ目は、部屋の中の他のどこよりも黒くなる。スライダーの全域が使えるように意図されている。地形のみが対象:エンティティはこのレンダラーが処理を終えた後にゲームが描画するため、生物は自分が立っている角に影を落とさない。 +vulkanmodnext.tooltip.ambient_occlusion=ある点が、周囲をどれだけ見渡せないかに応じてどれだけ暗くなるか。ゲームは面をどちらに向いているかだけでシェーディングしており、それ以外は何も見ていない。そのため内側の角も開けた壁も同じ明るさで照らされ、部屋には立体感がない。欠けているのは表面についての問いではなく周囲についての問いであり、それに答えるのが深度バッファである——そして深度バッファはすでに存在しているため、この機能に追加のジオメトリもワールドへの2回目のパスも必要ない。16個の近傍点がその表面より手前にあるかどうかを尋ねられる。解像度は半分でぼかしがかかっている。答えが問題にしているのは角や隙間であって、テクセル単位の話ではないからだ。これは画面に映る唯一のオクルージョンではなく、そのことを踏まえてスケーリングされている:ゲームはチャンク構築中に、それ自身のオクルージョンをすべてのブロックの角に焼き込んでおり、この機能が加えるものはそれを置き換えるのではなく上乗せする。そのため二重に暗くなった継ぎ目は、部屋の中の他のどこよりも黒くなる。スライダーの全域が使えるように意図されている。地形のみが対象:エンティティはこのレンダラーが処理を終えた後にゲームが描画するため、生物は自分が立っている角に影を落とさない。 vulkanmodnext.applies.ambient_occlusion=Vulkan地形がオンである必要がある。 vulkanmodnext.option.ambient_occlusion_reach=アンビエントオクルージョンの届く範囲 vulkanmodnext.tooltip.ambient_occlusion_reach=角の影がブロック単位でどこまで届くか。上の行は「どれだけ暗いか」を、この行は「どこまで届くか」を決めており、実際に影として見えるかどうかを左右するのは後者である:届く範囲が1ブロック未満だと、影が徐々に消えていくのではなく、壁と天井が接する継ぎ目に沿って暗い線が引かれるだけになる。この効果が語れることのすべてが、わずか数ピクセルの中で語り尽くされてしまうからだ。2ブロックはドア枠の高さより少し低い程度で、これは部屋の角が読み取られるスケールに相当する。値を大きくするほど柔らかくなり、届く範囲も広がる。追加のコストはかからず、同じ16の近傍点を尋ねているだけで、間隔が広がるだけである——そのため範囲が広がるほど答えは粗くなり、4ブロックを超えると、部屋全体に広がったサンプル群では小さな窪みを完全に見逃してしまう。 diff --git a/src/main/resources/assets/vulkanmodnext/lang/pt_br.lang b/src/main/resources/assets/vulkanmodnext/lang/pt_br.lang index bd58425..7d5744c 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/pt_br.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/pt_br.lang @@ -98,7 +98,7 @@ vulkanmodnext.option.screen_reflections=Reflexos de Tela vulkanmodnext.tooltip.screen_reflections=O quanto de um reflexo na água é o mundo que está de fato ali, em vez da cor de neblina plana que a linha acima aplica a ele. O raio refletido é seguido pela imagem que já foi desenhada — o que só é possível porque a água é desenhada em um passe próprio, depois que o mundo opaco termina e é devolvido, então a cor e a profundidade de tudo atrás da superfície já existem no momento em que um pixel de água está sendo sombreado. Nada é traçado contra o mundo em si, o que é o que faz isso custar um laço em vez de uma segunda cópia do mundo na memória. O que ele consegue encontrar é exatamente o que está na tela e nada além disso: um raio que sai pela borda do quadro, ou que se volta de novo para você em um lugar onde nada nunca foi desenhado, não tem resposta, e a cor de neblina o encerra — o que não é um remendo, já que a cor de neblina é o horizonte, e o horizonte é o que água plana mostra naquele ângulo de qualquer forma. Criaturas estão ausentes dele pelo mesmo motivo que estão ausentes de tudo o mais aqui: o jogo as desenha depois que este renderizador termina. Refletir o que está fora da tela exige raios lançados dentro do próprio mundo, o que é algo totalmente diferente e não é isto. vulkanmodnext.applies.screen_reflections=Experimental. Precisa de Água e Vidro Vulkan ativado, e Reflexo da Água acima de zero. vulkanmodnext.option.ambient_occlusion=Oclusão de Ambiente -vulkanmodnext.tooltip.ambient_occlusion=O quanto um ponto é escurecido pelo quão pouco ele consegue ver do que o cerca. O jogo sombreia uma face pelo lado para o qual ela aponta e por mais nada, então um canto interno é iluminado exatamente como uma parede aberta, e um cômodo não tem forma nenhuma. O que falta é uma pergunta sobre a vizinhança, não sobre a superfície, e é isso que um buffer de profundidade responde — e o buffer de profundidade já está aqui, então isso não custa geometria nenhuma nem um segundo passe pelo mundo. Oito vizinhos são consultados para saber se estão na frente da superfície, em metade da resolução e borrados, porque a resposta é sobre cantos e frestas, não sobre texels. Esta não é a única oclusão na imagem, e é dimensionada sabendo disso: o jogo assa a sua própria nos cantos de cada bloco enquanto o chunk é construído, e o que isto adiciona cai por cima daquilo em vez de substituí-lo, então uma costura escurecida duas vezes sai mais preta do que qualquer outra coisa em um cômodo. A extensão inteira do controle deslizante foi pensada para ser utilizável. Só terreno: entidades são desenhadas pelo jogo depois que este renderizador termina, então uma criatura não projeta sombra nenhuma no canto em que está. +vulkanmodnext.tooltip.ambient_occlusion=O quanto um ponto é escurecido pelo quão pouco ele consegue ver do que o cerca. O jogo sombreia uma face pelo lado para o qual ela aponta e por mais nada, então um canto interno é iluminado exatamente como uma parede aberta, e um cômodo não tem forma nenhuma. O que falta é uma pergunta sobre a vizinhança, não sobre a superfície, e é isso que um buffer de profundidade responde — e o buffer de profundidade já está aqui, então isso não custa geometria nenhuma nem um segundo passe pelo mundo. Dezesseis vizinhos são consultados para saber se estão na frente da superfície, em metade da resolução e borrados, porque a resposta é sobre cantos e frestas, não sobre texels. Esta não é a única oclusão na imagem, e é dimensionada sabendo disso: o jogo assa a sua própria nos cantos de cada bloco enquanto o chunk é construído, e o que isto adiciona cai por cima daquilo em vez de substituí-lo, então uma costura escurecida duas vezes sai mais preta do que qualquer outra coisa em um cômodo. A extensão inteira do controle deslizante foi pensada para ser utilizável. Só terreno: entidades são desenhadas pelo jogo depois que este renderizador termina, então uma criatura não projeta sombra nenhuma no canto em que está. vulkanmodnext.applies.ambient_occlusion=Precisa de Terreno Vulkan ativado. vulkanmodnext.option.ambient_occlusion_reach=Alcance da Oclusão de Ambiente vulkanmodnext.tooltip.ambient_occlusion_reach=A que distância a sombra de um canto alcança, em blocos. A linha acima diz o quão escura, esta diz até onde, e é a segunda que decide se isso sequer parece uma sombra: um alcance menor que um bloco desenha uma linha escura ao longo da costura onde uma parede encontra um teto, em vez de uma sombra que se dissipa a partir dela, porque tudo que o efeito tem a dizer é dito ali dentro de poucos pixels. Dois blocos é um pouco menos que a altura de um vão de porta, que é a escala em que os cantos de um cômodo são percebidos. Maior é mais suave e alcança mais longe; não custa nada extra, os mesmos dezesseis vizinhos são consultados, só que mais espaçados — então quanto mais largo, mais grosseira fica a resposta, e além de quatro blocos um pequeno nicho passa despercebido por completo por um conjunto de amostras espalhadas por um cômodo inteiro. diff --git a/src/main/resources/assets/vulkanmodnext/lang/ru_ru.lang b/src/main/resources/assets/vulkanmodnext/lang/ru_ru.lang index c0111f8..a99bffa 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/ru_ru.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/ru_ru.lang @@ -108,7 +108,7 @@ vulkanmodnext.option.screen_reflections=Экранные отражения vulkanmodnext.tooltip.screen_reflections=Какая часть отражения в воде — это действительно стоящий там мир, а не плоский цвет тумана, который на него кладёт строка выше. Отражённый луч прослеживается по уже нарисованной картинке — а это вообще возможно только потому, что вода рисуется в собственном отдельном проходе, после того как непрозрачный мир закончен и передан обратно, так что цвет и глубина всего, что находится за поверхностью, уже существуют к моменту, когда затеняется пиксель воды. Ничто не трассируется относительно самого мира, и именно поэтому это стоит цикла, а не второй копии мира в памяти. Найти он может ровно то, что есть на экране, и не больше: у луча, выходящего за край кадра, или разворачивающегося обратно к тебе там, где никогда ничего не рисовалось, нет ответа, и цвет тумана его завершает — и это не заплатка, потому что цвет тумана и есть горизонт, а горизонт — это то, что плоская вода всё равно показывает под таким углом. Существ в нём нет по той же причине, по которой их нет во всём остальном здесь: игра рисует их уже после того, как этот рендерер закончил работу. Чтобы отражать то, что находится за пределами экрана, нужны лучи в сам мир, а это совсем другое дело и не то, что делает эта настройка. vulkanmodnext.applies.screen_reflections=Экспериментально. Нужны включённые «Вода и стекло на Vulkan» и «Отражение воды» больше нуля. vulkanmodnext.option.ambient_occlusion=Затенение окружения -vulkanmodnext.tooltip.ambient_occlusion=Насколько точка темнеет от того, как мало из своего окружения она может видеть. Игра затеняет грань только по тому, куда она направлена, и больше ни по чему, поэтому внутренний угол освещается точно так же, как открытая стена, а у комнаты нет формы. Не хватает вопроса об окружении, а не о самой поверхности, и именно на него отвечает буфер глубины — а буфер глубины здесь уже есть, поэтому это не стоит ни геометрии, ни второго прохода по миру. У восьми соседей спрашивается, стоят ли они перед поверхностью, в половинном разрешении и размыто, потому что ответ — это вопрос об углах и щелях, а не о текселях. Это не единственное затенение в кадре, и его масштаб это учитывает: игра запекает своё собственное в углы каждого блока прямо во время постройки чанка, и то, что добавляет этот эффект, ложится поверх этого, а не вместо него, поэтому шов, затемнённый дважды, получается чернее всего остального в комнате. Весь диапазон ползунка задуман рабочим. Только рельеф: сущности рисует сама игра уже после того, как этот рендерер закончил работу, поэтому существо не отбрасывает тень в угол, в котором стоит. +vulkanmodnext.tooltip.ambient_occlusion=Насколько точка темнеет от того, как мало из своего окружения она может видеть. Игра затеняет грань только по тому, куда она направлена, и больше ни по чему, поэтому внутренний угол освещается точно так же, как открытая стена, а у комнаты нет формы. Не хватает вопроса об окружении, а не о самой поверхности, и именно на него отвечает буфер глубины — а буфер глубины здесь уже есть, поэтому это не стоит ни геометрии, ни второго прохода по миру. У шестнадцати соседей спрашивается, стоят ли они перед поверхностью, в половинном разрешении и размыто, потому что ответ — это вопрос об углах и щелях, а не о текселях. Это не единственное затенение в кадре, и его масштаб это учитывает: игра запекает своё собственное в углы каждого блока прямо во время постройки чанка, и то, что добавляет этот эффект, ложится поверх этого, а не вместо него, поэтому шов, затемнённый дважды, получается чернее всего остального в комнате. Весь диапазон ползунка задуман рабочим. Только рельеф: сущности рисует сама игра уже после того, как этот рендерер закончил работу, поэтому существо не отбрасывает тень в угол, в котором стоит. vulkanmodnext.applies.ambient_occlusion=Нужен включённый «Рельеф на Vulkan». vulkanmodnext.option.ambient_occlusion_reach=Дальность затенения vulkanmodnext.tooltip.ambient_occlusion_reach=Насколько далеко тянется тень от угла, в блоках. Строка выше говорит, насколько темно, эта — насколько далеко, и именно второе решает, читается ли это вообще как тень: дальность меньше блока рисует тёмную линию вдоль шва, где стена сходится с потолком, вместо тени, затухающей от него, потому что всё, что эффект может сказать, тогда сказано в пределах нескольких пикселей. Два блока — это чуть меньше высоты дверного проёма, а это и есть тот масштаб, на котором считываются углы комнаты. Больше значение — мягче и дальше; это ничего не стоит дополнительно, опрашиваются те же шестнадцать соседей, просто на большем расстоянии друг от друга — поэтому чем шире, тем грубее ответ, а после четырёх блоков маленькая ниша полностью пропускается набором сэмплов, разбросанных по всей комнате. diff --git a/src/main/resources/assets/vulkanmodnext/lang/zh_cn.lang b/src/main/resources/assets/vulkanmodnext/lang/zh_cn.lang index c9e1232..c67ff68 100644 --- a/src/main/resources/assets/vulkanmodnext/lang/zh_cn.lang +++ b/src/main/resources/assets/vulkanmodnext/lang/zh_cn.lang @@ -102,7 +102,7 @@ vulkanmodnext.option.screen_reflections=屏幕反射 vulkanmodnext.tooltip.screen_reflections=水面倒影中有多少是真正矗立在那里的世界,而不是上一行设置铺上去的那种平坦雾色。反射光线会沿着已经画好的画面进行追踪——这之所以可行,只是因为水是在自己单独的一个通道里绘制的,在不透明的世界画完并交还回来之后,所以在一个水面像素被着色的那一刻,它背后一切事物的颜色和深度都已经存在。没有任何东西是针对世界本身进行追踪的,这正是它的开销只是一次循环、而不是在内存里再保留一份世界副本的原因。它能找到的,正是屏幕上已有的内容,仅此而已:一条离开画面边缘的光线,或者在什么都没画过的地方折返回来的光线,都得不到答案,最后由雾色来收尾——这不是权宜之计,因为雾色本来就是地平线,而地平线正是平坦的水面在那个角度下本来就会显示的东西。生物在这里同样缺席,原因和其他一切东西缺席的原因一样:游戏是在这个渲染器完成之后才绘制它们的。要反射屏幕之外的内容,需要向世界本身发射光线,那是完全不同的另一件事,不是这个功能做的事。 vulkanmodnext.applies.screen_reflections=实验性。需要开启 Vulkan 水与玻璃,并且水面反射大于零。 vulkanmodnext.option.ambient_occlusion=环境光遮蔽 -vulkanmodnext.tooltip.ambient_occlusion=一个点因为能看到的周围空间有多少而被压暗多少。游戏给一个面着色时只看它朝向哪个方向,除此之外什么都不看,所以一个内墙角和一面敞开的墙被照得一样亮,房间因此没有立体感。缺的是一个关于周围环境而不是关于表面本身的问题,而这正是深度缓冲区能回答的——深度缓冲区本来就已经存在,所以这个效果不需要额外的几何体,也不需要对世界再扫描一遍。它会询问八个邻近采样点是否挡在表面前方,分辨率减半并做模糊处理,因为答案关心的是墙角和缝隙,而不是逐个纹素。这并不是画面里唯一的遮蔽效果,它的强度也是在知道这一点的前提下设定的:游戏在区块构建时会把自己的遮蔽烘焙进每个方块的角上,而这个效果叠加在那之上,而不是取代它,所以一条被压暗两次的接缝,会比房间里任何其他地方都更黑。滑块的整个范围都设计成可用的。仅限地形:实体是游戏在这个渲染器完成之后才绘制的,所以一个生物不会在它所站的墙角投下任何阴影。 +vulkanmodnext.tooltip.ambient_occlusion=一个点因为能看到的周围空间有多少而被压暗多少。游戏给一个面着色时只看它朝向哪个方向,除此之外什么都不看,所以一个内墙角和一面敞开的墙被照得一样亮,房间因此没有立体感。缺的是一个关于周围环境而不是关于表面本身的问题,而这正是深度缓冲区能回答的——深度缓冲区本来就已经存在,所以这个效果不需要额外的几何体,也不需要对世界再扫描一遍。它会询问十六个邻近采样点是否挡在表面前方,分辨率减半并做模糊处理,因为答案关心的是墙角和缝隙,而不是逐个纹素。这并不是画面里唯一的遮蔽效果,它的强度也是在知道这一点的前提下设定的:游戏在区块构建时会把自己的遮蔽烘焙进每个方块的角上,而这个效果叠加在那之上,而不是取代它,所以一条被压暗两次的接缝,会比房间里任何其他地方都更黑。滑块的整个范围都设计成可用的。仅限地形:实体是游戏在这个渲染器完成之后才绘制的,所以一个生物不会在它所站的墙角投下任何阴影。 vulkanmodnext.applies.ambient_occlusion=需要开启 Vulkan 地形。 vulkanmodnext.option.ambient_occlusion_reach=环境光遮蔽范围 vulkanmodnext.tooltip.ambient_occlusion_reach=一个墙角的阴影能延伸多远,单位是格。上一行决定的是多暗,这一行决定的是多远,而决定这个效果读起来到底像不像阴影的,其实是后者:范围小于一格时,画出来的会是墙和天花板交界处的一条暗线,而不是逐渐消退的阴影,因为这个效果能表达的全部内容都被压缩在了几个像素之内。两格大约略低于一扇门的高度,这也是房间墙角被观察时的尺度。数值越大,效果越柔和、延伸越远;这不会带来额外开销,询问的仍是同样的十六个邻近采样点,只是彼此间隔更远——所以范围越宽,答案就越粗糙,超过四格之后,散布在整个房间里的一组采样点会完全漏掉一个小小的壁龛。 From 661d8cf2de16c7bebf09fde03a8bcf21c13f685d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:08:45 +0000 Subject: [PATCH 4/6] chain the opaque pass's layout changes onto the wait, round the staging check Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../vulkanmodnext/vkimpl/VkChunkMirror.java | 7 +++++-- .../vkimpl/VkTerrainRenderer.java | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java b/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java index 5ea486a..19a7af4 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkChunkMirror.java @@ -1300,11 +1300,14 @@ private Entry createEntry(int capacity) { * than it would run on into the builder region. */ private void ensureStagingRing(int needed) { - if (stagingCapacity - stagingCapacity / 2 >= needed && stagingBuffer != 0) { + // Rounded the way allocateStagingRange rounds it, or a request just + // under half the ring would pass here and overrun there. + long aligned = (needed + 15L) & ~15L; + if (stagingCapacity - stagingCapacity / 2 >= aligned && stagingBuffer != 0) { return; } long capacity = Math.max(STAGING_RING_MIN, stagingCapacity == 0 ? STAGING_RING_MIN : stagingCapacity); - while (capacity - capacity / 2 < needed) { + while (capacity - capacity / 2 < aligned) { capacity *= 2; } if (stagingBuffer != 0) { diff --git a/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java b/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java index aef09ae..6745c64 100644 --- a/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java +++ b/src/main/java/net/vulkanmodnext/vkimpl/VkTerrainRenderer.java @@ -8541,10 +8541,27 @@ private void createRenderPass(MemoryStack stack) { .pColorAttachments(colorRef) .pDepthStencilAttachment(depthRef); + // Chains the layout changes and the clears onto the submit's semaphore + // wait. The implicit dependency starts at the top of the pipe, which + // the wait does not cover, so without this the images could be + // repacked while OpenGL is still reading the last frame out of them. + VkSubpassDependency.Buffer dependency = VkSubpassDependency.calloc(1, stack); + dependency.get(0) + .srcSubpass(VK_SUBPASS_EXTERNAL) + .dstSubpass(0) + .srcStageMask(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) + .srcAccessMask(0) + .dstStageMask(VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) + .dstAccessMask(VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + VkRenderPassCreateInfo rpInfo = VkRenderPassCreateInfo.calloc(stack) .sType(VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO) .pAttachments(attachments) - .pSubpasses(subpass); + .pSubpasses(subpass) + .pDependencies(dependency); LongBuffer pRenderPass = stack.mallocLong(1); check(vkCreateRenderPass(device(), rpInfo, null, pRenderPass), "vkCreateRenderPass(terrain)"); renderPass = pRenderPass.get(0); From 2188c99f7ceaaf3d849963af2bbef478233d5f62 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:10:40 +0000 Subject: [PATCH 5/6] stand the Vulkan half aside on Android whatever the processor PojavLauncher and its forks also run on 64-bit x86 tablets, where the architecture check alone would let the loader try desktop Linux libraries against Android's C library. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../java/net/vulkanmodnext/VulkanLoader.java | 6 +-- .../java/net/vulkanmodnext/core/Platform.java | 39 +++++++++++++++++++ .../vulkanmodnext/core/VulkanCorePlugin.java | 6 +-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/VulkanLoader.java b/src/main/java/net/vulkanmodnext/VulkanLoader.java index 3d5f3c5..aa11f33 100644 --- a/src/main/java/net/vulkanmodnext/VulkanLoader.java +++ b/src/main/java/net/vulkanmodnext/VulkanLoader.java @@ -169,10 +169,10 @@ public static synchronized VulkanBridge bridge() { + " -Dvulkanmodnext.javaCeiling=" + java + " — what decides this is the JNI version" + " rather than the Java one, and it moves far more rarely."); } - String arch = net.vulkanmodnext.core.Platform.unsupportedArchitecture(); - if (arch != null) { + String platform = net.vulkanmodnext.core.Platform.unsupportedPlatform(); + if (platform != null) { throw new VulkanUnavailableException("This mod ships its Vulkan half with native" - + " libraries for 64-bit x86 only, and this machine is " + arch + ". Nothing" + + " libraries for 64-bit x86 desktops only, and this machine is " + platform + ". Nothing" + " here can load, so the game renders on OpenGL as it always did." + " Phones and tablets running Minecraft Java through a translation layer" + " land here: the world is drawn by that layer, and everything this mod" diff --git a/src/main/java/net/vulkanmodnext/core/Platform.java b/src/main/java/net/vulkanmodnext/core/Platform.java index f8ea83a..fd579ca 100644 --- a/src/main/java/net/vulkanmodnext/core/Platform.java +++ b/src/main/java/net/vulkanmodnext/core/Platform.java @@ -28,6 +28,45 @@ public final class Platform { private Platform() { } + /** + * What about this machine rules the Vulkan half out, or null when nothing + * does. Android first, because that answer holds whatever the processor: + * PojavLauncher and its forks also run on 64-bit x86 tablets and + * Chromebooks, and there the architecture check alone would let the loader + * try desktop Linux libraries against Android's C library. + */ + public static String unsupportedPlatform() { + if (android()) { + return "Android (" + System.getProperty("os.arch", "unknown") + ")"; + } + return unsupportedArchitecture(); + } + + /** + * Whether the game is running on Android, under PojavLauncher or one of the + * launchers built from it. + * + * Those report {@code os.name} as plain Linux, so the question is asked of + * what they cannot hide: the environment PojavLauncher sets up for its JVM, + * and the system partition every Android device has and no desktop does. + * Each check is only a lookup, and one that is refused counts as no. + */ + public static boolean android() { + try { + if (System.getenv("POJAV_NATIVEDIR") != null || System.getenv("POJAV_RENDERER") != null) { + return true; + } + } catch (SecurityException ignored) { + // Not allowed to look is not evidence either way. + } + try { + return new java.io.File("/system/build.prop").isFile() + || new java.io.File("/system/bin/app_process").exists(); + } catch (SecurityException ignored) { + return false; + } + } + /** * The processor this build has no Vulkan half for, or null when it does. * diff --git a/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java b/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java index e2d876d..12eea9b 100644 --- a/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java +++ b/src/main/java/net/vulkanmodnext/core/VulkanCorePlugin.java @@ -102,10 +102,10 @@ public List getMixinConfigs() { // vanilla. That works, and it is a great deal of transformed bytecode // to arrive at the picture the game would have drawn anyway — on the // one class of device where the game can least afford it. - String architecture = Platform.unsupportedArchitecture(); - if (architecture != null) { + String platform = Platform.unsupportedPlatform(); + if (platform != null) { System.out.println("[VulkanModNext] This mod carries its Vulkan renderer for 64-bit x86" - + " only and this machine is " + architecture + ", so the renderer is not" + + " desktops only and this machine is " + platform + ", so the renderer is not" + " loaded and the world is drawn the way it always was. The settings and the" + " game-side optimisations stay available."); return configs; From d67d71ab22b8cf6a51fe93593382799b7bb60398 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:15:06 +0000 Subject: [PATCH 6/6] keep a phone playable when the Vulkan half is off On Android the render distance keeps vanilla's ceiling instead of 64, the background frame cap no longer trusts the launcher's emulated focus, the HDR handler stops querying the frame texture when nothing can want it, and the fallback names the platform before the Java version. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XgyQ6udAHEtvknGZJzyhre --- .../java/net/vulkanmodnext/VulkanLoader.java | 18 +++++++++--------- .../client/BackgroundThrottle.java | 10 +++++++++- .../net/vulkanmodnext/client/HdrFrame.java | 7 +++++++ .../client/RenderDistanceLimit.java | 19 ++++++++++++++++++- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/vulkanmodnext/VulkanLoader.java b/src/main/java/net/vulkanmodnext/VulkanLoader.java index aa11f33..c2263a7 100644 --- a/src/main/java/net/vulkanmodnext/VulkanLoader.java +++ b/src/main/java/net/vulkanmodnext/VulkanLoader.java @@ -158,6 +158,15 @@ public static synchronized VulkanBridge bridge() { if (Boolean.getBoolean("vulkanmodnext.forceFallback")) { throw new VulkanUnavailableException("Vulkan renderer switched off by vulkanmodnext.forceFallback"); } + String platform = net.vulkanmodnext.core.Platform.unsupportedPlatform(); + if (platform != null) { + throw new VulkanUnavailableException("This mod ships its Vulkan half with native" + + " libraries for 64-bit x86 desktops only, and this machine is " + platform + ". Nothing" + + " here can load, so the game renders on OpenGL as it always did." + + " Phones and tablets running Minecraft Java through a translation layer" + + " land here: the world is drawn by that layer, and everything this mod" + + " does besides the renderer still works."); + } int java = javaFeatureVersion(); int ceiling = maxSupportedJava(); if (java > ceiling) { @@ -169,15 +178,6 @@ public static synchronized VulkanBridge bridge() { + " -Dvulkanmodnext.javaCeiling=" + java + " — what decides this is the JNI version" + " rather than the Java one, and it moves far more rarely."); } - String platform = net.vulkanmodnext.core.Platform.unsupportedPlatform(); - if (platform != null) { - throw new VulkanUnavailableException("This mod ships its Vulkan half with native" - + " libraries for 64-bit x86 desktops only, and this machine is " + platform + ". Nothing" - + " here can load, so the game renders on OpenGL as it always did." - + " Phones and tablets running Minecraft Java through a translation layer" - + " land here: the world is drawn by that layer, and everything this mod" - + " does besides the renderer still works."); - } reserveStackSpace(); try { URLClassLoader loader = new IsolatingLoader(collectUrls(), VulkanLoader.class.getClassLoader()); diff --git a/src/main/java/net/vulkanmodnext/client/BackgroundThrottle.java b/src/main/java/net/vulkanmodnext/client/BackgroundThrottle.java index 29df5db..86e7fb5 100644 --- a/src/main/java/net/vulkanmodnext/client/BackgroundThrottle.java +++ b/src/main/java/net/vulkanmodnext/client/BackgroundThrottle.java @@ -31,10 +31,18 @@ private BackgroundThrottle() { */ private static boolean published; + /** + * Off on Android. There the "window" is a surface the launcher's shim + * reports focus for, and a focus that reads as lost while the game is on + * screen would hold it at the background cap for the whole session. The + * system already stops drawing an app that is not in front. + */ + private static final boolean ANDROID = net.vulkanmodnext.core.Platform.android(); + /** Called at the end of every rendered frame. */ public static void afterFrame() { int limit = VulkanConfig.getBackgroundFpsLimit(); - boolean throttling = limit > 0 && !Display.isActive(); + boolean throttling = limit > 0 && !ANDROID && !Display.isActive(); if (throttling != published) { published = throttling; System.setProperty("vulkanmodnext.frameThrottled", Boolean.toString(throttling)); diff --git a/src/main/java/net/vulkanmodnext/client/HdrFrame.java b/src/main/java/net/vulkanmodnext/client/HdrFrame.java index 8614efa..5826ecc 100644 --- a/src/main/java/net/vulkanmodnext/client/HdrFrame.java +++ b/src/main/java/net/vulkanmodnext/client/HdrFrame.java @@ -120,6 +120,13 @@ static void ensure() { return; } boolean want = VulkanConfig.isHdrFrame() && !refused && resolveWillRun(); + if (!want && !active) { + // Nothing asked for and nothing of ours to undo: the frame is the + // game's own eight bits. Not asking the driver keeps a texture + // query out of translation layers (gl4es on a phone) that answer + // it poorly, on the machines where this can never be wanted. + return; + } boolean is; if (frame.framebufferTexture == knownTexture && frame.framebufferTextureWidth == knownWidth diff --git a/src/main/java/net/vulkanmodnext/client/RenderDistanceLimit.java b/src/main/java/net/vulkanmodnext/client/RenderDistanceLimit.java index dffa0d8..591cc36 100644 --- a/src/main/java/net/vulkanmodnext/client/RenderDistanceLimit.java +++ b/src/main/java/net/vulkanmodnext/client/RenderDistanceLimit.java @@ -44,10 +44,25 @@ public final class RenderDistanceLimit { /** What the switch unlocks. */ public static final int EXTREME = 128; + /** + * Phones keep vanilla's own ceiling. The grid above is allocated whatever + * draws it, and on Android there is no Vulkan renderer to draw it at all — + * only a translation layer and a heap of a gigabyte or two, where a slider + * that reaches 64 is a slider that reaches an out-of-memory crash. + */ + private static final boolean ANDROID = net.vulkanmodnext.core.Platform.android(); + private RenderDistanceLimit() { } public static int max() { + if (ANDROID) { + // Vanilla's own rule, restated: 32 on a 64-bit JVM with a heap of + // a gigabyte or more, 16 otherwise. apply() leaves the game's + // slider alone here, so this is the same answer it already has. + return Runtime.getRuntime().maxMemory() >= 1000000000L + && System.getProperty("os.arch", "").contains("64") ? 32 : 16; + } return VulkanConfig.isExtremeRenderDistance() ? EXTREME : NORMAL; } @@ -57,7 +72,9 @@ public static int max() { */ public static void apply() { int max = max(); - GameSettings.Options.RENDER_DISTANCE.setValueMax(max); + if (!ANDROID) { + GameSettings.Options.RENDER_DISTANCE.setValueMax(max); + } Minecraft mc = Minecraft.getMinecraft(); if (mc == null || mc.gameSettings == null) { return;