Skip to content

Commit

Permalink
Start on scripting and off-thread requests of the graphics system 🥳
Browse files Browse the repository at this point in the history
  • Loading branch information
ultraq committed Jan 27, 2024
1 parent 4734a1e commit 5a745db
Show file tree
Hide file tree
Showing 18 changed files with 462 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
/*
* Copyright 2021, 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.
Expand All @@ -21,9 +21,12 @@ import nz.net.ultraq.redhorizon.classic.filetypes.MixFile
import org.slf4j.Logger
import picocli.CommandLine.Parameters

import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType

/**
* A mixin of shareable file options.
*
*
* @author Emanuel Rabina
*/
class FileOptions {
Expand All @@ -37,11 +40,9 @@ class FileOptions {
/**
* Load the file indicated by the {@code file} and {@code entryName}
* parameters, passing it along to the given closure.
*
* @param logger
* @param closure
*/
void useFile(Logger logger, Closure closure) {
void useFile(Logger logger,
@ClosureParams(value = SimpleType, options = 'nz.net.ultraq.redhorizon.filetypes.ResourceFile') Closure closure) {

logger.info('Loading {}...', file)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024, 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.cli.mediaplayer

import nz.net.ultraq.redhorizon.engine.scenegraph.Scene
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.SpriteScript

import groovy.transform.InheritConstructors

/**
* A script to make a sprite node behave as a full-screen image.
*
* @author Emanuel Rabina
*/
@InheritConstructors
class ImageScript extends SpriteScript {

@Override
void onSceneAdded(Scene scene) {

var width = sprite.imageFile.width
var height = sprite.imageFile.height
sprite
.scaleXY(scene.window.renderResolution.calculateScaleToFit(width, height))
.translate(-width / 2, -height / 2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,11 @@ import nz.net.ultraq.redhorizon.engine.Application
import nz.net.ultraq.redhorizon.engine.audio.AudioConfiguration
import nz.net.ultraq.redhorizon.engine.graphics.GraphicsConfiguration
import nz.net.ultraq.redhorizon.engine.input.KeyEvent
import nz.net.ultraq.redhorizon.engine.media.AnimationLoader
import nz.net.ultraq.redhorizon.engine.media.ImageLoader
import nz.net.ultraq.redhorizon.engine.media.ImagesLoader
import nz.net.ultraq.redhorizon.engine.media.MediaLoader
import nz.net.ultraq.redhorizon.engine.media.Playable
import nz.net.ultraq.redhorizon.engine.media.SoundLoader
import nz.net.ultraq.redhorizon.engine.media.StopEvent
import nz.net.ultraq.redhorizon.engine.media.VideoLoader
import nz.net.ultraq.redhorizon.filetypes.AnimationFile
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sprite
import nz.net.ultraq.redhorizon.filetypes.ImageFile
import nz.net.ultraq.redhorizon.filetypes.ImagesFile
import nz.net.ultraq.redhorizon.filetypes.Palette
import nz.net.ultraq.redhorizon.filetypes.ResourceFile
import nz.net.ultraq.redhorizon.filetypes.SoundFile
import nz.net.ultraq.redhorizon.filetypes.VideoFile

import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -64,16 +54,11 @@ class MediaPlayer extends Application {

/**
* Constructor, create a new application around the given media file.
*
* @param mediaFile
* @param audioConfig
* @param graphicsConfig
* @param paletteType
*/
MediaPlayer(ResourceFile mediaFile, AudioConfiguration audioConfig, GraphicsConfiguration graphicsConfig, Palette palette) {
MediaPlayer(ResourceFile resourceFile, AudioConfiguration audioConfig, GraphicsConfiguration graphicsConfig, Palette palette) {

super('Media Player', audioConfig, graphicsConfig)
this.mediaFile = mediaFile
this.mediaFile = resourceFile
this.palette = palette
}

Expand All @@ -82,22 +67,28 @@ class MediaPlayer extends Application {

logger.info('File details: {}', mediaFile)

mediaLoader = switch (mediaFile) {
case VideoFile -> new VideoLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream)
case AnimationFile -> new AnimationLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream)
case SoundFile -> new SoundLoader(mediaFile, scene, gameClock, inputEventStream)
case ImageFile -> new ImageLoader(mediaFile, scene, graphicsSystem)
case ImagesFile -> new ImagesLoader(mediaFile, palette, scene, graphicsSystem, inputEventStream)
default -> throw new UnsupportedOperationException("No media player for the associated file class of ${mediaFile}")
switch (mediaFile) {
case ImageFile:
scene << new Sprite(mediaFile).attachScript(new ImageScript())
break
}

var media = mediaLoader.load()
if (media instanceof Playable) {
media.on(StopEvent) { event ->
stop()
}
media.play()
}
// mediaLoader = switch (mediaFile) {
// case VideoFile -> new VideoLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream)
// case AnimationFile -> new AnimationLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream)
// case SoundFile -> new SoundLoader(mediaFile, scene, gameClock, inputEventStream)
// case ImageFile -> new ImageLoader(mediaFile, scene)
// case ImagesFile -> new ImagesLoader(mediaFile, palette, scene, graphicsSystem, inputEventStream)
// default -> throw new UnsupportedOperationException("No media player for the associated file class of ${mediaFile}")
// }
//
// var media = mediaLoader.load()
// if (media instanceof Playable) {
// media.on(StopEvent) { event ->
// stop()
// }
// media.play()
// }

// Universal quit on exit
inputEventStream.on(KeyEvent) { event ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -84,8 +84,8 @@ class MediaPlayerCli implements Callable<Integer> {
renderResolution: new Dimension(1280, 800)
)

fileOptions.useFile(logger) { mediaFile ->
new MediaPlayer(mediaFile, audioConfig, graphicsConfig, paletteOptions.loadPalette()).start()
fileOptions.useFile(logger) { resourceFile ->
new MediaPlayer(resourceFile, audioConfig, graphicsConfig, paletteOptions.loadPalette()).start()
}

return 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2024, 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.engine.graphics

import nz.net.ultraq.redhorizon.filetypes.ColourFormat

import org.joml.primitives.Rectanglef

import groovy.transform.ImmutableOptions
import java.nio.ByteBuffer
import java.util.concurrent.Future

/**
* Interface to make requests of the graphics system.
*
* @author Emanuel Rabina
*/
interface GraphicsRequests {

static interface Request<T> {}

static record ShaderRequest(String name) implements Request<Shader> {}

@ImmutableOptions(knownImmutables = ['surface'])
static record SpriteMeshRequest(Rectanglef surface) implements Request<Mesh> {}

@ImmutableOptions(knownImmutables = ['data'])
static record TextureRequest(int width, int height, ColourFormat format, ByteBuffer data) implements Request<Texture> {
}

/**
* Request the creation of a mesh from the graphics system.
*/
Future<Mesh> requestMesh(SpriteMeshRequest spriteMeshRequest)

/**
* Request an existing shader by name.
*/
Future<Shader> requestShader(ShaderRequest shaderRequest)

/**
* Request the creation of a texture from the graphics system.
*/
Future<Texture> requestTexture(TextureRequest textureRequest)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,30 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static org.lwjgl.glfw.GLFW.*

import java.util.concurrent.BlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue

/**
* Graphics system, creates a display which drives the rendering loop of drawing
* graphics objects.
*
* @author Emanuel Rabina
*/
class GraphicsSystem extends EngineSystem implements EventTarget {
class GraphicsSystem extends EngineSystem implements GraphicsRequests, EventTarget {

private static final Logger logger = LoggerFactory.getLogger(GraphicsSystem)

private final String windowTitle
private final GraphicsConfiguration config
private final InputEventStream inputEventStream
private final ExecutorService executorService = Executors.newCachedThreadPool()
private final BlockingQueue<Tuple2<SpriteMeshRequest, BlockingQueue<Mesh>>> meshRequests = new LinkedBlockingQueue<>()
private final BlockingQueue<Tuple2<ShaderRequest, BlockingQueue<Shader>>> shaderRequests = new LinkedBlockingQueue<>()
private final BlockingQueue<Tuple2<TextureRequest, BlockingQueue<Texture>>> textureRequests = new LinkedBlockingQueue<>()

private OpenGLContext context
private OpenGLCamera camera
Expand Down Expand Up @@ -124,6 +135,67 @@ class GraphicsSystem extends EngineSystem implements EventTarget {
return context.window
}

/**
* Run through all of the queued requests for graphics resources.
*
* @param renderer
*/
void processRequests(GraphicsRenderer renderer) {

if (meshRequests) {
meshRequests.drain().each { meshRequestAndPipe ->
def (meshRequest, pipe) = meshRequestAndPipe
pipe.add(renderer.createSpriteMesh(surface: meshRequest.surface()))
}
}

if (shaderRequests) {
shaderRequests.drain().each { shaderRequestAndPipe ->
def (shaderRequest, pipe) = shaderRequestAndPipe
pipe.add(renderer.getShader(shaderRequest.name()))
}
}

if (textureRequests) {
textureRequests.drain().each { textureRequestAndPipe ->
def (textureRequest, pipe) = textureRequestAndPipe
pipe.add(renderer.createTexture(textureRequest.width(), textureRequest.height(),
textureRequest.format(), textureRequest.data()))
}
}
}

/**
* Load a request onto the given queue, returning a future of the requested
* resource.
*/
<R extends Request<V>, V> Future<V> queueRequest(R request, BlockingQueue<Tuple2<R, BlockingQueue<V>>> requestQueue) {

return executorService.submit({ ->
var pipe = new LinkedBlockingQueue<V>(1)
requestQueue.add(new Tuple2(request, pipe))
return pipe.take()
} as Callable<V>)
}

@Override
Future<Mesh> requestMesh(SpriteMeshRequest spriteMeshRequest) {

return queueRequest(spriteMeshRequest, meshRequests)
}

@Override
Future<Shader> requestShader(ShaderRequest shaderRequest) {

return queueRequest(shaderRequest, shaderRequests)
}

@Override
Future<Texture> requestTexture(TextureRequest textureRequest) {

return queueRequest(textureRequest, textureRequests)
}

/**
* Start the graphics system loop: creates a new window in which to render the
* elements in the current scene, cleaning it all up when made to shut down.
Expand All @@ -140,6 +212,7 @@ class GraphicsSystem extends EngineSystem implements EventTarget {
var window = context.window
window.relay(FramebufferSizeEvent, this)
window.relay(WindowMaximizedEvent, this)
scene.window = window

// Only do quick window mode switching on Windows - the macOS experience
// is quite different from using the fullscreen button which assigns the
Expand All @@ -160,6 +233,8 @@ class GraphicsSystem extends EngineSystem implements EventTarget {
trigger(new WindowCreatedEvent(window))

new OpenGLRenderer(config, window).withCloseable { renderer ->
scene.graphicsRequestHandler = this

camera = new OpenGLCamera(window.renderResolution)
camera.withCloseable { camera ->
new ImGuiLayer(config, window, inputEventStream).withCloseable { imGuiLayer ->
Expand All @@ -181,6 +256,8 @@ class GraphicsSystem extends EngineSystem implements EventTarget {
window.toggleVsync()
shouldToggleVsync = false
}

processRequests(renderer)
pipeline.render()
window.swapBuffers()
window.pollEvents()
Expand Down
Loading

0 comments on commit 5a745db

Please sign in to comment.