diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 0000000..39270bf --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,27 @@ +## Declaration order + +Member declaration order: +1. `private` +2. `protected` +3. `package` +4. `public` +5. `export` + +## Attributes + +- Visibility modifiers (and static) in C++ syntax + - Not required for public-only structs (PODs) +- Function attributes after brackets +- Use `in` instead of `const ref` + +## Casing + +- Non-public member variables: `m + PascalCase` +- Non-public static member variables: `s + PascalCase` +- Non-public module variables: `p + PascalCase` + +- Classes, Structs, Interfaces, Unions: `PascalCase` +- Aliases: Dependent on type +- Constants: camelCase + +Everything else: camelCase \ No newline at end of file diff --git a/TODO.md b/TODO.md index b7cb5bc..3ece65b 100644 --- a/TODO.md +++ b/TODO.md @@ -47,12 +47,16 @@ A lot of stuff has already been done before this file has been started. [X] Markup for creating GUI [X] Get audio working [X] Add streaming audio -[ ] Fix profiler omg +[X] Fix profiler omg [ ] Add unittests +[X] DOCUMENTATION! [ ] MMAP shenanigans for ZPK files [ ] Make buildable and executable on Windows [ ] Make executable on MacOS [ ] Localization improvements (i18n-d)? +[ ] 2D batch renderer add custom shader support +[ ] Move ECS into a separate sub-package +[ ] Rethink the different backend situation [ ] Implement texture atlases in 3D meshes [ ] Add CLI (like Angular) (maybe now even more important due to Github workflows?) diff --git a/dub.sdl b/dub.sdl index c8322c8..0cc1b09 100644 --- a/dub.sdl +++ b/dub.sdl @@ -20,6 +20,5 @@ configuration "sdl-opengl" { dependency "bindbc-openal" version="~>1.0.0" dependency "bindbc-sdl" version="~>1.0.1" targetType "library" - sourcePaths "platform/opengl" "platform/openal" - versions "GL_41" "SDL_204" "GL_KHR_debug" + versions "GL_41" "SDL_204" "GL_KHR_debug" "ZWBackendOpenGL" "ZWBackendOpenAL" } diff --git a/platform/gamemixer/api.d b/platform/gamemixer/api.d deleted file mode 100644 index dadf362..0000000 --- a/platform/gamemixer/api.d +++ /dev/null @@ -1,91 +0,0 @@ -module zyeware.audio.api; - -import std.exception : enforce; -import std.string : format; -import std.typecons : Rebindable; - -import gamemixer; - -import zyeware.common; -import zyeware.audio; - -struct AudioAPI -{ - @disable this(); - @disable this(this); - -package static: - IMixer sMixer; - - AudioBus[string] sBusses; - AudioBus sMasterBus; - -public static: - void initialize() - { - sMixer = mixerCreate(); - - sMasterBus = addBus("master"); - } - - void cleanup() - { - mixerDestroy(sMixer); - } - - AudioBus getBus(string name) - { - AudioBus result = sBusses.get(name, null); - enforce!AudioException(result, format!"No audio bus named '%s' exists."(name)); - - return result; - } - - AudioBus addBus(string name) - { - enforce!AudioException(!(name in sBusses), format!"Audio bus named '%s' already exists."(name)); - - auto bus = new AudioBus(name); - sBusses[name] = bus; - - return bus; - } - - void removeBus(string name) - { - if (name in sBusses) - { - sBusses.remove(name); - } - } - - void stopAll() nothrow - { - sMixer.stopAllChannels(); - } - - void play(AudioSample sample, in PlayProperties properties = PlayProperties.init) - { - PlayOptions options; - - // First, get mixer and multiply volume - Rebindable!(const AudioBus) bus = properties.bus; - if (!bus) - bus = sMasterBus; - - options.volume = properties.volume * bus.volume; - options.loopCount = properties.looping ? loopForever : 1; - options.channel = properties.channel; - - sMixer.play(sample.mSource, options); - } - - Vector3f listenerLocation() nothrow - { - return Vector3f(0); - } - - void listenerLocation(Vector3f value) nothrow - { - } -} \ No newline at end of file diff --git a/platform/gamemixer/sample.d b/platform/gamemixer/sample.d deleted file mode 100644 index edd35a3..0000000 --- a/platform/gamemixer/sample.d +++ /dev/null @@ -1,34 +0,0 @@ -module zyeware.audio.sample; - -import std.exception : enforce; -import std.string : format; - -import gamemixer; - -import zyeware.common; -import zyeware.audio; - -@asset(Yes.cache) -class AudioSample -{ -package: - IAudioSource mSource; - - this(IAudioSource source) - { - mSource = source; - } - -public: - static AudioSample load(string path) - { - VFSFile file = VFS.getFile(path); - ubyte[] data = file.readAll!(ubyte[]); - file.close(); - - IAudioSource source = AudioAPI.sMixer.createSourceFromMemory(data); - enforce!AudioException(source, format!"Failed to load audio sample '%s'."(path)); - - return new AudioSample(source); - } -} \ No newline at end of file diff --git a/platform/openal/api.d b/platform/openal/api.d deleted file mode 100644 index c8f9a7a..0000000 --- a/platform/openal/api.d +++ /dev/null @@ -1,113 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.audio.api; - -import std.string : format, fromStringz; -import std.exception : enforce; - -import bindbc.openal; - -import zyeware.common; -import zyeware.audio; - -struct AudioAPI -{ - @disable this(); - @disable this(this); - -private static: - ALCdevice* sDevice; - ALCcontext* sContext; - - AudioBus[string] sBusses; - -package(zyeware) static: - void initialize() - { - loadLibraries(); - - addBus("master"); - - enforce!AudioException(sDevice = alcOpenDevice(null), "Failed to create audio device."); - enforce!AudioException(sContext = alcCreateContext(sDevice, null), "Failed to create audio context."); - - enforce!AudioException(alcMakeContextCurrent(sContext), "Failed to make audio context current."); - } - - void loadLibraries() - { - import loader = bindbc.loader.sharedlib; - import std.string : fromStringz; - - if (isOpenALLoaded()) - return; - - immutable alResult = loadOpenAL(); - if (alResult != alSupport) - { - foreach (info; loader.errors) - Logger.core.log(LogLevel.warning, "OpenAL loader: %s", info.message.fromStringz); - - switch (alResult) - { - case ALSupport.noLibrary: - throw new AudioException("Could not find OpenAL shared library."); - - case ALSupport.badLibrary: - throw new AudioException("Provided OpenAL shared is corrupted."); - - default: - Logger.core.log(LogLevel.warning, "Got older OpenAL version than expected. This might lead to errors."); - } - } - } - - void cleanup() - { - alcCloseDevice(sDevice); - } - -public static: - - AudioBus getBus(string name) - in (name, "Name cannot be null.") - { - AudioBus result = sBusses.get(name, null); - enforce!AudioException(result, format!"No audio bus named '%s' exists."(name)); - - return result; - } - - AudioBus addBus(string name) - in (name, "Name cannot be null.") - { - enforce!AudioException(!(name in sBusses), format!"Audio bus named '%s' already exists."(name)); - - auto bus = new AudioBus(name); - sBusses[name] = bus; - - return bus; - } - - void removeBus(string name) - in (name, "Name cannot be null.") - { - if (name in sBusses) - { - sBusses.remove(name); - } - } - - Vector3f listenerLocation() nothrow - { - return Vector3f(0); - } - - void listenerLocation(Vector3f value) nothrow - { - - } -} \ No newline at end of file diff --git a/platform/opengl/api.d b/platform/opengl/api.d deleted file mode 100644 index 8a943ad..0000000 --- a/platform/opengl/api.d +++ /dev/null @@ -1,305 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.api; - -import bindbc.opengl; - -import zyeware.common; -import zyeware.core.debugging.profiler; -import zyeware.rendering; - -/// Implementation order is extremely important! -struct RenderAPI -{ -private static: - bool[RenderFlag] sFlagValues; - - version (Windows) - { - extern(Windows) static void glErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, - const(char)* message, void* userParam) nothrow - { - glGetError(); - - string typeName; - LogLevel logLevel; - - switch (type) - { - case GL_DEBUG_TYPE_ERROR: - typeName = "Error"; - break; - - case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: - typeName = "Deprecated Behavior"; - break; - - case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: - typeName = "Undefined Behavior"; - break; - - case GL_DEBUG_TYPE_PERFORMANCE: - typeName = "Performance"; - break; - - case GL_DEBUG_TYPE_OTHER: - default: - return; - } - - switch (severity) - { - case GL_DEBUG_SEVERITY_LOW: - logLevel = LogLevel.info; - break; - - case GL_DEBUG_SEVERITY_MEDIUM: - logLevel = LogLevel.warning; - break; - - case GL_DEBUG_SEVERITY_HIGH: - logLevel = LogLevel.error; - break; - - default: - logLevel = LogLevel.debug_; - break; - } - - Logger.core.log(logLevel, "%s: %s", typeName, cast(string) message[0..length]); - } - } - else - { - extern(C) static void glErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, - const(char)* message, void* userParam) nothrow - { - glGetError(); - - string typeName; - LogLevel logLevel; - - switch (type) - { - case GL_DEBUG_TYPE_ERROR: - typeName = "Error"; - break; - - case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: - typeName = "Deprecated Behavior"; - break; - - case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: - typeName = "Undefined Behavior"; - break; - - case GL_DEBUG_TYPE_PERFORMANCE: - typeName = "Performance"; - break; - - case GL_DEBUG_TYPE_OTHER: - default: - return; - } - - switch (severity) - { - case GL_DEBUG_SEVERITY_LOW: - logLevel = LogLevel.info; - break; - - case GL_DEBUG_SEVERITY_MEDIUM: - logLevel = LogLevel.warning; - break; - - case GL_DEBUG_SEVERITY_HIGH: - logLevel = LogLevel.error; - break; - - default: - logLevel = LogLevel.debug_; - break; - } - - Logger.core.log(logLevel, "%s: %s", typeName, cast(string) message[0..length]); - } - } - - -package(zyeware) static: - void initialize() nothrow - { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - - //glAlphaFunc(GL_GREATER, 0); - - glDepthFunc(GL_LEQUAL); - - glEnable(GL_DEBUG_OUTPUT); - glDebugMessageCallback(&glErrorCallback, null); - - glLineWidth(2); - glPointSize(4); - - { - GLboolean resultBool; - GLint resultInt; - - glGetBooleanv(GL_DEPTH_TEST, &resultBool); - sFlagValues[RenderFlag.depthTesting] = cast(bool) resultBool; - glGetBooleanv(GL_DEPTH_WRITEMASK, &resultBool); - sFlagValues[RenderFlag.depthBufferWriting] = cast(bool) resultBool; - glGetBooleanv(GL_CULL_FACE, &resultBool); - sFlagValues[RenderFlag.culling] = cast(bool) resultBool; - glGetBooleanv(GL_STENCIL_TEST, &resultBool); - sFlagValues[RenderFlag.stencilTesting] = cast(bool) resultBool; - glGetIntegerv(GL_POLYGON_MODE, &resultInt); - sFlagValues[RenderFlag.wireframe] = resultInt == GL_LINE; - } - } - - void loadLibraries() - { - import loader = bindbc.loader.sharedlib; - import std.string : fromStringz; - - if (isOpenGLLoaded()) - return; - - immutable glResult = loadOpenGL(); - - if (glResult != glSupport) - { - foreach (info; loader.errors) - Logger.core.log(LogLevel.warning, "OpenGL loader: %s", info.message.fromStringz); - - switch (glResult) - { - case GLSupport.noLibrary: - throw new GraphicsException("Could not find OpenGL shared library."); - - case GLSupport.badLibrary: - throw new GraphicsException("Provided OpenGL shared is corrupted."); - - case GLSupport.noContext: - throw new GraphicsException("No OpenGL context available."); - - default: - Logger.core.log(LogLevel.warning, "Got older OpenGL version than expected. This might lead to errors."); - } - } - } - - void cleanup() - { - } - -public static: - void setClearColor(Color value) nothrow - { - glClearColor(value.r, value.g, value.b, value.a); - } - - void clear() nothrow - { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - - void setViewport(int x, int y, uint width, uint height) nothrow - { - glViewport(x, y, cast(GLsizei) width, cast(GLsizei) height); - } - - void drawIndexed(size_t count) nothrow - { - glDrawElements(GL_TRIANGLES, cast(int) count, GL_UNSIGNED_INT, null); - - version (Profiling) - { - ++Profiler.sRenderData.drawCalls; - Profiler.sRenderData.polygonCount += count / 3; - } - } - - void packLightConstantBuffer(ref ConstantBuffer buffer, in Renderer3D.Light[] lights) nothrow - { - Vector4f[10] positions; - Vector4f[10] colors; - Vector4f[10] attenuations; - - for (size_t i; i < lights.length; ++i) - { - positions[i] = Vector4f(lights[i].position, 0); - colors[i] = lights[i].color; - attenuations[i] = Vector4f(lights[i].attenuation, 0); - } - - buffer.setData(buffer.getEntryOffset("position"), positions); - buffer.setData(buffer.getEntryOffset("color"), colors); - buffer.setData(buffer.getEntryOffset("attenuation"), attenuations); - } - - bool getFlag(RenderFlag flag) nothrow - { - return sFlagValues[flag]; - } - - void setFlag(RenderFlag flag, bool value) nothrow - { - if (sFlagValues[flag] == value) - return; - - final switch (flag) with (RenderFlag) - { - case depthTesting: - if (value) - glEnable(GL_DEPTH_TEST); - else - glDisable(GL_DEPTH_TEST); - break; - - case depthBufferWriting: - glDepthMask(value); - break; - - case culling: - if (value) - glEnable(GL_CULL_FACE); - else - glDisable(GL_CULL_FACE); - break; - - case stencilTesting: - if (value) - glEnable(GL_STENCIL_TEST); - else - glDisable(GL_STENCIL_TEST); - break; - - case wireframe: - glPolygonMode(GL_FRONT_AND_BACK, value ? GL_LINE : GL_FILL); - break; - } - - sFlagValues[flag] = value; - } - - size_t getCapability(RenderCapability capability) nothrow - { - final switch (capability) with (RenderCapability) - { - case maxTextureSlots: - GLint result; - glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &result); - return result; - } - } -} \ No newline at end of file diff --git a/platform/opengl/renderer2d.d b/platform/opengl/renderer2d.d deleted file mode 100644 index 0afa3f4..0000000 --- a/platform/opengl/renderer2d.d +++ /dev/null @@ -1,348 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.renderer.renderer2d; - -import std.traits : isSomeString; -import std.string : lineSplitter; -import std.typecons : Rebindable; -import std.exception : enforce; - -import bmfont : BMFont = Font; - -import zyeware.common; -import zyeware.core.debugging.profiler; -import zyeware.rendering; - -/// The `Renderer2D` struct gives access to the 2D rendering API. -struct Renderer2D -{ - @disable this(); - @disable this(this); - -private static: - enum maxQuadsPerBatch = 5000; - enum maxVerticesPerBatch = maxQuadsPerBatch * 4; - enum maxIndicesPerBatch = maxQuadsPerBatch * 6; - - struct QuadVertex - { - Vector4f position; - Color color; - Vector2f uv; - float textureIndex; - } - - bool sOldCullingValue; - - Shader sDefaultShader; - BufferGroup[] sBatchBuffers; - size_t sActiveBatchBufferIndex; - ConstantBuffer sMatrixData; - - QuadVertex[maxVerticesPerBatch] sBatchVertices; - uint[maxIndicesPerBatch] sBatchIndices; - size_t sCurrentQuad; - - Rebindable!(const Texture2D)[] sBatchTextures; - size_t sNextFreeTexture = 1; // because 0 is the white texture - - size_t getIndexForTexture(in Texture2D texture) nothrow - { - for (size_t i = 1; i < sNextFreeTexture; ++i) - if (texture is sBatchTextures[i]) - return i; - - if (sNextFreeTexture == sBatchTextures.length) - return size_t.max; - - sBatchTextures[sNextFreeTexture++] = texture; - return sNextFreeTexture - 1; - } - -package(zyeware) static: - /// Initializes 2D rendering. - void initialize() - { - sBatchTextures = new Rebindable!(const Texture2D)[8]; - - sMatrixData = new ConstantBuffer(BufferLayout([ - BufferElement("viewProjection", BufferElement.Type.mat4) - ])); - - for (size_t i; i < 2; ++i) - { - auto batchBuffer = new BufferGroup(); - - batchBuffer.dataBuffer = new DataBuffer(maxVerticesPerBatch * QuadVertex.sizeof, BufferLayout([ - BufferElement("aPosition", BufferElement.Type.vec4), - BufferElement("aColor", BufferElement.Type.vec4), - BufferElement("aUV", BufferElement.Type.vec2), - BufferElement("aTexIndex", BufferElement.Type.float_) - ]), Yes.dynamic); - - batchBuffer.indexBuffer = new IndexBuffer(maxIndicesPerBatch * uint.sizeof, Yes.dynamic); - - sBatchBuffers ~= batchBuffer; - } - - // To circumvent a bug in MacOS builds that require a VAO to be bound before validating a - // shader program in OpenGL. Due to Renderer2D being initialized early during the - // engines lifetime, this should fix all further shader loadings. - sBatchBuffers[0].bind(); - - sDefaultShader = AssetManager.load!Shader("core://shaders/2d/default.shd"); - - static ubyte[3] pixels = [255, 255, 255]; - sBatchTextures[0] = new Texture2D(new Image(pixels, 3, 8, Vector2i(1)), TextureProperties.init); - } - - /// Cleans up all used resources. - void cleanup() - { - sDefaultShader.dispose(); - sBatchTextures[0].dispose(); - sBatchTextures.dispose(); - - foreach (BufferGroup buffer; sBatchBuffers) - buffer.dispose(); - } - -public static: - /// Starts a 2D scene. This must be called before any 2D drawing commands. - /// - /// Params: - /// projectionMatrix = A 4x4 matrix used for projection. - /// viewMatrix = A 4x4 matrix used for view. - void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix) - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.none, - "A renderer is currently active, cannot begin."); - - sMatrixData.bind(ConstantBuffer.Slot.matrices); - sMatrixData.setData(sMatrixData.getEntryOffset("viewProjection"), - (projectionMatrix * viewMatrix).matrix); - - RenderAPI.setFlag(RenderFlag.depthTesting, false); - sOldCullingValue = RenderAPI.getFlag(RenderFlag.culling); - RenderAPI.setFlag(RenderFlag.culling, false); - - debug currentRenderer = CurrentRenderer.renderer2D; - } - - /// Ends a 2D scene. This must be called at the end of all 2D drawing commands, as it flushes - /// everything to the screen. - void end() - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, - "2D renderer is not active, cannot end."); - - flush(); - - RenderAPI.setFlag(RenderFlag.culling, sOldCullingValue); - - debug currentRenderer = CurrentRenderer.none; - } - - /// Flushes all currently cached drawing commands to the screen. - void flush() - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, - "2D renderer is not active, cannot flush."); - - BufferGroup activeGroup = sBatchBuffers[sActiveBatchBufferIndex++]; - sActiveBatchBufferIndex %= sBatchBuffers.length; - - activeGroup.bind(); - activeGroup.dataBuffer.setData(sBatchVertices); - activeGroup.indexBuffer.setData(sBatchIndices); - - sDefaultShader.bind(); - - for (int i = 0; i < sNextFreeTexture; ++i) - sBatchTextures[i].bind(i); - - RenderAPI.drawIndexed(sCurrentQuad * 6); - - sCurrentQuad = 0; - sNextFreeTexture = 1; - } - - /// Draws a rectangle. - /// - /// Params: - /// dimensions = The dimensions of the rectangle to draw. - /// position = 2D position where to draw the rectangle to. - /// scale = How much to scale the dimensions. - /// modulate = The color of the rectangle. If a texture is supplied, it will be tinted in this color. - /// texture = The texture to use. If `null`, draws a blank rectangle. - /// region = The region of the rectangle to use. Has no effect if no texture is supplied. - pragma(inline, true) - void drawRect(in Rect2f dimensions, in Vector2f position, in Vector2f scale, in Color modulate = Vector4f(1), - in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)) - { - drawRect(dimensions, Matrix4f.translation(Vector3f(position, 0)) * Matrix4f.scaling(scale.x, scale.y, 1), - modulate, texture, region); - } - - /// Draws a rectangle. - /// - /// Params: - /// dimensions = The dimensions of the rectangle to draw. - /// position = 2D position where to draw the rectangle to. - /// scale = How much to scale the dimensions. - /// rotation = The rotation of the rectangle, in radians. - /// modulate = The color of the rectangle. If a texture is supplied, it will be tinted in this color. - /// texture = The texture to use. If `null`, draws a blank rectangle. - /// region = The region of the rectangle to use. Has no effect if no texture is supplied. - pragma(inline, true) - void drawRect(in Rect2f dimensions, in Vector2f position, in Vector2f scale, float rotation, in Color modulate = Vector4f(1), - in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)) - { - drawRect(dimensions, Matrix4f.translation(Vector3f(position, 0)) * Matrix4f.rotation(rotation, Vector3f(0, 0, 1)) - * Matrix4f.scaling(scale.x, scale.y, 1), modulate, texture, region); - } - - /// Draws a rectangle. - /// - /// Params: - /// dimensions = The dimensions of the rectangle to draw. - /// transform = A 4x4 matrix used for transformation of the rectangle. - /// modulate = The color of the rectangle. If a texture is supplied, it will be tinted in this color. - /// texture = The texture to use. If `null`, draws a blank rectangle. - /// region = The region of the rectangle to use. Has no effect if no texture is supplied. - void drawRect(in Rect2f dimensions, in Matrix4f transform, in Color modulate = Vector4f(1), in Texture2D texture = null, - in Rect2f region = Rect2f(0, 0, 1, 1)) - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, - "2D renderer is not active, cannot draw."); - - static Vector4f[4] quadPositions; - quadPositions[0] = Vector4f(dimensions.min.x, dimensions.min.y, 0.0f, 1); - quadPositions[1] = Vector4f(dimensions.max.x, dimensions.min.y, 0.0f, 1); - quadPositions[2] = Vector4f(dimensions.max.x, dimensions.max.y, 0.0f, 1); - quadPositions[3] = Vector4f(dimensions.min.x, dimensions.max.y, 0.0f, 1); - - static Vector2f[4] quadUVs; - quadUVs[0] = Vector2f(region.min.x, region.min.y); - quadUVs[1] = Vector2f(region.max.x, region.min.y); - quadUVs[2] = Vector2f(region.max.x, region.max.y); - quadUVs[3] = Vector2f(region.min.x, region.max.y); - - if (sCurrentQuad == maxQuadsPerBatch) - flush(); - - float texIdx = 0; - if (texture) - { - size_t idx = getIndexForTexture(texture); - if (idx == size_t.max) // No more room for new textures - { - flush(); - idx = getIndexForTexture(texture); - } - - texIdx = cast(float) idx; - } - - for (size_t i; i < 4; ++i) - sBatchVertices[sCurrentQuad * 4 + i] = QuadVertex(transform * quadPositions[i], modulate, - quadUVs[i], texIdx); - - immutable uint currentQuadIndex = cast(uint) sCurrentQuad * 4; - immutable size_t baseIndex = sCurrentQuad * 6; - - sBatchIndices[baseIndex] = currentQuadIndex + 2; - sBatchIndices[baseIndex + 1] = currentQuadIndex + 1; - sBatchIndices[baseIndex + 2] = currentQuadIndex; - sBatchIndices[baseIndex + 3] = currentQuadIndex; - sBatchIndices[baseIndex + 4] = currentQuadIndex + 3; - sBatchIndices[baseIndex + 5] = currentQuadIndex + 2; - - ++sCurrentQuad; - - version (Profiling) ++Profiler.sRenderData.rectCount; - } - - /// Draws some text to screen. - /// - /// Params: - /// text = The text to draw. May be of any string type. - /// font = The font to use. - /// position = 2D position where to draw the text to. - /// modulate = The color of the text. - /// alignment = How to align the text. Horizontal and vertical alignment can be OR'd together. - void drawText(T)(in T text, in Font font, in Vector2f position, in Color modulate = Color.white, - ubyte alignment = Font.Alignment.left | Font.Alignment.top) - if (isSomeString!T) - in (text && font) - { - Vector2f cursor = Vector2f(0); - - if (alignment & Font.Alignment.middle || alignment & Font.Alignment.bottom) - { - immutable int height = font.getTextHeight(text); - cursor.y -= (alignment & Font.Alignment.middle) ? height / 2 : height; - } - - foreach (T line; text.lineSplitter) - { - if (alignment & Font.Alignment.center || alignment & Font.Alignment.right) - { - immutable int width = font.getTextWidth(line); - cursor.x = -((alignment & Font.Alignment.center) ? width / 2 : width); - } - else - cursor.x = 0; - - for (size_t i; i < line.length; ++i) - { - switch (line[i]) - { - case '\t': - cursor.x += 40; - break; - - default: - BMFont.Char c = font.bmFont.getChar(line[i]); - if (c == BMFont.Char.init) - break; - - immutable int kerning = i > 0 ? font.bmFont.getKerning(line[i - 1], line[i]) : 1; - - const(Texture2D) pageTexture = font.getPageTexture(c.page); - immutable Vector2f size = pageTexture.size; - - immutable Rect2f region = Rect2f(cast(float) c.x / size.x, cast(float) c.y / size.y, - cast(float) (c.x + c.width) / size.x, cast(float) (c.y + c.height) / size.y); - - drawRect(Rect2f(0, 0, c.width, c.height), Vector2f(position + cursor + Vector2f(c.xoffset, c.yoffset)), - Vector2f(1), modulate, pageTexture, region); - - cursor.x += c.xadvance + kerning; - } - } - - cursor.y += font.bmFont.common.lineHeight; - } - } -} - -package(zyeware.rendering.renderer): - -debug -{ - /// This enum and associated variable is used to keep track - /// which of the renderers has the current "context", as mixing - /// submit and render calls is undocumented behavior. - enum CurrentRenderer : ubyte - { - none, - renderer2D, - renderer3D - } - - CurrentRenderer currentRenderer; -} \ No newline at end of file diff --git a/platform/opengl/renderer3d.d b/platform/opengl/renderer3d.d deleted file mode 100644 index 41c83ab..0000000 --- a/platform/opengl/renderer3d.d +++ /dev/null @@ -1,283 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.renderer.renderer3d; - -import std.typecons : Rebindable; -import std.exception : enforce; - -import zyeware.common; -import zyeware.rendering; - -debug import zyeware.rendering.renderer.renderer2d : currentRenderer, CurrentRenderer; - -/// This struct gives access to the 3D rendering API. -struct Renderer3D -{ - @disable this(); - @disable this(this); - -private static: - enum maxMaterialsPerBatch = 10; - enum maxBufferGroupsPerMaterial = 10; - enum maxTransformsPerBufferGroup = 10; - - struct MaterialBatch - { - Material material; - BufferGroupBatch[maxBufferGroupsPerMaterial] bufferGroupBatches; - size_t currentBufferGroupBatch; - } - - struct BufferGroupBatch - { - BufferGroup bufferGroup; - Matrix4f[maxTransformsPerBufferGroup] transforms; - size_t currentTransform; - } - - Matrix4f sActiveViewMatrix; - Matrix4f sActiveProjectionMatrix; - Rebindable!(Environment3D) sActiveEnvironment; - - MaterialBatch[maxMaterialsPerBatch] sMaterialBatches; - size_t sCurrentMaterialBatch; - ConstantBuffer sMatricesBuffer, sLightsBuffer, sEnvironmentBuffer; - - void renderSky(Renderable sky) - in (sky, "Argument cannot be null.") - { - // Eliminate translation from current view matrix - Matrix4f viewMatrix = sActiveViewMatrix; - viewMatrix[0][3] = 0f; - viewMatrix[1][3] = 0f; - viewMatrix[2][3] = 0f; - - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("view"), viewMatrix.matrix); - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("mvp"), - (sActiveProjectionMatrix * viewMatrix).matrix); - - sky.material.bind(); - sky.bufferGroup.bind(); - - RenderAPI.drawIndexed(sky.bufferGroup.indexBuffer.length); - } - -package(zyeware) static: - /// Initializes 3D rendering. - void initialize() - { - sMatricesBuffer = new ConstantBuffer(BufferLayout([ - BufferElement("mvp", BufferElement.Type.mat4), - BufferElement("projection", BufferElement.Type.mat4), - BufferElement("view", BufferElement.Type.mat4), - BufferElement("model", BufferElement.Type.mat4) - ])); - - sEnvironmentBuffer = new ConstantBuffer(BufferLayout([ - BufferElement("cameraPosition", BufferElement.Type.vec4), - BufferElement("ambientColor", BufferElement.Type.vec4), - BufferElement("fogColor", BufferElement.Type.vec4), - ])); - - sLightsBuffer = new ConstantBuffer(BufferLayout([ - BufferElement("position", BufferElement.Type.vec4, maxLights), - BufferElement("color", BufferElement.Type.vec4, maxLights), - BufferElement("attenuation", BufferElement.Type.vec4, maxLights), - BufferElement("count", BufferElement.Type.int_), - ])); - - // Make sure to always initialize the count variable with 0, in case lights - // are not uploaded. - sLightsBuffer.setData(sLightsBuffer.getEntryOffset("count"), [0]); - } - - /// Cleans up all used resources. - void cleanup() - { - sMatricesBuffer.dispose(); - sEnvironmentBuffer.dispose(); - } - -public static: - /// How many lights can be rendered in one draw call. - enum maxLights = 10; - - /// Represents a light. - struct Light - { - public: - Vector3f position; /// The source position of the light. - Color color; /// The color of the light. - Vector3f attenuation; /// The attenuation of the light. - } - - /// Uploads a struct of light arrays to the rendering API for the next draw call. - /// - /// Params: - /// lights = The array of lights to upload. - void uploadLights(Light[] lights) - { - if (lights.length > maxLights) - { - Logger.core.log(LogLevel.warning, "Too many lights in scene."); - return; - } - - sLightsBuffer.setData(sLightsBuffer.getEntryOffset("count"), [cast(int) lights.length]); - RenderAPI.packLightConstantBuffer(sLightsBuffer, lights); - } - - /// Starts a 3D scene. This must be called before any 3D drawing commands. - /// - /// Params: - /// projectionMatrix = A 4x4 matrix used for projection. - /// viewMatrix = A 4x4 matrix used for view. - /// environment = The rendering environment for this scene. May be `null`. - /// depthTest = Whether to use depth testing for this scene or not. - void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix, Environment3D environment) - in (environment, "Environment cannot be null.") - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.none, - "A renderer is currently active, cannot begin."); - - sActiveProjectionMatrix = projectionMatrix; - sActiveViewMatrix = viewMatrix; - sActiveEnvironment = environment; - - sMatricesBuffer.bind(ConstantBuffer.Slot.matrices); - sEnvironmentBuffer.bind(ConstantBuffer.Slot.environment); - sLightsBuffer.bind(ConstantBuffer.Slot.lights); - - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("projection"), - sActiveProjectionMatrix); - - sEnvironmentBuffer.setData(sEnvironmentBuffer.getEntryOffset("cameraPosition"), - (sActiveViewMatrix.inverse * Vector4f(0, 0, 0, 1)).vector); - sEnvironmentBuffer.setData(sEnvironmentBuffer.getEntryOffset("ambientColor"), sActiveEnvironment.ambientColor.vector); - sEnvironmentBuffer.setData(sEnvironmentBuffer.getEntryOffset("fogColor"), sActiveEnvironment.fogColor.vector); - - RenderAPI.setFlag(RenderFlag.depthTesting, true); - - debug currentRenderer = CurrentRenderer.renderer3D; - } - - /// Ends a 3D scene. This must be called at the end of all 3D drawing commands, as it flushes - /// everything to the screen. - void end() - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer3D, - "3D renderer is not active, cannot end."); - - flush(); - - if (sActiveEnvironment.sky) - renderSky(sActiveEnvironment.sky); - - debug currentRenderer = CurrentRenderer.none; - } - - /// Flushes all currently cached drawing commands to the screen. - void flush() - { - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("view"), sActiveViewMatrix); - - for (size_t i; i < sCurrentMaterialBatch; ++i) - { - MaterialBatch* materialBatch = &sMaterialBatches[i]; - materialBatch.material.bind(); - - for (size_t j; j < materialBatch.currentBufferGroupBatch; ++j) - { - BufferGroupBatch* bufferGroupBatch = &materialBatch.bufferGroupBatches[j]; - bufferGroupBatch.bufferGroup.bind(); - - for (size_t k; k < bufferGroupBatch.currentTransform; ++k) - { - // TODO: Maybe look if this can be optimized. - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("model"), bufferGroupBatch.transforms[k].matrix); - sMatricesBuffer.setData(sMatricesBuffer.getEntryOffset("mvp"), (sActiveProjectionMatrix * sActiveViewMatrix - * bufferGroupBatch.transforms[k]).matrix); - RenderAPI.drawIndexed(bufferGroupBatch.bufferGroup.indexBuffer.length); - } - - bufferGroupBatch.currentTransform = 0; - } - - materialBatch.currentBufferGroupBatch = 0; - } - } - - /// Submits a draw command. - /// - /// Params: - /// renderable = The renderable instance to draw. - /// transform = A 4x4 matrix used for transformation. - pragma(inline, true) - void submit(Renderable renderable, in Matrix4f transform) - in (renderable) - { - submit(renderable.bufferGroup, renderable.material, transform); - } - - // TODO: Check constness! - /// Submits a draw command. - /// - /// Params: - /// group = The buffer group to draw. - /// material = The material to use for drawing. - /// transform = A 4x4 matrix used for transformation. - void submit(BufferGroup group, Material material, in Matrix4f transform) - in (group && material) - { - debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer3D, - "3D renderer is not active, cannot submit."); - - size_t i, j; - - retryInsert: - // Find fitting material batch first - for (; i < sCurrentMaterialBatch; ++i) - if (sMaterialBatches[i].material is material) - break; - - if (i == sCurrentMaterialBatch) - { - if (sCurrentMaterialBatch == maxMaterialsPerBatch) - flush(); - - sMaterialBatches[sCurrentMaterialBatch++].material = material; - } - - MaterialBatch* materialBatch = &sMaterialBatches[i]; - - // Find fitting buffer group batch next - for (; j < materialBatch.currentBufferGroupBatch; ++j) - if (materialBatch.bufferGroupBatches[j].bufferGroup is group) - break; - - if (j == materialBatch.currentBufferGroupBatch) - { - if (materialBatch.currentBufferGroupBatch == maxBufferGroupsPerMaterial) - { - flush(); - goto retryInsert; - } - - materialBatch.bufferGroupBatches[materialBatch.currentBufferGroupBatch++].bufferGroup = group; - } - - BufferGroupBatch* bufferGroupBatch = &materialBatch.bufferGroupBatches[j]; - - // Add transform last. - if (bufferGroupBatch.currentTransform == maxTransformsPerBufferGroup) - { - flush(); - goto retryInsert; - } - - bufferGroupBatch.transforms[bufferGroupBatch.currentTransform++] = transform; - } -} \ No newline at end of file diff --git a/source/zyeware/audio/api.d b/source/zyeware/audio/api.d new file mode 100644 index 0000000..0f01d26 --- /dev/null +++ b/source/zyeware/audio/api.d @@ -0,0 +1,103 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.audio.api; + +import zyeware.common; +import zyeware.audio; + +/// Used for selecting an audio backend at the start of the engine. +enum AudioBackend +{ + headless, /// A dummy API, does nothing. + openAl /// Used OpenAL for audio playback. +} + +/// Allows direct access to the audio API. +struct AudioAPI +{ + @disable this(); + @disable this(this); + +package(zyeware) static: + void function() sInitializeImpl; + void function() sLoadLibrariesImpl; + void function() sCleanupImpl; + + AudioBus function(string) sGetBusImpl; + AudioBus function(string) sAddBusImpl; + void function(string) sRemoveBusImpl; + Vector3f function() nothrow sGetListenerLocationImpl; + void function(Vector3f) nothrow sSetListenerLocationImpl; + + Sound function(const(ubyte)[], AudioProperties) sCreateSoundImpl; + Sound function(string) sLoadSoundImpl; + AudioSource function(AudioBus) sCreateAudioSourceImpl; + + pragma(inline, true) + void initialize() + { + sInitializeImpl(); + } + + pragma(inline, true) + void loadLibraries() + { + sLoadLibrariesImpl(); + } + + pragma(inline, true) + void cleanup() + { + sCleanupImpl(); + } + +public static: + /// Requests an already registered bus from the audio subsystem. + /// Params: + /// name = The name of the requested bus. + /// Returns: The bus registered with the given name. + /// Throws: `AudioException` if no bus with the given name exists. + pragma(inline, true) + AudioBus getBus(string name) + { + return sGetBusImpl(name); + } + + /// Adds a new audio bus with the given name to the audio subsystem. + /// Params: + /// name = The name to register the new bus with. + /// Returns: The newly registered bus. + /// Throws: `AudioException` if a bus with the given name already exists. + pragma(inline, true) + AudioBus addBus(string name) + { + return sAddBusImpl(name); + } + + /// Removes the bus with the given name from the audio subsystem. If no such + /// bus exists, nothing happens. + /// Params: + /// name = The name of the bus to remove. + pragma(inline, true) + void removeBus(string name) + { + sRemoveBusImpl(name); + } + + /// The location of the audio listener in 3D space. + pragma(inline, true) + Vector3f listenerLocation() nothrow + { + return sGetListenerLocationImpl(); + } + + /// ditto + pragma(inline, true) + void listenerLocation(Vector3f value) nothrow + { + return sSetListenerLocationImpl(value); + } +} \ No newline at end of file diff --git a/source/zyeware/audio/api.di b/source/zyeware/audio/api.di deleted file mode 100644 index 83fc48e..0000000 --- a/source/zyeware/audio/api.di +++ /dev/null @@ -1,28 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.audio.api; - -import zyeware.common; -import zyeware.audio; - -struct AudioAPI -{ - @disable this(); - @disable this(this); - -package(zyeware) static: - void initialize(); - void loadLibraries(); - void cleanup(); - -public static: - AudioBus getBus(string name); - AudioBus addBus(string name); - void removeBus(string name); - - Vector3f listenerLocation() nothrow; - void listenerLocation(Vector3f value) nothrow; -} \ No newline at end of file diff --git a/source/zyeware/audio/buffer.d b/source/zyeware/audio/buffer.d new file mode 100644 index 0000000..87cf20c --- /dev/null +++ b/source/zyeware/audio/buffer.d @@ -0,0 +1,86 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.audio.buffer; + +import std.sumtype; + +import zyeware.common; +import zyeware.audio; + +deprecated("This was a joke.") +{ + /// Joke alias. Do not use in production. + alias NoiseBitties = Sound; + /// Joke alias. Do not use in production. + alias AirVibrationData = Sound; + /// Joke alias. Do not use in production. + alias EarMassager = Sound; + /// Joke alias. Do not use in production. + alias SonicStream = Sound; + /// Joke alias. Do not use in production. + alias SoundFrame = Sample; + /// Joke alias. Do not use in production. + alias EarDrumPosition = Sample; + /// Joke alias. Do not use in production. + alias AirPressure = Sample; + /// Joke alias. Do not use in production. + alias SpeakerCoilCurrent = Sample; +} + +/// Contains information about a loop point for a module sound file. +struct ModuleLoopPoint +{ + int pattern; /// The pattern to loop from. + int row; /// The row to loop from. +} + +/// Represents an audio sample position. +alias Sample = int; + +/// A SumType for a loop point, containing either a sample position (`int`) or +/// pattern and row (`ModuleLoopPoint`). +alias LoopPoint = SumType!(Sample, ModuleLoopPoint); + +/// Contains various data for Sound initialisation. +struct AudioProperties +{ + LoopPoint loopPoint = LoopPoint(0); /// The point to loop at. It differentiates between a sample or pattern & row (for modules) +} + +/// Contains an encoded audio segment, plus various information like +/// loop point etc. +@asset(Yes.cache) +interface Sound +{ +public: + /// The point where this sound should loop, if played through an `AudioSource`. + LoopPoint loopPoint() pure const nothrow; + + /// ditto + void loopPoint(LoopPoint value) pure nothrow; + + /// The encoded audio data. + const(ubyte)[] encodedMemory() pure nothrow; + + /// Creates a new sound buffer with the given data. + /// Params: + /// encodedMemory = An array of unsigned bytes that contains the encoded audio data. + /// properties = Instance of an `AudioProperties` struct to initialize the Sound with. + static Sound create(const(ubyte)[] encodedMemory, AudioProperties properties = AudioProperties.init) + { + return AudioAPI.sCreateSoundImpl(encodedMemory, properties); + } + + /// Loads a sound from a given VFS path. + /// Params: + /// path = The path inside the VFS. + /// Returns: A newly created `Sound` instance. + /// Throws: `VFSException` if the given file can't be loaded. + static Sound load(string path) + { + return AudioAPI.sLoadSoundImpl(path); + } +} \ No newline at end of file diff --git a/source/zyeware/audio/buffer.di b/source/zyeware/audio/buffer.di deleted file mode 100644 index a848722..0000000 --- a/source/zyeware/audio/buffer.di +++ /dev/null @@ -1,34 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.audio.buffer; - -import std.sumtype; - -import zyeware.common; -import zyeware.audio; - -deprecated("This was a joke.") -{ - alias NoiseBitties = Audio; - alias AirVibrationData = Audio; - alias EarMassager = Audio; - alias SonicStream = Audio; -} - -@asset(Yes.cache) -class Audio -{ -public: - this(const(ubyte)[] encodedMemory, AudioProperties properties = AudioProperties.init); - - LoopPoint loopPoint() pure const nothrow; - - void loopPoint(LoopPoint value) pure nothrow; - - const(ubyte)[] encodedMemory() pure nothrow; - - static Audio load(string path); -} \ No newline at end of file diff --git a/source/zyeware/audio/bus.d b/source/zyeware/audio/bus.d index 7452687..e1be559 100644 --- a/source/zyeware/audio/bus.d +++ b/source/zyeware/audio/bus.d @@ -10,6 +10,7 @@ import std.algorithm : clamp; import zyeware.common; import zyeware.audio.thread; +/// Controls the mixing of various sounds which are assigned to this audio bus, class AudioBus { protected: @@ -17,11 +18,14 @@ protected: float mVolume = 1; public: + /// Params: + /// name = The name of this audio bus. this(string name) pure nothrow { mName = name; } + /// The name of this audio bus, as registered in the audio subsystem. string name() const nothrow { return mName; diff --git a/source/zyeware/audio/package.d b/source/zyeware/audio/package.d index 4672484..831f7c3 100644 --- a/source/zyeware/audio/package.d +++ b/source/zyeware/audio/package.d @@ -9,7 +9,6 @@ public { import zyeware.audio.api; import zyeware.audio.bus; - import zyeware.audio.properties; import zyeware.audio.buffer; import zyeware.audio.source; } \ No newline at end of file diff --git a/source/zyeware/audio/properties.d b/source/zyeware/audio/properties.d deleted file mode 100644 index 6ea2b59..0000000 --- a/source/zyeware/audio/properties.d +++ /dev/null @@ -1,24 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.audio.properties; - -import std.sumtype : SumType; - -import zyeware.common; -import zyeware.audio; - -struct ModuleLoopPoint -{ - int pattern; - int row; -} - -alias LoopPoint = SumType!(int, ModuleLoopPoint); - -struct AudioProperties -{ - LoopPoint loopPoint = LoopPoint(0); /// The point to loop at. It differentiates between a frame or pattern & row (for modules) -} \ No newline at end of file diff --git a/source/zyeware/audio/source.d b/source/zyeware/audio/source.d new file mode 100644 index 0000000..fbe2b4a --- /dev/null +++ b/source/zyeware/audio/source.d @@ -0,0 +1,72 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.audio.source; + +import zyeware.common; +import zyeware.audio; + +/// Represents an individual source that can play sounds. Only one sound +/// can play at a time. +interface AudioSource +{ +public: + /// Represents what state the `AudioSource` is currently in. + enum State + { + stopped, /// Currently no playback. + paused, /// Playback was paused, `play()` resumes. + playing /// Currently playing audio. + } + + /// Starts playback, or resumes if the source has been paused previously. + void play(); + /// Pauses playback. If playback wasn't started, nothing happens. + void pause(); + /// Stops playback completely. If playback wasn't started, nothing happens. + void stop(); + + /// The `Sound` instance assigned to this source. + inout(Sound) sound() inout; + + /// ditto + void sound(Sound value); + + /// Determines whether the source is looping it's sound. The loop point is defined by + /// the assigned `Sound`. + bool looping() pure const nothrow; + + /// ditto + void looping(bool value) pure nothrow; + + /// The volume of this source, ranging from 0 to 1. + float volume() pure const nothrow; + + /// ditto + void volume(float value) nothrow; + + /// The pitch of this source, ranging from 0 to 1. + /// This controls pitch as well as speed. + float pitch() pure const nothrow; + + /// ditto + void pitch(float value) nothrow; + + /// The state this source is currently in. + State state() pure const nothrow; + + /// ZyeWare internal call, do not use! + void updateBuffers(); + /// ZyeWare internal call, do not use! + void updateVolume(); + + /// Creates a new audio source. + /// Params: + /// bus = The audio bus this source belongs to. + static AudioSource create(AudioBus bus = null) + { + return AudioAPI.sCreateAudioSourceImpl(bus); + } +} \ No newline at end of file diff --git a/source/zyeware/audio/source.di b/source/zyeware/audio/source.di deleted file mode 100644 index d30efe6..0000000 --- a/source/zyeware/audio/source.di +++ /dev/null @@ -1,46 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.audio.source; - -import zyeware.common; -import zyeware.audio; - -class AudioSource -{ -package(zyeware): - void updateBuffers(); - void updateVolume(); - -public: - enum State - { - stopped, - paused, - playing - } - - this(AudioBus bus = null); - - ~this(); - - void play(); - void pause(); - void stop(); - - inout(Audio) audio() pure inout nothrow; - void audio(Audio value) pure nothrow; - - bool looping() pure const nothrow; - void looping(bool value) pure nothrow; - - float volume() pure const nothrow; - void volume(float value) nothrow; - - float pitch() pure const nothrow; - void pitch(float value) nothrow; - - State state() pure const nothrow; -} \ No newline at end of file diff --git a/source/zyeware/audio/thread.d b/source/zyeware/audio/thread.d index 101a3c2..8f42eab 100644 --- a/source/zyeware/audio/thread.d +++ b/source/zyeware/audio/thread.d @@ -1,6 +1,6 @@ module zyeware.audio.thread; -import core.thread : Thread, Duration, msecs; +import core.thread : Thread, Duration, msecs, thread_detachThis, rt_moduleTlsDtor; import std.algorithm : countUntil, remove; import zyeware.common; @@ -9,17 +9,23 @@ import zyeware.core.weakref; debug import std.stdio; -package(zyeware) struct AudioThread +package(zyeware) +struct AudioThread { private static: Thread sThread; - __gshared WeakReference!AudioSource[] sRegisteredSources; + __gshared WeakReference!AudioSource[64] sRegisteredSources; + __gshared size_t sRegisteredSourcesPointer; __gshared bool sRunning; __gshared Object sMutex = new Object(); void threadBody() { + thread_detachThis(); + // Just as a safety precaution, we want to call the TLS destructors. + scope (exit) rt_moduleTlsDtor(); + // Determine the sleep time between updating the buffers. // YukieVT supplied the following formula for this: // (BuffTotalLen / BuffCount) / SampleRate / 2 * 1000 @@ -32,13 +38,13 @@ private static: { synchronized (sMutex) { - for (size_t i; i < sRegisteredSources.length; ++i) + for (size_t i; i < sRegisteredSourcesPointer; ++i) { if (!sRegisteredSources[i].alive) { debug writefln("Removing source #%d...", i); - sRegisteredSources[i] = sRegisteredSources[$ - 1]; - --sRegisteredSources.length; + sRegisteredSources[i] = sRegisteredSources[sRegisteredSourcesPointer - 1]; + --sRegisteredSourcesPointer; --i; continue; } @@ -56,7 +62,8 @@ package(zyeware): { synchronized (sMutex) { - sRegisteredSources ~= weakReference(source); + assert(sRegisteredSourcesPointer < sRegisteredSources.length, "Cannot add more sources."); + sRegisteredSources[sRegisteredSourcesPointer++] = weakReference(source); } } @@ -74,7 +81,6 @@ package(zyeware): } } -public static: void initialize() { sRunning = true; diff --git a/source/zyeware/core/application.d b/source/zyeware/core/application.d index 5e3a240..63d0127 100644 --- a/source/zyeware/core/application.d +++ b/source/zyeware/core/application.d @@ -93,9 +93,12 @@ public: } /// Change the current state to the given one. + /// This method should not be called during event emission. Use a deferred call + /// for this purpose. /// /// Params: /// state = The game state to switch to. + /// See_Also: ZyeWare.callDeferred void changeState(GameState state) in (state, "Game state cannot be null.") { @@ -112,9 +115,12 @@ public: } /// Pushes the given state onto the stack, and switches to it. + /// This method should not be called during event emission. Use a deferred call + /// for this purpose. /// /// Params: /// state = The state to push and switch to. + /// See_Also: ZyeWare.callDeferred void pushState(GameState state) in (state, "Game state cannot be null.") { @@ -131,6 +137,9 @@ public: } /// Pops the current state from the stack, restoring the previous state. + /// This method should not be called during event emission. Use a deferred call + /// for this purpose. + /// See_Also: ZyeWare.callDeferred void popState() { debug if (ZyeWare.isEmittingEvent) @@ -144,12 +153,14 @@ public: ZyeWare.collect(); } + /// The current game state. pragma(inline, true) GameState currentState() { return mStateStack.peek; } + /// If this application currently has a game state loaded. pragma(inline, true) bool hasState() const nothrow { diff --git a/source/zyeware/core/asset.d b/source/zyeware/core/asset.d index 081757a..3967a12 100644 --- a/source/zyeware/core/asset.d +++ b/source/zyeware/core/asset.d @@ -24,15 +24,15 @@ private template isAsset(E) import std.traits : hasUDA; static if(__traits(compiles, hasUDA!(E, asset))) - enum bool isAsset = hasUDA!(E, asset) && __traits(compiles, cast(E) E.load("test")) && is(E : Object); + enum bool isAsset = hasUDA!(E, asset) && __traits(compiles, cast(E) E.load("test"));// && is(E : Object); else enum bool isAsset = false; } /// Responsible for loading assets into memory. It caches all loaded assets, therefore /// it will not invoke a loader again as long as the reference in memory is valid. -/// The `AssetManager` will only keep weak references, e.g. it will not cause memory -/// to not be freed. +/// The `AssetManager` will only keep weak references, e.g. it will not keep an unused +/// asset from being collected by the GC. struct AssetManager { @disable this(); @@ -45,7 +45,8 @@ private static: string path; } - alias LoadFunction = Tuple!(Object function(string), "callback", bool, "cache"); + alias LoadCallback = Object function(string); + alias LoadFunction = Tuple!(LoadCallback, "callback", bool, "cache"); LoadFunction[string] sLoaders; WeakReference!Object[AssetUID] sCache; @@ -56,19 +57,21 @@ package(zyeware.core) static: //registerDefaultLoaders(); import zyeware.rendering : Shader, Image, Texture2D, TextureCubeMap, Mesh, Font, Material, SpriteFrames, Cursor; import zyeware.core.translation : Translation; - import zyeware.audio : Audio; - - register!Shader(); - register!Image(); - register!Texture2D(); - register!TextureCubeMap(); - register!Mesh(); - register!Font(); - register!Material(); - register!Translation(); - register!Audio(); - register!SpriteFrames(); - register!Cursor(); + import zyeware.audio : Sound; + + register!Shader((path) => cast(Object) Shader.load(path)); + register!Texture2D((path) => cast(Object) Texture2D.load(path)); + register!TextureCubeMap((path) => cast(Object) TextureCubeMap.load(path)); + + register!Sound((path) => cast(Object) Sound.load(path)); + + register!Image(&Image.load); + register!Mesh(&Mesh.load); + register!Font(&Font.load); + register!Material(&Material.load); + register!Translation(&Translation.load); + register!SpriteFrames(&SpriteFrames.load); + register!Cursor(&Cursor.load); } void cleanup() @@ -94,6 +97,8 @@ public static: LoadFunction* loader = fqn in sLoaders; enforce!CoreException(loader, format!"'%s' was not registered as an asset."(fqn)); + path = TranslationManager.remapAssetPath(path); + auto uid = AssetUID(fqn, path); // Check if we have it cached, and if so, if it's still alive @@ -120,13 +125,13 @@ public static: /// /// Params: /// T = The asset type to register. - void register(T)() + void register(T)(LoadCallback callback) if (isAsset!T) { import std.traits : getUDAs; auto data = getUDAs!(T, asset)[0]; - sLoaders[fullyQualifiedName!T] = LoadFunction(&T.load, data.cache); + sLoaders[fullyQualifiedName!T] = LoadFunction(callback, data.cache); } /// Unregisters an asset. @@ -174,7 +179,7 @@ public static: if (!sCache[key].alive) { sCache.remove(key).assumeWontThrow; - Logger.core.log(LogLevel.trace, "Uncaching '%s' (%s)...", key.path, key.typeFQN); + Logger.core.log(LogLevel.verbose, "Uncaching '%s' (%s)...", key.path, key.typeFQN); ++cleaned; } } diff --git a/source/zyeware/core/debugging/info.d b/source/zyeware/core/debugging/info.d index 11a6082..cf80fbc 100644 --- a/source/zyeware/core/debugging/info.d +++ b/source/zyeware/core/debugging/info.d @@ -5,6 +5,7 @@ import core.memory; import zyeware.common; version (Profiling) +package(zyeware) struct DebugInfoManager { @disable this(); diff --git a/source/zyeware/core/debugging/profiler.d b/source/zyeware/core/debugging/profiler.d index 395319c..49d2956 100644 --- a/source/zyeware/core/debugging/profiler.d +++ b/source/zyeware/core/debugging/profiler.d @@ -10,25 +10,48 @@ import std.datetime : Duration; import zyeware.common; -version (Profiling) +version (Profiling): + +/// Contains various functions for profiling. struct Profiler { private static: - Result[] sResults; + Data[2] sData; + size_t sReadDataPointer; + size_t sWriteDataPointer = 1; package(zyeware) static: - RenderData sRenderData; + struct Data + { + RenderData renderData; + Result[] results; + } + + struct RenderData + { + size_t drawCalls; + size_t polygonCount; + size_t rectCount; + } + ushort sFPS; void initialize() nothrow { - sResults.reserve(200); } - void clear() nothrow + void clearAndSwap() nothrow { - sResults.length = 0; - sRenderData = RenderData.init; + if (++sReadDataPointer == sData.length) + sReadDataPointer = 0; + + if (++sWriteDataPointer == sData.length) + sWriteDataPointer = 0; + + Data* data = currentWriteData; + + data.results.length = 0; + data.renderData = RenderData.init; } public static: @@ -38,6 +61,8 @@ public static: immutable Duration duration; } + /// Represents a profiling timer, allowing easy profiling of code sections. + /// Use the mixins `ProfileScope` and `ProfileFunction` for convenience. struct Timer { private: @@ -45,6 +70,8 @@ public static: immutable string mName; public: + /// Params: + /// name = The name of the section that is timed. this(string name) nothrow in (name, "Name cannot be null.") { @@ -53,37 +80,37 @@ public static: mWatch.start(); } + /// Stops the timer. void stop() nothrow { mWatch.stop(); - Profiler.sResults ~= Profiler.Result(mName, mWatch.peek); + Profiler.currentWriteData.results ~= Profiler.Result(mName, mWatch.peek); } } - struct RenderData - { - size_t drawCalls; - size_t polygonCount; - size_t rectCount; - } - - Result[] results() nothrow + /// Returns the profiling data from last frame. This should only be read from. + const(Data)* currentReadData() nothrow { - return sResults; + return &sData[sReadDataPointer]; } - RenderData renderData() nothrow + /// Returns the profiling data of the current frame. This should only be written to. + Data* currentWriteData() nothrow { - return sRenderData; + return &sData[sWriteDataPointer]; } + /// The current frames per second. ushort fps() nothrow { return sFPS; } } +/// Convenience mixin template that creates a profiling timer for the +/// current function. If not assigned a custom name, it will take +/// the pretty name of the function. template ProfileFunction(string customName = null) { static if (!customName) @@ -97,6 +124,9 @@ template ProfileFunction(string customName = null) }`; } +/// Convenience mixin template that creates a profiling timer for +/// the enclosing scope. The name will always contain the function, +/// and if not given a custom name, also contains the line number. template ProfileScope(string customName = null) { static if (!customName) diff --git a/source/zyeware/core/engine.d b/source/zyeware/core/engine.d index 6f9c0f0..634549b 100644 --- a/source/zyeware/core/engine.d +++ b/source/zyeware/core/engine.d @@ -27,13 +27,17 @@ import zyeware.utils.format; import zyeware.core.startupapp; /// Struct that holds information about the project. +/// Note that the author name and project name are used to determine the save data directory. struct ProjectProperties { - string authorName = "Anonymous"; - string projectName = "ZyeWare Project"; + string authorName = "Anonymous"; /// The author of the game. Can be anything, from a person to a company. + string projectName = "ZyeWare Project"; /// The name of the project. - LogLevel coreLogLevel = LogLevel.trace; /// The log level for the core logger. - LogLevel clientLogLevel = LogLevel.trace; /// The log level for the client logger. + LogLevel coreLogLevel = LogLevel.verbose; /// The log level for the core logger. + LogLevel clientLogLevel = LogLevel.verbose; /// The log level for the client logger. + + RenderBackend renderBackend = RenderBackend.openGl; /// Determines the rendering backend used. + AudioBackend audioBackend = AudioBackend.openAl; /// Determines the audio backend used. Application mainApplication; /// The application to use. CrashHandler crashHandler; /// The crash handler to use. @@ -55,10 +59,10 @@ struct FrameTime /// Holds information about a SemVer version. struct Version { - int major; - int minor; - int patch; - string prerelease; + int major; /// The major release version. + int minor; /// The minor release version. + int patch; /// The patch version. + string prerelease; /// Any additional version declarations, e.g. "alpha". string toString() immutable pure { @@ -127,7 +131,7 @@ private static: { version (Profiling) { - Profiler.clear(); + Profiler.clearAndSwap(); scope (exit) ++fpsCounter; } @@ -177,7 +181,7 @@ private static: { FramebufferProperties fbProps; fbProps.size = sMainWindow.size; - sMainFramebuffer = new Framebuffer(fbProps); + sMainFramebuffer = Framebuffer.create(fbProps); sWindowProjection = Matrix4f.orthographic(0, sMainWindow.size.x, 0, sMainWindow.size.y, -1, 1); sFramebufferProjection = Matrix4f.orthographic(0, fbProps.size.x, fbProps.size.y, 0, -1, 1); @@ -246,6 +250,74 @@ private static: sMainWindow.swapBuffers(); } + void parseCmdArgs(string[] args, ref ProjectProperties properties) + { + import std.getopt : getopt, defaultGetoptPrinter, config; + import std.stdio : writeln, writefln; + import std.traits : EnumMembers; + import core.stdc.stdlib : exit; + + try + { + auto helpInfo = getopt(args, + config.passThrough, + "render-backend", "The rendering backend to use.", &properties.renderBackend, + "audio-backend", "The audio backend to use.", &properties.audioBackend, + "loglevel-core", "The minimum log level for engine logs to be displayed.", &properties.coreLogLevel, + "loglevel-client", "The minimum log level for game logs to be displayed.", &properties.clientLogLevel, + "target-frame-rate", "The ideal targeted frame rate.", &properties.targetFrameRate + ); + + if (helpInfo.helpWanted) + { + defaultGetoptPrinter(format!"ZyeWare Game Engine v%s"(engineVersion), helpInfo.options); + writeln("If no arguments are given, the selection of said options are to the disgression of the game developer."); + writeln("All arguments not understood by the engine are passed through to the game."); + writeln("------------------------------------------"); + writefln("Available log levels: %(%s, %)", [EnumMembers!LogLevel]); + writefln("Available rendering backends: %(%s, %)", [EnumMembers!RenderBackend]); + writefln("Available audio backends: %(%s, %)", [EnumMembers!AudioBackend]); + exit(0); + } + } + catch (Exception ex) + { + writeln("Could not parse arguments: ", ex.message); + writeln("Please use -h or --help to show information about the command line arguments."); + exit(1); + } + } + + void loadBackends(const ProjectProperties properties) + { + import zyeware.platform.opengl.impl; + import zyeware.platform.openal.impl; + + switch (properties.renderBackend) with (RenderBackend) + { + case openGl: + default: + version (ZWBackendOpenGL) + { + loadOpenGLBackend(); + break; + } + else throw new CoreException("ZyeWare has not been compiled with OpenGL support."); + } + + switch (properties.audioBackend) with (AudioBackend) + { + case openAl: + default: + version (ZWBackendOpenAL) + { + loadOpenALBackend(); + break; + } + else throw new CoreException("ZyeWare has not been compiled with OpenAL support."); + } + } + package(zyeware.core) static: CrashHandler crashHandler; @@ -253,12 +325,19 @@ package(zyeware.core) static: { GC.disable(); + parseCmdArgs(args, properties); + loadBackends(properties); + sCmdArgs = args; sProjectProperties = properties; - - sFrameTime = dur!"msecs"(1000 / properties.targetFrameRate); + targetFrameRate = properties.targetFrameRate; sRandom = new RandomNumberGenerator(); + // Initialize profiler and logger before anything else. + version (Profiling) Profiler.initialize(); + Logger.initialize(properties.coreLogLevel, properties.clientLogLevel); + + // Initialize crash handler afterwards because it relies on the logger. if (properties.crashHandler) crashHandler = properties.crashHandler; else @@ -268,13 +347,9 @@ package(zyeware.core) static: else crashHandler = new DefaultCrashHandler(); } - - // Initialize profiler and logger before anything else. - version (Profiling) Profiler.initialize(); - Logger.initialize(properties.coreLogLevel, properties.clientLogLevel); // Creates a new window and render context. - sMainWindow = new Window(properties.mainWindowProperties); + sMainWindow = Window.create(properties.mainWindowProperties); enforce!CoreException(sMainWindow, "Main window creation failed."); createFramebuffer(); @@ -321,14 +396,15 @@ package(zyeware.core) static: } public static: - immutable Version engineVersion = Version(0, 3, 0, "alpha"); + /// The current version of the engine. + immutable Version engineVersion = Version(0, 5, 0, "alpha"); /// How the framebuffer should be scaled on resizing. enum ScaleMode { center, /// Keep the original size at the center of the window. keepAspect, /// Scale with window, but keep the aspect. - fill, /// Fill the window completly. + fill, /// Fill the window completely. changeWindowSize /// Resize the framebuffer itself. } @@ -405,7 +481,9 @@ public static: bytesToString(memoryBeforeCollection - GC.stats().usedSize)); } - // TODO: Change name? + /// Changes the window size, respecting various window states with it (e.g. full screen, minimised etc.) + /// Params: + /// size = The new size of the window. void changeWindowSize(Vector2i size) in (size.x > 0 && size.y > 0, "Application size cannot be negative.") { @@ -454,6 +532,7 @@ public static: return sUpTime; } + /// The target framerate to hit. This is not a guarantee. void targetFrameRate(int fps) in (fps > 0, "Target FPS must be greater than 0.") { @@ -545,7 +624,7 @@ public static: /// The `ProjectProperties` the engine was started with. /// See_Also: ProjectProperties - const(ProjectProperties) projectProperties() nothrow + const(ProjectProperties) projectProperties() nothrow @nogc { return sProjectProperties; } diff --git a/source/zyeware/core/events/gui.d b/source/zyeware/core/events/gui.d index 2500d96..296a72b 100644 --- a/source/zyeware/core/events/gui.d +++ b/source/zyeware/core/events/gui.d @@ -10,99 +10,9 @@ import std.format : format; import zyeware.common; import zyeware.gui; -/* -abstract class VirtualCursorEvent : Event -{ -protected: - VirtualCursor mCursor; - - this(VirtualCursor cursor) pure nothrow - { - mCursor = cursor; - } - -public: - inout(VirtualCursor) cursor() @property pure inout nothrow - { - return mCursor; - } -} - -class VirtualCursorEventButton : VirtualCursorEvent -{ -protected: - Vector2f mPosition; - MouseCode mButton; - bool mIsPressed; - -public: - /// Params: - /// button = The mouse code of the button that was activated. - /// isPressed = Whether the button was pressed or not. - this(VirtualCursor cursor, Vector2f position, MouseCode button, bool isPressed) pure nothrow - { - super(cursor); - - mPosition = position; - mButton = button; - mIsPressed = isPressed; - } - - /// The current cursor position. - Vector2f position() @property const pure nothrow - { - return mPosition; - } - - /// The mouse code of the button that was activated. - MouseCode button() @property const pure nothrow - { - return mButton; - } - - /// Whether the button was pressed or not. - bool isPressed() @property const pure nothrow - { - return mIsPressed; - } - - override string toString() const - { - return format!"VirtualCursorEventButton(cursor: %s, button: %d, isPressed: %s)"(mCursor, mButton, mIsPressed); - } -} - -class VirtualCursorEventMotion : VirtualCursorEvent -{ -protected: - Vector2f mPosition; - -public: - /// Params: - /// window = The window this event was sent from. - /// position = The current cursor position. - this(VirtualCursor cursor, Vector2f position) pure nothrow - { - super(cursor); - - mPosition = position; - } - - /// The current cursor position. - Vector2f position() @property const pure nothrow - { - return mPosition; - } - - override string toString() const - { - return format!"VirtualCursorEventMotion(cursor: %s, position: %s)"(mCursor, mPosition); - } -} -*/ - // ============================================================== +/// Represents any kind of event that happened in the GUI layer. abstract class GUIEvent : Event { protected: @@ -114,12 +24,14 @@ protected: } public: + /// The `GUINode` instance that caused this event to occur. GUINode emitter() @property pure nothrow { return mEmitter; } } +/// This event is emitted when a GUI button is interacted with. class GUIEventButton : GUIEvent { protected: @@ -127,13 +39,18 @@ protected: MouseCode mButton; public: + /// The type of the interaction. enum Type { - pressed, - released, - clicked + pressed, /// Mouse button has been pressed down. + released, /// Mouse button has been depressed. + clicked /// A click sequence occurred, which happens if a press and release have occurred on the button. } + /// Params: + /// emitter = The node that caused this event to occur. + /// type = The type of interaction that happened. + /// button = The mouse button that caused the interaction. this(GUINode emitter, Type type, MouseCode button) { super(emitter); @@ -142,11 +59,13 @@ public: mButton = button; } + /// The type of interaction that happened. Type type() @property pure const nothrow { return mType; } + /// The mouse button that caused the interaction. MouseCode button() @property pure nothrow { return mButton; diff --git a/source/zyeware/core/events/window.d b/source/zyeware/core/events/window.d index ed62be7..112e824 100644 --- a/source/zyeware/core/events/window.d +++ b/source/zyeware/core/events/window.d @@ -88,25 +88,4 @@ public: { return format!"WindowMovedEvent(position: %s)"(mPosition); } -} - -version(none) -{ - class WindowOpenedEvent : WindowEvent - { - public: - this(Window window) pure nothrow - { - super(window); - } - } - - class WindowClosedEvent : WindowEvent - { - public: - this(Window window) pure nothrow - { - super(window); - } - } } \ No newline at end of file diff --git a/source/zyeware/core/interpolator.d b/source/zyeware/core/interpolator.d index f9a7663..28ec49f 100644 --- a/source/zyeware/core/interpolator.d +++ b/source/zyeware/core/interpolator.d @@ -5,6 +5,10 @@ import std.typecons : Tuple; import zyeware.common; +/// The `Interpolator` allows to create various keypoints on a +/// one dimensional line, and interpolating between them. +/// You can supply a custom type and lerping function for various +/// different types. struct Interpolator(T, alias lerp) { protected: @@ -14,18 +18,24 @@ protected: bool mMustSortPoints; public: + /// Add a keypoint to the interpolator with the given offset and value. + /// Params: + /// offset = The offset of the keypoint. + /// value = The value of the keypoint. void addPoint(float offset, const T value) pure nothrow { mPoints ~= Point(offset, value); mMustSortPoints = true; } + /// Removes a keypoint with the given index. void removePoint(size_t idx) pure nothrow { mPoints = mPoints.remove(idx); mMustSortPoints = true; } + /// Removes all keypoints from this interpolator. void clearPoints() pure nothrow { mPoints.length = 0; @@ -33,6 +43,10 @@ public: // Thanks to the Godot Engine for this code! // https://github.com/godotengine/godot/blob/master/scene/resources/gradient.h + /// Interpolates a value from the keypoints in this interpolator from the given offset. + /// Params: + /// offset = The offset to use. + /// Returns: The interpolated value. T interpolate(float offset) pure nothrow { if (mPoints.length == 0) diff --git a/source/zyeware/core/logging.d b/source/zyeware/core/logging.d index 697d3a9..80a28ac 100644 --- a/source/zyeware/core/logging.d +++ b/source/zyeware/core/logging.d @@ -19,26 +19,16 @@ import terminal; import zyeware.common; -// LogLevel usage is as follows: -// -// Fatal: Extremely severe incidents which almost certainly are followed by a crash. -// Error: Severe incidents that can impact the stability of the application. -// Warning: Incidents that can impact the usability of the application. -// Info: Messages with useful information when troubleshooting, but which have no -// visible effect on the application itself. -// Debug: Messages useful for debugging, containing information a normal user wouldn't -// make much sense of. -// Trace: Used when traversing through code, "tracing" each step with messages. - +/// The log level to use for various logs. enum LogLevel { - off, - fatal, - error, - warning, - info, - debug_, - trace + off, /// No logs should go through. This is only useful for setting a "minimum log level." + fatal, /// Extremely severe incidents which almost certainly are followed by a crash. + error, /// Severe incidents that can impact the stability of the application. + warning, // Incidents that can impact the usability of the application. + info, // Messages with useful information when troubleshooting, but which have no visible effect on the application itself. + debug_, // Messages useful for debugging, containing information a normal user wouldn't make much sense of. + verbose /// Used when logging very minute details. } private immutable dstring[] levelNames = [ @@ -47,9 +37,10 @@ private immutable dstring[] levelNames = [ "Warning", "Info", "Debug", - "Trace" + "Verbose" ]; +/// Represents a single logger, and also contains the standard core and client loggers. final class Logger { private: @@ -75,6 +66,10 @@ package(zyeware): } public: + /// Params: + /// baseSink = The log sink to use for writing messages. + /// logLevel = The minimum log level that should be logged. + /// name = The name of the logger. this(LogSink baseSink, LogLevel logLevel, dstring name) pure { addSink(baseSink); @@ -83,11 +78,16 @@ public: mName = name; } + /// Add a log sink to this logger. void addSink(LogSink sink) @trusted pure { mSinks ~= sink; } + /// Remove the specified log sink from this logger. + /// If the given sink doesn't exist, nothing happens. + /// Params: + /// sink = The sink to remove. void removeSink(LogSink sink) @trusted { for (size_t i; i < mSinks.length; ++i) @@ -98,6 +98,11 @@ public: } } + /// Writes a message to this log. + /// Params: + /// level = The log level the message should be written as. + /// message = The message itself. Can be a format string. + /// args = Arguments used for formatting. void log(S, T...)(LogLevel level, S message, T args) nothrow if (isSomeString!S) { @@ -129,44 +134,58 @@ public: } } + /// Flushes all log sinks connected to this log. void flush() { foreach (LogSink sink; mSinks) sink.flush(); } - static Logger client() nothrow + /// The default client logger. + static Logger client() nothrow { return sClientLogger; } - static LogSink defaultLogSink() nothrow + /// The default log sink that all loggers have as a base sink. + static LogSink defaultLogSink() nothrow { return sDefaultLogSink; } } +/// Represents a sink to write a message into. This can be either a file, a console, +/// a in-game window, etc. abstract class LogSink { public: + /// The data that should be logged. struct LogData { - dstring loggerName; - LogLevel level; - TimeOfDay time; - dstring message; + dstring loggerName; /// The name of the logger. + LogLevel level; /// The log level of the message. + TimeOfDay time; /// The time this message was sent. + dstring message; /// The message itself. } + /// Logs the given data. + /// Params: + /// data = The data to log. abstract void log(LogData data); + + /// Flushes the current sink. abstract void flush() ; } +/// Represents a log sink that logs into a real file. class FileLogSink : LogSink { private: File mFile; public: + /// Params: + /// file = The file to log into. this(File file) { mFile = file; @@ -179,12 +198,15 @@ public: } } +/// Represents a sink that writes to a (colour) terminal. class TerminalLogSink : LogSink { private: Terminal mTerm; public: + /// Params: + /// terminal = The terminal to log into. this(Terminal terminal) { mTerm = terminal; diff --git a/source/zyeware/core/main.d b/source/zyeware/core/main.d index ef54a85..d7e17f5 100644 --- a/source/zyeware/core/main.d +++ b/source/zyeware/core/main.d @@ -7,6 +7,8 @@ module zyeware.core.main; import core.stdc.stdlib; +import std.stdio : stderr; + import zyeware.common; import zyeware.core.application; @@ -19,7 +21,7 @@ version (unittest) } else { - int main(string[] args) + export int main(string[] args) { try { @@ -32,6 +34,9 @@ else { if (ZyeWare.crashHandler) ZyeWare.crashHandler.show(t); + else + stderr.writeln(t.toString()); + abort(); } } diff --git a/source/zyeware/core/math/matrix.d b/source/zyeware/core/math/matrix.d index 93abdb0..1f617ee 100644 --- a/source/zyeware/core/math/matrix.d +++ b/source/zyeware/core/math/matrix.d @@ -5,9 +5,12 @@ import inmath.linalg; import zyeware.common; +/// A quaternion with float values. alias Quaternionf = Quaternion!(float); +/// A 4x4 matrix with float values. alias Matrix4f = Matrix!(float, 4, 4); +/// A 3x3 matrix with float values. alias Matrix3f = Matrix!(float, 3, 3); /// Convert a 2D position from world to local space. diff --git a/source/zyeware/core/math/rect.d b/source/zyeware/core/math/rect.d index 4e6c620..cb26616 100644 --- a/source/zyeware/core/math/rect.d +++ b/source/zyeware/core/math/rect.d @@ -6,9 +6,10 @@ import inmath.linalg; import zyeware.common; +/// A two dimensional rectangle with float values. alias Rect2f = Rect!float; +/// A two dimensional rectangle with int values. alias Rect2i = Rect!int; -alias Rect2ui = Rect!uint; /// Represents an axis-aligned rectangle in 2D space. struct Rect(T) @@ -42,12 +43,18 @@ struct Rect(T) } /// Checks if a point falls within the rectangle. + /// Params: + /// v = The point to check for. + /// Returns: Whether the point is inside the rectangle. bool contains(VT v) pure nothrow const { return v.x >= min.x && v.y >= min.y && v.x <= max.x && v.y <= max.y; } /// Check if any of the area bounded by this rectangle is bounded by another + /// Params: + /// b = The rectangle to check for. + /// Returns: Whether the rectangle is overlapping. bool overlaps(Rect!T b) pure nothrow const { // TODO check if this works (unit test!) @@ -58,6 +65,7 @@ struct Rect(T) /// Move the rectangle so it is entirely contained with another /// If the rectangle is moved it will always be flush with a border of the given area + // TODO: implement this! version(none) Rect!T constrain(Rect!T outer) const { diff --git a/source/zyeware/core/math/shape2d.d b/source/zyeware/core/math/shape2d.d index c900906..e1bbd8a 100644 --- a/source/zyeware/core/math/shape2d.d +++ b/source/zyeware/core/math/shape2d.d @@ -348,7 +348,7 @@ public: } /// A shape representing a rectangle in 2D space. -/// Using this in favor of a rectangle `PolygonShape2D` can increase performance. +/// Using this in favour of a rectangle `PolygonShape2D` can increase performance. /// See_Also: PolygonShape2D // TODO: Documentation is a lie. Improve performance! class RectangleShape2D : PolygonShape2D diff --git a/source/zyeware/core/math/vector.d b/source/zyeware/core/math/vector.d index f7bf67c..3a11085 100644 --- a/source/zyeware/core/math/vector.d +++ b/source/zyeware/core/math/vector.d @@ -7,13 +7,19 @@ public import inmath.linalg : dot, cross; import zyeware.common; +/// A two dimensional vector with float values. alias Vector2f = Vector!(float, 2); +/// A two dimensional vector with int values. alias Vector2i = Vector!(int, 2); +/// A three dimensional vector with float values. alias Vector3f = Vector!(float, 3); +/// A three dimensional vector with int values. alias Vector3i = Vector!(int, 3); +/// A four dimensional vector with float values. alias Vector4f = Vector!(float, 4); +/// A four dimensional vector with int values. alias Vector4i = Vector!(int, 4); /// Calculates the height on a specific point of a triangle using the barycentric algorithm. diff --git a/source/zyeware/core/random.d b/source/zyeware/core/random.d index 25bac80..1971e91 100644 --- a/source/zyeware/core/random.d +++ b/source/zyeware/core/random.d @@ -4,6 +4,8 @@ import std.traits : isFloatingPoint, isSigned, isNumeric, CommonType; import std.datetime : MonoTime; import std.random; +/// A simple implementation of a random number generator, with the option of +/// using a custom seed. class RandomNumberGenerator { protected: @@ -16,12 +18,19 @@ public: this(MonoTime.currTime().ticks); } + /// Params: + /// seed = The seed to initialise this RNG with. this(size_t seed) nothrow { this.seed = seed; } + /// Gets the next random number. + /// Params: + /// T = The type of number to generate. + /// Returns: The next generated random number. T get(T)() pure nothrow + if (isNumeric!T) { alias UIntType = typeof(mEngine.front); immutable UIntType next = mEngine.front; @@ -35,6 +44,11 @@ public: return cast(T) next; } + /// Generates a random number that lies between the range given. + /// Params: + /// min = The minimum number to return. + /// max = The maximum number to return, exclusive. + /// Returns: The generated random number. auto getRange(T, U)(T min, U max) pure nothrow if (isNumeric!T && isNumeric!U) { @@ -46,12 +60,14 @@ public: return cast(R) (min + max * multiplier); } - + + /// The seed of this random number generator. size_t seed() pure const nothrow { return mSeed; } + /// ditto void seed(size_t value) pure nothrow { mSeed = value; diff --git a/source/zyeware/core/startupapp.d b/source/zyeware/core/startupapp.d index e8d5e8f..aca2ee7 100644 --- a/source/zyeware/core/startupapp.d +++ b/source/zyeware/core/startupapp.d @@ -82,7 +82,7 @@ public: Renderer2D.drawRect(Rect2f(min, max), Matrix4f.identity, Color(1, 1, 1, alpha), mEngineLogo); - Renderer2D.drawText(mVersionString, mInternalFont, Vector2f(-1, -1), Color(1, 1, 1, alpha), + Renderer2D.drawString(mVersionString, mInternalFont, Vector2f(-1, -1), Color(1, 1, 1, alpha), Font.Alignment.left | Font.Alignment.bottom); Renderer2D.end(); diff --git a/source/zyeware/core/translation.d b/source/zyeware/core/translation.d index 7522b48..917ae0a 100644 --- a/source/zyeware/core/translation.d +++ b/source/zyeware/core/translation.d @@ -5,7 +5,7 @@ // Copyright 2021 ZyeByte module zyeware.core.translation; -import std.string : format; +import std.string : format, startsWith; import std.exception : enforce, assumeWontThrow; import sdlang; @@ -39,6 +39,20 @@ public static: return key; } + /// Remaps an asset path. + /// + /// Params: + /// origPath = The original VFS path to remap. + /// + /// Returns: The remapped asset, or `origPath` if noo remapping exists. + string remapAssetPath(string origPath) nothrow + { + if (sActiveLocale) + return sActiveLocale.remapAssetPath(origPath); + + return origPath; + } + /// Registers a locale to the manager. If the given locale is already registered, then /// the new translation gets merged into the existing one. /// @@ -94,6 +108,7 @@ public static: return sActiveLocale.mLocale; } + /// ditto void locale(string locale) in (locale, "Locale cannot be null.") { @@ -115,17 +130,17 @@ class Translation protected: string mLocale; string[string] mTranslations; + string[string] mAssetRemaps; public: /// Params: /// locale = The ISO 639-1 name for the language. - /// translations = Key-to-language translations in a AA. - this(string locale, string[string] translations) + /// translations = Key-to-language translations in an AA. + /// assetRemaps = The resource remaps in an AA. + this(string locale) in (locale, "Locale cannot be null.") - in (translations, "Translation AA cannot be null.") { mLocale = locale; - mTranslations = translations; } /// Adds a key with the corresponding translation to the locale. @@ -150,10 +165,30 @@ public: mTranslations.remove(key); } + /// Adds an asset remap path. + /// + /// Params: + /// origPath = The original path to remap + /// newPath = The new path + void addAssetRemap(string origPath, string newPath) pure + { + enforce!CoreException(VFS.isValidVFSPath(origPath) && VFS.isValidVFSPath(newPath), "Malformed VFS paths for asset remapping."); + enforce!CoreException(!origPath.startsWith("core://"), "Cannot remap assets from core package."); + + mAssetRemaps[origPath] = newPath; + } + + /// Removes an asset remap path. + void removeAssetRemap(string origPath) pure nothrow + { + mAssetRemaps.remove(origPath); + } + /// Tries to optimize all further key lookups. void optimize() pure nothrow { mTranslations = mTranslations.rehash; + mAssetRemaps = mAssetRemaps.rehash; } /// Translates a key to it's specified translation. @@ -168,6 +203,17 @@ public: return mTranslations.get(key, key).assumeWontThrow; } + /// Remaps a resource path. + /// + /// Params: + /// origPath = The original VFS path to remap. + /// + /// Returns: The remapped resource, or `origPath` if noo remapping exists. + string remapAssetPath(string origPath) pure const nothrow + { + return mAssetRemaps.get(origPath, origPath).assumeWontThrow; + } + /// The ISO 639-1 name of this locale. string locale() const nothrow { @@ -188,17 +234,36 @@ public: file.close(); immutable string locale = root.expectTagValue!string("locale"); - - string[string] translations; + auto translation = new Translation(locale); - foreach (Tag translationTag; root.all.tags.filter!(a => a.name == "translate")) + foreach (Tag tag; root.all.tags) { - immutable string old = translationTag.expectTag("old").expectValue!string; - immutable string new_ = translationTag.expectTag("new").expectValue!string; + switch (tag.name) + { + case "locale": + break; + + case "translate": + immutable string orig = tag.expectTag("old").expectValue!string; + immutable string new_ = tag.expectTag("new").expectValue!string; + + translation.addTranslation(orig, new_); + break; + + case "remap": + immutable string orig = tag.expectTag("old").expectValue!string; + immutable string new_ = tag.expectTag("new").expectValue!string; + + translation.addAssetRemap(orig, new_); + break; - translations[old] = new_; + default: + Logger.core.log(LogLevel.warning, "%s(%d): Unknown top-level declaration '%s'.", path, + tag.location.line, tag.name); + } } - return new Translation(locale, translations); + translation.optimize(); + return translation; } } \ No newline at end of file diff --git a/source/zyeware/ecs/system/collision2d.d b/source/zyeware/ecs/system/collision2d.d index e06d128..60e7ab1 100644 --- a/source/zyeware/ecs/system/collision2d.d +++ b/source/zyeware/ecs/system/collision2d.d @@ -155,7 +155,7 @@ public: immutable Rect2f bb = collision.shape.getBoundingBox(transform.globalMatrix); immutable Vector2f pos = transform.position; - Rect2ui cellExtremes = Rect2ui( + Rect2i cellExtremes = Rect2i( cast(uint) ((pos.x + bb.min.x) / mGridCellSize), cast(uint) ((pos.y + bb.min.y) / mGridCellSize), cast(uint) ((pos.x + bb.max.x) / mGridCellSize), diff --git a/source/zyeware/gui/button.d b/source/zyeware/gui/button.d index 5376a06..d2f0cba 100644 --- a/source/zyeware/gui/button.d +++ b/source/zyeware/gui/button.d @@ -21,13 +21,11 @@ protected: override void onCursorEnter() nothrow { - //cursor.shape = VirtualCursor.Shape.pointingHand; mColor = Color.red; } override void onCursorExit() nothrow { - //cursor.shape = VirtualCursor.Shape.arrow; mColor = Color.white; } diff --git a/source/zyeware/gui/label.d b/source/zyeware/gui/label.d index 1fd15a9..92e6acf 100644 --- a/source/zyeware/gui/label.d +++ b/source/zyeware/gui/label.d @@ -36,7 +36,7 @@ protected: override void customDraw(in FrameTime nextFrameTime) const { - Renderer2D.drawText(text, font, mTextPosition, color, mAlignment); + Renderer2D.drawString(text, font, mTextPosition, color, mAlignment); } override void updateArea(Rect2f parentArea) nothrow diff --git a/source/zyeware/platform/openal/api.d b/source/zyeware/platform/openal/api.d new file mode 100644 index 0000000..0ad4c1d --- /dev/null +++ b/source/zyeware/platform/openal/api.d @@ -0,0 +1,105 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.openal.api; + +version (ZWBackendOpenAL): +package(zyeware.platform.openal): + +import std.string : format, fromStringz; +import std.exception : enforce; + +import bindbc.openal; + +import zyeware.common; +import zyeware.audio; + +ALCdevice* pDevice; +ALCcontext* pContext; +AudioBus[string] pBusses; + +void apiInitialize() +{ + apiLoadLibraries(); + + apiAddBus("master"); + + enforce!AudioException(pDevice = alcOpenDevice(null), "Failed to create audio device."); + enforce!AudioException(pContext = alcCreateContext(pDevice, null), "Failed to create audio context."); + + enforce!AudioException(alcMakeContextCurrent(pContext), "Failed to make audio context current."); +} + +void apiLoadLibraries() +{ + import loader = bindbc.loader.sharedlib; + import std.string : fromStringz; + + if (isOpenALLoaded()) + return; + + immutable alResult = loadOpenAL(); + if (alResult != alSupport) + { + foreach (info; loader.errors) + Logger.core.log(LogLevel.warning, "OpenAL loader: %s", info.message.fromStringz); + + switch (alResult) + { + case ALSupport.noLibrary: + throw new AudioException("Could not find OpenAL shared library."); + + case ALSupport.badLibrary: + throw new AudioException("Provided OpenAL shared is corrupted."); + + default: + Logger.core.log(LogLevel.warning, "Got older OpenAL version than expected. This might lead to errors."); + } + } +} + +void apiCleanup() +{ + alcCloseDevice(pDevice); +} + +AudioBus apiGetBus(string name) + in (name, "Name cannot be null.") +{ + AudioBus result = pBusses.get(name, null); + enforce!AudioException(result, format!"No audio bus named '%s' exists."(name)); + + return result; +} + +AudioBus apiAddBus(string name) + in (name, "Name cannot be null.") +{ + enforce!AudioException(!(name in pBusses), format!"Audio bus named '%s' already exists."(name)); + + auto bus = new AudioBus(name); + pBusses[name] = bus; + + return bus; +} + +void apiRemoveBus(string name) + in (name, "Name cannot be null.") +{ + if (name in pBusses) + { + pBusses.remove(name); + } +} + +Vector3f apiGetListenerLocation() nothrow +{ + return Vector3f(0); +} + +void apiSetListenerLocation(Vector3f value) nothrow +{ + +} \ No newline at end of file diff --git a/platform/openal/buffer.d b/source/zyeware/platform/openal/buffer.d similarity index 89% rename from platform/openal/buffer.d rename to source/zyeware/platform/openal/buffer.d index 8b9fb97..da11a28 100644 --- a/platform/openal/buffer.d +++ b/source/zyeware/platform/openal/buffer.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.audio.buffer; +module zyeware.platform.openal.buffer; + +version (ZWBackendOpenAL): +package(zyeware.platform.openal): import std.sumtype; @@ -12,36 +15,20 @@ import bindbc.openal; import zyeware.common; import zyeware.audio; -@asset(Yes.cache) -class Audio +class OALSound : Sound { protected: const(ubyte)[] mEncodedMemory; LoopPoint mLoopPoint; -public: +package(zyeware.platform.openal): this(const(ubyte)[] encodedMemory, AudioProperties properties = AudioProperties.init) { mEncodedMemory = encodedMemory; mLoopPoint = properties.loopPoint; } - LoopPoint loopPoint() pure const nothrow - { - return mLoopPoint; - } - - void loopPoint(LoopPoint value) pure nothrow - { - mLoopPoint = value; - } - - const(ubyte)[] encodedMemory() pure nothrow - { - return mEncodedMemory; - } - - static Audio load(string path) + static Sound load(string path) { VFSFile source = VFS.getFile(path); ubyte[] bestCommunityData = source.readAll!(ubyte[])(); @@ -81,6 +68,22 @@ public: Logger.core.log(LogLevel.debug_, "Loaded file '%s' into memory for streaming.", path); - return new Audio(bestCommunityData, properties); + return new OALSound(bestCommunityData, properties); + } + +public: + LoopPoint loopPoint() pure const nothrow + { + return mLoopPoint; + } + + void loopPoint(LoopPoint value) pure nothrow + { + mLoopPoint = value; + } + + const(ubyte)[] encodedMemory() pure nothrow + { + return mEncodedMemory; } } \ No newline at end of file diff --git a/source/zyeware/platform/openal/impl.d b/source/zyeware/platform/openal/impl.d new file mode 100644 index 0000000..981a496 --- /dev/null +++ b/source/zyeware/platform/openal/impl.d @@ -0,0 +1,33 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.openal.impl; + +version (ZWBackendOpenAL): +package(zyeware): + +import zyeware.audio; + +import zyeware.platform.openal.api; +import zyeware.platform.openal.buffer; +import zyeware.platform.openal.source; + +void loadOpenALBackend() +{ + AudioAPI.sInitializeImpl = &apiInitialize; + AudioAPI.sLoadLibrariesImpl = &apiLoadLibraries; + AudioAPI.sCleanupImpl = &apiCleanup; + + AudioAPI.sAddBusImpl = &apiAddBus; + AudioAPI.sGetBusImpl = &apiGetBus; + AudioAPI.sRemoveBusImpl = &apiRemoveBus; + AudioAPI.sSetListenerLocationImpl = &apiSetListenerLocation; + AudioAPI.sGetListenerLocationImpl = &apiGetListenerLocation; + + AudioAPI.sCreateSoundImpl = (encMem, props) => new OALSound(encMem, props); + AudioAPI.sCreateAudioSourceImpl = (bus) => new OALAudioSource(bus); + + AudioAPI.sLoadSoundImpl = (path) => OALSound.load(path); +} \ No newline at end of file diff --git a/platform/openal/source.d b/source/zyeware/platform/openal/source.d similarity index 86% rename from platform/openal/source.d rename to source/zyeware/platform/openal/source.d index ae98809..e8c4a18 100644 --- a/platform/openal/source.d +++ b/source/zyeware/platform/openal/source.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.audio.source; +module zyeware.platform.openal.source; + +version (ZWBackendOpenAL): +package(zyeware.platform.openal): import std.exception : enforce; import std.algorithm : clamp; @@ -17,12 +20,12 @@ import zyeware.common; import zyeware.audio; import zyeware.audio.thread; -class AudioSource +class OALAudioSource : AudioSource { private: /// Loads up `mProcBuffer` and returns the amount of samples read. pragma(inline, true) - size_t readFromDecoder() + size_t readFromDecoder() @nogc in (mDecoder.isOpenForReading(), "Tried to decode while decoder is not open for reading.") { return mDecoder.readSamplesFloat(&mProcBuffer[0], cast(int)(mProcBuffer.length/mDecoder.getNumChannels())) @@ -32,109 +35,36 @@ private: protected: float[] mProcBuffer; - Audio mAudioStream; + Sound mSound; AudioStream mDecoder; uint mSourceId; uint[] mBufferIDs; int mProcessed; State mState; - float mVolume; - float mPitch; + float mVolume = 1f; + float mPitch = 1f; bool mLooping; AudioBus mBus; -package(zyeware): - final void updateBuffers() - { - if (mState == State.stopped) - return; - - size_t lastReadLength; - int processed; - uint pBuf; - alGetSourcei(mSourceId, AL_BUFFERS_PROCESSED, &processed); - - while (processed--) - { - alSourceUnqueueBuffers(mSourceId, 1, &pBuf); - - - lastReadLength = readFromDecoder(); - - if (lastReadLength <= 0) - { - if (mLooping) - { - mAudioStream.loopPoint.match!( - (int sample) - { - enforce!AudioException(!mDecoder.isModule, "Cannot seek by sample in tracker files."); - - if (!mDecoder.seekPosition(sample)) - Logger.core.log(LogLevel.warning, "Seeking to sample %d failed.", sample); - }, - (ModuleLoopPoint mod) - { - enforce!AudioException(mDecoder.isModule, "Cannot seek by pattern/row in non-tracker files."); - - if (!mDecoder.seekPosition(mod.pattern, mod.row)) - Logger.core.log(LogLevel.warning, "Seeking to pattern %d, row %d failed.", mod.pattern, mod.row); - } - ); - - lastReadLength = readFromDecoder(); - } - else - break; - } - - alBufferData(pBuf, mDecoder.getNumChannels() == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, - &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.getSamplerate()); - - alSourceQueueBuffers(mSourceId, 1, &pBuf); - } - - int buffersQueued; - alGetSourcei(mSourceId, AL_BUFFERS_QUEUED, &buffersQueued); - if (buffersQueued == 0) - stop(); - } - - final void updateVolume() nothrow - { - alSourcef(mSourceId, AL_GAIN, mVolume * mBus.volume); - } - -public: - enum State - { - stopped, - paused, - playing - } - - this(AudioBus bus = null) +package(zyeware.platform.openal): + this(AudioBus bus) { mState = State.stopped; mBus = bus ? bus : AudioAPI.getBus("master"); mProcBuffer = new float[ZyeWare.projectProperties.audioBufferSize]; mBufferIDs = new uint[ZyeWare.projectProperties.audioBufferCount]; - //mDecoder = new AudioStream(); alGenSources(1, &mSourceId); alGenBuffers(cast(int) mBufferIDs.length, &mBufferIDs[0]); AudioThread.register(this); - mVolume = 1.0f; - mPitch = 1.0f; - mLooping = false; - updateVolume(); } +public: ~this() { if (mDecoder.isOpenForReading()) @@ -172,12 +102,18 @@ public: void pause() { + if (mState != State.playing) + return; + mState = State.paused; alSourcePause(mSourceId); } void stop() { + if (mState == State.stopped) + return; + mState = State.stopped; alSourceStop(mSourceId); @@ -194,22 +130,22 @@ public: } } - inout(Audio) audio() inout nothrow + inout(Sound) sound() inout nothrow { - return mAudioStream; + return mSound; } - void audio(Audio value) - in (value, "Audio cannot be null.") + void sound(Sound value) + in (value, "Sound cannot be null.") { if (mState != State.stopped) stop(); - mAudioStream = value; + mSound = value; try { - mDecoder.openFromMemory(mAudioStream.encodedMemory); + mDecoder.openFromMemory(mSound.encodedMemory); } catch (AudioFormatsException ex) { @@ -261,4 +197,64 @@ public: { return mState; } + + final void updateBuffers() + { + if (mState == State.stopped) + return; + + size_t lastReadLength; + int processed; + uint pBuf; + alGetSourcei(mSourceId, AL_BUFFERS_PROCESSED, &processed); + + while (processed--) + { + alSourceUnqueueBuffers(mSourceId, 1, &pBuf); + + lastReadLength = readFromDecoder(); + + if (lastReadLength <= 0) + { + if (mLooping) + { + mSound.loopPoint.match!( + (int sample) + { + //enforce!AudioException(!mDecoder.isModule, "Cannot seek by sample in tracker files."); + + if (!mDecoder.seekPosition(sample)) + Logger.core.log(LogLevel.warning, "Seeking to sample %d failed.", sample); + }, + (ModuleLoopPoint mod) + { + //enforce!AudioException(mDecoder.isModule, "Cannot seek by pattern/row in non-tracker files."); + + if (!mDecoder.seekPosition(mod.pattern, mod.row)) + Logger.core.log(LogLevel.warning, "Seeking to pattern %d, row %d failed.", mod.pattern, mod.row); + } + ); + + lastReadLength = readFromDecoder(); + } + else + break; + } + + alBufferData(pBuf, mDecoder.getNumChannels() == 1 ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32, + &mProcBuffer[0], cast(int) (lastReadLength * float.sizeof), cast(int) mDecoder.getSamplerate()); + + alSourceQueueBuffers(mSourceId, 1, &pBuf); + } + + int buffersQueued; + alGetSourcei(mSourceId, AL_BUFFERS_QUEUED, &buffersQueued); + if (buffersQueued == 0) + stop(); + } + + final void updateVolume() nothrow + { + alSourcef(mSourceId, AL_GAIN, mVolume * mBus.volume); + } } \ No newline at end of file diff --git a/source/zyeware/platform/opengl/api.d b/source/zyeware/platform/opengl/api.d new file mode 100644 index 0000000..f7b7dbc --- /dev/null +++ b/source/zyeware/platform/opengl/api.d @@ -0,0 +1,261 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.opengl.api; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): + +import bindbc.opengl; + +import zyeware.common; +import zyeware.core.debugging.profiler; +import zyeware.rendering; + +import zyeware.platform.opengl.buffer; + +bool[RenderFlag] pFlagValues; + +version (Windows) +{ + extern(Windows) static void glErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, + const(char)* message, void* userParam) nothrow + { + glErrorCallbackImpl(source, type, id, severity, length, message, userParam); + } +} +else +{ + extern(C) static void glErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, + const(char)* message, void* userParam) nothrow + { + glErrorCallbackImpl(source, type, id, severity, length, message, userParam); + } +} + +void glErrorCallbackImpl(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, + const(char)* message, void* userParam) nothrow +{ + glGetError(); + + string typeName; + LogLevel logLevel; + + switch (type) + { + case GL_DEBUG_TYPE_ERROR: + typeName = "Error"; + break; + + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + typeName = "Deprecated Behavior"; + break; + + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + typeName = "Undefined Behavior"; + break; + + case GL_DEBUG_TYPE_PERFORMANCE: + typeName = "Performance"; + break; + + case GL_DEBUG_TYPE_OTHER: + default: + return; + } + + switch (severity) + { + case GL_DEBUG_SEVERITY_LOW: + logLevel = LogLevel.info; + break; + + case GL_DEBUG_SEVERITY_MEDIUM: + logLevel = LogLevel.warning; + break; + + case GL_DEBUG_SEVERITY_HIGH: + logLevel = LogLevel.error; + break; + + default: + logLevel = LogLevel.debug_; + break; + } + + Logger.core.log(logLevel, "%s: %s", typeName, cast(string) message[0..length]); +} + +void apiInitialize() +{ + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + + //glAlphaFunc(GL_GREATER, 0); + + glDepthFunc(GL_LEQUAL); + + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(&glErrorCallback, null); + + glLineWidth(2); + glPointSize(4); + + { + GLboolean resultBool; + GLint resultInt; + + glGetBooleanv(GL_DEPTH_TEST, &resultBool); + pFlagValues[RenderFlag.depthTesting] = cast(bool) resultBool; + glGetBooleanv(GL_DEPTH_WRITEMASK, &resultBool); + pFlagValues[RenderFlag.depthBufferWriting] = cast(bool) resultBool; + glGetBooleanv(GL_CULL_FACE, &resultBool); + pFlagValues[RenderFlag.culling] = cast(bool) resultBool; + glGetBooleanv(GL_STENCIL_TEST, &resultBool); + pFlagValues[RenderFlag.stencilTesting] = cast(bool) resultBool; + glGetIntegerv(GL_POLYGON_MODE, &resultInt); + pFlagValues[RenderFlag.wireframe] = resultInt == GL_LINE; + } +} + +void apiLoadLibraries() +{ + import loader = bindbc.loader.sharedlib; + import std.string : fromStringz; + + if (isOpenGLLoaded()) + return; + + immutable glResult = loadOpenGL(); + + if (glResult != glSupport) + { + foreach (info; loader.errors) + Logger.core.log(LogLevel.warning, "OpenGL loader: %s", info.message.fromStringz); + + switch (glResult) + { + case GLSupport.noLibrary: + throw new GraphicsException("Could not find OpenGL shared library."); + + case GLSupport.badLibrary: + throw new GraphicsException("Provided OpenGL shared is corrupted."); + + case GLSupport.noContext: + throw new GraphicsException("No OpenGL context available."); + + default: + Logger.core.log(LogLevel.warning, "Got older OpenGL version than expected. This might lead to errors."); + } + } +} + +void apiCleanup() +{ +} + +void apiSetClearColor(in Color value) nothrow +{ + glClearColor(value.r, value.g, value.b, value.a); +} + +void apiClear() nothrow +{ + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void apiSetViewport(int x, int y, uint width, uint height) nothrow +{ + glViewport(x, y, cast(GLsizei) width, cast(GLsizei) height); +} + +void apiDrawIndexed(size_t count) nothrow +{ + glDrawElements(GL_TRIANGLES, cast(int) count, GL_UNSIGNED_INT, null); + + version (Profiling) + { + ++Profiler.currentWriteData.renderData.drawCalls; + Profiler.currentWriteData.renderData.polygonCount += count / 3; + } +} + +void apiPackLightConstantBuffer(ref ConstantBuffer buffer, in Renderer3D.Light[] lights) nothrow +{ + Vector4f[10] positions; + Vector4f[10] colors; + Vector4f[10] attenuations; + + for (size_t i; i < lights.length; ++i) + { + positions[i] = Vector4f(lights[i].position, 0); + colors[i] = lights[i].color; + attenuations[i] = Vector4f(lights[i].attenuation, 0); + } + + buffer.setData(buffer.getEntryOffset("position"), positions); + buffer.setData(buffer.getEntryOffset("color"), colors); + buffer.setData(buffer.getEntryOffset("attenuation"), attenuations); +} + +bool apiGetFlag(RenderFlag flag) nothrow +{ + return pFlagValues[flag]; +} + +void apiSetFlag(RenderFlag flag, bool value) nothrow +{ + if (pFlagValues[flag] == value) + return; + + final switch (flag) with (RenderFlag) + { + case depthTesting: + if (value) + glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); + break; + + case depthBufferWriting: + glDepthMask(value); + break; + + case culling: + if (value) + glEnable(GL_CULL_FACE); + else + glDisable(GL_CULL_FACE); + break; + + case stencilTesting: + if (value) + glEnable(GL_STENCIL_TEST); + else + glDisable(GL_STENCIL_TEST); + break; + + case wireframe: + glPolygonMode(GL_FRONT_AND_BACK, value ? GL_LINE : GL_FILL); + break; + } + + pFlagValues[flag] = value; +} + +size_t apiGetCapability(RenderCapability capability) nothrow +{ + final switch (capability) with (RenderCapability) + { + case maxTextureSlots: + GLint result; + glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &result); + return result; + } +} \ No newline at end of file diff --git a/platform/opengl/buffer.d b/source/zyeware/platform/opengl/buffer.d similarity index 68% rename from platform/opengl/buffer.d rename to source/zyeware/platform/opengl/buffer.d index da01b95..04fd226 100644 --- a/platform/opengl/buffer.d +++ b/source/zyeware/platform/opengl/buffer.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.rendering.buffer; +module zyeware.platform.opengl.buffer; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import std.exception : enforce, assumeWontThrow; import std.typecons : Rebindable; @@ -14,137 +17,15 @@ import bindbc.opengl; import zyeware.common; import zyeware.rendering; -struct BufferElement -{ -private: - string mName; - Type mType; - uint mSize; - uint mOffset; - bool mNormalized; - uint mDivisor; - uint mAmount; - - static uint getTypeSize(Type type) pure nothrow - { - final switch (type) with (Type) - { - case none: return 1; - case vec2: return float.sizeof * 2; - case vec3: return float.sizeof * 3; - case vec4: return float.sizeof * 4; - case mat3: return float.sizeof * 3 * 3; - case mat4: return float.sizeof * 4 * 4; - case float_: return float.sizeof; - case int_: return int.sizeof; - case bool_: return 1; - } - } - -public: - enum Type : ubyte - { - none, - vec2, vec3, vec4, - mat3, mat4, - float_, int_, bool_ - } - - this(string name, Type type, uint amount = 1, Flag!"normalized" normalized = No.normalized, uint divisor = 0) - { - mName = name; - mType = type; - mNormalized = normalized; - mSize = elementSize * amount; - mAmount = amount; - mDivisor = divisor; - } - - string name() pure const nothrow - { - return mName; - } - - Type type() pure const nothrow - { - return mType; - } - - uint size() pure const nothrow - { - return mSize; - } - - uint amount() pure const nothrow - { - return mAmount; - } - - uint elementSize() pure const nothrow - { - return getTypeSize(mType); - } - - uint offset() pure const nothrow - { - return mOffset; - } - - bool normalized() pure const nothrow - { - return mNormalized; - } - - uint divisor() pure const nothrow - { - return mDivisor; - } -} - -struct BufferLayout -{ -private: - BufferElement[] mElements; - uint mStride; - - void calculateOffsetAndStride() pure nothrow - { - mStride = 0; - - foreach (ref BufferElement element; mElements) - { - element.mOffset = mStride; - mStride += element.size; - } - } - -public: - this(BufferElement[] elements) - { - mElements = elements; - calculateOffsetAndStride(); - } - - uint stride() pure const nothrow - { - return mStride; - } - - inout(BufferElement[]) elements() pure inout nothrow - { - return mElements; - } -} - // OpenGL implements BufferGroup as VertexArray -class BufferGroup +class OGLBufferGroup : BufferGroup { protected: uint mVertexArrayID; DataBuffer mDataBuffer; IndexBuffer mIndexBuffer; -public: +package(zyeware.platform.opengl): this() { glGenVertexArrays(1, &mVertexArrayID); @@ -152,7 +33,13 @@ public: glBindVertexArray(0); } + + static BufferGroup create() + { + return new OGLBufferGroup(); + } +public: ~this() { glDeleteVertexArrays(1, &mVertexArrayID); @@ -212,7 +99,7 @@ public: -class DataBuffer +class OGLDataBuffer : DataBuffer { protected: uint mBufferID; @@ -221,35 +108,40 @@ protected: size_t mLength; bool mInitialized; -public: - this(size_t size, BufferLayout layout, Flag!"dynamic" dynamic) +package(zyeware.platform.opengl): + this(size_t size, BufferLayout layout, bool dynamic) { glGenBuffers(1, &mBufferID); enforce!GraphicsException(mBufferID != 0, "Failed to create OpenGL vertex buffer!"); - //glBindBuffer(GL_ARRAY_BUFFER, mBufferID); - //glBufferStorage(GL_ARRAY_BUFFER, size, null, dynamic ? GL_DYNAMIC_STORAGE_BIT : 0); - mLayout = layout; mLength = size; mDynamic = dynamic; } - this(const void[] data, BufferLayout layout, Flag!"dynamic" dynamic) + this(const void[] data, BufferLayout layout, bool dynamic) { glGenBuffers(1, &mBufferID); enforce!GraphicsException(mBufferID != 0, "Failed to create OpenGL vertex buffer!"); - //glBindBuffer(GL_ARRAY_BUFFER, mBufferID); - //glBufferStorage(GL_ARRAY_BUFFER, data.length, data.ptr, dynamic ? GL_DYNAMIC_STORAGE_BIT : 0); - mLayout = layout; mLength = data.length; mDynamic = dynamic; setData(data); } + + static DataBuffer create(size_t size, BufferLayout layout, bool dynamic) + { + return new OGLDataBuffer(size, layout, dynamic); + } + static DataBuffer createWithData(const void[] data, BufferLayout layout, bool dynamic) + { + return new OGLDataBuffer(data, layout, dynamic); + } + +public: ~this() { glDeleteBuffers(1, &mBufferID); @@ -262,7 +154,6 @@ public: void setData(const void[] data) in (data.length <= mLength, "Too much data for buffer size.") - //in (mDynamic, "Data buffer is not set as dynamic.") { glBindBuffer(GL_ARRAY_BUFFER, mBufferID); @@ -293,7 +184,7 @@ public: -class IndexBuffer +class OGLIndexBuffer : IndexBuffer { protected: uint mBufferID; @@ -301,34 +192,38 @@ protected: bool mDynamic; bool mInitialized; -public: - this(size_t size, Flag!"dynamic" dynamic) +package(zyeware.platform.opengl): + this(size_t size, bool dynamic) { glGenBuffers(1, &mBufferID); enforce!GraphicsException(mBufferID != 0, "Failed to create OpenGL index buffer!"); - //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mBufferID); - //glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, size, null, dynamic ? GL_DYNAMIC_STORAGE_BIT : 0); - mLength = size; mDynamic = dynamic; } - this(const uint[] indices, Flag!"dynamic" dynamic) + this(const uint[] indices, bool dynamic) { glGenBuffers(1, &mBufferID); enforce!GraphicsException(mBufferID != 0, "Failed to create OpenGL index buffer!"); - //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mBufferID); - //glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, indices.length * uint.sizeof, indices.ptr, - // dynamic ? GL_DYNAMIC_STORAGE_BIT : 0); - mLength = indices.length; mDynamic = dynamic; setData(indices); } + static IndexBuffer create(size_t size, bool dynamic) + { + return new OGLIndexBuffer(size, dynamic); + } + + static IndexBuffer createWithData(const uint[] indices, bool dynamic) + { + return new OGLIndexBuffer(indices, dynamic); + } + +public: ~this() { glDeleteBuffers(1, &mBufferID); @@ -341,7 +236,6 @@ public: void setData(const uint[] indices) in (indices.length <= mLength, "Too much data for buffer size.") - //in (mDynamic, "Index buffer is not set as dynamic.") { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mBufferID); @@ -363,7 +257,7 @@ public: -class ConstantBuffer +class OGLConstantBuffer : ConstantBuffer { protected: uint mBufferID; @@ -384,15 +278,7 @@ protected: mLength = offset; } -public: - enum Slot - { - matrices, - environment, - lights, - modelVariables - } - +package(zyeware.platform.opengl): this(in BufferLayout layout) { glGenBuffers(1, &mBufferID); @@ -404,6 +290,12 @@ public: glBufferData(GL_UNIFORM_BUFFER, mLength, null, GL_DYNAMIC_DRAW); } + static ConstantBuffer create(in BufferLayout layout) + { + return new OGLConstantBuffer(layout); + } + +public: ~this() { glDeleteBuffers(1, &mBufferID); diff --git a/platform/opengl/framebuffer.d b/source/zyeware/platform/opengl/framebuffer.d similarity index 83% rename from platform/opengl/framebuffer.d rename to source/zyeware/platform/opengl/framebuffer.d index 0fc5e01..2460e5e 100644 --- a/platform/opengl/framebuffer.d +++ b/source/zyeware/platform/opengl/framebuffer.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.rendering.framebuffer; +module zyeware.platform.opengl.framebuffer; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import std.exception : enforce; @@ -11,25 +14,30 @@ import bindbc.opengl; import zyeware.common; import zyeware.rendering; +import zyeware.platform.opengl.texture; -class Framebuffer +class OGLFramebuffer : Framebuffer { protected: FramebufferProperties mProperties; uint mID; Texture2D mColorAttachment, mDepthAttachment; -public: +package(zyeware.platform.opengl): this(in FramebufferProperties properties) { mProperties = properties; invalidate(); } + static Framebuffer create(in FramebufferProperties properties) + { + return new OGLFramebuffer(properties); + } + +public: ~this() { - mColorAttachment.dispose(); - mDepthAttachment.dispose(); glDeleteFramebuffers(1, &mID); } @@ -66,8 +74,8 @@ public: enforce!GraphicsException(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Framebuffer is incomplete."); - mColorAttachment = new Texture2D(colorAttachmentID); - mDepthAttachment = new Texture2D(depthAttachmentID); + mColorAttachment = new OGLTexture2D(colorAttachmentID); + mDepthAttachment = new OGLTexture2D(depthAttachmentID); glBindFramebuffer(GL_FRAMEBUFFER, 0); } diff --git a/source/zyeware/platform/opengl/impl.d b/source/zyeware/platform/opengl/impl.d new file mode 100644 index 0000000..c9a9355 --- /dev/null +++ b/source/zyeware/platform/opengl/impl.d @@ -0,0 +1,75 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.opengl.impl; + +version (ZWBackendOpenGL): +package(zyeware): + +import zyeware.platform.opengl.api; +import zyeware.platform.opengl.renderer2d; +import zyeware.platform.opengl.renderer3d; +import zyeware.platform.opengl.buffer; +import zyeware.platform.opengl.texture; +import zyeware.platform.opengl.framebuffer; +import zyeware.platform.opengl.shader; +import zyeware.platform.opengl.window; + +import zyeware.rendering; + +void loadOpenGLBackend() +{ + // ===== RenderAPI ===== + RenderAPI.sInitializeImpl = &apiInitialize; + RenderAPI.sLoadLibrariesImpl = &apiLoadLibraries; + RenderAPI.sCleanupImpl = &apiCleanup; + + RenderAPI.sSetClearColorImpl = &apiSetClearColor; + RenderAPI.sClearImpl = &apiClear; + RenderAPI.sSetViewportImpl = &apiSetViewport; + RenderAPI.sDrawIndexedImpl = &apiDrawIndexed; + RenderAPI.sPackLightConstantBufferImpl = &apiPackLightConstantBuffer; + RenderAPI.sGetFlagImpl = &apiGetFlag; + RenderAPI.sSetFlagImpl = &apiSetFlag; + RenderAPI.sGetCapabilityImpl = &apiGetCapability; + + RenderAPI.sCreateBufferGroupImpl = () => new OGLBufferGroup(); + RenderAPI.sCreateDataBufferImpl = (size, layout, dynamic) => new OGLDataBuffer(size, layout, dynamic); + RenderAPI.sCreateDataBufferWithDataImpl = (data, layout, dynamic) => new OGLDataBuffer(data, layout, dynamic); + RenderAPI.sCreateIndexBufferImpl = (size, dynamic) => new OGLIndexBuffer(size, dynamic); + RenderAPI.sCreateIndexBufferWithDataImpl = (indices, dynamic) => new OGLIndexBuffer(indices, dynamic); + RenderAPI.sCreateConstantBufferImpl = (layout) => new OGLConstantBuffer(layout); + + RenderAPI.sCreateFramebufferImpl = (props) => new OGLFramebuffer(props); + RenderAPI.sCreateTexture2DImpl = (image, props) => new OGLTexture2D(image, props); + RenderAPI.sCreateTextureCubeMapImpl = (images, props) => new OGLTextureCubeMap(images, props); + RenderAPI.sCreateWindowImpl = (props) => new OGLWindow(props); + RenderAPI.sCreateShaderImpl = () => new OGLShader(); + + RenderAPI.sLoadTexture2DImpl = (path) => OGLTexture2D.load(path); + RenderAPI.sLoadTextureCubeMapImpl = (path) => OGLTextureCubeMap.load(path); + RenderAPI.sLoadShaderImpl = (path) => OGLShader.load(path); + + // ===== Renderer2D ===== + Renderer2D.sInitializeImpl = &r2dInitialize; + Renderer2D.sCleanupImpl = &r2dCleanup; + Renderer2D.sBeginImpl = &r2dBegin; + Renderer2D.sDrawRectImpl = &r2dDrawRect; + Renderer2D.sEndImpl = &r2dEnd; + Renderer2D.sFlushImpl = &r2dFlush; + + Renderer2D.sDrawStringImpl = &r2dDrawString!string; + Renderer2D.sDrawWStringImpl = &r2dDrawString!wstring; + Renderer2D.sDrawDStringImpl = &r2dDrawString!dstring; + + // ===== Renderer3D ===== + Renderer3D.sCleanupImpl = &r3dCleanup; + Renderer3D.sInitializeImpl = &r3dInitialize; + Renderer3D.sUploadLightsImpl = &r3dUploadLights; + Renderer3D.sBeginImpl = &r3dBegin; + Renderer3D.sEndImpl = &r3dEnd; + Renderer3D.sFlushImpl = &r3dFlush; + Renderer3D.sSubmitImpl = &r3dSubmit; +} \ No newline at end of file diff --git a/source/zyeware/platform/opengl/renderer2d.d b/source/zyeware/platform/opengl/renderer2d.d new file mode 100644 index 0000000..60a24ab --- /dev/null +++ b/source/zyeware/platform/opengl/renderer2d.d @@ -0,0 +1,283 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.opengl.renderer2d; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): + +import std.traits : isSomeString; +import std.string : lineSplitter; +import std.typecons : Rebindable; +import std.exception : enforce; + +import bmfont : BMFont = Font; + +import zyeware.common; +import zyeware.core.debugging.profiler; +import zyeware.rendering; + +enum maxQuadsPerBatch = 5000; +enum maxVerticesPerBatch = maxQuadsPerBatch * 4; +enum maxIndicesPerBatch = maxQuadsPerBatch * 6; + +struct QuadVertex +{ + Vector4f position; + Color color; + Vector2f uv; + float textureIndex; +} + +bool pOldCullingValue; + +Shader pDefaultShader; +BufferGroup[] pBatchBuffers; +size_t pActiveBatchBufferIndex; +ConstantBuffer pMatrixData; + +QuadVertex[maxVerticesPerBatch] pBatchVertices; +uint[maxIndicesPerBatch] pBatchIndices; +size_t pCurrentQuad; + +Rebindable!(const Texture2D)[] pBatchTextures; +size_t pNextFreeTexture = 1; // because 0 is the white texture + +size_t r2dGetIndexForTexture(in Texture2D texture) nothrow +{ + for (size_t i = 1; i < pNextFreeTexture; ++i) + if (texture is pBatchTextures[i]) + return i; + + if (pNextFreeTexture == pBatchTextures.length) + return size_t.max; + + pBatchTextures[pNextFreeTexture++] = texture; + return pNextFreeTexture - 1; +} + +void r2dInitialize() +{ + pBatchTextures = new Rebindable!(const Texture2D)[8]; + + pMatrixData = ConstantBuffer.create(BufferLayout([ + BufferElement("viewProjection", BufferElement.Type.mat4) + ])); + + for (size_t i; i < 2; ++i) + { + auto batchBuffer = BufferGroup.create(); + + batchBuffer.dataBuffer = DataBuffer.create(maxVerticesPerBatch * QuadVertex.sizeof, BufferLayout([ + BufferElement("aPosition", BufferElement.Type.vec4), + BufferElement("aColor", BufferElement.Type.vec4), + BufferElement("aUV", BufferElement.Type.vec2), + BufferElement("aTexIndex", BufferElement.Type.float_) + ]), Yes.dynamic); + + batchBuffer.indexBuffer = IndexBuffer.create(maxIndicesPerBatch * uint.sizeof, Yes.dynamic); + + pBatchBuffers ~= batchBuffer; + } + + // To circumvent a bug in MacOS builds that require a VAO to be bound before validating a + // shader program in OpenGL. Due to Renderer2D being initialized early during the + // engines lifetime, this should fix all further shader loadings. + pBatchBuffers[0].bind(); + + pDefaultShader = AssetManager.load!Shader("core://shaders/2d/default.shd"); + + static ubyte[3] pixels = [255, 255, 255]; + pBatchTextures[0] = Texture2D.create(new Image(pixels, 3, 8, Vector2i(1)), TextureProperties.init); +} + +void r2dCleanup() +{ + pDefaultShader.dispose(); + pBatchTextures[0].dispose(); + pBatchTextures.dispose(); + + foreach (BufferGroup buffer; pBatchBuffers) + buffer.dispose(); +} + +void r2dBegin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix) +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.none, + "A renderer is currently active, cannot begin."); + + pMatrixData.bind(ConstantBuffer.Slot.matrices); + pMatrixData.setData(pMatrixData.getEntryOffset("viewProjection"), + (projectionMatrix * viewMatrix).matrix); + + RenderAPI.setFlag(RenderFlag.depthTesting, false); + pOldCullingValue = RenderAPI.getFlag(RenderFlag.culling); + RenderAPI.setFlag(RenderFlag.culling, false); + + debug currentRenderer = CurrentRenderer.renderer2D; +} + +void r2dEnd() +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, + "2D renderer is not active, cannot end."); + + r2dFlush(); + + RenderAPI.setFlag(RenderFlag.culling, pOldCullingValue); + + debug currentRenderer = CurrentRenderer.none; +} + +void r2dFlush() +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, + "2D renderer is not active, cannot flush."); + + BufferGroup activeGroup = pBatchBuffers[pActiveBatchBufferIndex++]; + pActiveBatchBufferIndex %= pBatchBuffers.length; + + activeGroup.bind(); + activeGroup.dataBuffer.setData(pBatchVertices); + activeGroup.indexBuffer.setData(pBatchIndices); + + pDefaultShader.bind(); + + for (int i = 0; i < pNextFreeTexture; ++i) + pBatchTextures[i].bind(i); + + RenderAPI.drawIndexed(pCurrentQuad * 6); + + pCurrentQuad = 0; + pNextFreeTexture = 1; +} + +void r2dDrawRect(in Rect2f dimensions, in Matrix4f transform, in Color modulate = Vector4f(1), in Texture2D texture = null, + in Rect2f region = Rect2f(0, 0, 1, 1)) +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer2D, + "2D renderer is not active, cannot draw."); + + static Vector4f[4] quadPositions; + quadPositions[0] = Vector4f(dimensions.min.x, dimensions.min.y, 0.0f, 1); + quadPositions[1] = Vector4f(dimensions.max.x, dimensions.min.y, 0.0f, 1); + quadPositions[2] = Vector4f(dimensions.max.x, dimensions.max.y, 0.0f, 1); + quadPositions[3] = Vector4f(dimensions.min.x, dimensions.max.y, 0.0f, 1); + + static Vector2f[4] quadUVs; + quadUVs[0] = Vector2f(region.min.x, region.min.y); + quadUVs[1] = Vector2f(region.max.x, region.min.y); + quadUVs[2] = Vector2f(region.max.x, region.max.y); + quadUVs[3] = Vector2f(region.min.x, region.max.y); + + if (pCurrentQuad == maxQuadsPerBatch) + r2dFlush(); + + float texIdx = 0; + if (texture) + { + size_t idx = r2dGetIndexForTexture(texture); + if (idx == size_t.max) // No more room for new textures + { + r2dFlush(); + idx = r2dGetIndexForTexture(texture); + } + + texIdx = cast(float) idx; + } + + for (size_t i; i < 4; ++i) + pBatchVertices[pCurrentQuad * 4 + i] = QuadVertex(transform * quadPositions[i], modulate, + quadUVs[i], texIdx); + + immutable uint currentQuadIndex = cast(uint) pCurrentQuad * 4; + immutable size_t baseIndex = pCurrentQuad * 6; + + pBatchIndices[baseIndex] = currentQuadIndex + 2; + pBatchIndices[baseIndex + 1] = currentQuadIndex + 1; + pBatchIndices[baseIndex + 2] = currentQuadIndex; + pBatchIndices[baseIndex + 3] = currentQuadIndex; + pBatchIndices[baseIndex + 4] = currentQuadIndex + 3; + pBatchIndices[baseIndex + 5] = currentQuadIndex + 2; + + ++pCurrentQuad; + + version (Profiling) ++Profiler.currentWriteData.renderData.rectCount; +} + +void r2dDrawString(T)(in T text, in Font font, in Vector2f position, in Color modulate = Color.white, + ubyte alignment = Font.Alignment.left | Font.Alignment.top) + in (text && font) +{ + Vector2f cursor = Vector2f(0); + + if (alignment & Font.Alignment.middle || alignment & Font.Alignment.bottom) + { + immutable int height = font.getTextHeight(text); + cursor.y -= (alignment & Font.Alignment.middle) ? height / 2 : height; + } + + foreach (T line; text.lineSplitter) + { + if (alignment & Font.Alignment.center || alignment & Font.Alignment.right) + { + immutable int width = font.getTextWidth(line); + cursor.x = -((alignment & Font.Alignment.center) ? width / 2 : width); + } + else + cursor.x = 0; + + for (size_t i; i < line.length; ++i) + { + switch (line[i]) + { + case '\t': + cursor.x += 40; + break; + + default: + BMFont.Char c = font.bmFont.getChar(line[i]); + if (c == BMFont.Char.init) + break; + + immutable int kerning = i > 0 ? font.bmFont.getKerning(line[i - 1], line[i]) : 1; + + const(Texture2D) pageTexture = font.getPageTexture(c.page); + immutable Vector2f size = pageTexture.size; + + immutable Rect2f region = Rect2f(cast(float) c.x / size.x, cast(float) c.y / size.y, + cast(float) (c.x + c.width) / size.x, cast(float) (c.y + c.height) / size.y); + + //r2dDrawRect(Rect2f(0, 0, c.width, c.height), Vector2f(position + cursor + Vector2f(c.xoffset, c.yoffset)), + // Vector2f(1), modulate, pageTexture, region); + + r2dDrawRect(Rect2f(0, 0, c.width, c.height), Matrix4f.translation(Vector3f(Vector2f(position + cursor + Vector2f(c.xoffset, c.yoffset)), 0)), + modulate, pageTexture, region); + + /*r2dDrawRect(dimensions, Matrix4f.translation(Vector3f(position, 0)) * Matrix4f.scaling(scale.x, scale.y, 1), + modulate, texture, region);*/ + + cursor.x += c.xadvance + kerning; + } + } + + cursor.y += font.bmFont.common.lineHeight; + } +} + +debug +{ + /// This enum and associated variable is used to keep track + /// which of the renderers has the current "context", as mixing + /// submit and render calls is undocumented behavior. + enum CurrentRenderer : ubyte + { + none, + renderer2D, + renderer3D + } + + CurrentRenderer currentRenderer; +} \ No newline at end of file diff --git a/source/zyeware/platform/opengl/renderer3d.d b/source/zyeware/platform/opengl/renderer3d.d new file mode 100644 index 0000000..a659dd0 --- /dev/null +++ b/source/zyeware/platform/opengl/renderer3d.d @@ -0,0 +1,230 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.platform.opengl.renderer3d; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): + +import std.typecons : Rebindable; +import std.exception : enforce; + +import zyeware.common; +import zyeware.rendering; + +debug import zyeware.platform.opengl.renderer2d : currentRenderer, CurrentRenderer; + +enum maxLights = 10; +enum maxMaterialsPerBatch = 10; +enum maxBufferGroupsPerMaterial = 10; +enum maxTransformsPerBufferGroup = 10; + +struct MaterialBatch +{ + Material material; + BufferGroupBatch[maxBufferGroupsPerMaterial] bufferGroupBatches; + size_t currentBufferGroupBatch; +} + +struct BufferGroupBatch +{ + BufferGroup bufferGroup; + Matrix4f[maxTransformsPerBufferGroup] transforms; + size_t currentTransform; +} + +Matrix4f pActiveViewMatrix; +Matrix4f pActiveProjectionMatrix; +Rebindable!(Environment3D) pActiveEnvironment; + +MaterialBatch[maxMaterialsPerBatch] pMaterialBatches; +size_t pCurrentMaterialBatch; +ConstantBuffer pMatricesBuffer, pLightsBuffer, pEnvironmentBuffer; + +void r3dRenderSky(Renderable sky) + in (sky, "Argument cannot be null.") +{ + // Eliminate translation from current view matrix + Matrix4f viewMatrix = pActiveViewMatrix; + viewMatrix[0][3] = 0f; + viewMatrix[1][3] = 0f; + viewMatrix[2][3] = 0f; + + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("view"), viewMatrix.matrix); + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("mvp"), + (pActiveProjectionMatrix * viewMatrix).matrix); + + sky.material.bind(); + sky.bufferGroup.bind(); + + RenderAPI.drawIndexed(sky.bufferGroup.indexBuffer.length); +} + +void r3dInitialize() +{ + pMatricesBuffer = ConstantBuffer.create(BufferLayout([ + BufferElement("mvp", BufferElement.Type.mat4), + BufferElement("projection", BufferElement.Type.mat4), + BufferElement("view", BufferElement.Type.mat4), + BufferElement("model", BufferElement.Type.mat4) + ])); + + pEnvironmentBuffer = ConstantBuffer.create(BufferLayout([ + BufferElement("cameraPosition", BufferElement.Type.vec4), + BufferElement("ambientColor", BufferElement.Type.vec4), + BufferElement("fogColor", BufferElement.Type.vec4), + ])); + + pLightsBuffer = ConstantBuffer.create(BufferLayout([ + BufferElement("position", BufferElement.Type.vec4, maxLights), + BufferElement("color", BufferElement.Type.vec4, maxLights), + BufferElement("attenuation", BufferElement.Type.vec4, maxLights), + BufferElement("count", BufferElement.Type.int_), + ])); + + // Make sure to always initialize the count variable with 0, in case lights + // are not uploaded. + pLightsBuffer.setData(pLightsBuffer.getEntryOffset("count"), [0]); +} + +void r3dCleanup() +{ + pMatricesBuffer.dispose(); + pEnvironmentBuffer.dispose(); +} + +void r3dUploadLights(in Renderer3D.Light[] lights) +{ + if (lights.length > maxLights) + { + Logger.core.log(LogLevel.warning, "Too many lights in scene."); + return; + } + + pLightsBuffer.setData(pLightsBuffer.getEntryOffset("count"), [cast(int) lights.length]); + RenderAPI.packLightConstantBuffer(pLightsBuffer, lights); +} + +void r3dBegin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix, Environment3D environment) + in (environment, "Environment cannot be null.") +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.none, + "A renderer is currently active, cannot begin."); + + pActiveProjectionMatrix = projectionMatrix; + pActiveViewMatrix = viewMatrix; + pActiveEnvironment = environment; + + pMatricesBuffer.bind(ConstantBuffer.Slot.matrices); + pEnvironmentBuffer.bind(ConstantBuffer.Slot.environment); + pLightsBuffer.bind(ConstantBuffer.Slot.lights); + + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("projection"), + pActiveProjectionMatrix); + + pEnvironmentBuffer.setData(pEnvironmentBuffer.getEntryOffset("cameraPosition"), + (pActiveViewMatrix.inverse * Vector4f(0, 0, 0, 1)).vector); + pEnvironmentBuffer.setData(pEnvironmentBuffer.getEntryOffset("ambientColor"), pActiveEnvironment.ambientColor.vector); + pEnvironmentBuffer.setData(pEnvironmentBuffer.getEntryOffset("fogColor"), pActiveEnvironment.fogColor.vector); + + RenderAPI.setFlag(RenderFlag.depthTesting, true); + + debug currentRenderer = CurrentRenderer.renderer3D; +} + +void r3dEnd() +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer3D, + "3D renderer is not active, cannot end."); + + r3dFlush(); + + if (pActiveEnvironment.sky) + r3dRenderSky(pActiveEnvironment.sky); + + debug currentRenderer = CurrentRenderer.none; +} + +void r3dFlush() +{ + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("view"), pActiveViewMatrix); + + for (size_t i; i < pCurrentMaterialBatch; ++i) + { + MaterialBatch* materialBatch = &pMaterialBatches[i]; + materialBatch.material.bind(); + + for (size_t j; j < materialBatch.currentBufferGroupBatch; ++j) + { + BufferGroupBatch* bufferGroupBatch = &materialBatch.bufferGroupBatches[j]; + bufferGroupBatch.bufferGroup.bind(); + + for (size_t k; k < bufferGroupBatch.currentTransform; ++k) + { + // TODO: Maybe look if this can be optimized. + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("model"), bufferGroupBatch.transforms[k].matrix); + pMatricesBuffer.setData(pMatricesBuffer.getEntryOffset("mvp"), (pActiveProjectionMatrix * pActiveViewMatrix + * bufferGroupBatch.transforms[k]).matrix); + RenderAPI.drawIndexed(bufferGroupBatch.bufferGroup.indexBuffer.length); + } + + bufferGroupBatch.currentTransform = 0; + } + + materialBatch.currentBufferGroupBatch = 0; + } +} + +void r3dSubmit(BufferGroup group, Material material, in Matrix4f transform) + in (group && material) +{ + debug enforce!RenderException(currentRenderer == CurrentRenderer.renderer3D, + "3D renderer is not active, cannot submit."); + + size_t i, j; + +retryInsert: + // Find fitting material batch first + for (; i < pCurrentMaterialBatch; ++i) + if (pMaterialBatches[i].material is material) + break; + + if (i == pCurrentMaterialBatch) + { + if (pCurrentMaterialBatch == maxMaterialsPerBatch) + r3dFlush(); + + pMaterialBatches[pCurrentMaterialBatch++].material = material; + } + + MaterialBatch* materialBatch = &pMaterialBatches[i]; + + // Find fitting buffer group batch next + for (; j < materialBatch.currentBufferGroupBatch; ++j) + if (materialBatch.bufferGroupBatches[j].bufferGroup is group) + break; + + if (j == materialBatch.currentBufferGroupBatch) + { + if (materialBatch.currentBufferGroupBatch == maxBufferGroupsPerMaterial) + { + r3dFlush(); + goto retryInsert; + } + + materialBatch.bufferGroupBatches[materialBatch.currentBufferGroupBatch++].bufferGroup = group; + } + + BufferGroupBatch* bufferGroupBatch = &materialBatch.bufferGroupBatches[j]; + + // Add transform last. + if (bufferGroupBatch.currentTransform == maxTransformsPerBufferGroup) + { + r3dFlush(); + goto retryInsert; + } + + bufferGroupBatch.transforms[bufferGroupBatch.currentTransform++] = transform; +} \ No newline at end of file diff --git a/platform/opengl/shader.d b/source/zyeware/platform/opengl/shader.d similarity index 95% rename from platform/opengl/shader.d rename to source/zyeware/platform/opengl/shader.d index 887ab64..31da961 100644 --- a/platform/opengl/shader.d +++ b/source/zyeware/platform/opengl/shader.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.rendering.shader; +module zyeware.platform.opengl.shader; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import std.typecons : Rebindable; import std.string : toStringz, fromStringz, format; @@ -19,8 +22,7 @@ import sdlang; import zyeware.common; import zyeware.rendering; -@asset(Yes.cache) -class Shader +class OGLShader : Shader { private: static Rebindable!(const Shader) sCurrentlyBoundShader; @@ -86,7 +88,7 @@ protected: } } -public: +package(zyeware.platform.opengl): this() { mProgramID = glCreateProgram(); @@ -100,6 +102,92 @@ public: ]; } + static Shader load(string path) + in (path, "Path cannot be null.") + { + Logger.core.log(LogLevel.debug_, "Loading shader '%s'...", path); + + string parseIncludes(string source) + { + enum includeRegex = ctRegex!("^#include \"(.*)\"$", "m"); + + char[] mutableSource = source.dup; + alias Include = Tuple!(char[], "path", size_t, "position", size_t, "length"); + + Include[] includes; + ptrdiff_t offset; + size_t includeIterations; + + do + { + enforce!GraphicsException(++includeIterations < 100, + "Too many iterations, possible infinite include recursion."); + + includes.length = 0; + foreach (m; matchAll(mutableSource, includeRegex)) + includes ~= Include(m[1], m.pre.length, m.hit.length); + + foreach (ref Include include; includes) + { + VFSFile includeFile = VFS.getFile(cast(string) include.path); + char[] includeSource = cast(char[]) includeFile.readAll!string; + includeFile.close(); + + immutable size_t from = include.position + offset; + immutable size_t to = from + include.length; + + mutableSource.replaceInPlace(from, to, includeSource); + offset += cast(ptrdiff_t) includeSource.length - include.length; + } + } while (includes.length > 0); + + return mutableSource.idup; + } + + VFSFile file = VFS.getFile(path); + immutable string source = file.readAll!string; + Tag root = parseSource(source); + file.close(); + + auto shader = new OGLShader(); + + void loadShader(ref Tag tag, int type) + { + if (string filePath = tag.getAttribute!string("file", null)) + { + Logger.core.log(LogLevel.verbose, "Loading external shader source '%s'...", filePath); + VFSFile shaderFile = VFS.getFile(filePath); + shader.compileShader(parseIncludes(shaderFile.readAll!string), type); + shaderFile.close(); + } + else + shader.compileShader(parseIncludes(tag.getValue!string), type); + } + + foreach (ref Tag tag; root.namespaces["opengl"].tags) + { + switch (tag.name) + { + case "vertex": + loadShader(tag, GL_VERTEX_SHADER); + break; + + case "fragment": + loadShader(tag, GL_FRAGMENT_SHADER); + break; + + default: + Logger.core.log(LogLevel.warning, "'%s' %s: Unknown tag '%s'.", + path, tag.location, tag.getFullName()); + } + } + + shader.link(); + + return shader; + } + +public: ~this() { glDeleteProgram(mProgramID); @@ -173,7 +261,7 @@ public: parseUniforms(); - Logger.client.log(LogLevel.debug_, "Shader linked successfully."); + Logger.core.log(LogLevel.debug_, "Shader linked successfully."); } void bind() const @@ -189,89 +277,4 @@ public: { return mTextureCount; } - - static Shader load(string path) - in (path, "Path cannot be null.") - { - Logger.core.log(LogLevel.debug_, "Loading shader '%s'...", path); - - string parseIncludes(string source) - { - enum includeRegex = ctRegex!("^#include \"(.*)\"$", "m"); - - char[] mutableSource = source.dup; - alias Include = Tuple!(char[], "path", size_t, "position", size_t, "length"); - - Include[] includes; - ptrdiff_t offset; - size_t includeIterations; - - do - { - enforce!GraphicsException(++includeIterations < 100, - "Too many iterations, possible infinite include recursion."); - - includes.length = 0; - foreach (m; matchAll(mutableSource, includeRegex)) - includes ~= Include(m[1], m.pre.length, m.hit.length); - - foreach (ref Include include; includes) - { - VFSFile includeFile = VFS.getFile(cast(string) include.path); - char[] includeSource = cast(char[]) includeFile.readAll!string; - includeFile.close(); - - immutable size_t from = include.position + offset; - immutable size_t to = from + include.length; - - mutableSource.replaceInPlace(from, to, includeSource); - offset += cast(ptrdiff_t) includeSource.length - include.length; - } - } while (includes.length > 0); - - return mutableSource.idup; - } - - VFSFile file = VFS.getFile(path); - immutable string source = file.readAll!string; - Tag root = parseSource(source); - file.close(); - - auto shader = new Shader(); - - void loadShader(ref Tag tag, int type) - { - if (string filePath = tag.getAttribute!string("file", null)) - { - Logger.core.log(LogLevel.trace, "Loading external shader source '%s'...", filePath); - VFSFile shaderFile = VFS.getFile(filePath); - shader.compileShader(parseIncludes(shaderFile.readAll!string), type); - shaderFile.close(); - } - else - shader.compileShader(parseIncludes(tag.getValue!string), type); - } - - foreach (ref Tag tag; root.namespaces["opengl"].tags) - { - switch (tag.name) - { - case "vertex": - loadShader(tag, GL_VERTEX_SHADER); - break; - - case "fragment": - loadShader(tag, GL_FRAGMENT_SHADER); - break; - - default: - Logger.core.log(LogLevel.warning, "'%s' %s: Unknown tag '%s'.", - path, tag.location, tag.getFullName()); - } - } - - shader.link(); - - return shader; - } } \ No newline at end of file diff --git a/platform/opengl/texture.d b/source/zyeware/platform/opengl/texture.d similarity index 92% rename from platform/opengl/texture.d rename to source/zyeware/platform/opengl/texture.d index d2519fe..38f3fa1 100644 --- a/platform/opengl/texture.d +++ b/source/zyeware/platform/opengl/texture.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.rendering.texture; +module zyeware.platform.opengl.texture; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import bindbc.opengl; import imagefmt; @@ -12,17 +15,7 @@ import sdlang; import zyeware.common; import zyeware.rendering; -interface Texture -{ -public: - void bind(uint unit = 0) const; - - const(TextureProperties) properties() pure const nothrow; - uint id() pure const nothrow; -} - -@asset(Yes.cache) -class Texture2D : Texture +class OGLTexture2D : Texture2D { protected: TextureProperties mProperties; @@ -30,7 +23,7 @@ protected: ubyte mChannels; uint mID; -public: +package(zyeware.platform.opengl): this(in Image image, in TextureProperties properties) { const(ubyte)[] pixels = image.pixels; @@ -66,9 +59,6 @@ public: glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, mID); - //glTextureStorage2D(mID, 1, internalFormat, mSize.x, mSize.y); - //glTextureSubImage2D(mID, 0, 0, 0, mSize.x, mSize.y, srcFormat, GL_UNSIGNED_BYTE, pixels.ptr); - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, mSize.x, mSize.y, 0, srcFormat, GL_UNSIGNED_BYTE, pixels.ptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, getGLFilter(properties.minFilter)); @@ -78,10 +68,7 @@ public: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, getGLWrapMode(properties.wrapT)); if (properties.generateMipmaps) - { - // glEnable(GL_TEXTURE_2D); // To circumvent a bug in certain ATI drivers glGenerateMipmap(GL_TEXTURE_2D); - } } this(uint id) @@ -89,6 +76,37 @@ public: mID = id; } + static Texture2D load(string path) + { + TextureProperties properties; + Image img = AssetManager.load!Image(path); + + if (VFS.hasFile(path ~ ".props")) // Properties file exists + { + import std.conv : to; + import sdlang; + + VFSFile propsFile = VFS.getFile(path ~ ".props"); + Tag root = parseSource(propsFile.readAll!string); + propsFile.close(); + + try + { + properties.minFilter = root.getTagValue!string("min-filter", "nearest").to!(TextureProperties.Filter); + properties.magFilter = root.getTagValue!string("mag-filter", "nearest").to!(TextureProperties.Filter); + properties.wrapS = root.getTagValue!string("wrap-s", "repeat").to!(TextureProperties.WrapMode); + properties.wrapT = root.getTagValue!string("wrap-t", "repeat").to!(TextureProperties.WrapMode); + } + catch (Exception ex) + { + Logger.core.log(LogLevel.warning, "Failed to parse properties file for '%s': %s", path, ex.msg); + } + } + + return new OGLTexture2D(img, properties); + } + +public: ~this() { glDeleteTextures(1, &mID); @@ -126,40 +144,9 @@ public: { return mChannels; } - - static Texture2D load(string path) - { - TextureProperties properties; - Image img = AssetManager.load!Image(path); - - if (VFS.hasFile(path ~ ".props")) // Properties file exists - { - import std.conv : to; - import sdlang; - - VFSFile propsFile = VFS.getFile(path ~ ".props"); - Tag root = parseSource(propsFile.readAll!string); - propsFile.close(); - - try - { - properties.minFilter = root.getTagValue!string("min-filter", "nearest").to!(TextureProperties.Filter); - properties.magFilter = root.getTagValue!string("mag-filter", "nearest").to!(TextureProperties.Filter); - properties.wrapS = root.getTagValue!string("wrap-s", "repeat").to!(TextureProperties.WrapMode); - properties.wrapT = root.getTagValue!string("wrap-t", "repeat").to!(TextureProperties.WrapMode); - } - catch (Exception ex) - { - Logger.core.log(LogLevel.warning, "Failed to parse properties file for '%s': %s", path, ex.msg); - } - } - - return new Texture2D(img, properties); - } } -@asset(Yes.cache) -class TextureCubeMap : Texture +class OGLTextureCubeMap : TextureCubeMap { protected: TextureProperties mProperties; @@ -275,7 +262,7 @@ public: } } - return new TextureCubeMap(images, properties); + return new OGLTextureCubeMap(images, properties); } } diff --git a/platform/opengl/utils.d b/source/zyeware/platform/opengl/utils.d similarity index 89% rename from platform/opengl/utils.d rename to source/zyeware/platform/opengl/utils.d index 0be44b9..64bcf68 100644 --- a/platform/opengl/utils.d +++ b/source/zyeware/platform/opengl/utils.d @@ -1,4 +1,7 @@ -module platform.opengl.utils; +module zyeware.platform.opengl.utils; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import bindbc.sdl; diff --git a/platform/opengl/window.d b/source/zyeware/platform/opengl/window.d similarity index 96% rename from platform/opengl/window.d rename to source/zyeware/platform/opengl/window.d index 44112d9..a0f5909 100644 --- a/platform/opengl/window.d +++ b/source/zyeware/platform/opengl/window.d @@ -3,7 +3,10 @@ // of this source code package. // // Copyright 2021 ZyeByte -module zyeware.rendering.window; +module zyeware.platform.opengl.window; + +version (ZWBackendOpenGL): +package(zyeware.platform.opengl): import core.stdc.string : memcpy; @@ -18,10 +21,9 @@ import bindbc.opengl; import zyeware.common; import zyeware.rendering; +import zyeware.platform.opengl.utils; -import platform.opengl.utils; - -class Window +class OGLWindow : Window { private: static size_t sWindowCount = 0; @@ -35,6 +37,7 @@ protected: SDL_Surface* mIconSurface; bool mVSync; bool mIsCursorCaptured; + bool mFullscreen; SDL_Cursor*[const Cursor] mSDLCursors; @@ -48,7 +51,7 @@ protected: LogLevel level; switch (priority) { - case SDL_LOG_PRIORITY_VERBOSE: level = LogLevel.trace; break; + case SDL_LOG_PRIORITY_VERBOSE: level = LogLevel.verbose; break; case SDL_LOG_PRIORITY_DEBUG: level = LogLevel.debug_; break; case SDL_LOG_PRIORITY_INFO: level = LogLevel.info; break; case SDL_LOG_PRIORITY_WARN: level = LogLevel.warning; break; @@ -130,8 +133,8 @@ protected: return getGamepadIndex(SDL_GameControllerFromInstanceID(instanceId)); } -public: - this(in WindowProperties properties = WindowProperties.init) +package(zyeware.platform.opengl): + this(in WindowProperties properties) { mTitle = properties.title; @@ -193,6 +196,7 @@ public: ++sWindowCount; } +public: ~this() { SDL_DestroyWindow(mHandle); @@ -518,6 +522,17 @@ public: SDL_RestoreWindow(mHandle); } + bool isFullscreen() nothrow + { + return mFullscreen; + } + + void isFullscreen(bool value) nothrow + { + SDL_SetWindowFullscreen(mHandle, value ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); + mFullscreen = value; + } + const(Image) icon() const nothrow { return mIcon; @@ -525,8 +540,6 @@ public: void icon(const Image value) { - import platform.opengl.utils; - if (mIconSurface) SDL_FreeSurface(mIconSurface); diff --git a/source/zyeware/rendering/api.d b/source/zyeware/rendering/api.d new file mode 100644 index 0000000..cd64278 --- /dev/null +++ b/source/zyeware/rendering/api.d @@ -0,0 +1,166 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.rendering.api; + +import zyeware.common; +import zyeware.rendering; + +/// Used for selecting a rendering backend at the start of the engine. +enum RenderBackend +{ + headless, /// A dummy API, does nothing. + openGl, /// Uses OpenGL for rendering. +} + +enum RenderFlag +{ + depthTesting, /// Whether to use depth testing or not. + depthBufferWriting, /// Whether to write to the depth buffer when drawing. + culling, /// Whether culling is enabled or not. + stencilTesting, /// Whether to use stencil testing or not. + wireframe /// Whether to render in wireframe or not. +} + +enum RenderCapability +{ + maxTextureSlots /// How many texture slots are available to use. +} + +struct RenderAPI +{ + @disable this(); + @disable this(this); + +package(zyeware) static: + void function() sInitializeImpl; + void function() sLoadLibrariesImpl; + void function() sCleanupImpl; + + void function(in Color) nothrow sSetClearColorImpl; + void function() nothrow sClearImpl; + void function(int, int, uint, uint) nothrow sSetViewportImpl; + void function(size_t) nothrow sDrawIndexedImpl; + void function(ref ConstantBuffer, in Renderer3D.Light[]) nothrow sPackLightConstantBufferImpl; + bool function(RenderFlag) nothrow sGetFlagImpl; + void function(RenderFlag, bool) nothrow sSetFlagImpl; + size_t function(RenderCapability) nothrow sGetCapabilityImpl; + + BufferGroup function() sCreateBufferGroupImpl; + DataBuffer function(size_t, BufferLayout, bool) sCreateDataBufferImpl; + DataBuffer function(const void[], BufferLayout, bool) sCreateDataBufferWithDataImpl; + IndexBuffer function(size_t, bool) sCreateIndexBufferImpl; + IndexBuffer function(const uint[], bool) sCreateIndexBufferWithDataImpl; + ConstantBuffer function(in BufferLayout) sCreateConstantBufferImpl; + + Framebuffer function(in FramebufferProperties) sCreateFramebufferImpl; + Texture2D function(in Image, in TextureProperties) sCreateTexture2DImpl; + TextureCubeMap function(in Image[6], in TextureProperties) sCreateTextureCubeMapImpl; + Window function(in WindowProperties) sCreateWindowImpl; + Shader function() sCreateShaderImpl; + + Texture2D function(string path) sLoadTexture2DImpl; + TextureCubeMap function(string path) sLoadTextureCubeMapImpl; + Shader function(string path) sLoadShaderImpl; + + pragma(inline, true) + void initialize() + { + sInitializeImpl(); + } + + pragma(inline, true) + void loadLibraries() + { + sLoadLibrariesImpl(); + } + + pragma(inline, true) + void cleanup() + { + sCleanupImpl(); + } + +public static: + /// Sets which color to use for clearing the screen. + /// + /// Params: + /// value = The color to use. + pragma(inline, true) + void setClearColor(Color value) nothrow + { + sSetClearColorImpl(value); + } + + /// Clears the screen with the color specified with `setClearColor`. + pragma(inline, true) + void clear() nothrow + { + sClearImpl(); + } + + /// Sets the viewport of the window. + /// + /// Params: + /// x = The x coordinate of the viewport. + /// y = The y coordinate of the viewport. + /// width = The width of the viewport. + /// height = The height of the viewport. + pragma(inline, true) + void setViewport(int x, int y, uint width, uint height) nothrow + { + // TODO: Change to vectors + sSetViewportImpl(x, y, width, height); + } + + /// Draws the currently bound `BufferGroup` to the screen. + /// + /// Params: + /// count = How many indicies to actually draw. + pragma(inline, true) + void drawIndexed(size_t count) nothrow + { + sDrawIndexedImpl(count); + } + + /// Packs a lights array into a constant buffer. + /// + /// Params: + /// buffer = The constant buffer to use. + /// lights = The lights array. + pragma(inline, true) + void packLightConstantBuffer(ref ConstantBuffer buffer, in Renderer3D.Light[] lights) nothrow + { + sPackLightConstantBufferImpl(buffer, lights); + } + + /// Gets a render flag. + /// + /// Params: + /// flag = The flag to query for. + pragma(inline, true) + bool getFlag(RenderFlag flag) nothrow + { + return sGetFlagImpl(flag); + } + + /// Sets a render flag. + /// + /// Params: + /// flag = The flag to set. + /// value = Whether to enable or disable the flag. + pragma(inline, true) + void setFlag(RenderFlag flag, bool value) nothrow + { + sSetFlagImpl(flag, value); + } + + /// Queries a render capability. + pragma(inline, true) + size_t getCapability(RenderCapability capability) nothrow + { + return sGetCapabilityImpl(capability); + } +} \ No newline at end of file diff --git a/source/zyeware/rendering/api.di b/source/zyeware/rendering/api.di deleted file mode 100644 index 78cbd5c..0000000 --- a/source/zyeware/rendering/api.di +++ /dev/null @@ -1,68 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.api; - -import zyeware.common; -import zyeware.rendering; - -struct RenderAPI -{ - @disable this(); - @disable this(this); - -package(zyeware) static: - void initialize(); - void loadLibraries(); - void cleanup(); - -public static: - /// Sets which color to use for clearing the screen. - /// - /// Params: - /// value = The color to use. - void setClearColor(Color value) nothrow; - - /// Clears the screen with the color specified with `setClearColor`. - void clear() nothrow; - - /// Sets the viewport of the window. - /// - /// Params: - /// x = The x coordinate of the viewport. - /// y = The y coordinate of the viewport. - /// width = The width of the viewport. - /// height = The height of the viewport. - void setViewport(int x, int y, uint width, uint height) nothrow; - - /// Draws the currently bound `BufferGroup` to the screen. - /// - /// Params: - /// count = How many indicies to actually draw. - void drawIndexed(size_t count) nothrow; - - /// Packs a lights array into a constant buffer. - /// - /// Params: - /// buffer = The constant buffer to use. - /// lights = The lights array. - void packLightConstantBuffer(ref ConstantBuffer buffer, in Renderer3D.Light[] lights) nothrow; - - /// Gets a render flag. - /// - /// Params: - /// flag = The flag to query for. - bool getFlag(RenderFlag flag) nothrow; - - /// Sets a render flag. - /// - /// Params: - /// flag = The flag to set. - /// value = Whether to enable or disable the flag. - void setFlag(RenderFlag flag, bool value) nothrow; - - /// Queries a render capability. - size_t getCapability(RenderCapability capability) nothrow; -} \ No newline at end of file diff --git a/source/zyeware/rendering/buffer.d b/source/zyeware/rendering/buffer.d new file mode 100644 index 0000000..bf8c846 --- /dev/null +++ b/source/zyeware/rendering/buffer.d @@ -0,0 +1,243 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.rendering.buffer; + +import inmath.linalg; + +import zyeware.common; +import zyeware.rendering; + +/// Represents an element in a tightly-bound data buffer. +struct BufferElement +{ +private: + string mName; + Type mType; + uint mSize; + uint mOffset; + bool mNormalized; + uint mDivisor; + uint mAmount; + + static uint getTypeSize(Type type) pure nothrow + { + final switch (type) with (Type) + { + case none: return 1; + case vec2: return float.sizeof * 2; + case vec3: return float.sizeof * 3; + case vec4: return float.sizeof * 4; + case mat3: return float.sizeof * 3 * 3; + case mat4: return float.sizeof * 4 * 4; + case float_: return float.sizeof; + case int_: return int.sizeof; + case bool_: return 1; + } + } + +public: + /// The type of the element. + enum Type : ubyte + { + none, + vec2, vec3, vec4, + mat3, mat4, + float_, int_, bool_ + } + + /// Params: + /// name = The name of the element. Used for debugging purposes. + /// type = The type of the element. + /// normalized = If this element is normalized. Only effective on vectors. + /// divisor = The divisor of this element. + this(string name, Type type, uint amount = 1, Flag!"normalized" normalized = No.normalized, uint divisor = 0) + { + mName = name; + mType = type; + mNormalized = normalized; + mSize = elementSize * amount; + mAmount = amount; + mDivisor = divisor; + } + + /// The name of the buffer element. + string name() pure const nothrow + { + return mName; + } + + /// The type of the buffer element. + Type type() pure const nothrow + { + return mType; + } + + /// The size of the buffer element in bytes. + uint size() pure const nothrow + { + return mSize; + } + + /// The amount of individual elements of this buffer element. + uint amount() pure const nothrow + { + return mAmount; + } + + /// The size of an individual element of this buffer element, in bytes. + uint elementSize() pure const nothrow + { + return getTypeSize(mType); + } + + /// Offset of this buffer element inside the buffer. + uint offset() pure const nothrow + { + return mOffset; + } + + /// If the values of this buffer element are normalized. + bool normalized() pure const nothrow + { + return mNormalized; + } + + /// The divisor of this element. + uint divisor() pure const nothrow + { + return mDivisor; + } +} + +/// Represents a layout of a tightly-bound data buffer. +struct BufferLayout +{ +private: + BufferElement[] mElements; + uint mStride; + + void calculateOffsetAndStride() pure nothrow + { + mStride = 0; + + foreach (ref BufferElement element; mElements) + { + element.mOffset = mStride; + mStride += element.size; + } + } + +public: + /// Params: + /// elements = The elements of one instance. + this(BufferElement[] elements) + { + mElements = elements; + calculateOffsetAndStride(); + } + + /// How many bytes one instance uses. + uint stride() pure const nothrow + { + return mStride; + } + + /// The elements of one instance. + inout(BufferElement[]) elements() pure inout nothrow + { + return mElements; + } +} + +/// A buffer group represents a data buffer and an index buffer combined. +/// +/// See_Also: DataBuffer, IndexBuffer. +interface BufferGroup +{ + /// Binds this buffer group (therefore the data and index buffers) for further use. + void bind() const nothrow; + + /// The data buffer of this buffer group. + void dataBuffer(DataBuffer buffer) nothrow; + /// ditto + inout(DataBuffer) dataBuffer() inout nothrow; + + /// The index buffer of this buffer group. + void indexBuffer(IndexBuffer buffer) nothrow; + /// ditto + inout(IndexBuffer) indexBuffer() inout nothrow; + + static BufferGroup create() + { + return RenderAPI.sCreateBufferGroupImpl(); + } +} + +interface DataBuffer +{ + void bind() const nothrow; + + void setData(const void[] data); + + size_t length() const nothrow; + + void layout(BufferLayout layout) nothrow; + const(BufferLayout) layout() const nothrow; + + static DataBuffer create(size_t size, BufferLayout layout, Flag!"dynamic" dynamic) + { + return RenderAPI.sCreateDataBufferImpl(size, layout, dynamic); + } + + static DataBuffer create(const void[] data, BufferLayout layout, Flag!"dynamic" dynamic) + { + return RenderAPI.sCreateDataBufferWithDataImpl(data, layout, dynamic); + } +} + +interface IndexBuffer +{ + void bind() const nothrow; + + void setData(const uint[] indices); + + size_t length() const nothrow; + + static IndexBuffer create(size_t size, Flag!"dynamic" dynamic) + { + return RenderAPI.sCreateIndexBufferImpl(size, dynamic); + } + + static IndexBuffer create(const uint[] indices, Flag!"dynamic" dynamic) + { + return RenderAPI.sCreateIndexBufferWithDataImpl(indices, dynamic); + } +} + +interface ConstantBuffer +{ + enum Slot + { + matrices, + environment, + lights, + modelVariables + } + + void bind(Slot slot) const nothrow; + + size_t getEntryOffset(string name) const nothrow; + + void setData(size_t offset, in void[] data) nothrow; + + size_t length() const nothrow; + const(string[]) entries() const nothrow; + + static ConstantBuffer create(in BufferLayout layout) + { + return RenderAPI.sCreateConstantBufferImpl(layout); + } +} \ No newline at end of file diff --git a/source/zyeware/rendering/buffer.di b/source/zyeware/rendering/buffer.di deleted file mode 100644 index 84f1bc4..0000000 --- a/source/zyeware/rendering/buffer.di +++ /dev/null @@ -1,128 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.buffer; - -import inmath.linalg; - -import zyeware.common; - -/// Represents an element in a tightly-bound data buffer. -struct BufferElement -{ -public: - /// The type of the element. - enum Type : ubyte - { - none, - vec2, vec3, vec4, - mat3, mat4, - float_, int_, bool_ - } - - /// Params: - /// name = The name of the element. Used for debugging purposes. - /// type = The type of the element. - /// normalized = If this element is normalized. Only effective on vectors. - /// divisor = The divisor of this element. - this(string name, Type type, uint amount = 1, Flag!"normalized" normalized = No.normalized, uint divisor = 0); - - string name() pure const nothrow; - Type type() pure const nothrow; - uint size() pure const nothrow; - uint amount() pure const nothrow; - uint elementSize() pure const nothrow; - uint offset() pure const nothrow; - bool normalized() pure const nothrow; - uint divisor() pure const nothrow; -} - -/// Represents a layout of a tightly-bound data buffer. -struct BufferLayout -{ -public: - /// Params: - /// elements = The elements of one instance. - this(BufferElement[] elements); - - /// How many bytes one instance uses. - uint stride() pure const nothrow; - - /// The elements of one instance. - inout(BufferElement[]) elements() pure inout nothrow; -} - -/// A buffer group represents a data buffer and an index buffer combined. -/// -/// See_Also: DataBuffer, IndexBuffer. -class BufferGroup -{ -public: - this(); - - /// Binds this buffer group (therefore the data and index buffers) for further use. - void bind() const nothrow; - - /// The data buffer of this buffer group. - void dataBuffer(DataBuffer buffer) nothrow; - /// ditto - inout(DataBuffer) dataBuffer() inout nothrow; - - /// The index buffer of this buffer group. - void indexBuffer(IndexBuffer buffer) nothrow; - /// ditto - inout(IndexBuffer) indexBuffer() inout nothrow; -} - -class DataBuffer -{ -public: - this(size_t size, BufferLayout layout, Flag!"dynamic" dynamic); - this(const void[] data, BufferLayout layout, Flag!"dynamic" dynamic); - - void bind() const nothrow; - - void setData(const void[] data); - - size_t length() const nothrow; - - void layout(BufferLayout layout) nothrow; - const(BufferLayout) layout() const nothrow; -} - -class IndexBuffer -{ -public: - this(size_t size, Flag!"dynamic" dynamic); - this(const uint[] indices, Flag!"dynamic" dynamic); - - void bind() const nothrow; - - void setData(const uint[] indices); - - size_t length() const nothrow; -} - -class ConstantBuffer -{ - enum Slot - { - matrices, - environment, - lights, - modelVariables - } - - this(in BufferLayout layout); - - void bind(Slot slot) const nothrow; - - size_t getEntryOffset(string name) const nothrow; - - void setData(size_t offset, in void[] data) nothrow; - - size_t length() const nothrow; - const(string[]) entries() const nothrow; -} \ No newline at end of file diff --git a/source/zyeware/rendering/camera.d b/source/zyeware/rendering/camera.d index 1cfddb8..26a83c2 100644 --- a/source/zyeware/rendering/camera.d +++ b/source/zyeware/rendering/camera.d @@ -8,11 +8,18 @@ module zyeware.rendering.camera; import zyeware.common; import zyeware.rendering; +/// Provides common functionality between different types of cameras. interface Camera { public: + /// The projection matrix of this camera. Matrix4f projectionMatrix() pure const nothrow; + /// Calculates a new view matrix based on the given translation. + /// Params: + /// position = The position of the camera. + /// rotation = The rotation of the camera. + /// Returns: A newly calculated view matrix. pragma(inline, true) static final Matrix4f calculateViewMatrix(Vector3f position, Quaternionf rotation) pure nothrow { @@ -20,12 +27,20 @@ public: } } +/// Represents a camera with a orthographic projection. class OrthographicCamera : Camera { protected: Matrix4f mProjectionMatrix; public: + /// Params: + /// left = + /// right = + /// bottom = + /// top = + /// near = + /// far = this(float left, float right, float bottom, float top, float near = -1f, float far = 1f) pure nothrow { setData(left, right, bottom, top, near, far); diff --git a/source/zyeware/rendering/font.d b/source/zyeware/rendering/font.d index 0041d71..9cfee3a 100644 --- a/source/zyeware/rendering/font.d +++ b/source/zyeware/rendering/font.d @@ -41,7 +41,7 @@ public: bmFont.info.fontName, bmFont.info.fontSize); foreach (string pagePath; bmFont.pages) - mPageTextures ~= Texture2D.load(pagePath);//AssetManager.load!Texture2D(pagePath); + mPageTextures ~= cast(Texture2D) Texture2D.load(pagePath);//AssetManager.load!Texture2D(pagePath); } @@ -114,7 +114,7 @@ public: static Font load(string path) in (path, "Path cannot be null.") { - Logger.core.log(LogLevel.trace, "Loading Font from '%s'...", path); + Logger.core.log(LogLevel.verbose, "Loading Font from '%s'...", path); VFSFile source = VFS.getFile(path); auto bmFont = parseFnt(source.readAll!(ubyte[])); diff --git a/source/zyeware/rendering/framebuffer.di b/source/zyeware/rendering/framebuffer.d similarity index 69% rename from source/zyeware/rendering/framebuffer.di rename to source/zyeware/rendering/framebuffer.d index 878e994..60e4edd 100644 --- a/source/zyeware/rendering/framebuffer.di +++ b/source/zyeware/rendering/framebuffer.d @@ -8,11 +8,16 @@ module zyeware.rendering.framebuffer; import zyeware.common; import zyeware.rendering; -class Framebuffer +struct FramebufferProperties { -public: - this(in FramebufferProperties properties); + Vector2i size; + ubyte channels; + bool swapChainTarget; +} +interface Framebuffer +{ +public: void bind() const; void unbind() const; void invalidate(); @@ -22,4 +27,9 @@ public: const(Texture2D) colorAttachment() const nothrow; const(Texture2D) depthAttachment() const nothrow; + + static Framebuffer create(in FramebufferProperties properties) + { + return RenderAPI.sCreateFramebufferImpl(properties); + } } \ No newline at end of file diff --git a/source/zyeware/rendering/image.d b/source/zyeware/rendering/image.d index de9cec6..6ee7f05 100644 --- a/source/zyeware/rendering/image.d +++ b/source/zyeware/rendering/image.d @@ -94,9 +94,7 @@ public: file.read(data); file.close(); - IFImage img = read_image(data); - - return new Image(img.buf8, img.c, img.bpc, Vector2i(img.w, img.h)); + return load(data); } static Image load(in ubyte[] data) diff --git a/source/zyeware/rendering/material.d b/source/zyeware/rendering/material.d index a611473..48874bd 100644 --- a/source/zyeware/rendering/material.d +++ b/source/zyeware/rendering/material.d @@ -60,7 +60,7 @@ public: mIsRoot = true; mTextureSlots.length = shader.textureCount; - mBuffer = new ConstantBuffer(layout); + mBuffer = ConstantBuffer.create(layout); } this(Material parent) diff --git a/source/zyeware/rendering/mesh.d b/source/zyeware/rendering/mesh.d index 7235aae..5e1f7db 100644 --- a/source/zyeware/rendering/mesh.d +++ b/source/zyeware/rendering/mesh.d @@ -225,15 +225,15 @@ public: { calculateNormals(vertices, indices); - mBufferGroup = new BufferGroup(); - mBufferGroup.dataBuffer = new DataBuffer(vertices, BufferLayout([ + mBufferGroup = BufferGroup.create(); + mBufferGroup.dataBuffer = DataBuffer.create(vertices, BufferLayout([ BufferElement("aPosition", BufferElement.Type.vec3), BufferElement("aUV", BufferElement.Type.vec2), BufferElement("aNormal", BufferElement.Type.vec3), BufferElement("aColor", BufferElement.Type.vec4) ]), No.dynamic); - mBufferGroup.indexBuffer = new IndexBuffer(indices, No.dynamic); + mBufferGroup.indexBuffer = IndexBuffer.create(indices, No.dynamic); mMaterial = material; } diff --git a/source/zyeware/rendering/package.d b/source/zyeware/rendering/package.d index 26bc16e..f3d0919 100644 --- a/source/zyeware/rendering/package.d +++ b/source/zyeware/rendering/package.d @@ -18,7 +18,6 @@ public import zyeware.rendering.image; import zyeware.rendering.material; import zyeware.rendering.mesh; - import zyeware.rendering.properties; import zyeware.rendering.renderable; import zyeware.rendering.shader; import zyeware.rendering.sky; diff --git a/source/zyeware/rendering/properties.d b/source/zyeware/rendering/properties.d deleted file mode 100644 index f24ea08..0000000 --- a/source/zyeware/rendering/properties.d +++ /dev/null @@ -1,64 +0,0 @@ -module zyeware.rendering.properties; - -import zyeware.common; -import zyeware.rendering; - -struct WindowProperties -{ - string title = "ZyeWare Engine"; - Vector2i size = Vector2i(1280, 720); - Image icon; -} - -struct FramebufferProperties -{ - Vector2i size; - ubyte channels; - bool swapChainTarget; -} - -struct TextureProperties -{ - enum Filter - { - nearest, - linear, - bilinear, - trilinear - } - - enum WrapMode - { - repeat, - mirroredRepeat, - clampToEdge - } - - Filter minFilter, magFilter; - WrapMode wrapS, wrapT; - bool generateMipmaps = true; -} - -struct TerrainProperties -{ - Vector2f size; - Vector2i vertexCount; - float[] heightData; // Row-major - Texture2D[4] textures; - Texture2D blendMap; - Vector2f textureTiling = Vector2f(1); -} - -enum RenderFlag -{ - depthTesting, /// Whether to use depth testing or not. - depthBufferWriting, /// Whether to write to the depth buffer when drawing. - culling, /// Whether culling is enabled or not. - stencilTesting, /// Whether to use stencil testing or not. - wireframe /// Whether to render in wireframe or not. -} - -enum RenderCapability -{ - maxTextureSlots /// How many texture slots are available to use. -} \ No newline at end of file diff --git a/source/zyeware/rendering/renderer/renderer2d.di b/source/zyeware/rendering/renderer/renderer2d.d similarity index 62% rename from source/zyeware/rendering/renderer/renderer2d.di rename to source/zyeware/rendering/renderer/renderer2d.d index 1003d86..a124a64 100644 --- a/source/zyeware/rendering/renderer/renderer2d.di +++ b/source/zyeware/rendering/renderer/renderer2d.d @@ -23,11 +23,27 @@ struct Renderer2D @disable this(this); package(zyeware) static: - /// Initializes 2D rendering. - void initialize(); + void function() sInitializeImpl; + void function() sCleanupImpl; + void function(in Matrix4f, in Matrix4f) sBeginImpl; + void function() sEndImpl; + void function() sFlushImpl; + void function(in Rect2f, in Matrix4f, in Color, in Texture2D, in Rect2f) sDrawRectImpl; + void function(in string, in Font, in Vector2f, in Color, ubyte alignment) sDrawStringImpl; + void function(in wstring, in Font, in Vector2f, in Color, ubyte alignment) sDrawWStringImpl; + void function(in dstring, in Font, in Vector2f, in Color, ubyte alignment) sDrawDStringImpl; - /// Cleans up all used resources. - void cleanup(); + pragma(inline, true) + void initialize() + { + sInitializeImpl(); + } + + pragma(inline, true) + void cleanup() + { + sCleanupImpl(); + } public static: /// Starts a 2D scene. This must be called before any 2D drawing commands. @@ -35,14 +51,26 @@ public static: /// Params: /// projectionMatrix = A 4x4 matrix used for projection. /// viewMatrix = A 4x4 matrix used for view. - void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix); + pragma(inline, true) + void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix) + { + sBeginImpl(projectionMatrix, viewMatrix); + } /// Ends a 2D scene. This must be called at the end of all 2D drawing commands, as it flushes /// everything to the screen. - void end(); + pragma(inline, true) + void end() + { + sEndImpl(); + } /// Flushes all currently cached drawing commands to the screen. - void flush(); + pragma(inline, true) + void flush() + { + sFlushImpl(); + } /// Draws a rectangle. /// @@ -55,7 +83,11 @@ public static: /// region = The region of the rectangle to use. Has no effect if no texture is supplied. pragma(inline, true) void drawRect(in Rect2f dimensions, in Vector2f position, in Vector2f scale, in Color modulate = Color.white, - in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)); + in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)) + { + sDrawRectImpl(dimensions, Matrix4f.translation(Vector3f(position, 0)) * Matrix4f.scaling(scale.x, scale.y, 1), + modulate, texture, region); + } /// Draws a rectangle. /// @@ -69,20 +101,28 @@ public static: /// region = The region of the rectangle to use. Has no effect if no texture is supplied. pragma(inline, true) void drawRect(in Rect2f dimensions, in Vector2f position, in Vector2f scale, float rotation, in Color modulate = Vector4f(1), - in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)); + in Texture2D texture = null, in Rect2f region = Rect2f(0, 0, 1, 1)) + { + sDrawRectImpl(dimensions, Matrix4f.translation(Vector3f(position, 0)) * Matrix4f.rotation(rotation, Vector3f(0, 0, 1)) + * Matrix4f.scaling(scale.x, scale.y, 1), modulate, texture, region); + } /// Draws a rectangle. /// /// Params: /// dimensions = The dimensions of the rectangle to draw. /// transform = A 4x4 matrix used for transformation of the rectangle. - /// modulate = The color of the rectangle. If a texture is supplied, it will be tinted in this color. + /// modulate = The color of the rectangle. If a texture is suppliedW, it will be tinted in this color. /// texture = The texture to use. If `null`, draws a blank rectangle. /// region = The region of the rectangle to use. Has no effect if no texture is supplied. + pragma(inline, true) void drawRect(in Rect2f dimensions, in Matrix4f transform, in Color modulate = Vector4f(1), in Texture2D texture = null, - in Rect2f region = Rect2f(0, 0, 1, 1)); + in Rect2f region = Rect2f(0, 0, 1, 1)) + { + sDrawRectImpl(dimensions, transform, modulate, texture, region); + } - /// Draws some text to screen. + /// Draws some string to the screen. /// /// Params: /// text = The text to draw. May be of any string type. @@ -90,7 +130,18 @@ public static: /// position = 2D position where to draw the text to. /// modulate = The color of the text. /// alignment = How to align the text. Horizontal and vertical alignment can be OR'd together. - void drawText(T)(in T text, in Font font, in Vector2f position, in Color modulate = Color.white, + pragma(inline, true) + void drawString(T)(in T text, in Font font, in Vector2f position, in Color modulate = Color.white, ubyte alignment = Font.Alignment.left | Font.Alignment.top) - if (isSomeString!T); + if (isSomeString!T) + { + static if (is(T == string)) + sDrawStringImpl(text, font, position, modulate, alignment); + else static if (is(T == wstring)) + sDrawWStringImpl(text, font, position, modulate, alignment); + else static if (is(T == dstring)) + sDrawDStringImpl(text, font, position, modulate, alignment); + else + static assert(false, "Cannot render string of type " ~ T.stringof); + } } \ No newline at end of file diff --git a/source/zyeware/rendering/renderer/renderer3d.di b/source/zyeware/rendering/renderer/renderer3d.d similarity index 63% rename from source/zyeware/rendering/renderer/renderer3d.di rename to source/zyeware/rendering/renderer/renderer3d.d index 6660e25..e1f2c81 100644 --- a/source/zyeware/rendering/renderer/renderer3d.di +++ b/source/zyeware/rendering/renderer/renderer3d.d @@ -18,11 +18,25 @@ struct Renderer3D @disable this(this); package(zyeware) static: - /// Initializes 3D rendering. - void initialize(); + void function() sInitializeImpl; + void function() sCleanupImpl; + void function(in Light[]) sUploadLightsImpl; + void function(in Matrix4f, in Matrix4f, Environment3D) sBeginImpl; + void function() sEndImpl; + void function() sFlushImpl; + void function(BufferGroup, Material, in Matrix4f) sSubmitImpl; - /// Cleans up all used resources. - void cleanup(); + pragma(inline, true) + void initialize() + { + sInitializeImpl(); + } + + pragma(inline, true) + void cleanup() + { + sCleanupImpl(); + } public static: /// Represents a light. @@ -34,11 +48,18 @@ public static: Vector3f attenuation; /// The attenuation of the light. } + /// How many lights can be rendered in one draw call. + enum maxLights = 10; + /// Uploads a struct of light arrays to the rendering API for the next draw call. /// /// Params: /// lights = The array of lights to upload. - void uploadLights(Light[] lights); + pragma(inline, true) + void uploadLights(Light[] lights) + { + sUploadLightsImpl(lights); + } /// Starts a 3D scene. This must be called before any 3D drawing commands. /// @@ -47,14 +68,26 @@ public static: /// viewMatrix = A 4x4 matrix used for view. /// environment = The rendering environment for this scene. May be `null`. /// depthTest = Whether to use depth testing for this scene or not. - void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix, Environment3D environment); + pragma(inline, true) + void begin(in Matrix4f projectionMatrix, in Matrix4f viewMatrix, Environment3D environment) + { + sBeginImpl(projectionMatrix, viewMatrix, environment); + } /// Ends a 3D scene. This must be called at the end of all 3D drawing commands, as it flushes /// everything to the screen. - void end(); + pragma(inline, true) + void end() + { + sEndImpl(); + } /// Flushes all currently cached drawing commands to the screen. - void flush(); + pragma(inline, true) + void flush() + { + sFlushImpl(); + } /// Submits a draw command. /// @@ -62,7 +95,10 @@ public static: /// renderable = The renderable instance to draw. /// transform = A 4x4 matrix used for transformation. pragma(inline, true) - void submit(Renderable renderable, in Matrix4f transform); + void submit(Renderable renderable, in Matrix4f transform) + { + sSubmitImpl(renderable.bufferGroup, renderable.material, transform); + } // TODO: Check constness! /// Submits a draw command. @@ -71,5 +107,9 @@ public static: /// group = The buffer group to draw. /// material = The material to use for drawing. /// transform = A 4x4 matrix used for transformation. - void submit(BufferGroup group, Material material, in Matrix4f transform); + pragma(inline, true) + void submit(BufferGroup group, Material material, in Matrix4f transform) + { + sSubmitImpl(group, material, transform); + } } \ No newline at end of file diff --git a/source/zyeware/rendering/shader.di b/source/zyeware/rendering/shader.d similarity index 63% rename from source/zyeware/rendering/shader.di rename to source/zyeware/rendering/shader.d index 17b4d4f..3f18420 100644 --- a/source/zyeware/rendering/shader.di +++ b/source/zyeware/rendering/shader.d @@ -8,16 +8,23 @@ module zyeware.rendering.shader; import inmath.linalg; import zyeware.common; +import zyeware.rendering; @asset(Yes.cache) -class Shader +interface Shader { public: - this(); - void bind() const; size_t textureCount() pure const nothrow; - static Shader load(string path); + static Shader create() + { + return RenderAPI.sCreateShaderImpl(); + } + + static Shader load(string path) + { + return RenderAPI.sLoadShaderImpl(path); + } } \ No newline at end of file diff --git a/source/zyeware/rendering/sky.d b/source/zyeware/rendering/sky.d index 91cfff0..ba269e3 100644 --- a/source/zyeware/rendering/sky.d +++ b/source/zyeware/rendering/sky.d @@ -44,12 +44,12 @@ public: 1, 2, 5, 5, 2, 6 ]; - mBufferGroup = new BufferGroup(); - mBufferGroup.dataBuffer = new DataBuffer(vertices, BufferLayout([ + mBufferGroup = BufferGroup.create(); + mBufferGroup.dataBuffer = DataBuffer.create(vertices, BufferLayout([ BufferElement("aPosition", BufferElement.Type.vec3) ]), No.dynamic); - mBufferGroup.indexBuffer = new IndexBuffer(indices, No.dynamic); + mBufferGroup.indexBuffer = IndexBuffer.create(indices, No.dynamic); } inout(BufferGroup) bufferGroup() inout pure nothrow diff --git a/source/zyeware/rendering/terrain.d b/source/zyeware/rendering/terrain.d index 96b1cd4..d60639c 100644 --- a/source/zyeware/rendering/terrain.d +++ b/source/zyeware/rendering/terrain.d @@ -10,6 +10,16 @@ import std.math : fmod; import zyeware.common; import zyeware.rendering; +struct TerrainProperties +{ + Vector2f size; + Vector2i vertexCount; + float[] heightData; // Row-major + Texture2D[4] textures; + Texture2D blendMap; + Vector2f textureTiling = Vector2f(1); +} + class Terrain : Renderable { protected: diff --git a/source/zyeware/rendering/texture.d b/source/zyeware/rendering/texture.d new file mode 100644 index 0000000..6d6bbf8 --- /dev/null +++ b/source/zyeware/rendering/texture.d @@ -0,0 +1,82 @@ +// This file is part of the ZyeWare Game Engine, and subject to the terms +// and conditions defined in the file 'LICENSE.txt', which is part +// of this source code package. +// +// Copyright 2021 ZyeByte +module zyeware.rendering.texture; + +import zyeware.common; +import zyeware.rendering; + +struct TextureProperties +{ + enum Filter + { + nearest, + linear, + bilinear, + trilinear + } + + enum WrapMode + { + repeat, + mirroredRepeat, + clampToEdge + } + + Filter minFilter, magFilter; + WrapMode wrapS, wrapT; + bool generateMipmaps = true; +} + +interface Texture +{ +public: + void bind(uint unit = 0) const; + + const(TextureProperties) properties() pure const nothrow; + uint id() pure const nothrow; +} + +@asset(Yes.cache) +interface Texture2D : Texture +{ + void bind(uint unit = 0) const; + void setPixels(const(ubyte)[] pixels); + + const(TextureProperties) properties() pure const nothrow; + uint id() pure const nothrow; + + Vector2i size() pure const nothrow; + ubyte channels() pure const nothrow; + + static Texture2D create(in Image image, in TextureProperties properties) + { + return RenderAPI.sCreateTexture2DImpl(image, properties); + } + + static Texture2D load(string path) + { + return RenderAPI.sLoadTexture2DImpl(path); + } +} + +@asset(Yes.cache) +interface TextureCubeMap : Texture +{ + void bind(uint unit = 0) const; + + const(TextureProperties) properties() pure const nothrow; + uint id() pure const nothrow; + + static TextureCubeMap create(in Image[6] images, in TextureProperties properties) + { + return RenderAPI.sCreateTextureCubeMapImpl(images, properties); + } + + static TextureCubeMap load(string path) + { + return RenderAPI.sLoadTextureCubeMapImpl(path); + } +} \ No newline at end of file diff --git a/source/zyeware/rendering/texture.di b/source/zyeware/rendering/texture.di deleted file mode 100644 index e091d0f..0000000 --- a/source/zyeware/rendering/texture.di +++ /dev/null @@ -1,48 +0,0 @@ -// This file is part of the ZyeWare Game Engine, and subject to the terms -// and conditions defined in the file 'LICENSE.txt', which is part -// of this source code package. -// -// Copyright 2021 ZyeByte -module zyeware.rendering.texture; - -import zyeware.common; -import zyeware.rendering; - -interface Texture -{ -public: - void bind(uint unit = 0) const; - - const(TextureProperties) properties() pure const nothrow; - uint id() pure const nothrow; -} - -@asset(Yes.cache) -class Texture2D : Texture -{ - this(in Image image, in TextureProperties properties); - - void bind(uint unit = 0) const; - void setPixels(const(ubyte)[] pixels); - - const(TextureProperties) properties() pure const nothrow; - uint id() pure const nothrow; - - Vector2i size() pure const nothrow; - ubyte channels() pure const nothrow; - - static Texture2D load(string path); -} - -@asset(Yes.cache) -class TextureCubeMap : Texture -{ - this(in Image[6] images, in TextureProperties properties); - - void bind(uint unit = 0) const; - - const(TextureProperties) properties() pure const nothrow; - uint id() pure const nothrow; - - static TextureCubeMap load(string path); -} \ No newline at end of file diff --git a/source/zyeware/rendering/window.di b/source/zyeware/rendering/window.d similarity index 80% rename from source/zyeware/rendering/window.di rename to source/zyeware/rendering/window.d index 9a3b8af..a99e0a2 100644 --- a/source/zyeware/rendering/window.di +++ b/source/zyeware/rendering/window.d @@ -8,10 +8,15 @@ module zyeware.rendering.window; import zyeware.common; import zyeware.rendering; -class Window +struct WindowProperties { - this(in WindowProperties properties = WindowProperties.init); + string title = "ZyeWare Engine"; + Vector2i size = Vector2i(1280, 720); + Image icon; +} +interface Window +{ void update(); void swapBuffers(); @@ -42,6 +47,9 @@ class Window bool isMinimized() nothrow; void isMinimized(bool value) nothrow; + bool isFullscreen() nothrow; + void isFullscreen(bool value) nothrow; + const(Image) icon() const nothrow; void icon(const Image value); @@ -50,4 +58,9 @@ class Window void cursor(Cursor value) nothrow; const(Cursor) cursor() const nothrow; + + static Window create(in WindowProperties properties) + { + return RenderAPI.sCreateWindowImpl(properties); + } } \ No newline at end of file diff --git a/source/zyeware/utils/collection.d b/source/zyeware/utils/collection.d index d01054e..e39b64e 100644 --- a/source/zyeware/utils/collection.d +++ b/source/zyeware/utils/collection.d @@ -8,44 +8,55 @@ module zyeware.utils.collection; import std.traits : hasIndirections, isDynamicArray; import std.algorithm : countUntil, remove; +/// A growable circular queue represents a FIFO collection that, +/// except if it needs to grow, doesn't allocate and free memory +/// with each push and pop. struct GrowableCircularQueue(T) { private: + size_t mLength; size_t mFirst, mLast; T[] mArray = [T.init]; public: - size_t length; - + /// Params: + /// items = The items to initialise this queue with. this(T[] items...) pure nothrow { foreach (x; items) push(x); } + /// Whether the collection is empty. bool empty() pure const nothrow { - return length == 0; + return mLength == 0; } + /// Returns the front element of the queue. inout(T) front() pure inout nothrow - in (length != 0, "Cannot get front, is empty.") + in (mLength != 0, "Cannot get front, is empty.") { return mArray[mFirst]; } + /// Returns the n-th element of the queue, starting from 0. inout(T) opIndex(in size_t i) pure inout nothrow - in (i < length, "OpIndex out of bounds!") + in (i < mLength, "OpIndex out of bounds!") { - return mArray[(mFirst + i) & (mArray.length - 1)]; + return mArray[(mFirst + i) & (mArray.mLength - 1)]; } + /// Pushes an item into the queue. Can cause a growth and allocation + /// if there is not enough space. + /// Params: + /// item = The item to push into the queue. void push(T item) pure nothrow { - if (length >= mArray.length) + if (mLength >= mArray.mLength) { // Double the queue. - immutable oldALen = mArray.length; - mArray.length *= 2; + immutable oldALen = mArray.mLength; + mArray.mLength *= 2; if (mLast < mFirst) { mArray[oldALen .. oldALen + mLast + 1] = mArray[0 .. mLast + 1]; @@ -55,21 +66,28 @@ public: } } - mLast = (mLast + 1) & (mArray.length - 1); + mLast = (mLast + 1) & (mArray.mLength - 1); mArray[mLast] = item; - length++; + mLength++; } + /// Pops the front-most item from the queue. T pop() pure nothrow - in (length != 0, "Cannot pop from queue, is empty.") + in (mLength != 0, "Cannot pop from queue, is empty.") { auto saved = mArray[mFirst]; static if (hasIndirections!T) mArray[mFirst] = T.init; // Help for the GC. - mFirst = (mFirst + 1) & (mArray.length - 1); - length--; + mFirst = (mFirst + 1) & (mArray.mLength - 1); + mLength--; return saved; } + + /// The length of the queue. + size_t length() pure const nothrow + { + return mLength; + } } struct GrowableStack(T) @@ -124,12 +142,12 @@ public: return saved; } - size_t length() pure const nothrow + size_t mLength() pure const nothrow { return mNextPointer; } - void length(size_t value) pure nothrow + void mLength(size_t value) pure nothrow { mNextPointer = value; static if (hasIndirections!T) diff --git a/source/zyeware/vfs/root.d b/source/zyeware/vfs/root.d index b37b752..05992b6 100644 --- a/source/zyeware/vfs/root.d +++ b/source/zyeware/vfs/root.d @@ -12,7 +12,7 @@ import std.typecons : Tuple; import std.range : empty; import std.string : fromStringz, format; import std.file : mkdirRecurse, thisExePath, exists; -import std.path : buildNormalizedPath, dirName; +import std.path : buildNormalizedPath, dirName, isValidPath; import zyeware.common; import zyeware.vfs; @@ -177,4 +177,10 @@ public static: { return sPortableMode; } + + bool isValidVFSPath(string path) pure nothrow + { + auto splitResult = path.findSplit("://"); + return !splitResult[0].empty && !splitResult[1].empty && !splitResult[2].empty; + } } \ No newline at end of file