Work around SDL Metal fence bugs and pump during present (#2191)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-02 21:28:29 -07:00
1 parent 38b1d9a0c3
commit 1587a12cd7
27 files changed
+1262 -220

No files matched your search

@@ -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);
}
}
@@ -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() {}
}
@@ -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());
}
}
@@ -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() {
@@ -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;
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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;
@@ -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();
}
}
}
@@ -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<PersistentMapping> 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) {
@@ -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);
}
@@ -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();
@@ -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;
}
}
@@ -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<DeferredUpload> queue = new ConcurrentLinkedQueue<>();
private final ArrayDeque<DeferredUpload> 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<PendingFree> FREE_POOL = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<PendingFree> pendingFrees = new ConcurrentLinkedQueue<>();
private final ArrayDeque<PendingFree> freePool = new ArrayDeque<>();
private final ArrayDeque<PendingFree> 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<StagingReadUpload> POOL = new ConcurrentLinkedQueue<>();
private static final ArrayDeque<StagingReadUpload> 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<PreCopiedUpload> POOL = new ConcurrentLinkedQueue<>();
private static final ArrayDeque<PreCopiedUpload> 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<GpuCopyUpload> POOL = new ConcurrentLinkedQueue<>();
private static final ArrayDeque<GpuCopyUpload> 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<TextureRegionUpload> POOL = new ConcurrentLinkedQueue<>();
private static final ArrayDeque<TextureRegionUpload> 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; }
@@ -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();
}
@@ -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);
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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)));
}
}
@@ -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<Runnable> 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));
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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");