diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPump.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPump.java index c0508a4d0..afe2c1cef 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPump.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPump.java @@ -34,6 +34,11 @@ public final class MainThreadPump { private Throwable failure; private ClassLoader callerLoader; + private boolean presentPumpPending; + private boolean presentInFlight; + private Throwable presentFailure; + private ClassLoader presentLoader; + public MainThreadPump(Executor mainExecutor, BooleanSupplier isMainThread, Runnable pumpBody, Runnable pollBody) { this.mainExecutor = mainExecutor; this.isMainThread = isMainThread; @@ -48,7 +53,11 @@ public final class MainThreadPump { pumpBody.run(); } else { synchronized (lock) { - runPumpOnMainThread(); + await(true); + final Throwable t = presentFailure; + presentFailure = null; + if (t != null) throw asUnchecked(t); + if (!presentInFlight) runPumpOnMainThread(); } } pollBody.run(); @@ -60,47 +69,86 @@ public final class MainThreadPump { private void runPumpOnMainThread() { final Thread self = Thread.currentThread(); callerLoader = self.getContextClassLoader(); - failure = null; done = false; mainExecutor.execute(task); - boolean interrupted = false; - boolean stallReported = false; - final long stallAt = System.nanoTime() + STALL_WARN_NANOS; - while (!done) { - try { - lock.wait(PARK_MILLIS); - } catch (InterruptedException e) { - interrupted = true; - } - if (!done && !stallReported && System.nanoTime() - stallAt >= 0L) { - stallReported = true; - LOGGER.warn("Display message pump has been waiting on the RFB main thread for over {} ms", STALL_WARN_NANOS / 1_000_000L); - } - } - if (interrupted) self.interrupt(); + await(false); final Throwable t = failure; if (t != null) { throw asUnchecked(t); } } + private boolean waiting(boolean presentPump) { + return presentPump ? presentPumpPending : !done; + } + + private void await(boolean presentPump) { + boolean interrupted = false; + boolean stallReported = false; + final long stallAt = System.nanoTime() + STALL_WARN_NANOS; + while (waiting(presentPump)) { + try { + lock.wait(PARK_MILLIS); + } catch (InterruptedException e) { + interrupted = true; + } + if (waiting(presentPump) && !stallReported && System.nanoTime() - stallAt >= 0L) { + stallReported = true; + LOGGER.warn("Display message pump has been waiting on the RFB main thread for over {} ms", STALL_WARN_NANOS / 1_000_000L); + } + } + if (interrupted) Thread.currentThread().interrupt(); + } + + public void beginPresent() { + synchronized (lock) { + presentInFlight = true; + presentPumpPending = true; + presentLoader = Thread.currentThread().getContextClassLoader(); + } + } + + public void runPresentPump() { + final ClassLoader loader; + synchronized (lock) { + loader = presentLoader; + } + final Throwable t = pumpWith(loader); + synchronized (lock) { + if (t != null) presentFailure = t; + presentPumpPending = false; + lock.notifyAll(); + } + } + + public void endPresent() { + synchronized (lock) { + presentInFlight = false; + presentPumpPending = false; + lock.notifyAll(); + } + } + private void runPump() { + final Throwable t = pumpWith(callerLoader); + synchronized (lock) { + failure = t; + done = true; + lock.notifyAll(); + } + } + + private Throwable pumpWith(ClassLoader loader) { final Thread self = Thread.currentThread(); final ClassLoader saved = self.getContextClassLoader(); try { - self.setContextClassLoader(callerLoader); + if (loader != null) self.setContextClassLoader(loader); pumpBody.run(); + return null; } catch (Throwable t) { - failure = t; + return t; } finally { - try { - self.setContextClassLoader(saved); - } finally { - synchronized (lock) { - done = true; - lock.notifyAll(); - } - } + self.setContextClassLoader(saved); } } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java new file mode 100644 index 000000000..2336453d2 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/profiling/DebugCounters.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.glsm.profiling; + +import java.util.concurrent.atomic.LongAdder; + +public final class DebugCounters { + public static final LongAdder FENCE_WAIT_NANOS = new LongAdder(); + public static final LongAdder TRANSFER_WAIT_NANOS = new LongAdder(); + public static final LongAdder PRESENTS = new LongAdder(); + public static final LongAdder EMPTY_ACQUIRES = new LongAdder(); + + private DebugCounters() {} +} diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPumpTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPumpTest.java index 8434ff575..2355611b8 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPumpTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/backend/MainThreadPumpTest.java @@ -150,4 +150,75 @@ class MainThreadPumpTest { caller.join(); assertEquals(1, stillInterrupted.get()); } + + @Test + void aClientPumpWaitsForAPendingPresentPumpWithoutEnqueueing() throws Exception { + final DeferredExecutor executor = new DeferredExecutor(); + final AtomicInteger pumps = new AtomicInteger(); + final AtomicInteger polls = new AtomicInteger(); + final MainThreadPump p = pump(executor, pumps::incrementAndGet, polls::incrementAndGet); + + p.beginPresent(); + final Thread caller = new Thread(p::pumpMessages, "pump-caller"); + caller.start(); + awaitWaiting(caller); + assertTrue(executor.tasks.isEmpty(), "no pump may queue behind an in-flight present"); + + p.runPresentPump(); + caller.join(); + assertEquals(1, pumps.get()); + assertEquals(1, polls.get()); + assertTrue(executor.tasks.isEmpty()); + } + + @Test + void aClientPumpDuringAPresentAfterItsPumpReturnsImmediatelyAndStillPolls() { + final DeferredExecutor executor = new DeferredExecutor(); + final AtomicInteger pumps = new AtomicInteger(); + final AtomicInteger polls = new AtomicInteger(); + final MainThreadPump p = pump(executor, pumps::incrementAndGet, polls::incrementAndGet); + + p.beginPresent(); + p.runPresentPump(); + p.pumpMessages(); + p.pumpMessages(); + + assertTrue(executor.tasks.isEmpty()); + assertEquals(1, pumps.get()); + assertEquals(2, polls.get()); + } + + @Test + void afterEndPresentAClientPumpUsesTheMainThreadAgain() { + final InlineExecutor executor = new InlineExecutor(); + final AtomicInteger pumps = new AtomicInteger(); + final MainThreadPump p = pump(executor, pumps::incrementAndGet, () -> {}); + + p.beginPresent(); + p.runPresentPump(); + p.endPresent(); + p.pumpMessages(); + + assertEquals(1, executor.submitted.size()); + assertEquals(2, pumps.get()); + } + + @Test + void aPresentPumpFailureIsThrownToTheNextClientPumpExactlyOnce() { + final InlineExecutor executor = new InlineExecutor(); + final AtomicInteger pumps = new AtomicInteger(); + final MainThreadPump p = pump(executor, () -> { + if (pumps.incrementAndGet() == 1) throw new IllegalStateException("present boom"); + }, () -> {}); + + p.beginPresent(); + p.runPresentPump(); + final IllegalStateException thrown = assertThrows(IllegalStateException.class, p::pumpMessages); + assertEquals("present boom", thrown.getMessage()); + + p.pumpMessages(); + p.endPresent(); + p.pumpMessages(); + assertEquals(2, pumps.get()); + } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java index c00e3451d..4de47fc3a 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java @@ -62,6 +62,7 @@ import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -143,6 +144,7 @@ public class SDLGPURenderBackend extends RenderBackend { private final FenceTracker fenceTracker = new FenceTracker(device, frameManager); + private final LongArrayFIFOQueue frameSyncs = new LongArrayFIFOQueue(); private final FBOClearTracker fboClearTracker = new FBOClearTracker(frameManager, resourceManager, shaderManager); private final SamplerBinder samplerBinder = new SamplerBinder(device, resourceManager, shaderManager); private final StorageTextureBinder storageTextureBinder = new StorageTextureBinder(resourceManager, shaderManager); @@ -331,7 +333,7 @@ public class SDLGPURenderBackend extends RenderBackend { fenceTracker.setUnresolvedFenceFlush(this::midFrameFenceFlush); frameManager.setMainThread(GLStateManager.getMainThread()); if (!SystemProperties.DISABLE_SDL_PRESENTER_THREAD) { - presenter = new Presenter(frameManager, MainStartOnFirstThread.instance()); + presenter = new Presenter(frameManager, MainStartOnFirstThread.instance(), Pump.INSTANCE); frameManager.setPresenter(presenter); LOG.info("SDL presenter thread enabled: presents run on '{}'", device.getWindowThread() != null ? device.getWindowThread().getName() : "window thread"); } @@ -488,6 +490,7 @@ public class SDLGPURenderBackend extends RenderBackend { SDL_WaitForGPUIdle(dev); frameManager.endFrame(); readbackShadows.dispose(); + frameSyncs.clear(); fenceTracker.dispose(); frameManager.releaseAllRegisteredFrames(); resourceManager.shutdownTexSamplerStates(); @@ -714,7 +717,7 @@ public class SDLGPURenderBackend extends RenderBackend { private void drainDeferredPersistentRegions(ContextState st) { if (!st.deferUploads) return; st.drawsSincePersistentDrain = 0; - persistentSync.enqueueDirtyPersistentRegions(); + if (persistentSync.enqueueDirtyPersistentRegions() > 0) wakeTransferThread(); } private void awaitUploadFlush() { @@ -733,8 +736,8 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void onFrameEnd() { if (shutdown) return; - requestUploadFlush(); endFrameUploadFlushNoWait(); + requestUploadFlush(); final FrameState f = frameManager.frame(); if (f.frameActive && !f.fbo0UsedThisFrame) { @@ -748,9 +751,19 @@ public class SDLGPURenderBackend extends RenderBackend { if (f.frameActive) fboClearTracker.materializeAllPendingClears(s()); awaitUploadFlush(); if (SystemProperties.FFP_TRACE) ffpTrace.frameEnd(f); + final long frameSync = f.frameActive ? fenceTracker.fenceSync() : 0L; frameManager.endFrame(); frameManager.presentFinalTarget(); fenceTracker.resolvePendingFences(); + device.fenceReleaser().drain(); + if (frameSync != 0L) { + frameSyncs.enqueue(frameSync); + while (frameSyncs.size() > Device.MAX_FRAMES_IN_FLIGHT - 1) { + final long sync = frameSyncs.dequeueLong(); + fenceTracker.clientWaitSync(sync, 0, Long.MAX_VALUE); + fenceTracker.deleteSync(sync); + } + } resourceManager.flushDeferredReleases(); resourceManager.recycleGpuBufferPool(frameManager.getFrameNumber()); } @@ -1777,34 +1790,12 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void samplerParameteri(int sampler, int pname, int param) { if (sampler == 0) return; final TextureSamplerState ss = resourceManager.getOrCreateSamplerObject(sampler); - switch (pname) { - case GL11.GL_TEXTURE_MIN_FILTER -> ss.minFilter = param; - case GL11.GL_TEXTURE_MAG_FILTER -> ss.magFilter = param; - case GL11.GL_TEXTURE_WRAP_S -> ss.wrapS = param; - case GL11.GL_TEXTURE_WRAP_T -> ss.wrapT = param; - case GL12.GL_TEXTURE_WRAP_R -> ss.wrapR = param; - case GL12.GL_TEXTURE_MIN_LOD -> ss.minLod = (float) param; - case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = (float) param; - case GL14.GL_TEXTURE_COMPARE_MODE -> ss.compareMode = param; - case GL14.GL_TEXTURE_COMPARE_FUNC -> ss.compareFunc = param; - case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param; - default -> { return; } - } - ss.sdlSampler = 0; - s().samplerBindGen++; + if (ss.invalidate(ss.seti(pname, param))) s().samplerBindGen++; } @Override public void samplerParameterf(int sampler, int pname, float param) { if (sampler == 0) return; final TextureSamplerState ss = resourceManager.getOrCreateSamplerObject(sampler); - switch (pname) { - case GL12.GL_TEXTURE_MIN_LOD -> ss.minLod = param; - case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = param; - case GL14.GL_TEXTURE_LOD_BIAS -> ss.lodBias = param; - case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param; - default -> { samplerParameteri(sampler, pname, (int) param); return; } - } - ss.sdlSampler = 0; - s().samplerBindGen++; + if (ss.invalidate(ss.setf(pname, param))) s().samplerBindGen++; } @Override public int genFramebuffers() { diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java index ddb25cd78..dd87d8967 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java @@ -44,6 +44,9 @@ public final class Device { private boolean claimed; private int supportedShaderFormats; private String driverName; + private boolean fencePollEnabled; + private boolean fenceQueryInverted; + private final FenceReleaser fenceReleaser = new FenceReleaser(fence -> device == 0 || FenceWait.isSignaled(this, fence), fence -> { if (device != 0) SDL_ReleaseGPUFence(device, fence); }); private String deviceName; private String driverVersion; private String driverInfo; @@ -84,8 +87,12 @@ public final class Device { return userHint.isEmpty() && windows ? "vulkan" : null; } + static boolean isMetal(String driverName) { + return "metal".equalsIgnoreCase(driverName); + } + static boolean metalNeedsNewerSdl(String driverName, int sdlVersion) { - return "metal".equalsIgnoreCase(driverName) && sdlVersion < SDLVersion.SDL_VERSIONNUM(3, 4, 6); + return isMetal(driverName) && sdlVersion < SDLVersion.SDL_VERSIONNUM(3, 4, 6); } public boolean createDevice() { @@ -129,9 +136,21 @@ public final class Device { return false; } + probeFences(ver); + return true; } + private void probeFences(int ver) { + fencePollEnabled = FenceWait.pollEnabled(driverName, ver); + final boolean expectInverted = isMetal(driverName) && ver == SDLVersion.SDL_VERSIONNUM(3, 4, 14); + fenceQueryInverted = FenceProbe.queryInverted(device, driverName, expectInverted); + LOG.info("SDL {}.{}.{} driver={} fenceQueryInverted={} fencePoll={}", SDLVersion.SDL_VERSIONNUM_MAJOR(ver), SDLVersion.SDL_VERSIONNUM_MINOR(ver), SDLVersion.SDL_VERSIONNUM_MICRO(ver), driverName, fenceQueryInverted, fencePollEnabled); + if (fenceQueryInverted != expectInverted) { + LOG.warn("Fence query probe disagrees with the SDL version table: inverted={}, expected {}", fenceQueryInverted, expectInverted); + } + } + private long createGPUDevice(boolean gpuDebug, String forcedDriver) { long dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, forcedDriver); if (dev == 0 && forcedDriver != null) { @@ -204,9 +223,12 @@ public final class Device { if (window != 0) SDL_ReleaseWindowFromGPUDevice(device, window); claimed = false; } + fenceReleaser.releaseAll(); SDL_DestroyGPUDevice(device); device = 0; driverName = null; + fencePollEnabled = false; + fenceQueryInverted = false; deviceName = null; driverVersion = null; driverInfo = null; @@ -268,6 +290,18 @@ public final class Device { return driverName; } + public boolean isFencePollEnabled() { + return fencePollEnabled; + } + + public boolean isFenceQueryInverted() { + return fenceQueryInverted; + } + + public FenceReleaser fenceReleaser() { + return fenceReleaser; + } + public Thread getWindowThread() { return windowThread; } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbe.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbe.java new file mode 100644 index 000000000..2a1170af6 --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbe.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.sdl.SDLError; +import org.lwjgl.system.MemoryStack; + +import static org.lwjgl.sdl.SDLGPU.SDL_AcquireGPUCommandBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_QueryGPUFence; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUFence; +import static org.lwjgl.sdl.SDLGPU.SDL_WaitForGPUFences; + +final class FenceProbe { + private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU"); + + private FenceProbe() {} + + static boolean queryInverted(long dev, String driverName, boolean onFailure) { + final long cb = SDL_AcquireGPUCommandBuffer(dev); + if (cb == 0) { + LOG.warn("Fence probe: SDL_AcquireGPUCommandBuffer failed: {}; using the SDL version table ({})", SDLError.SDL_GetError(), onFailure); + return onFailure; + } + final long fence = Submits.submitAndAcquireFence(cb); + if (fence == 0) { + LOG.warn("Fence probe: submit and acquire fence failed: {}; using the SDL version table ({})", SDLError.SDL_GetError(), onFailure); + return onFailure; + } + final boolean waitOk; + final boolean rawSignaled; + try (MemoryStack stack = MemoryStack.stackPush()) { + waitOk = SDL_WaitForGPUFences(dev, true, stack.pointers(fence)); + rawSignaled = SDL_QueryGPUFence(dev, fence); + } finally { + SDL_ReleaseGPUFence(dev, fence); + } + final boolean metal = Device.isMetal(driverName); + if (waitOk && !rawSignaled && !metal) { + LOG.error("SDL_QueryGPUFence reports a waited fence as busy on driver {}; not inverting the query outside Metal", driverName); + } + return metal && waitOk && !rawSignaled; + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaser.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaser.java new file mode 100644 index 000000000..a0beec0bc --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaser.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import it.unimi.dsi.fastutil.longs.LongArrayList; + +import java.util.function.LongConsumer; +import java.util.function.LongPredicate; + +public final class FenceReleaser { + private final LongPredicate signaled; + private final LongConsumer releaser; + private final LongArrayList pending = new LongArrayList(); + + FenceReleaser(LongPredicate signaled, LongConsumer releaser) { + this.signaled = signaled; + this.releaser = releaser; + } + + public synchronized void release(long fence) { + if (fence == 0) return; + if (signaled.test(fence)) { + releaser.accept(fence); + } else { + pending.add(fence); + } + } + + public synchronized void drain() { + for (int i = pending.size() - 1; i >= 0; i--) { + final long fence = pending.getLong(i); + if (!signaled.test(fence)) continue; + releaser.accept(fence); + final long last = pending.popLong(); + if (i < pending.size()) pending.set(i, last); + } + } + + public synchronized void releaseAll() { + for (int i = 0; i < pending.size(); i++) { + releaser.accept(pending.getLong(i)); + } + pending.clear(); + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWait.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWait.java new file mode 100644 index 000000000..c95ec3733 --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWait.java @@ -0,0 +1,94 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; +import org.lwjgl.system.MemoryStack; + +import java.util.concurrent.locks.LockSupport; + +import static org.lwjgl.sdl.SDLGPU.SDL_QueryGPUFence; +import static org.lwjgl.sdl.SDLGPU.SDL_WaitForGPUFences; +import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM; + +// SDL < 3.4.14 METAL_WaitForFences busy-spins on the completion flag; poll + park there instead. +public final class FenceWait { + public static final long FOREVER = Long.MAX_VALUE; + + static final long SPIN_NS = 20_000L; + static final long PARK_MAX_NS = 50_000L; + + interface Ops { + boolean query(long dev, long fence); + void waitNative(long dev, long fence); + long nanoTime(); + void park(long nanos); + } + + private static final class NativeOps implements Ops { + private final boolean inverted; + + NativeOps(boolean inverted) { this.inverted = inverted; } + + @Override public boolean query(long dev, long fence) { return SDL_QueryGPUFence(dev, fence) != inverted; } + + @Override public void waitNative(long dev, long fence) { + try (MemoryStack stack = MemoryStack.stackPush()) { + SDL_WaitForGPUFences(dev, true, stack.pointers(fence)); + } + } + + @Override public long nanoTime() { return System.nanoTime(); } + @Override public void park(long nanos) { LockSupport.parkNanos(nanos); } + } + + private static final Ops NATIVE = new NativeOps(false); + private static final Ops NATIVE_INVERTED = new NativeOps(true); + + private FenceWait() {} + + private static Ops ops(Device device) { + return device.isFenceQueryInverted() ? NATIVE_INVERTED : NATIVE; + } + + public static boolean isSignaled(Device device, long fence) { + return ops(device).query(device.getDevice(), fence); + } + + public static boolean await(Device device, long fence, long timeoutNs) { + final long start = System.nanoTime(); + final boolean signaled = await(device.getDevice(), fence, timeoutNs, device.isFencePollEnabled(), ops(device)); + DebugCounters.FENCE_WAIT_NANOS.add(System.nanoTime() - start); + return signaled; + } + + static boolean pollEnabled(String driverName, int sdlVersion) { + return Device.isMetal(driverName) && sdlVersion < SDL_VERSIONNUM(3, 4, 14); + } + + // timeoutNs < 0 is GL_TIMEOUT_IGNORED read as signed; treated as forever + static boolean await(long dev, long fence, long timeoutNs, boolean pollForever, Ops ops) { + if (ops.query(dev, fence)) return true; + if (timeoutNs == 0) return false; + final boolean forever = timeoutNs < 0 || timeoutNs == FOREVER; + if (forever && !pollForever) { + ops.waitNative(dev, fence); + return true; + } + boolean interrupted = false; + try { + final long start = ops.nanoTime(); + while (true) { + final long elapsed = ops.nanoTime() - start; + if (ops.query(dev, fence)) return true; + if (!forever && elapsed >= timeoutNs) return false; + if (elapsed < SPIN_NS) { + Thread.onSpinWait(); + continue; + } + if (Thread.interrupted()) interrupted = true; + ops.park(forever ? PARK_MAX_NS : Math.min(PARK_MAX_NS, timeoutNs - elapsed)); + } + } finally { + if (interrupted) Thread.currentThread().interrupt(); + } + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Submits.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Submits.java new file mode 100644 index 000000000..4be1e1720 --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Submits.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import static org.lwjgl.sdl.SDLGPU.SDL_SubmitGPUCommandBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_SubmitGPUCommandBufferAndAcquireFence; + +public final class Submits { + private static final Object LOCK = new Object(); + + private Submits() {} + + public static boolean submit(long commandBuffer) { + synchronized (LOCK) { + return SDL_SubmitGPUCommandBuffer(commandBuffer); + } + } + + public static long submitAndAcquireFence(long commandBuffer) { + synchronized (LOCK) { + return SDL_SubmitGPUCommandBufferAndAcquireFence(commandBuffer); + } + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FenceTracker.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FenceTracker.java index c9e42ee04..df134b033 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FenceTracker.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FenceTracker.java @@ -2,17 +2,14 @@ package com.gtnewhorizons.angelica.sdlgpu.frame; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; import com.gtnewhorizons.angelica.sdlgpu.device.Device; +import com.gtnewhorizons.angelica.sdlgpu.device.FenceWait; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArrayList; -import org.lwjgl.PointerBuffer; import org.lwjgl.opengl.GL32; -import org.lwjgl.system.MemoryStack; - -import static org.lwjgl.sdl.SDLGPU.*; public final class FenceTracker { private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU"); @@ -87,16 +84,16 @@ public final class FenceTracker { fence = flushForUnresolved(sync); if (fence == 0) return GL32.GL_TIMEOUT_EXPIRED; } - if (SDL_QueryGPUFence(device.getDevice(), fence)) return GL32.GL_ALREADY_SIGNALED; + if (FenceWait.isSignaled(device, fence)) return GL32.GL_ALREADY_SIGNALED; if (timeout == 0) return GL32.GL_TIMEOUT_EXPIRED; + final boolean signaled; Tracy.beginZone(Z_SDL_FENCE_WAIT); - try (var stack = MemoryStack.stackPush()) { - final PointerBuffer fences = stack.mallocPointer(1).put(0, fence); - SDL_WaitForGPUFences(device.getDevice(), true, fences); + try { + signaled = FenceWait.await(device, fence, timeout); } finally { Tracy.endZone(); } - return GL32.GL_CONDITION_SATISFIED; + return signaled ? GL32.GL_CONDITION_SATISFIED : GL32.GL_TIMEOUT_EXPIRED; } public void deleteSync(long sync) { @@ -105,15 +102,14 @@ public final class FenceTracker { final int newCount = fenceRefcounts.addTo(fence, -1) - 1; if (newCount <= 0) { fenceRefcounts.remove(fence); - SDL_ReleaseGPUFence(device.getDevice(), fence); + device.fenceReleaser().release(fence); } } public void dispose() { - final long dev = device.getDevice(); - if (dev != 0) { + if (device.getDevice() != 0) { for (long fence : fenceRefcounts.keySet()) { - SDL_ReleaseGPUFence(dev, fence); + device.fenceReleaser().release(fence); } } fenceRefcounts.clear(); @@ -125,7 +121,7 @@ public final class FenceTracker { if (!fenceMap.containsKey(sync)) return true; final long fence = fenceMap.get(sync); if (fence == 0) return false; - return SDL_QueryGPUFence(device.getDevice(), fence); + return FenceWait.isSignaled(device, fence); } public int getSyncStatus(long sync) { @@ -139,11 +135,9 @@ public final class FenceTracker { flushForUnresolved(sync); return; } - if (SDL_QueryGPUFence(device.getDevice(), fence)) return; Tracy.beginZone(Z_SDL_FENCE_WAIT); - try (var stack = MemoryStack.stackPush()) { - final PointerBuffer fences = stack.mallocPointer(1).put(0, fence); - SDL_WaitForGPUFences(device.getDevice(), true, fences); + try { + FenceWait.await(device, fence, FenceWait.FOREVER); } finally { Tracy.endZone(); } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java index 906be6b5f..7ecef04fd 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java @@ -2,8 +2,11 @@ package com.gtnewhorizons.angelica.sdlgpu.frame; import com.gtnewhorizons.angelica.config.SystemProperties; import com.gtnewhorizons.angelica.glsm.backend.BackendManager; +import com.gtnewhorizons.angelica.glsm.backend.VSyncMode; +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; import com.gtnewhorizons.angelica.sdlgpu.device.Device; +import com.gtnewhorizons.angelica.sdlgpu.device.Submits; import com.gtnewhorizons.angelica.sdlgpu.resource.FboState; import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess; @@ -584,14 +587,7 @@ public final class FrameManager { if (f.commandBuffer != 0) { Tracy.beginZone(Z_SDL_SUBMIT); try { - if (f.wantFenceOnNextSubmit) { - f.wantFenceOnNextSubmit = false; - if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence); - f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.commandBuffer); - if (f.lastAcquiredFence == 0) { - device.reportGpuFailure("submit+acquireFence GPU command buffer"); - } - } else if (!SDL_SubmitGPUCommandBuffer(f.commandBuffer)) { + if (!submitTracked(f, f.commandBuffer)) { device.reportGpuFailure("submit GPU command buffer"); } } finally { @@ -658,14 +654,7 @@ public final class FrameManager { endRenderPassIfActive(f, PASS_END_FRAME_END); Tracy.beginZone(Z_SDL_SUBMIT); try { - if (f.wantFenceOnNextSubmit) { - f.wantFenceOnNextSubmit = false; - if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence); - f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.commandBuffer); - if (f.lastAcquiredFence == 0) { - device.reportGpuFailure("mid-frame submit+acquireFence"); - } - } else if (!SDL_SubmitGPUCommandBuffer(f.commandBuffer)) { + if (!submitTracked(f, f.commandBuffer)) { device.reportGpuFailure("mid-frame submit"); } } finally { @@ -737,6 +726,14 @@ public final class FrameManager { flushPendingUploadCommandBuffer(frame()); } + private boolean submitTracked(FrameState f, long cb) { + if (!f.wantFenceOnNextSubmit) return Submits.submit(cb); + f.wantFenceOnNextSubmit = false; + if (f.lastAcquiredFence != 0) device.fenceReleaser().release(f.lastAcquiredFence); + f.lastAcquiredFence = Submits.submitAndAcquireFence(cb); + return f.lastAcquiredFence != 0; + } + private void flushPendingUploadCommandBuffer(FrameState f) { final boolean sync = f.syncOnNextFlush; if (f.pendingUploadCommandBuffer == 0) { @@ -753,14 +750,7 @@ public final class FrameManager { Tracy.beginZone(Z_SDL_SUBMIT); final boolean submitted; try { - if (f.wantFenceOnNextSubmit) { - f.wantFenceOnNextSubmit = false; - if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence); - f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.pendingUploadCommandBuffer); - submitted = f.lastAcquiredFence != 0; - } else { - submitted = SDL_SubmitGPUCommandBuffer(f.pendingUploadCommandBuffer); - } + submitted = submitTracked(f, f.pendingUploadCommandBuffer); } finally { Tracy.endZone(); } @@ -907,7 +897,7 @@ public final class FrameManager { f.uboPushViewSource.clear(); if (f.lastAcquiredFence != 0) { - SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence); + device.fenceReleaser().release(f.lastAcquiredFence); f.lastAcquiredFence = 0; } } @@ -947,7 +937,11 @@ public final class FrameManager { final Presenter p = presenter; if (p != null) { f.presentedThisFrame = true; - p.requestPresent(srcTexture, srcW, srcH, flipMode); + if (!splash && BackendManager.RENDER_BACKEND.getEffectiveVSyncMode() == VSyncMode.OFF) { + if (!p.tryRequestPresent(srcTexture, srcW, srcH, flipMode)) notePresentSkip(f); + } else { + p.requestPresent(srcTexture, srcW, srcH, flipMode); + } } else { presentBlit(f, srcTexture, srcW, srcH, flipMode); } @@ -1027,7 +1021,7 @@ public final class FrameManager { final IntBuffer pHeight = stack.ints(0); Tracy.beginZone(Z_SDL_ACQUIRE_WAIT); try { - callOk = SDL_AcquireGPUSwapchainTexture(cb, window, pTexture, pWidth, pHeight); + callOk = device.isFenceQueryInverted() ? SDL_WaitAndAcquireGPUSwapchainTexture(cb, window, pTexture, pWidth, pHeight) : SDL_AcquireGPUSwapchainTexture(cb, window, pTexture, pWidth, pHeight); } finally { Tracy.endZone(); } @@ -1042,6 +1036,7 @@ public final class FrameManager { final long tEnd = System.nanoTime(); if (!callOk || tex == 0) { + if (callOk) DebugCounters.EMPTY_ACQUIRES.increment(); SDL_CancelGPUCommandBuffer(cb); f.swapchainUnavailable = true; notePresentSkip(f); @@ -1060,10 +1055,11 @@ public final class FrameManager { nSDL_BlitGPUTexture(cb, info); } - if (!SDL_SubmitGPUCommandBuffer(cb)) { + if (!Submits.submit(cb)) { device.reportGpuFailure("submit present blit"); return false; } + DebugCounters.PRESENTS.increment(); f.presentedThisFrame = true; if (afterPresent != null) afterPresent.run(); return true; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/Presenter.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/Presenter.java index 054b2eb7c..c5e90e8a0 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/Presenter.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/Presenter.java @@ -1,5 +1,7 @@ package com.gtnewhorizons.angelica.sdlgpu.frame; +import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump; + import java.util.concurrent.Executor; import java.util.concurrent.Semaphore; @@ -7,6 +9,7 @@ public final class Presenter { private final FrameManager frameManager; private final Executor windowThreadExecutor; + private final MainThreadPump pump; private final Semaphore pending = new Semaphore(1); private final Runnable task; @@ -15,13 +18,16 @@ public final class Presenter { private int srcH; private int flipMode; - public Presenter(FrameManager frameManager, Executor windowThreadExecutor) { + public Presenter(FrameManager frameManager, Executor windowThreadExecutor, MainThreadPump pump) { this.frameManager = frameManager; this.windowThreadExecutor = windowThreadExecutor; + this.pump = pump; this.task = () -> { try { + pump.runPresentPump(); frameManager.presentOnWindowThread(srcTexture, srcW, srcH, flipMode); } finally { + pump.endPresent(); pending.release(); } }; @@ -29,16 +35,30 @@ public final class Presenter { public void requestPresent(long srcTexture, int srcW, int srcH, int flipMode) { pending.acquireUninterruptibly(); + submit(srcTexture, srcW, srcH, flipMode); + } + + public boolean tryRequestPresent(long srcTexture, int srcW, int srcH, int flipMode) { + if (!pending.tryAcquire()) return false; + submit(srcTexture, srcW, srcH, flipMode); + return true; + } + + private void submit(long srcTexture, int srcW, int srcH, int flipMode) { this.srcTexture = srcTexture; this.srcW = srcW; this.srcH = srcH; this.flipMode = flipMode; + pump.beginPresent(); boolean submitted = false; try { windowThreadExecutor.execute(task); submitted = true; } finally { - if (!submitted) pending.release(); + if (!submitted) { + pump.endPresent(); + pending.release(); + } } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PersistentBufferSync.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PersistentBufferSync.java index 9df5ce985..2a0188b80 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PersistentBufferSync.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PersistentBufferSync.java @@ -41,11 +41,11 @@ public final class PersistentBufferSync { } public void uploadDirtyPersistentRegion() { processDirtyPersistentRegions(false); } - public void enqueueDirtyPersistentRegions() { processDirtyPersistentRegions(true); } + public int enqueueDirtyPersistentRegions() { return processDirtyPersistentRegions(true); } - private void processDirtyPersistentRegions(boolean defer) { - if (!resourceManager.hasDirtyPersistentRegions()) return; - if (!defer && frameManager.getCommandBuffer() == 0) return; + private int processDirtyPersistentRegions(boolean defer) { + if (!resourceManager.hasDirtyPersistentRegions()) return 0; + if (!defer && frameManager.getCommandBuffer() == 0) return 0; final Snapshot snap = snapshotTL.get(); final IntArrayList keys = snap.keys; final ArrayList vals = snap.vals; @@ -55,6 +55,7 @@ public final class PersistentBufferSync { snap.version = version; } final int n = keys.size(); + int enqueued = 0; for (int i = 0; i < n; i++) { final PersistentMapping pm = vals.get(i); if (!pm.isDirty()) continue; @@ -69,6 +70,7 @@ public final class PersistentBufferSync { final long seq = sink.nextSeq(); pm.lastEnqueuedSeq = seq; sink.enqueue(TransferThread.StagingReadUpload.acquire(pm.staging, off, size, gpuHandle, off, seq, false)); + enqueued++; } else { pm.staging.position((int) off).limit((int) (off + size)); resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), pm.staging, gpuHandle, off, false); @@ -76,6 +78,7 @@ public final class PersistentBufferSync { } resourceManager.clearPersistentDirty(); } + return enqueued; } public static void mirrorPersistentCopy(ByteBuffer srcStaging, long srcOffset, ByteBuffer dstStaging, long dstOffset, long size) { diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java index bad446614..1466de118 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java @@ -4,7 +4,7 @@ import java.util.function.LongConsumer; import com.gtnewhorizons.angelica.sdlgpu.pipeline.Hashing; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.PointerBuffer; +import com.gtnewhorizons.angelica.sdlgpu.device.FenceWait; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; @@ -47,6 +47,7 @@ import static org.lwjgl.system.MemoryUtil.memFree; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; import com.gtnewhorizons.angelica.glsm.texture.TextureStaging; import com.gtnewhorizons.angelica.sdlgpu.device.Device; +import com.gtnewhorizons.angelica.sdlgpu.device.Submits; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import com.gtnewhorizons.angelica.sdlgpu.pipeline.EBOSplitScanner; @@ -1561,7 +1562,7 @@ public final class ResourceManager { f.arenaCopyPass = SDL_BeginGPUCopyPass(f.arenaCommandBuffer); if (f.arenaCopyPass == 0) { LOG.error("Failed to begin arena copy pass: {}", SDLError.SDL_GetError()); - SDL_SubmitGPUCommandBuffer(f.arenaCommandBuffer); + Submits.submit(f.arenaCommandBuffer); f.arenaCommandBuffer = 0; unmapArena(f); return false; @@ -1589,7 +1590,7 @@ public final class ResourceManager { f.arenaCopyPass = 0; } if (f.arenaCommandBuffer != 0) { - if (!SDL_SubmitGPUCommandBuffer(f.arenaCommandBuffer)) { + if (!Submits.submit(f.arenaCommandBuffer)) { device.reportGpuFailure("submit arena upload command buffer"); } f.arenaCommandBuffer = 0; @@ -1884,17 +1885,16 @@ public final class ResourceManager { SDL_DownloadFromGPUTexture(copyPass, src, dst); SDL_EndGPUCopyPass(copyPass); - final long fence = SDL_SubmitGPUCommandBufferAndAcquireFence(commandBuffer); + final long fence = Submits.submitAndAcquireFence(commandBuffer); if (fence == 0) { LOG.error("Failed to submit download command buffer: {}", SDLError.SDL_GetError()); SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer); return; } try { - final PointerBuffer fences = stack.pointers(fence); - SDL_WaitForGPUFences(device.getDevice(), true, fences); + FenceWait.await(device, fence, FenceWait.FOREVER); } finally { - SDL_ReleaseGPUFence(device.getDevice(), fence); + device.fenceReleaser().release(fence); } final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false); @@ -1937,17 +1937,16 @@ public final class ResourceManager { SDL_DownloadFromGPUBuffer(copyPass, src, dst); SDL_EndGPUCopyPass(copyPass); - final long fence = SDL_SubmitGPUCommandBufferAndAcquireFence(commandBuffer); + final long fence = Submits.submitAndAcquireFence(commandBuffer); if (fence == 0) { LOG.error("Failed to submit buffer download command buffer: {}", SDLError.SDL_GetError()); SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer); return; } try { - final PointerBuffer fences = stack.pointers(fence); - SDL_WaitForGPUFences(device.getDevice(), true, fences); + FenceWait.await(device, fence, FenceWait.FOREVER); } finally { - SDL_ReleaseGPUFence(device.getDevice(), fence); + device.fenceReleaser().release(fence); } final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false); @@ -2124,7 +2123,7 @@ public final class ResourceManager { SDL_UploadToGPUTexture(copyPass, src, dst, false); SDL_EndGPUCopyPass(copyPass); } - SDL_SubmitGPUCommandBuffer(cb); + Submits.submit(cb); } releaseTransferBuffer(xfer); } @@ -2151,7 +2150,7 @@ public final class ResourceManager { SDL_UploadToGPUBuffer(copyPass, src, dst, false); SDL_EndGPUCopyPass(copyPass); } - SDL_SubmitGPUCommandBuffer(cb); + Submits.submit(cb); } releaseTransferBuffer(xfer); } @@ -2264,7 +2263,7 @@ public final class ResourceManager { final var dst = SDL_GPUBufferRegion.calloc(stack).buffer(dummyVertexBuffer).offset(0).size(DUMMY_VBO_SIZE); SDL_UploadToGPUBuffer(copyPass, src, dst, false); SDL_EndGPUCopyPass(copyPass); - SDL_SubmitGPUCommandBuffer(cb); + Submits.submit(cb); } releaseTransferBufferHandle(xfer); } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java index 63af862a2..c51afca9e 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java @@ -1,15 +1,13 @@ package com.gtnewhorizons.angelica.sdlgpu.resource; import com.gtnewhorizons.angelica.sdlgpu.device.Device; +import com.gtnewhorizons.angelica.sdlgpu.device.Submits; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.opengl.EXTTextureFilterAnisotropic; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.lwjgl.opengl.GL14; import org.lwjgl.sdl.SDLError; import org.lwjgl.sdl.SDL_GPUBlitInfo; import org.lwjgl.sdl.SDL_GPUTextureLocation; @@ -70,38 +68,13 @@ public final class TextureOps { public void texParameteri(ContextState st, int pname, int param, int glId) { if (glId == 0) return; final TextureSamplerState ss = resourceManager.getOrCreateTexSamplerState(glId); - switch (pname) { - case GL11.GL_TEXTURE_MIN_FILTER -> ss.minFilter = param; - case GL11.GL_TEXTURE_MAG_FILTER -> ss.magFilter = param; - case GL11.GL_TEXTURE_WRAP_S -> ss.wrapS = param; - case GL11.GL_TEXTURE_WRAP_T -> ss.wrapT = param; - case GL12.GL_TEXTURE_WRAP_R -> ss.wrapR = param; - case GL12.GL_TEXTURE_MAX_LEVEL -> { - ss.maxLevel = param; return; - } - case GL12.GL_TEXTURE_MIN_LOD -> ss.minLod = (float) param; - case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = (float) param; - case GL14.GL_TEXTURE_COMPARE_MODE -> ss.compareMode = param; - case GL14.GL_TEXTURE_COMPARE_FUNC -> ss.compareFunc = param; - case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param; - default -> { return; } - } - ss.sdlSampler = 0; - st.samplerBindGen++; + if (ss.invalidate(ss.seti(pname, param))) st.samplerBindGen++; } public void texParameterf(ContextState st, int pname, float param, int glId) { if (glId == 0) return; final TextureSamplerState ss = resourceManager.getOrCreateTexSamplerState(glId); - switch (pname) { - case GL12.GL_TEXTURE_MIN_LOD -> ss.minLod = param; - case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = param; - case GL14.GL_TEXTURE_LOD_BIAS -> ss.lodBias = param; - case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param; - default -> { texParameteri(st, pname, (int) param, glId); return; } - } - ss.sdlSampler = 0; - st.samplerBindGen++; + if (ss.invalidate(ss.setf(pname, param))) st.samplerBindGen++; } /// requires a flush + submit to not read stale data @@ -291,7 +264,7 @@ public final class TextureOps { SDL_GenerateMipmapsForGPUTexture(uploadCb, handle); } - if (!SDL_SubmitGPUCommandBuffer(uploadCb)) { + if (!Submits.submit(uploadCb)) { device.reportGpuFailure("submit mipmap generation CB"); } frameManager.noteMipGenSubmit(); diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java index eb4b801e8..935804942 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java @@ -1,6 +1,9 @@ package com.gtnewhorizons.angelica.sdlgpu.resource; +import org.lwjgl.opengl.EXTTextureFilterAnisotropic; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL14; public final class TextureSamplerState { public int minFilter = GL11.GL_NEAREST_MIPMAP_LINEAR; @@ -16,4 +19,37 @@ public final class TextureSamplerState { public int compareMode = 0; public int compareFunc = GL11.GL_LEQUAL; public long sdlSampler; + + public boolean invalidate(boolean changed) { + if (!changed && sdlSampler != 0) return false; + sdlSampler = 0; + return true; + } + + public boolean seti(int pname, int param) { + switch (pname) { + case GL11.GL_TEXTURE_MIN_FILTER -> { if (minFilter == param) return false; minFilter = param; } + case GL11.GL_TEXTURE_MAG_FILTER -> { if (magFilter == param) return false; magFilter = param; } + case GL11.GL_TEXTURE_WRAP_S -> { if (wrapS == param) return false; wrapS = param; } + case GL11.GL_TEXTURE_WRAP_T -> { if (wrapT == param) return false; wrapT = param; } + case GL12.GL_TEXTURE_WRAP_R -> { if (wrapR == param) return false; wrapR = param; } + case GL12.GL_TEXTURE_MAX_LEVEL -> { maxLevel = param; return false; } + case GL14.GL_TEXTURE_COMPARE_MODE -> { if (compareMode == param) return false; compareMode = param; } + case GL14.GL_TEXTURE_COMPARE_FUNC -> { if (compareFunc == param) return false; compareFunc = param; } + case GL12.GL_TEXTURE_MIN_LOD, GL12.GL_TEXTURE_MAX_LOD, EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> { return setf(pname, (float) param); } + default -> { return false; } + } + return true; + } + + public boolean setf(int pname, float param) { + switch (pname) { + case GL12.GL_TEXTURE_MIN_LOD -> { if (minLod == param) return false; minLod = param; } + case GL12.GL_TEXTURE_MAX_LOD -> { if (maxLod == param) return false; maxLod = param; } + case GL14.GL_TEXTURE_LOD_BIAS -> { if (lodBias == param) return false; lodBias = param; } + case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> { if (maxAnisotropy == param) return false; maxAnisotropy = param; } + default -> { return seti(pname, (int) param); } + } + return true; + } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThread.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThread.java index 51ddc15cb..3227b089b 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThread.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThread.java @@ -10,13 +10,15 @@ import org.lwjgl.sdl.SDL_GPUTransferBufferLocation; import org.lwjgl.sdl.SDLError; import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.ArrayDeque; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.LockSupport; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend; import com.gtnewhorizons.angelica.sdlgpu.device.Device; +import com.gtnewhorizons.angelica.sdlgpu.device.Submits; import com.gtnewhorizons.angelica.sdlgpu.device.GpuDeviceLostException; import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import it.unimi.dsi.fastutil.longs.LongArrayList; @@ -44,7 +46,7 @@ public final class TransferThread { private final Device device; private final ResourceManager resourceManager; private final Thread thread; - private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + private final ArrayDeque queue = new ArrayDeque<>(); private volatile long submittedSeq; private volatile long flushRequestedSeq; private volatile boolean shutdown; @@ -71,6 +73,7 @@ public final class TransferThread { private static final long FLUSH_COALESCE_BYTES = 8L * 1024 * 1024; private static final int FLUSH_COALESCE_COMMANDS = 128; private static final long WAKE_COALESCE_ENQUEUES = 16; + private static final long AWAIT_SPIN_NS = 30_000L; private long openCB; private long openCopyPass; @@ -87,8 +90,8 @@ public final class TransferThread { private final SDL_GPUTextureTransferInfo texXferInfo = SDL_GPUTextureTransferInfo.calloc(); private final SDL_GPUTextureRegion texRegion = SDL_GPUTextureRegion.calloc(); - private static final ConcurrentLinkedQueue FREE_POOL = new ConcurrentLinkedQueue<>(); - private final ConcurrentLinkedQueue pendingFrees = new ConcurrentLinkedQueue<>(); + private final ArrayDeque freePool = new ArrayDeque<>(); + private final ArrayDeque pendingFrees = new ArrayDeque<>(); public TransferThread(Device device, ResourceManager resourceManager) { this.device = device; @@ -103,10 +106,18 @@ public final class TransferThread { public long getSubmittedSeq() { return submittedSeq; } public void enqueue(DeferredUpload upload) { - queue.offer(upload); + synchronized (queue) { queue.addLast(upload); } if (shouldWakeOnEnqueue(upload.seq())) wake(); } + private DeferredUpload pollUpload() { + synchronized (queue) { return queue.pollFirst(); } + } + + private boolean queueEmpty() { + synchronized (queue) { return queue.isEmpty(); } + } + static boolean shouldWakeOnEnqueue(long seq) { return (seq & (WAKE_COALESCE_ENQUEUES - 1)) == 0; } @@ -123,32 +134,38 @@ public final class TransferThread { public void freeAfterSeq(ByteBuffer buf, long seq) { if (buf == null) return; if (seq <= submittedSeq) { MemoryUtil.memFree(buf); return; } - PendingFree pf = FREE_POOL.poll(); - if (pf == null) pf = new PendingFree(); - pf.seq = seq; - pf.buf = buf; - pendingFrees.offer(pf); + synchronized (pendingFrees) { + PendingFree pf = freePool.pollLast(); + if (pf == null) pf = new PendingFree(); + pf.seq = seq; + pf.buf = buf; + pendingFrees.addLast(pf); + } wake(); } private void drainDuePendingFrees() { final long sub = submittedSeq; - PendingFree head = pendingFrees.peek(); - while (head != null && head.seq <= sub) { - pendingFrees.poll(); - MemoryUtil.memFree(head.buf); - head.buf = null; - FREE_POOL.offer(head); - head = pendingFrees.peek(); + synchronized (pendingFrees) { + PendingFree head = pendingFrees.peekFirst(); + while (head != null && head.seq <= sub) { + pendingFrees.pollFirst(); + MemoryUtil.memFree(head.buf); + head.buf = null; + freePool.addLast(head); + head = pendingFrees.peekFirst(); + } } } private void drainAllPendingFreesOnShutdown() { - PendingFree pf; - while ((pf = pendingFrees.poll()) != null) { - MemoryUtil.memFree(pf.buf); - pf.buf = null; - FREE_POOL.offer(pf); + synchronized (pendingFrees) { + PendingFree pf; + while ((pf = pendingFrees.pollFirst()) != null) { + MemoryUtil.memFree(pf.buf); + pf.buf = null; + freePool.addLast(pf); + } } } @@ -165,19 +182,31 @@ public final class TransferThread { if (seq > flushRequestedSeq) flushRequestedSeq = seq; LockSupport.unpark(thread); final long startNs = System.nanoTime(); - synchronized (submittedLock) { - while (submittedSeq < seq) { - try { - submittedLock.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; + if (!spinUntilSubmitted(seq)) { + synchronized (submittedLock) { + while (submittedSeq < seq) { + try { + submittedLock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } } } } final long waitNs = System.nanoTime() - startNs; if (waitNs > maxWaitNs) maxWaitNs = waitNs; pendingFrameWaitNanos += waitNs; + DebugCounters.TRANSFER_WAIT_NANOS.add(waitNs); + } + + private boolean spinUntilSubmitted(long seq) { + final long start = System.nanoTime(); + while (submittedSeq < seq) { + if (System.nanoTime() - start >= AWAIT_SPIN_NS) return false; + Thread.onSpinWait(); + } + return true; } public void shutdown() { @@ -234,11 +263,11 @@ public final class TransferThread { while (!shutdown) { final long workStart = Tracy.ENABLED ? System.nanoTime() : 0L; // Drain all pending uploads - DeferredUpload upload = queue.poll(); + DeferredUpload upload = pollUpload(); if (upload != null) { do { processOne(upload); - upload = queue.poll(); + upload = pollUpload(); } while (upload != null); } @@ -251,9 +280,9 @@ public final class TransferThread { if (Tracy.ENABLED) pendingActiveNanos += System.nanoTime() - workStart; - if (queue.isEmpty() && !shutdown) { + if (queueEmpty() && !shutdown) { parked = true; - if (queue.isEmpty() && !shutdown && flushRequestedSeq <= submittedSeq) { + if (queueEmpty() && !shutdown && flushRequestedSeq <= submittedSeq) { LockSupport.parkNanos(1_000_000L); // 1ms } parked = false; @@ -405,7 +434,7 @@ public final class TransferThread { if (openCB == 0) return; SDL_EndGPUCopyPass(openCopyPass); - if (!SDL_SubmitGPUCommandBuffer(openCB)) { + if (!Submits.submit(openCB)) { device.reportGpuFailure("submit transfer command buffer"); } @@ -459,7 +488,7 @@ public final class TransferThread { } public static final class StagingReadUpload implements DeferredUpload { - private static final ConcurrentLinkedQueue POOL = new ConcurrentLinkedQueue<>(); + private static final ArrayDeque POOL = new ArrayDeque<>(); public ByteBuffer stagingBuffer; public long srcOffset; @@ -471,8 +500,8 @@ public final class TransferThread { private StagingReadUpload() {} - public static StagingReadUpload acquire(ByteBuffer stagingBuffer, long srcOffset, long size, long dstGpuBuffer, long dstOffset, long seq, boolean cycle) { - StagingReadUpload obj = POOL.poll(); + public static synchronized StagingReadUpload acquire(ByteBuffer stagingBuffer, long srcOffset, long size, long dstGpuBuffer, long dstOffset, long seq, boolean cycle) { + StagingReadUpload obj = POOL.pollLast(); if (obj == null) obj = new StagingReadUpload(); obj.stagingBuffer = stagingBuffer; obj.srcOffset = srcOffset; @@ -484,16 +513,16 @@ public final class TransferThread { return obj; } - public static void release(StagingReadUpload obj) { + public static synchronized void release(StagingReadUpload obj) { obj.stagingBuffer = null; - POOL.offer(obj); + POOL.addLast(obj); } @Override public long seq() { return seq; } } public static final class PreCopiedUpload implements DeferredUpload { - private static final ConcurrentLinkedQueue POOL = new ConcurrentLinkedQueue<>(); + private static final ArrayDeque POOL = new ArrayDeque<>(); public long transferBuffer; public long size; @@ -504,8 +533,8 @@ public final class TransferThread { private PreCopiedUpload() {} - public static PreCopiedUpload acquire(long transferBuffer, long size, long dstGpuBuffer, long dstOffset, long seq, boolean cycle) { - PreCopiedUpload obj = POOL.poll(); + public static synchronized PreCopiedUpload acquire(long transferBuffer, long size, long dstGpuBuffer, long dstOffset, long seq, boolean cycle) { + PreCopiedUpload obj = POOL.pollLast(); if (obj == null) obj = new PreCopiedUpload(); obj.transferBuffer = transferBuffer; obj.size = size; @@ -516,15 +545,15 @@ public final class TransferThread { return obj; } - public static void release(PreCopiedUpload obj) { - POOL.offer(obj); + public static synchronized void release(PreCopiedUpload obj) { + POOL.addLast(obj); } @Override public long seq() { return seq; } } public static final class GpuCopyUpload implements DeferredUpload { - private static final ConcurrentLinkedQueue POOL = new ConcurrentLinkedQueue<>(); + private static final ArrayDeque POOL = new ArrayDeque<>(); public long srcHandle; public long dstHandle; @@ -535,9 +564,9 @@ public final class TransferThread { private GpuCopyUpload() {} - public static GpuCopyUpload acquire(long srcHandle, long dstHandle, long readOffset, + public static synchronized GpuCopyUpload acquire(long srcHandle, long dstHandle, long readOffset, long writeOffset, long size, long seq) { - GpuCopyUpload obj = POOL.poll(); + GpuCopyUpload obj = POOL.pollLast(); if (obj == null) obj = new GpuCopyUpload(); obj.srcHandle = srcHandle; obj.dstHandle = dstHandle; @@ -548,15 +577,15 @@ public final class TransferThread { return obj; } - public static void release(GpuCopyUpload obj) { - POOL.offer(obj); + public static synchronized void release(GpuCopyUpload obj) { + POOL.addLast(obj); } @Override public long seq() { return seq; } } public static final class TextureRegionUpload implements DeferredUpload { - private static final ConcurrentLinkedQueue POOL = new ConcurrentLinkedQueue<>(); + private static final ArrayDeque POOL = new ArrayDeque<>(); public long transferBuffer; public long texHandle; @@ -570,8 +599,8 @@ public final class TransferThread { private TextureRegionUpload() {} - public static TextureRegionUpload acquire(long transferBuffer, long texHandle, int x, int y, int w, int h, int level, long size, long seq) { - TextureRegionUpload obj = POOL.poll(); + public static synchronized TextureRegionUpload acquire(long transferBuffer, long texHandle, int x, int y, int w, int h, int level, long size, long seq) { + TextureRegionUpload obj = POOL.pollLast(); if (obj == null) obj = new TextureRegionUpload(); obj.transferBuffer = transferBuffer; obj.texHandle = texHandle; @@ -585,8 +614,8 @@ public final class TransferThread { return obj; } - public static void release(TextureRegionUpload obj) { - POOL.offer(obj); + public static synchronized void release(TextureRegionUpload obj) { + POOL.addLast(obj); } @Override public long seq() { return seq; } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java index fd7204cb2..05458f151 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java @@ -28,6 +28,7 @@ public final class SamplerBinder { private final Device device; private final ResourceManager resourceManager; private final ShaderManager shaderManager; + private final SamplerCache.Factory samplerFactory = this::createSamplerForKey; public SamplerBinder(Device device, ResourceManager resourceManager, ShaderManager shaderManager) { this.device = device; @@ -58,7 +59,7 @@ public final class SamplerBinder { ss.minLod, ss.maxLod, ss.lodBias, ss.maxAnisotropy, ss.compareMode, ss.compareFunc); - final long handle = resourceManager.samplerCache().getOrCreate(key, this::createSamplerForKey); + final long handle = resourceManager.samplerCache().getOrCreate(key, samplerFactory); ss.sdlSampler = handle; return handle != 0 ? handle : resourceManager.getOrCreateDefaultSampler(); } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java index d1f90ba78..9e97acc5c 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java @@ -14,6 +14,7 @@ import java.lang.reflect.Field; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUDevice; import static org.lwjgl.sdl.SDLGPU.SDL_DestroyGPUDevice; +import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUDeviceDriver; import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUShaderFormats; import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_DXBC; import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_DXIL; @@ -21,6 +22,7 @@ import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_MSL; import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_SPIRV; import static org.lwjgl.sdl.SDLGPU.SDL_WaitForGPUIdle; import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO; +import static org.lwjgl.sdl.SDLVersion.SDL_GetVersion; public final class SdlTestRig { @@ -39,6 +41,8 @@ public final class SdlTestRig { final Field formatsField = Device.class.getDeclaredField("supportedShaderFormats"); formatsField.setAccessible(true); formatsField.setInt(this.device, SDL_GetGPUShaderFormats(sdlHandle)); + Reflect.set(this.device, "driverName", SDL_GetGPUDeviceDriver(sdlHandle)); + Reflect.invoke(this.device, "probeFences", new Class[] { int.class }, SDL_GetVersion()); } this.frameManager = new FrameManager(device); this.resourceManager = new ResourceManager(device, frameManager); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java new file mode 100644 index 000000000..f038c4733 --- /dev/null +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java @@ -0,0 +1,50 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.lwjgl.system.MemoryStack; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.lwjgl.sdl.SDLGPU.SDL_AcquireGPUCommandBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUDeviceDriver; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUFence; +import static org.lwjgl.sdl.SDLGPU.SDL_SubmitGPUCommandBufferAndAcquireFence; +import static org.lwjgl.sdl.SDLGPU.SDL_WaitForGPUFences; +import static org.lwjgl.sdl.SDLVersion.SDL_GetVersion; +import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM; + +class FenceProbeRealDeviceTest { + + private static SdlTestRig rig; + + @BeforeAll + static void setUp() throws Exception { + rig = SdlTestRig.acquireRealDevice(); + } + + @AfterAll + static void tearDown() { + SdlTestRig.releaseRealDevice(); + } + + @Test + void probeMatchesVersionTableAndCorrectedQueryReadsSignaled() { + final String driver = SDL_GetGPUDeviceDriver(rig.sdlHandle); + assertEquals("metal".equalsIgnoreCase(driver) && SDL_GetVersion() == SDL_VERSIONNUM(3, 4, 14), rig.device.isFenceQueryInverted(), "driver=" + driver + " sdl=" + SDL_GetVersion()); + + final long cb = SDL_AcquireGPUCommandBuffer(rig.sdlHandle); + assertNotEquals(0L, cb); + final long fence = SDL_SubmitGPUCommandBufferAndAcquireFence(cb); + assertNotEquals(0L, fence); + try (MemoryStack stack = MemoryStack.stackPush()) { + assertTrue(SDL_WaitForGPUFences(rig.sdlHandle, true, stack.pointers(fence))); + assertTrue(FenceWait.isSignaled(rig.device, fence)); + } finally { + SDL_ReleaseGPUFence(rig.sdlHandle, fence); + } + } +} diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaserTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaserTest.java new file mode 100644 index 000000000..a63a46c7c --- /dev/null +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceReleaserTest.java @@ -0,0 +1,88 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FenceReleaserTest { + + private static final class FakeOps { + final LongOpenHashSet signaled = new LongOpenHashSet(); + final LongArrayList released = new LongArrayList(); + + FenceReleaser releaser() { return new FenceReleaser(signaled::contains, released::add); } + } + + @Test + void signaledFenceIsReleasedImmediately() { + final FakeOps ops = new FakeOps(); + final FenceReleaser r = ops.releaser(); + ops.signaled.add(7L); + r.release(7L); + assertEquals(LongArrayList.of(7L), ops.released); + } + + @Test + void zeroFenceIsIgnored() { + final FakeOps ops = new FakeOps(); + final FenceReleaser r = ops.releaser(); + r.release(0L); + r.releaseAll(); + assertTrue(ops.released.isEmpty()); + } + + @Test + void unsignaledFencePendsUntilDrainSeesItSignaled() { + final FakeOps ops = new FakeOps(); + final FenceReleaser r = ops.releaser(); + r.release(1L); + r.release(2L); + r.release(3L); + assertTrue(ops.released.isEmpty()); + r.drain(); + assertTrue(ops.released.isEmpty()); + ops.signaled.add(2L); + r.drain(); + assertEquals(LongArrayList.of(2L), ops.released); + r.drain(); + assertEquals(LongArrayList.of(2L), ops.released); + } + + @Test + void drainReleasesEverySignaledFenceExactlyOnce() { + final FakeOps ops = new FakeOps(); + final FenceReleaser r = ops.releaser(); + for (long f = 1; f <= 5; f++) r.release(f); + ops.signaled.add(1L); + ops.signaled.add(3L); + ops.signaled.add(5L); + r.drain(); + assertEquals(new LongOpenHashSet(new long[]{1L, 3L, 5L}), new LongOpenHashSet(ops.released)); + assertEquals(3, ops.released.size()); + ops.signaled.add(2L); + ops.signaled.add(4L); + r.drain(); + r.drain(); + assertEquals(new LongOpenHashSet(new long[]{1L, 2L, 3L, 4L, 5L}), new LongOpenHashSet(ops.released)); + assertEquals(5, ops.released.size()); + } + + @Test + void releaseAllEmptiesPendingWithoutDoubleRelease() { + final FakeOps ops = new FakeOps(); + final FenceReleaser r = ops.releaser(); + r.release(1L); + r.release(2L); + ops.signaled.add(1L); + r.drain(); + r.releaseAll(); + assertEquals(LongArrayList.of(1L, 2L), ops.released); + ops.signaled.add(2L); + r.drain(); + r.releaseAll(); + assertEquals(LongArrayList.of(1L, 2L), ops.released); + } +} diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWaitTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWaitTest.java new file mode 100644 index 000000000..2248f6cc7 --- /dev/null +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceWaitTest.java @@ -0,0 +1,141 @@ +package com.gtnewhorizons.angelica.sdlgpu.device; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM; + +class FenceWaitTest { + + private static final class FakeOps implements FenceWait.Ops { + long now; + long signalAt = Long.MAX_VALUE; + long tick = 1_000L; + int queries; + int nativeWaits; + int parks; + long parkedNs; + long maxPark; + long firstParkAt = -1; + + @Override public boolean query(long dev, long fence) { + queries++; + return now >= signalAt; + } + + @Override public void waitNative(long dev, long fence) { + nativeWaits++; + now = Math.max(now, signalAt); + } + + @Override public long nanoTime() { + now += tick; + return now; + } + + @Override public void park(long nanos) { + assertTrue(nanos > 0, "park must never be asked for a non-positive duration"); + if (firstParkAt < 0) firstParkAt = now; + parks++; + parkedNs += nanos; + maxPark = Math.max(maxPark, nanos); + now += nanos; + } + } + + @Test + void alreadySignalledReturnsWithoutWaiting() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 0; + assertTrue(FenceWait.await(1L, 2L, FenceWait.FOREVER, true, ops)); + assertEquals(1, ops.queries); + assertEquals(0, ops.parks); + assertEquals(0, ops.nativeWaits); + } + + @Test + void zeroTimeoutIsAPureQuery() { + final FakeOps ops = new FakeOps(); + assertFalse(FenceWait.await(1L, 2L, 0L, true, ops)); + assertEquals(1, ops.queries); + assertEquals(0, ops.parks); + } + + @Test + void spinsBeforeParkingThenParksInBoundedSlices() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 500_000L; + assertTrue(FenceWait.await(1L, 2L, FenceWait.FOREVER, true, ops)); + assertTrue(ops.firstParkAt >= FenceWait.SPIN_NS, "must spin for the first " + FenceWait.SPIN_NS + "ns, parked at " + ops.firstParkAt); + assertTrue(ops.parks > 0); + assertEquals(FenceWait.PARK_MAX_NS, ops.maxPark); + assertEquals(0, ops.nativeWaits); + } + + @Test + void signalDuringSpinNeverParks() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 5_000L; + assertTrue(FenceWait.await(1L, 2L, FenceWait.FOREVER, true, ops)); + assertEquals(0, ops.parks); + } + + @Test + void finiteTimeoutExpiresWithoutOvershootingTheDeadline() { + final FakeOps ops = new FakeOps(); + final long timeout = 130_000L; + final long start = ops.now; + assertFalse(FenceWait.await(1L, 2L, timeout, true, ops)); + final long elapsed = ops.now - start; + assertTrue(elapsed >= timeout, "returned early: " + elapsed); + assertTrue(elapsed <= timeout + 2 * ops.tick, "last park must be clamped to the remaining time: " + elapsed); + } + + @Test + void finiteTimeoutPollsWhenForeverPollingIsDisabled() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 60_000L; + assertTrue(FenceWait.await(1L, 2L, 100_000L, false, ops)); + assertEquals(0, ops.nativeWaits, "a finite timeout must poll on every backend so it can expire"); + assertTrue(ops.parks > 0); + } + + @Test + void interruptFlagSurvivesAPollingWait() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 500_000L; + Thread.currentThread().interrupt(); + assertTrue(FenceWait.await(1L, 2L, FenceWait.FOREVER, true, ops)); + assertTrue(ops.parks > 0); + assertTrue(Thread.interrupted(), "the caller's interrupt must be restored"); + } + + @Test + void foreverWithoutPollingUsesTheNativeWait() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 1_000_000L; + assertTrue(FenceWait.await(1L, 2L, FenceWait.FOREVER, false, ops)); + assertEquals(1, ops.nativeWaits); + assertEquals(0, ops.parks); + } + + @Test + void timeoutIgnoredIsForever() { + final FakeOps ops = new FakeOps(); + ops.signalAt = 1_000_000L; + assertTrue(FenceWait.await(1L, 2L, -1L, false, ops)); + assertEquals(1, ops.nativeWaits); + } + + @Test + void pollingOnlyOnMetalWhileItsWaitSpins() { + assertTrue(FenceWait.pollEnabled("metal", SDL_VERSIONNUM(3, 4, 12))); + assertFalse(FenceWait.pollEnabled("metal", SDL_VERSIONNUM(3, 4, 14))); + assertFalse(FenceWait.pollEnabled("metal", SDL_VERSIONNUM(3, 5, 0))); + assertFalse(FenceWait.pollEnabled("vulkan", SDL_VERSIONNUM(3, 4, 12))); + assertFalse(FenceWait.pollEnabled("direct3d12", SDL_VERSIONNUM(3, 4, 12))); + assertFalse(FenceWait.pollEnabled(null, SDL_VERSIONNUM(3, 4, 12))); + } +} diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/frame/PresenterTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/frame/PresenterTest.java index 5e9e9d338..3b604b77f 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/frame/PresenterTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/frame/PresenterTest.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.sdlgpu.frame; +import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump; import com.gtnewhorizons.angelica.sdlgpu.device.Device; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -7,15 +8,23 @@ import org.junit.jupiter.api.Timeout; import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE; @Timeout(10) class PresenterTest { + private static MainThreadPump noPump() { + return new MainThreadPump(Runnable::run, () -> true, () -> {}, () -> {}); + } + private static final class DeferredExecutor implements Executor { final Deque tasks = new ArrayDeque<>(); @@ -30,7 +39,7 @@ class PresenterTest { } private static Presenter presenter(DeferredExecutor executor) { - return new Presenter(new FrameManager(new Device()), executor); + return new Presenter(new FrameManager(new Device()), executor, noPump()); } private static void awaitWaiting(Thread t) throws InterruptedException { @@ -90,7 +99,7 @@ class PresenterTest { @Test void anInlineExecutorReleasesTheSlotImmediately() { - final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run); + final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run, noPump()); p.requestPresent(1L, 10, 10, SDL_FLIP_NONE); p.requestPresent(2L, 10, 10, SDL_FLIP_NONE); p.drain(); @@ -100,7 +109,7 @@ class PresenterTest { void presentingDoesNotEnterTheFrameRegistry() throws Exception { final DeferredExecutor executor = new DeferredExecutor(); final FrameManager fm = new FrameManager(new Device()); - final Presenter p = new Presenter(fm, executor); + final Presenter p = new Presenter(fm, executor, noPump()); fm.frame(); final int before = fm.registeredFrameCount(); @@ -117,7 +126,7 @@ class PresenterTest { void theSplashPresentGoesThroughTheWindowThreadToo() { final DeferredExecutor executor = new DeferredExecutor(); final FrameManager fm = new FrameManager(new Device()); - fm.setPresenter(new Presenter(fm, executor)); + fm.setPresenter(new Presenter(fm, executor, noPump())); fm.frame(); final int before = fm.registeredFrameCount(); @@ -126,4 +135,94 @@ class PresenterTest { assertEquals(1, executor.tasks.size(), "the splash blit must be enqueued on the window thread"); assertEquals(before, fm.registeredFrameCount(), "the splash present must not touch the calling thread's frame"); } + + @Test + void tryRequestEnqueuesWhenIdle() { + final DeferredExecutor executor = new DeferredExecutor(); + final Presenter p = presenter(executor); + + assertTrue(p.tryRequestPresent(1L, 10, 10, SDL_FLIP_NONE)); + assertEquals(1, executor.tasks.size()); + } + + @Test + void tryRequestDropsWithoutBlockingWhileAPresentIsPending() { + final DeferredExecutor executor = new DeferredExecutor(); + final Presenter p = presenter(executor); + + p.requestPresent(1L, 10, 10, SDL_FLIP_NONE); + assertFalse(p.tryRequestPresent(2L, 10, 10, SDL_FLIP_NONE)); + assertEquals(1, executor.tasks.size(), "a dropped present must not enqueue"); + + executor.runNext(); + assertTrue(p.tryRequestPresent(3L, 10, 10, SDL_FLIP_NONE)); + assertEquals(1, executor.tasks.size()); + } + + @Test + void drainBlocksUntilAnAcceptedTryRequestRuns() throws Exception { + final DeferredExecutor executor = new DeferredExecutor(); + final Presenter p = presenter(executor); + + assertTrue(p.tryRequestPresent(1L, 10, 10, SDL_FLIP_NONE)); + + final Thread drainer = new Thread(p::drain); + drainer.start(); + awaitWaiting(drainer); + + executor.runNext(); + drainer.join(); + } + + @Test + void aBlockingRequestWaitsBehindAnAcceptedTryRequest() throws Exception { + final DeferredExecutor executor = new DeferredExecutor(); + final Presenter p = presenter(executor); + + assertTrue(p.tryRequestPresent(1L, 10, 10, SDL_FLIP_NONE)); + + final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10, SDL_FLIP_NONE)); + second.start(); + awaitWaiting(second); + assertEquals(1, executor.tasks.size()); + + executor.runNext(); + second.join(); + assertEquals(1, executor.tasks.size()); + } + + @Test + void thePresentTaskPumpsFirstAndEndsThePresentWindow() throws Exception { + final DeferredExecutor executor = new DeferredExecutor(); + final AtomicInteger pumps = new AtomicInteger(); + final MainThreadPump pump = new MainThreadPump(Runnable::run, () -> false, pumps::incrementAndGet, () -> {}); + final Presenter p = new Presenter(new FrameManager(new Device()), executor, pump); + + p.requestPresent(1L, 10, 10, SDL_FLIP_NONE); + final Thread client = new Thread(pump::pumpMessages, "client"); + client.start(); + awaitWaiting(client); + assertEquals(0, pumps.get(), "a client pump must wait for the present task's pump, not run its own"); + + executor.runNext(); + client.join(); + final int afterPresent = pumps.get(); + assertTrue(afterPresent >= 1, "the present task must pump"); + + pump.pumpMessages(); + assertEquals(afterPresent + 1, pumps.get(), "after the present ends, client pumps run normally again"); + } + + @Test + void aRejectedSubmitEndsThePresentWindowAndReleasesTheSlot() { + final AtomicInteger pumps = new AtomicInteger(); + final MainThreadPump pump = new MainThreadPump(Runnable::run, () -> false, pumps::incrementAndGet, () -> {}); + final Presenter p = new Presenter(new FrameManager(new Device()), r -> { throw new RejectedExecutionException(); }, pump); + + assertThrows(RejectedExecutionException.class, () -> p.requestPresent(1L, 10, 10, SDL_FLIP_NONE)); + + pump.pumpMessages(); + assertEquals(1, pumps.get()); + assertThrows(RejectedExecutionException.class, () -> p.tryRequestPresent(2L, 10, 10, SDL_FLIP_NONE)); + } } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerStateTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerStateTest.java new file mode 100644 index 000000000..5d1a77258 --- /dev/null +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerStateTest.java @@ -0,0 +1,52 @@ +package com.gtnewhorizons.angelica.sdlgpu.resource; + +import org.junit.jupiter.api.Test; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL14; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TextureSamplerStateTest { + + @Test + void changedIntValueReportsChange() { + final TextureSamplerState ss = new TextureSamplerState(); + assertTrue(ss.seti(GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)); + assertEquals(GL11.GL_LINEAR, ss.minFilter); + assertFalse(ss.seti(GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)); + } + + @Test + void changedFloatValueReportsChange() { + final TextureSamplerState ss = new TextureSamplerState(); + assertTrue(ss.setf(GL14.GL_TEXTURE_LOD_BIAS, 0.5f)); + assertEquals(0.5f, ss.lodBias); + assertFalse(ss.setf(GL14.GL_TEXTURE_LOD_BIAS, 0.5f)); + } + + @Test + void setfFallsThroughToIntPname() { + final TextureSamplerState ss = new TextureSamplerState(); + assertTrue(ss.setf(GL11.GL_TEXTURE_MIN_FILTER, (float) GL11.GL_LINEAR)); + assertEquals(GL11.GL_LINEAR, ss.minFilter); + assertFalse(ss.setf(GL11.GL_TEXTURE_MIN_FILTER, (float) GL11.GL_LINEAR)); + } + + @Test + void setiRoutesLodToTheFloatField() { + final TextureSamplerState ss = new TextureSamplerState(); + assertTrue(ss.seti(GL12.GL_TEXTURE_MIN_LOD, 2)); + assertEquals(2.0f, ss.minLod); + assertFalse(ss.seti(GL12.GL_TEXTURE_MIN_LOD, 2)); + } + + @Test + void maxLevelIsStoredButNeverAffectsTheSampler() { + final TextureSamplerState ss = new TextureSamplerState(); + assertFalse(ss.seti(GL12.GL_TEXTURE_MAX_LEVEL, 3)); + assertEquals(3, ss.maxLevel); + } +} diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThreadWakePolicyTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThreadWakePolicyTest.java index c874719f0..5549f2961 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThreadWakePolicyTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/TransferThreadWakePolicyTest.java @@ -1,9 +1,14 @@ package com.gtnewhorizons.angelica.sdlgpu.resource; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import org.junit.jupiter.api.Test; +import java.util.concurrent.locks.LockSupport; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class TransferThreadWakePolicyTest { @@ -43,4 +48,41 @@ class TransferThreadWakePolicyTest { } assertEquals(100, woken); } + + private static TransferThread stoppedThread(long submittedSeq) { + final SdlTestRig rig = SdlTestRig.create(); + final TransferThread tt = new TransferThread(rig.device, rig.resourceManager); + tt.shutdown(); + try { + tt.getThread().join(5_000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + assertFalse(tt.getThread().isAlive()); + Reflect.set(tt, "submittedSeq", submittedSeq); + return tt; + } + + @Test + void awaitFallsBackToTheLockWhenSpinExpires() throws InterruptedException { + final TransferThread tt = stoppedThread(5L); + final Thread waiter = new Thread(() -> tt.awaitSubmittedUpTo(10L)); + waiter.start(); + LockSupport.parkNanos(20_000_000L); + assertTrue(waiter.isAlive(), "must still be blocked; nothing published"); + Reflect.set(tt, "openHighestSeq", 10L); + Reflect.invoke(tt, "publishSubmittedSeq", new Class[0]); + waiter.join(5_000L); + assertFalse(waiter.isAlive(), "publish must release a waiter parked on submittedLock"); + } + + @Test + void releasedUploadIsReusedByTheNextAcquire() { + final TransferThread.StagingReadUpload a = TransferThread.StagingReadUpload.acquire(null, 0L, 0L, 0L, 0L, 1L, false); + TransferThread.StagingReadUpload.release(a); + final TransferThread.StagingReadUpload b = TransferThread.StagingReadUpload.acquire(null, 0L, 0L, 0L, 0L, 2L, false); + assertSame(a, b); + TransferThread.StagingReadUpload.release(b); + } } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBindGenerationTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBindGenerationTest.java index 741f4dcaf..a510e2053 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBindGenerationTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBindGenerationTest.java @@ -1,8 +1,10 @@ package com.gtnewhorizons.angelica.sdlgpu.sampler; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; +import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import org.junit.jupiter.api.Test; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; @@ -67,4 +69,36 @@ class SamplerBindGenerationTest { assertEquals(55, st.boundTextures[2], "unit 2 keeps its binding after the active unit moves away"); } + + @Test + void settingAnUnchangedTexParameterOnAResolvedSamplerDoesNotBumpTheGeneration() { + final SDLGPURenderBackend backend = new SDLGPURenderBackend(); + final ContextState st = SdlTestRig.contextState(); + final ResourceManager resourceManager = Reflect.get(backend, "resourceManager"); + + backend.activeTexture(GL13.GL_TEXTURE0); + backend.bindTexture(GL11.GL_TEXTURE_2D, 42); + backend.texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + resourceManager.getTexSamplerState(42).sdlSampler = 1L; + final int before = st.samplerBindGen; + + backend.texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + assertEquals(before, st.samplerBindGen); + + backend.texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + assertNotEquals(before, st.samplerBindGen); + } + + @Test + void theFirstTexParameterOnAStatelessTextureBumpsEvenAtTheDefaultValue() { + final SDLGPURenderBackend backend = new SDLGPURenderBackend(); + final ContextState st = SdlTestRig.contextState(); + + backend.activeTexture(GL13.GL_TEXTURE0); + backend.bindTexture(GL11.GL_TEXTURE_2D, 43); + final int before = st.samplerBindGen; + + backend.texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_LINEAR); + assertNotEquals(before, st.samplerBindGen, "creating the sampler state replaces the default sampler"); + } } diff --git a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java index 620702d87..97bf79ddf 100644 --- a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java +++ b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java @@ -8,9 +8,12 @@ import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder.StopStatus; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.backend.VSyncMode; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; +import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters; import com.gtnewhorizons.angelica.rendering.FpsReducer; import com.gtnewhorizons.angelica.rendering.FramePacer; import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer; +import com.sun.management.OperatingSystemMXBean; +import com.sun.management.ThreadMXBean; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; import net.minecraft.client.Minecraft; @@ -33,6 +36,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lwjgl.opengl.Display; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -110,11 +115,23 @@ public final class FlybyRunner { private long legSection; private long[] frameTimesNs = NO_FRAMES; + private long[] frameAllocBytes = NO_FRAMES; + private long lastAllocBytes; + private long gcCountStart; + private long gcTimeStart; + private long cpuClientStartNs; + private long cpuProcessStartNs; + private long fenceWaitStartNs; + private long transferWaitStartNs; + private long presentsStart; + private long emptyAcquiresStart; + private long allocClientStart; + private long allocProcessStart; private int frameCount; private final int[] phaseStartFrame = new int[64]; private final int[] phaseKey = new int[64]; private int phaseCount; - private boolean framesTruncated; + private int framesSeen; private long lastFrameNs; private long runStartNs; @@ -174,7 +191,7 @@ public final class FlybyRunner { this.warmupTicks = Math.max(0, warmupTicks); this.tick = 0; this.frameCount = 0; - this.framesTruncated = false; + this.framesSeen = 0; this.waitForTracy = false; this.waitForFocus = false; this.exitWhenDone = false; @@ -292,14 +309,16 @@ public final class FlybyRunner { if (event.phase != TickEvent.Phase.END || this.state != State.RUNNING) return; final long now = System.nanoTime(); + final long alloc = AllocBean.allocatedBytes(); if (this.lastFrameNs != 0L) { if (this.frameCount < this.frameTimesNs.length) { + this.frameAllocBytes[this.frameCount] = alloc >= 0L && this.lastAllocBytes >= 0L ? alloc - this.lastAllocBytes : -1L; this.frameTimesNs[this.frameCount++] = now - this.lastFrameNs; - } else { - this.framesTruncated = true; } + this.framesSeen++; } this.lastFrameNs = now; + this.lastAllocBytes = alloc; if (this.shot < SystemProperties.FLYBY_SCREENSHOTS && this.tick >= this.nextShot) { final Minecraft mc = Minecraft.getMinecraft(); @@ -493,8 +512,21 @@ public final class FlybyRunner { this.plotLeg = Tracy.plotHandle("flyby.leg"); this.plotTurning = Tracy.plotHandle("flyby.turning"); this.lastPhase = -1; - this.frameTimesNs = new long[Math.min(MAX_RECORDED_FRAMES, Math.max(1024, this.runTicks * 20))]; + this.frameTimesNs = new long[Math.min(MAX_RECORDED_FRAMES, Math.max(1024, this.runTicks * 50))]; + this.frameAllocBytes = new long[this.frameTimesNs.length]; + this.lastAllocBytes = -1L; + this.gcCountStart = gcCount(); + this.gcTimeStart = gcTimeMs(); + this.cpuClientStartNs = clientCpuNs(); + this.cpuProcessStartNs = processCpuNs(); + this.fenceWaitStartNs = DebugCounters.FENCE_WAIT_NANOS.sum(); + this.transferWaitStartNs = DebugCounters.TRANSFER_WAIT_NANOS.sum(); + this.presentsStart = DebugCounters.PRESENTS.sum(); + this.emptyAcquiresStart = DebugCounters.EMPTY_ACQUIRES.sum(); + this.allocClientStart = AllocBean.allocatedBytes(); + this.allocProcessStart = AllocBean.totalAllocatedBytes(); this.frameCount = 0; + this.framesSeen = 0; this.phaseCount = 0; this.lastFrameNs = 0L; this.runStartNs = System.nanoTime(); @@ -720,7 +752,7 @@ public final class FlybyRunner { this.legSection = 0L; Tracy.sectionLeave(this.runSection); this.runSection = 0L; - if (Tracy.ENABLED) Tracy.message("flyby end route=" + this.route.id() + " frames=" + this.frameCount); + if (Tracy.ENABLED) Tracy.message("flyby end route=" + this.route.id() + " frames=" + this.framesSeen); final String summary = this.summarise(elapsedNs, player); LOGGER.info(summary); @@ -800,13 +832,28 @@ public final class FlybyRunner { } private String summarise(long elapsedNs, EntityClientPlayerMP player) { + final long allocClientEnd = AllocBean.allocatedBytes(); + final long allocProcessEnd = AllocBean.totalAllocatedBytes(); final StringBuilder sb = new StringBuilder(192); sb.append("Flyby ").append(this.route.id()) - .append(": ").append(this.frameCount).append(" frames in ") + .append(": ").append(this.framesSeen).append(" frames in ") .append(String.format("%.2fs", elapsedNs / 1_000_000_000.0D)); - if (this.frameCount > 0) appendFrameStats(sb, 0, this.frameCount); - if (this.framesTruncated) sb.append(" (frame samples truncated)"); + if (this.frameCount > 0) { + appendFrameStats(sb, 0, this.frameCount); + appendAllocStats(sb, allocClientEnd, allocProcessEnd); + } + sb.append(", gc ").append(gcCount() - this.gcCountStart).append(" collections ").append(gcTimeMs() - this.gcTimeStart).append("ms"); + appendCpuStats(sb); + if (this.framesSeen > 0) { + sb.append(String.format(", wait fence %.4fms/frame, transfer %.4fms/frame", + (DebugCounters.FENCE_WAIT_NANOS.sum() - this.fenceWaitStartNs) / (double) this.framesSeen / 1e6D, + (DebugCounters.TRANSFER_WAIT_NANOS.sum() - this.transferWaitStartNs) / (double) this.framesSeen / 1e6D)); + sb.append(String.format(", presents %d, empty acquires %d", + DebugCounters.PRESENTS.sum() - this.presentsStart, + DebugCounters.EMPTY_ACQUIRES.sum() - this.emptyAcquiresStart)); + } + if (this.framesSeen > this.frameCount) sb.append(" (frame samples truncated)"); for (int i = 0; i < this.phaseCount; i++) { final int from = this.phaseStartFrame[i]; final int to = i + 1 < this.phaseCount ? this.phaseStartFrame[i + 1] : this.frameCount; @@ -832,6 +879,82 @@ public final class FlybyRunner { sorted[n - 1] / 1e6D)); } + private void appendAllocStats(StringBuilder sb, long clientEnd, long processEnd) { + if (this.allocClientStart < 0L || clientEnd < 0L) { + sb.append(", alloc unavailable"); + return; + } + final long total = clientEnd - this.allocClientStart; + sb.append(String.format(", alloc client avg %.0fB/frame, total %.1fMB", total / (double) this.framesSeen, total / 1048576.0D)); + final long[] sorted = Arrays.copyOf(this.frameAllocBytes, this.frameCount); + Arrays.sort(sorted); + int first = 0; + while (first < sorted.length && sorted[first] < 0L) first++; + final int n = sorted.length - first; + if (n > 0) sb.append(String.format(", p50 %dB, p99 %dB", sorted[first + n / 2], sorted[first + Math.min(n - 1, (int) (n * 0.99D))])); + if (this.allocProcessStart >= 0L && processEnd >= 0L) { + sb.append(String.format(", alloc process %.0fB/frame", (processEnd - this.allocProcessStart) / (double) this.framesSeen)); + } + } + + private void appendCpuStats(StringBuilder sb) { + final long client = clientCpuNs(); + final long process = processCpuNs(); + if (this.framesSeen == 0 || this.cpuClientStartNs < 0L || this.cpuProcessStartNs < 0L || client < 0L || process < 0L) { + sb.append(", cpu unavailable"); + return; + } + sb.append(String.format(", cpu client %.3fms/frame, process %.3fms/frame", + (client - this.cpuClientStartNs) / (double) this.framesSeen / 1e6D, + (process - this.cpuProcessStartNs) / (double) this.framesSeen / 1e6D)); + } + + private static long clientCpuNs() { + final var bean = ManagementFactory.getThreadMXBean(); + return bean.isCurrentThreadCpuTimeSupported() ? bean.getCurrentThreadCpuTime() : -1L; + } + + private static long processCpuNs() { + return ManagementFactory.getOperatingSystemMXBean() instanceof OperatingSystemMXBean os ? os.getProcessCpuTime() : -1L; + } + + private static long gcCount() { + long total = 0L; + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) total += Math.max(0L, gc.getCollectionCount()); + return total; + } + + private static long gcTimeMs() { + long total = 0L; + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) total += Math.max(0L, gc.getCollectionTime()); + return total; + } + + private static final class AllocBean { + static final ThreadMXBean BEAN; + static final boolean TOTAL; + static { + ThreadMXBean bean = null; + boolean total = false; + try { + if (ManagementFactory.getThreadMXBean() instanceof ThreadMXBean sun && sun.isThreadAllocatedMemorySupported()) { + bean = sun; + total = sun.getTotalThreadAllocatedBytes() >= 0L; + } + } catch (Throwable ignored) {} + BEAN = bean; + TOTAL = total; + } + + static long allocatedBytes() { + return BEAN == null ? -1L : BEAN.getCurrentThreadAllocatedBytes(); + } + + static long totalAllocatedBytes() { + return TOTAL ? BEAN.getTotalThreadAllocatedBytes() : -1L; + } + } + public void cancel() { if (this.isActive()) { LOGGER.info("Flyby cancelled");