diff --git a/redhorizon-async/build.gradle b/redhorizon-async/build.gradle deleted file mode 100644 index 652762c9..00000000 --- a/redhorizon-async/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -description = 'Utilities to aid with multi-threaded programming' diff --git a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/ControlledLoop.groovy b/redhorizon-async/source/nz/net/ultraq/redhorizon/async/ControlledLoop.groovy deleted file mode 100644 index 3df475b9..00000000 --- a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/ControlledLoop.groovy +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nz.net.ultraq.redhorizon.async - -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import java.util.concurrent.FutureTask -import java.util.concurrent.RunnableFuture - -/** - * A special {@link RunnableFuture} that has a loop of repeating work that can - * be queried and controlled from other objects. Useful as a {@code @Delegate} - * property in a class that needs to fulfil a {@code RunnableFuture} - * class contract. - * - * @author Emanuel Rabina - */ -class ControlledLoop implements RunnableWorker { - - private static final Logger logger = LoggerFactory.getLogger(ControlledLoop) - - @Delegate - private final FutureTask loopTask - - /** - * Constructor, build a {@link FutureTask} with a loop solely controlled by - * the task state. - * - * @param loop - */ - ControlledLoop(Closure loop) { - - this({ true }, loop) - } - - /** - * Constructor, build a {@link FutureTask} with a loop based around the given - * parameters. - * - * @param loopCondition - * @param loop - */ - ControlledLoop(Closure loopCondition, Closure loop) { - - loopTask = new FutureTask<>({ -> - try { - while (!cancelled && loopCondition()) { - loop() - } - } - catch (Throwable ex) { - logger.error("An error occurred in controlled loop \"${Thread.currentThread().name}\". Exiting...", ex) - } - }, null) - } - - @Override - boolean isStopped() { - - return isDone() - } - - @Override - void stop() { - - cancel() - } -} diff --git a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RateLimitedLoop.groovy b/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RateLimitedLoop.groovy deleted file mode 100644 index 59403549..00000000 --- a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RateLimitedLoop.groovy +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nz.net.ultraq.redhorizon.async - -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import groovy.transform.CompileStatic -import java.util.concurrent.FutureTask - -/** - * Similar to {@link ControlledLoop}, with the addition of the Thread being put - * into wait states at the end of each loop execution to not exceed a specified - * frequency. - * - * @author Emanuel Rabina - */ -@CompileStatic -class RateLimitedLoop implements RunnableWorker { - - private final Logger logger = LoggerFactory.getLogger(RateLimitedLoop) - - @Delegate - private final FutureTask loopTask - - /** - * Constructor, build a {@link FutureTask} with a rate-limited loop solely - * controlled by the task state. - - * @param frequency - * @param loop - */ - RateLimitedLoop(float frequency, Closure loop) { - - this(frequency, { true }, loop) - } - - /** - * Constructor, build a {@link FutureTask} with a rate-limited loop based - * around the given parameters. - * - * @param frequency - * @param loopCondition - * @param loop - */ - RateLimitedLoop(float frequency, Closure loopCondition, Closure loop) { - - double maxRunTimeNanos = 1000000000 / frequency - - loopTask = new FutureTask<>({ -> - try { - def lastTimeNanos = System.nanoTime() - while (!cancelled && loopCondition()) { - loop() - - // Add a wait if loop() was too quick - long executionTimeNanos = System.nanoTime() - lastTimeNanos - if (executionTimeNanos < maxRunTimeNanos) { - double diffTimeNanos = maxRunTimeNanos - executionTimeNanos - // If the amount to wait is rather large, sleep for most of that time - // (all but the remaining 5ms of the time because sleep is a minimum, - // it can take a bit to calculate all of this, and to 'wake up') - if (diffTimeNanos > 5000000) { - Thread.sleep((long) (diffTimeNanos / 1000000 - 5)) - } - // Go into a spin-lock for the remaining time - while (System.nanoTime() - lastTimeNanos < maxRunTimeNanos) { - Thread.onSpinWait() - } - } - - lastTimeNanos = System.nanoTime() - } - } - catch (Throwable ex) { - logger.error("An error occurred in rate-limited loop \"${Thread.currentThread().name}\". Exiting...", ex) - } - }, null) - } - - @Override - boolean isStopped() { - - return isDone() - } - - @Override - void stop() { - - cancel() - } -} diff --git a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RunnableWorker.groovy b/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RunnableWorker.groovy deleted file mode 100644 index f7607786..00000000 --- a/redhorizon-async/source/nz/net/ultraq/redhorizon/async/RunnableWorker.groovy +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nz.net.ultraq.redhorizon.async - -import java.util.concurrent.RunnableFuture - -/** - * A {@link Runnable} with methods more geared towards threads that perform - * repeated/looped tasks. - * - * @author Emanuel Rabina - */ -interface RunnableWorker extends RunnableFuture { - - /** - * Return whether or not this worker has been stopped. - * - * @return - */ - boolean isStopped() - - /** - * Signal to the worker to stop. - */ - void stop() -} diff --git a/redhorizon-classic/build.gradle b/redhorizon-classic/build.gradle index b7947837..418661f8 100644 --- a/redhorizon-classic/build.gradle +++ b/redhorizon-classic/build.gradle @@ -24,7 +24,6 @@ description = 'Code that bridges the classic C&C file formats for use with the R year = '2007' dependencies { - implementation project(':redhorizon-async') implementation project(':redhorizon-engine') implementation project(':redhorizon-events') implementation project(':redhorizon-filetypes') @@ -35,7 +34,6 @@ dependencies { shadowJar { archiveClassifier.set('') dependencies { - include(project(':redhorizon-async')) include(project(':redhorizon-engine')) include(project(':redhorizon-events')) include(project(':redhorizon-filetypes')) @@ -49,7 +47,6 @@ groovydoc { here, and serve as working examples for other programmers. ''') // Can't link to these until they get published on maven central themselves -// link("https://javadoc.io/doc/nz.net.ultraq.redhorizon/redhorizon-async/${version}/", 'nz.net.ultraq.redhorizon.async.') // link("https://javadoc.io/doc/nz.net.ultraq.redhorizon/redhorizon-events/${version}/", 'nz.net.ultraq.redhorizon.events.') // link("https://javadoc.io/doc/nz.net.ultraq.redhorizon/redhorizon-filetypes/${version}/", 'nz.net.ultraq.redhorizon.filetypes.') } diff --git a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/AudFile.groovy b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/AudFile.groovy index cc4ab256..cdd029e4 100644 --- a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/AudFile.groovy +++ b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/AudFile.groovy @@ -16,14 +16,14 @@ package nz.net.ultraq.redhorizon.classic.filetypes -import nz.net.ultraq.redhorizon.async.ControlledLoop import nz.net.ultraq.redhorizon.classic.codecs.IMAADPCM16bit import nz.net.ultraq.redhorizon.classic.codecs.WSADPCM8bit +import nz.net.ultraq.redhorizon.events.EventTarget import nz.net.ultraq.redhorizon.filetypes.FileExtensions import nz.net.ultraq.redhorizon.filetypes.SoundFile import nz.net.ultraq.redhorizon.filetypes.Streaming +import nz.net.ultraq.redhorizon.filetypes.StreamingDecoder import nz.net.ultraq.redhorizon.filetypes.StreamingSampleEvent -import nz.net.ultraq.redhorizon.filetypes.Worker import nz.net.ultraq.redhorizon.filetypes.io.NativeDataInputStream import org.slf4j.Logger @@ -100,7 +100,7 @@ class AudFile implements SoundFile, Streaming { return ByteBuffer.fromBuffers( Executors.newSingleThreadExecutor().executeAndShutdown { executorService -> - def worker = streamingDataWorker + def worker = streamingDecoder def samples = [] worker.on(StreamingSampleEvent) { event -> samples << event.sample @@ -114,15 +114,13 @@ class AudFile implements SoundFile, Streaming { } /** - * Returns a worker that can be run to start streaming sound data. The worker - * will emit {@link StreamingSampleEvent}s for new samples available. - * - * @return Worker for streaming sound data. + * Returns a decoder that can be run to start streaming sound data. The + * decoder will emit {@link StreamingSampleEvent}s for new samples available. */ @Override - Worker getStreamingDataWorker() { + StreamingDecoder getStreamingDecoder() { - return new AudFileWorker() + return new StreamingDecoder(new AudFileDecoder()) } @Override @@ -145,12 +143,9 @@ class AudFile implements SoundFile, Streaming { } /** - * A worker for decoding AUD file sound data. + * Decode AUD file sound data and emit as {@link StreamingSampleEvent}s. */ - class AudFileWorker extends Worker { - - @Delegate - private ControlledLoop workLoop + class AudFileDecoder implements Runnable, EventTarget { @Override void run() { @@ -162,7 +157,7 @@ class AudFile implements SoundFile, Streaming { // Decompress the aud file data by chunks def headerSize = input.bytesRead - workLoop = new ControlledLoop({ input.bytesRead < headerSize + compressedSize }, { -> + while (input.bytesRead < headerSize + compressedSize && !Thread.interrupted()) { def sample = average('Decoding sample', 1f, logger) { -> // Chunk header @@ -177,8 +172,8 @@ class AudFile implements SoundFile, Streaming { ) } trigger(new StreamingSampleEvent(sample)) - }) - workLoop.run() + Thread.sleep(20) + } logger.debug('Decoding complete') } diff --git a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/VqaFile.groovy b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/VqaFile.groovy index 7a69f971..da8f3f07 100644 --- a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/VqaFile.groovy +++ b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/VqaFile.groovy @@ -16,17 +16,17 @@ package nz.net.ultraq.redhorizon.classic.filetypes -import nz.net.ultraq.redhorizon.async.ControlledLoop import nz.net.ultraq.redhorizon.classic.codecs.IMAADPCM16bit import nz.net.ultraq.redhorizon.classic.codecs.LCW import nz.net.ultraq.redhorizon.classic.codecs.WSADPCM8bit +import nz.net.ultraq.redhorizon.events.EventTarget import nz.net.ultraq.redhorizon.filetypes.ColourFormat import nz.net.ultraq.redhorizon.filetypes.FileExtensions import nz.net.ultraq.redhorizon.filetypes.Palette +import nz.net.ultraq.redhorizon.filetypes.StreamingDecoder import nz.net.ultraq.redhorizon.filetypes.StreamingFrameEvent import nz.net.ultraq.redhorizon.filetypes.StreamingSampleEvent import nz.net.ultraq.redhorizon.filetypes.VideoFile -import nz.net.ultraq.redhorizon.filetypes.Worker import nz.net.ultraq.redhorizon.filetypes.codecs.Decoder import nz.net.ultraq.redhorizon.filetypes.io.NativeDataInputStream import static nz.net.ultraq.redhorizon.filetypes.ColourFormat.FORMAT_RGB @@ -176,7 +176,7 @@ class VqaFile implements VideoFile { return Executors.newSingleThreadExecutor().executeAndShutdown { executorService -> def frames = [] - def worker = streamingDataWorker + def worker = streamingDecoder worker.on(StreamingFrameEvent) { event -> frames << event.frame } @@ -193,7 +193,7 @@ class VqaFile implements VideoFile { return ByteBuffer.fromBuffers( Executors.newSingleThreadExecutor().executeAndShutdown { ExecutorService executorService -> def samples = [] - def worker = streamingDataWorker + def worker = streamingDecoder worker.on(StreamingSampleEvent) { event -> samples << event.sample } @@ -206,16 +206,14 @@ class VqaFile implements VideoFile { } /** - * Return a worker that can be used for streaming video. The worker will emit - * {@link StreamingFrameEvent}s for new frames, and - * {@link StreamingSampleEvent}s for new samples. - * - * @return Worker for streaming video data. + * Return a decoder that can be used for streaming video. The decoder will + * emit {@link StreamingFrameEvent}s for new frames and + * {@link StreamingSampleEvent}s for new sound samples. */ @Override - Worker getStreamingDataWorker() { + StreamingDecoder getStreamingDecoder() { - return new VqaFileWorker() + return new StreamingDecoder(new VqaFileDecoder()) } /** @@ -234,9 +232,10 @@ class VqaFile implements VideoFile { } /** - * A worker for decoding VQA file video data. + * Decode VQA file frame and sound data and emit as {@link StreamingFrameEvent}s + * and {@link StreamingSampleEvent}s respectively. */ - class VqaFileWorker extends Worker { + class VqaFileDecoder implements Runnable, EventTarget { private final LCW lcw = new LCW() private final Decoder audioDecoder @@ -247,13 +246,10 @@ class VqaFile implements VideoFile { private final int modifier private final int numBlocks - @Delegate - private ControlledLoop workLoop - /** * Constructor, create a new worker for decoding the VQA video data. */ - VqaFileWorker() { + VqaFileDecoder() { audioDecoder = bits == 16 ? new IMAADPCM16bit() : new WSADPCM8bit() @@ -370,7 +366,7 @@ class VqaFile implements VideoFile { } // Header + Offsets - workLoop = new ControlledLoop({ input.bytesRead < formLength }, { -> + while (input.bytesRead < formLength && !Thread.interrupted()) { def chunkHeader = new VqaChunkHeader(input) switch (chunkHeader.name) { @@ -439,8 +435,8 @@ class VqaFile implements VideoFile { } discardNullByte() - }) - workLoop.run() + Thread.sleep(25) + } logger.debug('Decoding complete') } diff --git a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/WsaFile.groovy b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/WsaFile.groovy index bf81912a..1b798c14 100644 --- a/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/WsaFile.groovy +++ b/redhorizon-classic/source/nz/net/ultraq/redhorizon/classic/filetypes/WsaFile.groovy @@ -16,16 +16,16 @@ package nz.net.ultraq.redhorizon.classic.filetypes -import nz.net.ultraq.redhorizon.async.ControlledLoop import nz.net.ultraq.redhorizon.classic.codecs.LCW import nz.net.ultraq.redhorizon.classic.codecs.XORDelta +import nz.net.ultraq.redhorizon.events.EventTarget import nz.net.ultraq.redhorizon.filetypes.AnimationFile import nz.net.ultraq.redhorizon.filetypes.ColourFormat import nz.net.ultraq.redhorizon.filetypes.FileExtensions import nz.net.ultraq.redhorizon.filetypes.Palette import nz.net.ultraq.redhorizon.filetypes.Streaming +import nz.net.ultraq.redhorizon.filetypes.StreamingDecoder import nz.net.ultraq.redhorizon.filetypes.StreamingFrameEvent -import nz.net.ultraq.redhorizon.filetypes.Worker import nz.net.ultraq.redhorizon.filetypes.io.NativeDataInputStream import org.slf4j.Logger @@ -118,7 +118,7 @@ class WsaFile implements AnimationFile, Streaming { return Executors.newSingleThreadExecutor().executeAndShutdown { executorService -> def frames = [] - def worker = streamingDataWorker + def worker = streamingDecoder worker.on(StreamingFrameEvent) { event -> frames << event.frame } @@ -130,15 +130,14 @@ class WsaFile implements AnimationFile, Streaming { } /** - * Return a worker that can be used for streaming the animation's frames. The - * worker will emit {@link StreamingFrameEvent}s for new frames available. - * - * @return Worker for streaming animation data. + * Return a decoder that can be used for streaming the animation's frames. + * The decoder will emit {@link StreamingFrameEvent}s for new frames + * available. */ @Override - Worker getStreamingDataWorker() { + StreamingDecoder getStreamingDecoder() { - return new WsaFileWorker() + return new StreamingDecoder(new WsaFileDecoder()) } /** @@ -156,12 +155,9 @@ class WsaFile implements AnimationFile, Streaming { } /** - * A worker for decoding WSA file frame data. + * Decode WSA file frame data and emit as {@link StreamingFrameEvent}s. */ - class WsaFileWorker extends Worker { - - @Delegate - private ControlledLoop workLoop + class WsaFileDecoder implements Runnable, EventTarget { @Override void run() { @@ -175,7 +171,7 @@ class WsaFile implements AnimationFile, Streaming { // Decode frame by frame def frame = 0 - workLoop = new ControlledLoop({ frame < numFrames }, { -> + while (frame < numFrames && !Thread.interrupted()) { def colouredFrame = average('Decoding frame', 1f, logger) { -> def indexedFrame = xorDelta.decode( lcw.decode( @@ -188,8 +184,8 @@ class WsaFile implements AnimationFile, Streaming { } trigger(new StreamingFrameEvent(colouredFrame)) frame++ - }) - workLoop.run() + Thread.sleep(50) + } logger.debug('Decoding complete') } diff --git a/redhorizon-engine/build.gradle b/redhorizon-engine/build.gradle index cf7ed5ca..e8a7820e 100644 --- a/redhorizon-engine/build.gradle +++ b/redhorizon-engine/build.gradle @@ -1,12 +1,12 @@ -/* +/* * Copyright 2016, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,7 +17,6 @@ description = 'Core rendering engine for Red Horizon' dependencies { - api project(':redhorizon-async') implementation project(':redhorizon-events') implementation project(':redhorizon-filetypes') implementation platform("org.lwjgl:lwjgl-bom:${lwjglVersion}") diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Engine.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Engine.groovy index 2937e767..6ff6a7e0 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Engine.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Engine.groovy @@ -19,6 +19,7 @@ package nz.net.ultraq.redhorizon.engine import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -66,9 +67,9 @@ class Engine { } /** - * Start the game engine. This will assign all entity systems their own - * thread to operate on the scene. This method will block until all systems - * have signalled their ready status. + * Start the game engine. This will assign all systems their own thread to + * run. This method will block until all systems have signalled their ready + * status. */ void start() { @@ -109,7 +110,7 @@ class Engine { enginesStoppingSemaphore.tryAcquireAndRelease { -> if (!engineStopped) { - systems*.stop() + systemTasks*.cancel(true) engineStopped = true logger.debug('Engine stopped') } @@ -121,7 +122,14 @@ class Engine { */ void waitUntilStopped() { - systemTasks*.get() + systemTasks.each { systemTask -> + try { + systemTask.get() + } + catch (CancellationException ignored) { + // Do nothing + } + } logger.debug('All systems stopped') } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy index a6f2d52d..df9651c5 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2023, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,12 +16,9 @@ package nz.net.ultraq.redhorizon.engine -import nz.net.ultraq.redhorizon.async.RunnableWorker import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.events.EventTarget -import groovy.transform.TupleConstructor - /** * A system provides behaviour for a component or set of components. Systems * traverse a {@link Scene}, looking for the components they work with, and then @@ -29,8 +26,31 @@ import groovy.transform.TupleConstructor * * @author Emanuel Rabina */ -@TupleConstructor(defaults = false) -abstract class EngineSystem implements EventTarget, RunnableWorker { +abstract class EngineSystem implements Runnable, EventTarget { final Scene scene + + protected EngineSystem(Scene scene) { + + this.scene = scene + } + + /** + * Execute an action and optionally wait, such that, if repeated, it would run + * no faster than the given frequency. + * + * @param frequency + * The number of times per second the action could be repeated. + * @param action + * @return + */ + protected static void rateLimit(float frequency, Closure action) { + + var maxExecTime = 1000f / frequency + var execTime = time(action) + var waitTime = maxExecTime - execTime + if (waitTime > 0) { + Thread.sleep((long)waitTime) + } + } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy index 886b22c0..918099be 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy @@ -16,7 +16,6 @@ package nz.net.ultraq.redhorizon.engine.audio -import nz.net.ultraq.redhorizon.async.RateLimitedLoop import nz.net.ultraq.redhorizon.engine.EngineSystem import nz.net.ultraq.redhorizon.engine.SystemReadyEvent import nz.net.ultraq.redhorizon.engine.SystemStoppedEvent @@ -45,11 +44,10 @@ class AudioSystem extends EngineSystem { final AudioConfiguration config // For object lifecycles + // TODO: Move to using the 'request' system from the scripting branch to remove these private final CopyOnWriteArrayList addedElements = new CopyOnWriteArrayList<>() private final CopyOnWriteArrayList removedElements = new CopyOnWriteArrayList<>() - - @Delegate - private RateLimitedLoop systemLoop + private final Set initialized = new HashSet<>() /** * Constructor, build a new engine for rendering audio. @@ -89,40 +87,47 @@ class AudioSystem extends EngineSystem { // Rendering loop logger.debug('Audio system in render loop...') - systemLoop = new RateLimitedLoop(10, { -> - - // Initialize or delete objects which have been added/removed to/from the scene - if (addedElements) { - def elementsToInit = new ArrayList(addedElements) - elementsToInit.each { elementToInit -> - elementToInit.accept { element -> - if (element instanceof AudioElement) { - element.init(renderer) + while (!Thread.interrupted()) { + try { + rateLimit(100) { -> + + // Initialize or delete objects which have been added/removed to/from the scene + if (addedElements) { + def elementsToInit = new ArrayList(addedElements) + elementsToInit.each { elementToInit -> + elementToInit.accept { element -> + if (element instanceof AudioElement) { + element.init(renderer) + initialized << element + } + } } + addedElements.removeAll(elementsToInit) } - } - addedElements.removeAll(elementsToInit) - } - if (removedElements) { - def elementsToDelete = new ArrayList(removedElements) - elementsToDelete.each { elementToInit -> - elementToInit.accept { element -> - if (element instanceof AudioElement) { - element.delete(renderer) + if (removedElements) { + def elementsToDelete = new ArrayList(removedElements) + elementsToDelete.each { elementToInit -> + elementToInit.accept { element -> + if (element instanceof AudioElement) { + element.delete(renderer) + } + } } + removedElements.removeAll(elementsToDelete) } - } - removedElements.removeAll(elementsToDelete) - } - // Run the audio elements - scene.accept { element -> - if (element instanceof AudioElement) { - element.render(renderer) + // Run the audio elements + scene.accept { element -> + if (element instanceof AudioElement && initialized.contains(element)) { + element.render(renderer) + } + } } } - }) - systemLoop.run() + catch (InterruptedException ignored) { + break + } + } // Shutdown scene.accept { sceneElement -> diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index 919be3ef..8361638e 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -16,7 +16,6 @@ package nz.net.ultraq.redhorizon.engine.graphics -import nz.net.ultraq.redhorizon.async.ControlledLoop import nz.net.ultraq.redhorizon.engine.EngineSystem import nz.net.ultraq.redhorizon.engine.SystemReadyEvent import nz.net.ultraq.redhorizon.engine.SystemStoppedEvent @@ -29,7 +28,6 @@ import nz.net.ultraq.redhorizon.engine.input.InputEventStream import nz.net.ultraq.redhorizon.engine.input.KeyEvent import nz.net.ultraq.redhorizon.engine.input.MouseButtonEvent import nz.net.ultraq.redhorizon.engine.scenegraph.Scene -import nz.net.ultraq.redhorizon.events.EventTarget import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -48,7 +46,7 @@ import java.util.concurrent.LinkedBlockingQueue * * @author Emanuel Rabina */ -class GraphicsSystem extends EngineSystem implements GraphicsRequests, EventTarget { +class GraphicsSystem extends EngineSystem implements GraphicsRequests { private static final Logger logger = LoggerFactory.getLogger(GraphicsSystem) @@ -67,9 +65,6 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests, EventTarg private boolean shouldToggleVsync private long lastClickTime - @Delegate - private ControlledLoop systemLoop - /** * Constructor, build a new system for rendering graphics. * @@ -234,22 +229,25 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests, EventTarg // Rendering loop logger.debug('Graphics system in render loop...') - systemLoop = new ControlledLoop({ !window.shouldClose() }, { -> - if (shouldToggleFullScreen) { - window.toggleFullScreen() - shouldToggleFullScreen = false + while (!window.shouldClose() && !Thread.interrupted()) { + try { + if (shouldToggleFullScreen) { + window.toggleFullScreen() + shouldToggleFullScreen = false + } + if (shouldToggleVsync) { + window.toggleVsync() + shouldToggleVsync = false + } + processRequests(renderer) + pipeline.render() + window.swapBuffers() + window.pollEvents() } - if (shouldToggleVsync) { - window.toggleVsync() - shouldToggleVsync = false + catch (InterruptedException ignored) { + break } - - processRequests(renderer) - pipeline.render() - window.swapBuffers() - window.pollEvents() - }) - systemLoop.run() + } // Shutdown logger.debug('Shutting down graphics system') diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Animation.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Animation.groovy index 18d21b6f..77f4b4c9 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Animation.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Animation.groovy @@ -28,8 +28,8 @@ import nz.net.ultraq.redhorizon.engine.time.Temporal import nz.net.ultraq.redhorizon.filetypes.AnimationFile import nz.net.ultraq.redhorizon.filetypes.ColourFormat import nz.net.ultraq.redhorizon.filetypes.Streaming +import nz.net.ultraq.redhorizon.filetypes.StreamingDecoder import nz.net.ultraq.redhorizon.filetypes.StreamingFrameEvent -import nz.net.ultraq.redhorizon.filetypes.Worker import org.joml.primitives.Rectanglef import org.slf4j.Logger @@ -59,7 +59,7 @@ class Animation implements GraphicsElement, Playable, Node, Temporal { final int numFrames final float frameRate - private final Worker animationDataWorker + private final StreamingDecoder animationDataWorker private final BlockingQueue frames private final int bufferSize private final CountDownLatch bufferReady = new CountDownLatch(1) @@ -83,9 +83,9 @@ class Animation implements GraphicsElement, Playable, Node, Temporal { this(animationFile.width, animationFile.height, animationFile.format, animationFile.numFrames, animationFile.frameRate, animationFile.frameRate as int, - animationFile instanceof Streaming ? animationFile.streamingDataWorker : null) + animationFile instanceof Streaming ? animationFile.streamingDecoder : null) - Executors.newSingleThreadExecutor().execute(animationDataWorker) + Executors.newVirtualThreadPerTaskExecutor().execute(animationDataWorker) } /** @@ -101,7 +101,7 @@ class Animation implements GraphicsElement, Playable, Node, Temporal { */ @PackageScope Animation(int width, int height, ColourFormat format, int numFrames, float frameRate, - int bufferSize = 10, Worker animationDataWorker) { + int bufferSize = 10, StreamingDecoder animationDataWorker) { if (!animationDataWorker) { throw new UnsupportedOperationException('Streaming configuration used, but source doesn\'t support streaming') @@ -129,7 +129,7 @@ class Animation implements GraphicsElement, Playable, Node, Temporal { @Override void delete(GraphicsRenderer renderer) { - animationDataWorker.stop() + animationDataWorker.cancel(true) frames.drain() renderer.deleteMesh(mesh) textures.each { texture -> diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/SoundTrack.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/SoundTrack.groovy index 3aaeb007..0aa66866 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/SoundTrack.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/SoundTrack.groovy @@ -22,8 +22,8 @@ import nz.net.ultraq.redhorizon.engine.scenegraph.Node import nz.net.ultraq.redhorizon.engine.time.Temporal import nz.net.ultraq.redhorizon.filetypes.SoundFile import nz.net.ultraq.redhorizon.filetypes.Streaming +import nz.net.ultraq.redhorizon.filetypes.StreamingDecoder import nz.net.ultraq.redhorizon.filetypes.StreamingSampleEvent -import nz.net.ultraq.redhorizon.filetypes.Worker import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -50,7 +50,7 @@ class SoundTrack implements AudioElement, Playable, Node, Temporal { final int channels final int frequency - private final Worker soundDataWorker + private final StreamingDecoder soundDataWorker private final BlockingQueue samples private final int bufferSize private final CountDownLatch bufferReady = new CountDownLatch(1) @@ -68,9 +68,9 @@ class SoundTrack implements AudioElement, Playable, Node, Temporal { SoundTrack(SoundFile soundFile) { this(soundFile.bits, soundFile.channels, soundFile.frequency, - soundFile instanceof Streaming ? soundFile.streamingDataWorker : null) + soundFile instanceof Streaming ? soundFile.streamingDecoder : null) - Executors.newSingleThreadExecutor().execute(soundDataWorker) + Executors.newVirtualThreadPerTaskExecutor().execute(soundDataWorker) } /** @@ -83,7 +83,7 @@ class SoundTrack implements AudioElement, Playable, Node, Temporal { * @param soundDataWorker */ @PackageScope - SoundTrack(int bits, int channels, int frequency, int bufferSize = 10, Worker soundDataWorker) { + SoundTrack(int bits, int channels, int frequency, int bufferSize = 10, StreamingDecoder soundDataWorker) { if (!soundDataWorker) { throw new UnsupportedOperationException('Streaming configuration used, but source doesn\'t support streaming') @@ -107,7 +107,7 @@ class SoundTrack implements AudioElement, Playable, Node, Temporal { @Override void delete(AudioRenderer renderer) { - soundDataWorker.stop() + soundDataWorker.cancel(true) samples.drain() renderer.deleteSource(sourceId) renderer.deleteBuffers(bufferIds as int[]) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Video.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Video.groovy index ead5c550..b8200923 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Video.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Video.groovy @@ -50,7 +50,7 @@ class Video implements AudioElement, GraphicsElement, Playable, Node { Video(VideoFile videoFile) { if (videoFile instanceof Streaming) { - def videoWorker = videoFile.streamingDataWorker + def videoWorker = videoFile.streamingDecoder animation = new Animation(videoFile.width, videoFile.height, videoFile.format, videoFile.numFrames, videoFile.frameRate, videoFile.frameRate * 2 as int, videoWorker) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/VideoLoader.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/VideoLoader.groovy index 9d6ced98..4f18a676 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/VideoLoader.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/VideoLoader.groovy @@ -83,7 +83,7 @@ class VideoLoader extends MediaLoader { if (media.playing) { media.stop() } - file.streamingDataWorker.stop() + file.streamingDecoder.stop() if (gameClock.paused) { gameClock.resume() } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/GameClock.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/GameClock.groovy index 7b549241..a25b2c25 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/GameClock.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/GameClock.groovy @@ -16,21 +16,22 @@ package nz.net.ultraq.redhorizon.engine.time -import nz.net.ultraq.redhorizon.async.ControlledLoop import nz.net.ultraq.redhorizon.engine.EngineSystem import nz.net.ultraq.redhorizon.engine.SystemReadyEvent import nz.net.ultraq.redhorizon.engine.SystemStoppedEvent -import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import org.slf4j.Logger import org.slf4j.LoggerFactory +import groovy.transform.InheritConstructors + /** * A separate time source from the usual system time, allowing game time to flow * at different speeds. * * @author Emanuel Rabina */ +@InheritConstructors class GameClock extends EngineSystem { private static Logger logger = LoggerFactory.getLogger(GameClock) @@ -38,19 +39,6 @@ class GameClock extends EngineSystem { private float speed = 1.0f private float lastSpeed - @Delegate - private ControlledLoop timeLoop - - /** - * Constructor, creates a new time system over a scene. - * - * @param scene - */ - GameClock(Scene scene) { - - super(scene) - } - /** * Return whether or not time has been paused. * @@ -95,30 +83,35 @@ class GameClock extends EngineSystem { long currentTimeMillis = lastSystemTimeMillis logger.debug('Game clock in update loop') - timeLoop = new ControlledLoop({ -> - Thread.sleep(10) - var currentSystemTimeMillis = System.currentTimeMillis() - var delta = currentSystemTimeMillis - lastSystemTimeMillis - - // Normal flow of time, accumulate ticks at the same rate as system time - if (speed == 1.0f) { - currentTimeMillis += delta - } - // Modified flow, accumulate ticks at system time * flow speed - else { - currentTimeMillis += (delta * speed) - } - - // Update time with scene objects - scene.accept { element -> - if (element instanceof Temporal) { - element.tick(currentTimeMillis) + while (!Thread.interrupted()) { + try { + rateLimit(100) { -> + var currentSystemTimeMillis = System.currentTimeMillis() + var delta = currentSystemTimeMillis - lastSystemTimeMillis + + // Normal flow of time, accumulate ticks at the same rate as system time + if (speed == 1.0f) { + currentTimeMillis += delta + } + // Modified flow, accumulate ticks at system time * flow speed + else { + currentTimeMillis += (delta * speed) + } + + // Update time with scene objects + scene.accept { element -> + if (element instanceof Temporal) { + element.tick(currentTimeMillis) + } + } + + lastSystemTimeMillis = currentSystemTimeMillis } } - - lastSystemTimeMillis = currentSystemTimeMillis - }) - timeLoop.run() + catch (InterruptedException ignored) { + break + } + } trigger(new SystemStoppedEvent()) logger.debug('Game clock stopped') diff --git a/redhorizon-filetypes/build.gradle b/redhorizon-filetypes/build.gradle index 51e96031..618b13ac 100644 --- a/redhorizon-filetypes/build.gradle +++ b/redhorizon-filetypes/build.gradle @@ -1,12 +1,12 @@ -/* +/* * Copyright 2015, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,6 +18,5 @@ description = 'Filetypes module for the Red Horizon project' dependencies { api 'org.reflections:reflections:0.10.2' - implementation project(':redhorizon-async') implementation project(':redhorizon-events') } diff --git a/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Streaming.groovy b/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Streaming.groovy index 3e83b390..f91d6371 100644 --- a/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Streaming.groovy +++ b/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Streaming.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,23 +18,23 @@ package nz.net.ultraq.redhorizon.filetypes /** * Certain files are better read by streaming their data out rather than - * obtaining it all in one go, usually for the purpose of saving memory that - * will be freed-up anyway. This interface provides the ability for files to - * expose that streaming data through {@link Worker} classes that can be started - * and listened to from the by the consuming thread. - * + * obtaining it all in one go, usually for the purpose of saving memory. This + * interface provides the ability for files to expose that streaming data + * through a {@link StreamingDecoder}. + * * @author Emanuel Rabina */ interface Streaming { /** - * Returns a worker that can be used for streaming file data. Workers can be - * listened to for events that contain data in useful portions that are usable - * by some kind of consumer. - * + * Returns an object for streaming file data. Streaming decoders can be + * submitted to executors to begin processing, and then listened to for + * events that contain data in useful portions that are usable by some kind of + * consumer. + * * @return - * A worker that can be executed as its own thread for generating the - * streaming data. + * A streaming decoder that can be executed as its own thread for generating + * the data. */ - Worker getStreamingDataWorker() + StreamingDecoder getStreamingDecoder() } diff --git a/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Worker.groovy b/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/StreamingDecoder.groovy similarity index 55% rename from redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Worker.groovy rename to redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/StreamingDecoder.groovy index 93aaf1d5..3898eb12 100644 --- a/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/Worker.groovy +++ b/redhorizon-filetypes/source/nz/net/ultraq/redhorizon/filetypes/StreamingDecoder.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,15 +16,28 @@ package nz.net.ultraq.redhorizon.filetypes -import nz.net.ultraq.redhorizon.async.RunnableWorker +import nz.net.ultraq.redhorizon.events.Event import nz.net.ultraq.redhorizon.events.EventTarget +import java.util.concurrent.FutureTask + /** - * A special {@link Runnable} for decoding file data and emitting the results as - * events. Workers can be stopped using the standard {@code Future.cancel} - * method, and worker implementations must respect these controls. - * + * A special kind of {@code RunnableFuture} for decoding file data and emitting + * the results as events. Workers can be stopped using the standard + * {@code Future.cancel} method, and worker implementations must respect these + * controls. + * * @author Emanuel Rabina */ -abstract class Worker implements EventTarget, RunnableWorker { +class StreamingDecoder extends FutureTask implements EventTarget { + + /** + * Constructor, set the work loop. + */ + StreamingDecoder(Runnable runnable) { + + super(runnable, null) + assert runnable instanceof EventTarget + runnable.relay(Event, this) + } } diff --git a/settings.gradle b/settings.gradle index 2f7564ad..03811350 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,6 @@ rootProject.name = 'redhorizon' include( - 'redhorizon-async', 'redhorizon-classic', 'redhorizon-cli', 'redhorizon-engine',