mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-25 20:22:22 +00:00
Work around SDL Metal fence bugs and pump during present (#2191)
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -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) {
|
||||
|
||||
+13
-14
@@ -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();
|
||||
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+83
-54
@@ -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);
|
||||
|
||||
+50
@@ -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));
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user