diff --git a/src/main/kotlin/team/exception/sakura/graphics/G2RenderSystem.kt b/src/main/kotlin/team/exception/sakura/graphics/G2RenderSystem.kt index 8bd7d13..40c2d84 100644 --- a/src/main/kotlin/team/exception/sakura/graphics/G2RenderSystem.kt +++ b/src/main/kotlin/team/exception/sakura/graphics/G2RenderSystem.kt @@ -1,12 +1,30 @@ package team.exception.sakura.graphics -import team.exception.sakura.graphics.buffer.G2Device +import org.lwjgl.opengl.GL41.* +import team.exception.sakura.graphics.geek2.G2Device import team.exception.sakura.graphics.gl.GlDevice object G2RenderSystem { val backend: Backends = Backends.OPENGL + val gpuType: GpuTypes = pickGpuType() + + private fun pickGpuType(): GpuTypes { + when (backend) { + Backends.OPENGL -> { + val glVendor = glGetString(GL_VENDOR) ?: return GpuTypes.OTHER + return when { + glVendor.contains("Intel") -> GpuTypes.INTEL + glVendor.contains("AMD") -> GpuTypes.AMD + glVendor.contains("NVIDIA") -> GpuTypes.NVIDIA + glVendor.contains("Apple") -> GpuTypes.APPLE + else -> GpuTypes.OTHER + } + } + } + } + val device: G2Device = when (backend) { Backends.OPENGL -> GlDevice() } @@ -15,4 +33,12 @@ object G2RenderSystem { OPENGL, } + enum class GpuTypes { + INTEL, + AMD, + NVIDIA, + APPLE, + OTHER, + } + } \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Buffer.kt b/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Buffer.kt deleted file mode 100644 index ff6d935..0000000 --- a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Buffer.kt +++ /dev/null @@ -1,23 +0,0 @@ -package team.exception.sakura.graphics.buffer - -import java.nio.ByteBuffer - -abstract class G2Buffer( - val size: Long, - val access: Access, -) { - - /** - * Get the mapped buffer. - * @return the mapped buffer - * @throws IllegalStateException if the buffer hasn't been mapped. - */ - abstract fun getMappedBuffer(): ByteBuffer - - enum class Access { - WRITE, - READ, - READ_WRITE - } - -} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2CommandList.kt b/src/main/kotlin/team/exception/sakura/graphics/buffer/G2CommandList.kt deleted file mode 100644 index eebf54a..0000000 --- a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2CommandList.kt +++ /dev/null @@ -1,11 +0,0 @@ -package team.exception.sakura.graphics.buffer - -abstract class G2CommandList { - - abstract fun summit() - - abstract fun clear() - - abstract fun summitAndClear() - -} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Device.kt b/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Device.kt deleted file mode 100644 index d58d4c4..0000000 --- a/src/main/kotlin/team/exception/sakura/graphics/buffer/G2Device.kt +++ /dev/null @@ -1,14 +0,0 @@ -package team.exception.sakura.graphics.buffer - -abstract class G2Device { - - abstract fun createCommandList(): G2CommandList - - abstract fun getTempCommandList(): G2CommandList - - abstract fun createBuffer( - size: Long, - access: G2Buffer.Access = G2Buffer.Access.READ_WRITE - ): G2Buffer - -} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Buffer.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Buffer.kt new file mode 100644 index 0000000..93a9f2d --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Buffer.kt @@ -0,0 +1,40 @@ +package team.exception.sakura.graphics.geek2 + +import java.nio.ByteBuffer + +abstract class G2Buffer( + open val device: G2Device, + val size: Long, +) { + + /** + * Get the mapped buffer. + * @return the mapped buffer + * @throws IllegalStateException if the buffer hasn't been mapped. + */ + abstract fun getMappedBuffer(): ByteBuffer + + /** + * Refresh modified data in the buffer. + * @throws IllegalStateException if the buffer hasn't been mapped. + */ + abstract fun refresh(modifiedRange: LongRange) + + /** + * Remap the buffer. + * @throws IllegalStateException if the buffer hasn't been mapped. + */ + abstract fun remap() + + /** + * Destroy & unmap the buffer. + */ + abstract fun destroy() + + enum class Access { + WRITE, + READ, + READ_WRITE + } + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2CommandList.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2CommandList.kt new file mode 100644 index 0000000..ed5f05a --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2CommandList.kt @@ -0,0 +1,55 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2CommandList { + + /** + * Binds the graphics pipeline to the command list. + */ + abstract fun bindGraphicsPipeline(pipeline: G2GraphicsPipeline) + + /** + * Draw array + */ + abstract fun draw(vertexCount: Int, instanceCount: Int, firstVertex: Int, firstInstance: Int) + + /** + * Draw indexed + */ + abstract fun drawIndexed(indexCount: Int, instanceCount: Int, firstIndex: Int, vertexOffset: Int, firstInstance: Int) + + /** + * Begin rendering (Dynamic rendering in vulkan) + */ + abstract fun beginRendering(renderingInfo: G2RenderingInfo) + + /** + * End rendering (Dynamic rendering in vulkan) + */ + abstract fun endRendering() + + /** + * Summit the command list + */ + abstract fun summit() + + /** + * Clear the command list + */ + abstract fun clear() + + /** + * Summit and clear the command list + */ + abstract fun summitAndClear() + + /** + * Destroy the command list + */ + abstract fun destroy() + + /** + * Summit and destroy the command list + */ + abstract fun summitAndDestroy() + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Device.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Device.kt new file mode 100644 index 0000000..5115bd6 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Device.kt @@ -0,0 +1,29 @@ +package team.exception.sakura.graphics.geek2 + +import java.nio.ByteBuffer + +abstract class G2Device { + + /** + * Create a new command list. + * + * Note: You should destroy the command list after use. + * @return A new command list. + */ + abstract fun createCommandList(): G2CommandList + + abstract fun createBuffer( + size: Long, + access: G2Buffer.Access = G2Buffer.Access.READ_WRITE + ): G2Buffer + + abstract fun createShader( + source: ByteBuffer, + sourceType: G2Shader.SourceType, + shaderType: G2Shader.ShaderType, + entryPoint: String + ): G2Shader + + abstract fun createShaderSet(shaders: List): G2ShaderSet + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Format.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Format.kt new file mode 100644 index 0000000..8ac5b4c --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Format.kt @@ -0,0 +1,5 @@ +package team.exception.sakura.graphics.geek2 + +enum class G2Format { + A8R8G8B8, +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2GraphicsPipeline.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2GraphicsPipeline.kt new file mode 100644 index 0000000..c429de6 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2GraphicsPipeline.kt @@ -0,0 +1,94 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2GraphicsPipeline( + val primitive: Primitive, + open val shaderSet: G2ShaderSet, + val pipelineStates: PipelineStates, + val vertexInput: G2VertexInput? = null, +) { + + data class PipelineStates( + var depthTest: Boolean = false, + var depthWrite: Boolean = true, + var depthFunc: DepthFunc = DepthFunc.LESS, + var cullEnable: Boolean = false, + var cullMode: CullMode = CullMode.BACK, + var frontFace: FrontFace = FrontFace.COUNTER_CLOCKWISE, + var fillMode: FillMode = FillMode.FILL, + var blendEnable: Boolean = false, + var blendEquation: BlendEquation = BlendEquation.ADD, + var srcColorBlend: BlendFunc = BlendFunc.SRC_ALPHA, + var dstColorBlend: BlendFunc = BlendFunc.ONE_MINUS_SRC_ALPHA, + var srcAlphaBlend: BlendFunc = BlendFunc.ONE, + var dstAlphaBlend: BlendFunc = BlendFunc.ZERO, + var colorWriteMask: ColorMask = ColorMask.ALL + ) + + enum class Primitive { + TRIANGLES, + TRIANGLE_FAN, + TRIANGLE_STRIP, + LINES, + LINE_STRIP, + POINTS + } + + enum class CullMode { + NONE, + FRONT, + BACK, + FRONT_AND_BACK + } + + enum class FillMode { + FILL, + LINE, + POINT + } + + enum class FrontFace { + CLOCKWISE, + COUNTER_CLOCKWISE + } + + enum class DepthFunc { + NEVER, + LESS, + EQUAL, + LEQUAL, + GREATER, + NOTEQUAL, + GEQUAL, + ALWAYS + } + + enum class BlendEquation { + ADD, + SUBTRACT, + REVERSE_SUBTRACT, + MIN, + MAX + } + + enum class BlendFunc { + ZERO, + ONE, + SRC_COLOR, + ONE_MINUS_SRC_COLOR, + DST_COLOR, + ONE_MINUS_DST_COLOR, + SRC_ALPHA, + ONE_MINUS_SRC_ALPHA, + DST_ALPHA, + ONE_MINUS_DST_ALPHA, + } + + enum class ColorMask { + NONE, + RED, + GREEN, + BLUE, + ALPHA, + ALL + } +} diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Image.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Image.kt new file mode 100644 index 0000000..b5eef1e --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Image.kt @@ -0,0 +1,5 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2Image { + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ImageView.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ImageView.kt new file mode 100644 index 0000000..21a43b0 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ImageView.kt @@ -0,0 +1,7 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2ImageView( + open val image: G2Image, +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RenderingInfo.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RenderingInfo.kt new file mode 100644 index 0000000..59a7a75 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RenderingInfo.kt @@ -0,0 +1,18 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2RenderingInfo { + + abstract val colorAttachment: Attachment? + + abstract val depthAttachment: Attachment? + + abstract val width: Int + + abstract val height: Int + + data class Attachment( + val image: G2ImageView, + val format: G2Format, + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RingBuffer.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RingBuffer.kt new file mode 100644 index 0000000..36531c4 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2RingBuffer.kt @@ -0,0 +1,121 @@ +package team.exception.sakura.graphics.geek2 + +import java.nio.ByteBuffer + +/** + * A GPU-backed ring buffer implementation built on top of {@link G2Buffer}. + *

+ * The ring buffer provides a continuously mapped memory region that supports + * sequential writes with automatic wrapping when the end of the buffer is reached. + * This is typically used for streaming data such as dynamic vertex or uniform buffers. + *

+ * + *

Note: This implementation does not perform GPU/CPU synchronization (no fences). + * It assumes that overwriting old data is safe when the user reuses the buffer.

+ */ +class G2RingBuffer( + override val device: G2Device, + val capacity: Long +) : G2Buffer(device, capacity) { + + private val buffer = device.createBuffer(capacity) + private var head = 0L + private var tail = 0L + private var used = 0L + + private val mapped: ByteBuffer by lazy { + buffer.getMappedBuffer() + } + + /** + * Allocates a continuous region of the ring buffer and provides access to it. + *

+ * If the remaining space from the current head to the end of the buffer is + * insufficient, the head wraps back to the beginning. + *

+ * + * @param size The number of bytes to allocate. + * @param func A callback that receives a {@link ByteBuffer} view of the allocated region + * and the byte offset of the start of that region. + * @throws IllegalArgumentException if {@code size} exceeds the total capacity. + */ + fun use(size: Long, func: (ByteBuffer, offset: Long) -> Unit) { + require(size <= capacity) { "Requested size ($size) exceeds buffer capacity ($capacity)" } + + // Wrap around if there is not enough remaining space + if (remaining() < size) { + head = 0 + used = tail + } + + val currentOffset = head + val bufferView = mapped.duplicate().apply { + position(currentOffset.toInt()) + limit((currentOffset + size).toInt()) + } + + func(bufferView, currentOffset) + + head = (head + size) % capacity + used += size + } + + /** + * Returns the remaining free space before wrapping occurs. + * + * @return The number of free bytes in the ring buffer. + */ + private fun remaining(): Long { + return if (head >= tail) { + capacity - (head - tail) + } else { + tail - head + } + } + + /** + * Flushes (synchronizes) a specified range of the buffer to the GPU. + * + * @param modifiedRange The byte range that has been modified. + * @throws IllegalStateException if the buffer has not been mapped. + */ + override fun refresh(modifiedRange: LongRange) { + buffer.refresh(modifiedRange) + } + + /** + * Flushes the last written region to the GPU. + * + * @param size The size of the most recently written region. + */ + fun refreshLast(size: Long) { + val start = (head - size + capacity) % capacity + refresh(start until start + size) + } + + /** + * Remaps the buffer memory. + *

+ * This can be used after a device reset or when the underlying + * buffer has been reallocated. + *

+ */ + override fun remap() { + buffer.remap() + } + + /** + * Returns the mapped {@link ByteBuffer} for direct access. + * + * @return The mapped buffer. + * @throws IllegalStateException if the buffer has not been mapped. + */ + override fun getMappedBuffer(): ByteBuffer = mapped + + /** + * Destroys the underlying buffer and releases its resources. + */ + override fun destroy() { + buffer.destroy() + } +} diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Shader.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Shader.kt new file mode 100644 index 0000000..4c6616d --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2Shader.kt @@ -0,0 +1,29 @@ +package team.exception.sakura.graphics.geek2 + +import java.nio.ByteBuffer + +/** + * Warn: This class can only be created by G2Device. + */ +abstract class G2Shader( + open val device: G2Device, + val source: ByteBuffer, + val sourceType: SourceType, + val entryPoint: String = "main", +) { + + abstract fun compile() + + abstract fun destroy() + + enum class SourceType { + SPIR_V, + GLSL, + } + + enum class ShaderType { + VERTEX, + FRAGMENT, + } + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ShaderSet.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ShaderSet.kt new file mode 100644 index 0000000..2d24403 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2ShaderSet.kt @@ -0,0 +1,13 @@ +package team.exception.sakura.graphics.geek2 + +/** + * Note: This class can only be created by G2Device. + */ +abstract class G2ShaderSet( + open val device: G2Device, + open val shaders: List, +) { + + abstract fun attachShaders() + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/geek2/G2VertexInput.kt b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2VertexInput.kt new file mode 100644 index 0000000..765676f --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/geek2/G2VertexInput.kt @@ -0,0 +1,23 @@ +package team.exception.sakura.graphics.geek2 + +abstract class G2VertexInput { + + abstract fun int(index: Int) + + abstract fun float(index: Int) + + abstract fun vec2(index: Int) + + abstract fun vec3(index: Int) + + abstract fun vec4(index: Int) + + abstract fun mat2(index: Int) + + abstract fun mat3(index: Int) + + abstract fun mat4(index: Int) + + abstract fun destroy() + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlBuffer.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlBuffer.kt index 80304b7..232c4ff 100644 --- a/src/main/kotlin/team/exception/sakura/graphics/gl/GlBuffer.kt +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlBuffer.kt @@ -1,30 +1,31 @@ package team.exception.sakura.graphics.gl +import com.mojang.blaze3d.opengl.GlStateManager import org.lwjgl.opengl.GL41.* -import team.exception.sakura.graphics.buffer.G2Buffer +import team.exception.sakura.graphics.geek2.G2Buffer import java.nio.ByteBuffer class GlBuffer( - device: GlDevice, + override val device: GlDevice, size: Long, - access: Access, -): G2Buffer(size, access) { + val access: Access, +): G2Buffer(device, size) { private val id: Int = glGenBuffers() private var mappedBuf: ByteBuffer? = null init { - val cmdList = device.getTempCommandList() + val cmdList = device.createCommandList() cmdList.add { - glBindBuffer(GL_ARRAY_BUFFER, id) + GlStateManager._glBindBuffer(GL_ARRAY_BUFFER, id) glBufferData(GL_ARRAY_BUFFER, size, GL_STATIC_DRAW) - mappedBuf = glMapBuffer(GL_ARRAY_BUFFER, getGlByG2Access(access)) - glBindBuffer(GL_ARRAY_BUFFER, 0) + mappedBuf = glMapBufferRange(GL_ARRAY_BUFFER, + 0, size, getGlByG2Access(access)) } - cmdList.summitAndClear() + cmdList.summitAndDestroy() } @@ -35,11 +36,51 @@ class GlBuffer( return mappedBuf!! } + override fun refresh(modifiedRange: LongRange) { + val cmdList = device.createCommandList() + + cmdList.add { + GlStateManager._glBindBuffer(GL_ARRAY_BUFFER, id) + glFlushMappedBufferRange(GL_ARRAY_BUFFER, modifiedRange.first, + modifiedRange.last - modifiedRange.first) + } + cmdList.summitAndDestroy() + } + + override fun remap() { + val cmdList = device.createCommandList() + + cmdList.add { + GlStateManager._glBindBuffer(GL_ARRAY_BUFFER, id) + if (mappedBuf == null) { + throw IllegalStateException("Buffer hasn't been mapped") + } + glUnmapBuffer(GL_ARRAY_BUFFER) + mappedBuf = glMapBufferRange(GL_ARRAY_BUFFER, 0, + size, getGlByG2Access(access)) + } + cmdList.summitAndDestroy() + } + + override fun destroy() { + val cmdList = device.createCommandList() + + cmdList.add { + GlStateManager._glBindBuffer(GL_ARRAY_BUFFER, id) + glUnmapBuffer(GL_ARRAY_BUFFER) + mappedBuf = null + glDeleteBuffers(id) + } + cmdList.summitAndDestroy() + } + companion object { + const val DEFAULT_ACCESS_BITS = GL_MAP_FLUSH_EXPLICIT_BIT + fun getGlByG2Access(access: Access): Int = when (access) { - Access.READ -> GL_READ_ONLY - Access.WRITE -> GL_WRITE_ONLY - Access.READ_WRITE -> GL_READ_WRITE + Access.READ -> GL_MAP_READ_BIT and DEFAULT_ACCESS_BITS + Access.WRITE -> GL_MAP_WRITE_BIT and DEFAULT_ACCESS_BITS + Access.READ_WRITE -> GL_MAP_READ_BIT and GL_MAP_WRITE_BIT and DEFAULT_ACCESS_BITS } } diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlCommandList.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlCommandList.kt index 556cfda..d6195e6 100644 --- a/src/main/kotlin/team/exception/sakura/graphics/gl/GlCommandList.kt +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlCommandList.kt @@ -1,15 +1,66 @@ package team.exception.sakura.graphics.gl -import team.exception.sakura.graphics.buffer.G2CommandList +import team.exception.sakura.graphics.geek2.G2CommandList +import team.exception.sakura.graphics.geek2.G2GraphicsPipeline +import team.exception.sakura.graphics.geek2.G2RenderingInfo +import org.lwjgl.opengl.GL41.* +import team.exception.sakura.graphics.gl.GlGraphicsPipeline.Companion.toGL +import java.util.Stack class GlCommandList: G2CommandList() { private val commands: MutableList = mutableListOf() + private var graphicsPipeline: GlGraphicsPipeline? = null + private val renderingInfoStack: Stack = Stack() internal fun add(command: () -> Unit) { commands.add(GlCommand(command)) } + override fun bindGraphicsPipeline(pipeline: G2GraphicsPipeline) { + graphicsPipeline = pipeline as GlGraphicsPipeline + graphicsPipeline?.bind() + } + + override fun draw( + vertexCount: Int, + instanceCount: Int, + firstVertex: Int, + firstInstance: Int + ) { + graphicsPipeline?.let { pipeline -> add { + glDrawArrays( + pipeline.primitive.toGL(), + firstVertex, vertexCount + ) + } } + } + + override fun drawIndexed( + indexCount: Int, + instanceCount: Int, + firstIndex: Int, + vertexOffset: Int, + firstInstance: Int + ) { + graphicsPipeline?.let { pipeline -> add { + glDrawElements( + pipeline.primitive.toGL(), + indexCount, GL_UNSIGNED_INT, (firstIndex * 4).toLong() + ) + } } + } + + override fun beginRendering(renderingInfo: G2RenderingInfo) { + renderingInfoStack.push(renderingInfo) + refreshRenderingInfo(renderingInfo) + } + + override fun endRendering() { + renderingInfoStack.pop() + refreshRenderingInfo(renderingInfoStack.peek()) + } + override fun summit() { commands.forEach { it.func() } } @@ -23,6 +74,25 @@ class GlCommandList: G2CommandList() { clear() } + override fun destroy() { + if (commands.isNotEmpty()) { + commands.clear() + } + } + + override fun summitAndDestroy() { + summit() + destroy() + } + + private fun refreshRenderingInfo(renderingInfo: G2RenderingInfo) { + val frameBuffer = GlFrameBufferManager.getFrameBufferByAttachments( + renderingInfo.colorAttachment, + renderingInfo.depthAttachment, + ) + glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer) + } + class GlCommand(val func: () -> Unit) } \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlDevice.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlDevice.kt index e3b4f8a..8513820 100644 --- a/src/main/kotlin/team/exception/sakura/graphics/gl/GlDevice.kt +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlDevice.kt @@ -1,19 +1,31 @@ package team.exception.sakura.graphics.gl -import team.exception.sakura.graphics.buffer.G2Buffer -import team.exception.sakura.graphics.buffer.G2Device +import team.exception.sakura.graphics.geek2.G2Buffer +import team.exception.sakura.graphics.geek2.G2Device +import team.exception.sakura.graphics.geek2.G2Shader +import team.exception.sakura.graphics.geek2.G2ShaderSet +import java.nio.ByteBuffer class GlDevice: G2Device() { - private val tmpCommandList = createCommandList() - override fun createCommandList(): GlCommandList = GlCommandList() - override fun getTempCommandList(): GlCommandList = tmpCommandList - override fun createBuffer( size: Long, access: G2Buffer.Access - ): G2Buffer = GlBuffer(this, size, access) + ): GlBuffer = GlBuffer(this, size, access) + + override fun createShader( + source: ByteBuffer, + sourceType: G2Shader.SourceType, + shaderType: G2Shader.ShaderType, + entryPoint: String + ): GlShader = GlShader(this, source, sourceType, shaderType, entryPoint).apply { + compile() + } + + override fun createShaderSet(shaders: List): GlShaderSet = GlShaderSet( + this, shaders.map { it as GlShader } + ).apply { attachShaders() } } \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlFrameBufferManager.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlFrameBufferManager.kt new file mode 100644 index 0000000..ea116da --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlFrameBufferManager.kt @@ -0,0 +1,121 @@ +package team.exception.sakura.graphics.gl + +import com.mojang.blaze3d.opengl.GlStateManager +import com.mojang.blaze3d.opengl.GlTexture +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.opengl.GlDevice as MojGlDevice +import net.minecraft.client.Minecraft +import team.exception.sakura.graphics.G2RenderSystem +import org.lwjgl.opengl.GL41.* +import team.exception.sakura.graphics.geek2.G2RenderingInfo + + +object GlFrameBufferManager { + + val mcFboId = (Minecraft.getInstance().mainRenderTarget.colorTexture as GlTexture) + .getFbo((RenderSystem.getDevice() as MojGlDevice).directStateAccess(), + Minecraft.getInstance().mainRenderTarget.depthTexture) + + // FrameBuffer -> OpenGl FBO ID + private val frameBuffers = mutableMapOf() + + /** + * Get a FrameBuffer by attachments. + * If attachments are null, return the Minecraft FrameBuffer. + * @return OpenGl FBO ID + */ + fun getFrameBufferByAttachments( + colorAttachment: GlImageView?, + depthAttachment: GlImageView?, + ): Int { + if (colorAttachment == null || depthAttachment == null) { + return mcFboId + } + val frameBuffer = FrameBuffer(colorAttachment, depthAttachment) + return frameBuffers.getOrPut(frameBuffer) { + val fboId = createFrameBuffer(colorAttachment, depthAttachment) + frameBuffers[frameBuffer] = fboId + fboId + } + } + + fun getFrameBufferByAttachments( + colorAttachment: G2RenderingInfo.Attachment?, + depthAttachment: G2RenderingInfo.Attachment?, + ): Int = getFrameBufferByAttachments( + colorAttachment?.image as GlImageView?, + depthAttachment?.image as GlImageView?, + ) + + /** + * Create a FrameBuffer with attachments. + * @param colorAttachment the color attachment of the FrameBuffer, null if not needed + * @param depthAttachment the depth attachment of the FrameBuffer, null if not needed + * @throws IllegalStateException if FrameBuffer creation failed + * @return OpenGl FBO ID + */ + private fun createFrameBuffer( + colorAttachment: GlImageView?, + depthAttachment: GlImageView?, + ): Int { + + val device = G2RenderSystem.device as GlDevice + + val fboId = glGenFramebuffers() + val cmdList = device.createCommandList() + + var successFlag = false + + cmdList.add { + val prevFboId = glGetInteger(GL_FRAMEBUFFER_BINDING) + GlStateManager._glBindFramebuffer(GL_FRAMEBUFFER, fboId) + colorAttachment?.let { + glBindTexture(GL_TEXTURE_2D, it.image.glId) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, it.image.glId, 0) + } + depthAttachment?.let { + glBindTexture(GL_TEXTURE_2D, it.image.glId) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, it.image.glId, 0) + } + val status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + if (status != GL_FRAMEBUFFER_COMPLETE) { + successFlag = true + } + GlStateManager._glBindFramebuffer(GL_FRAMEBUFFER, prevFboId) + } + cmdList.summitAndDestroy() + + if (successFlag) { + throw IllegalStateException("FrameBuffer creation failed") + } + + return fboId + + } + + class FrameBuffer( + val colorImage: GlImageView? = null, + val depthImage: GlImageView? = null, + ) { + override fun hashCode(): Int { + val colorHash = colorImage?.image?.glId ?: 0 + val depthHash = depthImage?.image?.glId ?: 0 + return colorHash + depthHash + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FrameBuffer + + if (colorImage?.image?.glId != other.colorImage?.image?.glId) return false + if (depthImage?.image?.glId != other.depthImage?.image?.glId) return false + + return true + } + } + +} diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlGraphicsPipeline.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlGraphicsPipeline.kt new file mode 100644 index 0000000..33a1a1a --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlGraphicsPipeline.kt @@ -0,0 +1,106 @@ +package team.exception.sakura.graphics.gl + +import com.mojang.blaze3d.opengl.GlStateManager +import org.lwjgl.opengl.GL11.* +import team.exception.sakura.graphics.geek2.G2GraphicsPipeline + +class GlGraphicsPipeline( + primitive: Primitive, + override val shaderSet: GlShaderSet, + pipelineStates: PipelineStates, + vertexInput: GlVertexInput? = null, +) : G2GraphicsPipeline(primitive, shaderSet, pipelineStates, vertexInput) { + + fun bind() { + val states = pipelineStates + + if (states.depthTest) GlStateManager._enableDepthTest() else GlStateManager._disableDepthTest() + GlStateManager._depthMask(states.depthWrite) + GlStateManager._depthFunc(states.depthFunc.toGl()) + + if (states.cullEnable) { + GlStateManager._enableCull() + glCullFace(states.cullMode.toGl()) + } else GlStateManager._disableCull() + + glFrontFace(states.frontFace.toGl()) + GlStateManager._polygonMode(GL_FRONT_AND_BACK, states.fillMode.toGl()) + + if (states.blendEnable) { + GlStateManager._enableBlend() + GlStateManager._blendFuncSeparate( + states.srcColorBlend.toGl(), + states.dstColorBlend.toGl(), + states.srcAlphaBlend.toGl(), + states.dstAlphaBlend.toGl() + ) + } else GlStateManager._disableBlend() + + GlStateManager._colorMask( + states.colorWriteMask.red(), + states.colorWriteMask.green(), + states.colorWriteMask.blue(), + states.colorWriteMask.alpha() + ) + + GlStateManager._glUseProgram(shaderSet.programId) + } + + companion object { + fun Primitive.toGL(): Int = when (this) { + Primitive.TRIANGLES -> GL_TRIANGLES + Primitive.TRIANGLE_FAN -> GL_TRIANGLE_FAN + Primitive.TRIANGLE_STRIP -> GL_TRIANGLE_STRIP + Primitive.LINES -> GL_LINES + Primitive.LINE_STRIP -> GL_LINE_STRIP + Primitive.POINTS -> GL_POINTS + } + + fun DepthFunc.toGl() = when (this) { + DepthFunc.NEVER -> GL_NEVER + DepthFunc.LESS -> GL_LESS + DepthFunc.EQUAL -> GL_EQUAL + DepthFunc.LEQUAL -> GL_LEQUAL + DepthFunc.GREATER -> GL_GREATER + DepthFunc.NOTEQUAL -> GL_NOTEQUAL + DepthFunc.GEQUAL -> GL_GEQUAL + DepthFunc.ALWAYS -> GL_ALWAYS + } + + fun CullMode.toGl() = when (this) { + CullMode.FRONT -> GL_FRONT + CullMode.BACK -> GL_BACK + CullMode.FRONT_AND_BACK -> GL_FRONT_AND_BACK + CullMode.NONE -> 0 + } + + fun FrontFace.toGl() = when (this) { + FrontFace.CLOCKWISE -> GL_CW + FrontFace.COUNTER_CLOCKWISE -> GL_CCW + } + + fun FillMode.toGl() = when (this) { + FillMode.FILL -> GL_FILL + FillMode.LINE -> GL_LINE + FillMode.POINT -> GL_POINT + } + + fun BlendFunc.toGl() = when (this) { + BlendFunc.ZERO -> GL_ZERO + BlendFunc.ONE -> GL_ONE + BlendFunc.SRC_COLOR -> GL_SRC_COLOR + BlendFunc.ONE_MINUS_SRC_COLOR -> GL_ONE_MINUS_SRC_COLOR + BlendFunc.DST_COLOR -> GL_DST_COLOR + BlendFunc.ONE_MINUS_DST_COLOR -> GL_ONE_MINUS_DST_COLOR + BlendFunc.SRC_ALPHA -> GL_SRC_ALPHA + BlendFunc.ONE_MINUS_SRC_ALPHA -> GL_ONE_MINUS_SRC_ALPHA + BlendFunc.DST_ALPHA -> GL_DST_ALPHA + BlendFunc.ONE_MINUS_DST_ALPHA -> GL_ONE_MINUS_DST_ALPHA + } + + fun ColorMask.red() = this == ColorMask.RED || this == ColorMask.ALL + fun ColorMask.green() = this == ColorMask.GREEN || this == ColorMask.ALL + fun ColorMask.blue() = this == ColorMask.BLUE || this == ColorMask.ALL + fun ColorMask.alpha() = this == ColorMask.ALPHA || this == ColorMask.ALL + } +} diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlImage.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlImage.kt new file mode 100644 index 0000000..57e4f7a --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlImage.kt @@ -0,0 +1,9 @@ +package team.exception.sakura.graphics.gl + +import team.exception.sakura.graphics.geek2.G2Image + +class GlImage: G2Image() { + + val glId: Int = 0 + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlImageView.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlImageView.kt new file mode 100644 index 0000000..5ac00d1 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlImageView.kt @@ -0,0 +1,7 @@ +package team.exception.sakura.graphics.gl + +import team.exception.sakura.graphics.geek2.G2ImageView + +class GlImageView( + override val image: GlImage +): G2ImageView(image) \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlShader.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlShader.kt new file mode 100644 index 0000000..f4a25b4 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlShader.kt @@ -0,0 +1,71 @@ +package team.exception.sakura.graphics.gl + +import team.exception.sakura.graphics.geek2.G2Shader +import org.lwjgl.opengl.GL41.* +import org.lwjgl.opengl.GL46.GL_SHADER_BINARY_FORMAT_SPIR_V +import org.lwjgl.opengl.GL46.glSpecializeShader +import team.exception.sakura.graphics.G2RenderSystem +import team.exception.sakura.utils.extension.ByteBufferUtils.readAsString +import team.exception.sakura.utils.extension.StringUtils.asCharSequence +import java.nio.ByteBuffer + +/** + * SPIR-V is not supported in OpenGL 4.1 (macOS) + */ +class GlShader( + override val device: GlDevice, + source: ByteBuffer, + sourceType: SourceType, + shaderType: ShaderType, + entryPoint: String = "main", +): G2Shader(device, source, sourceType, entryPoint) { + + init { + if (sourceType == SourceType.SPIR_V && + G2RenderSystem.gpuType == G2RenderSystem.GpuTypes.APPLE) { + throw Exception("SPIR-V is not supported in OpenGL 4.1 (macOS)") + } + } + + val shaderId: Int = glCreateShader(shaderType.toGl()) + + override fun compile() { + val cmdList = device.createCommandList() + + cmdList.add { + when (sourceType) { + SourceType.SPIR_V -> { + glShaderBinary( + intArrayOf(shaderId), + GL_SHADER_BINARY_FORMAT_SPIR_V, + source + ) + glSpecializeShader( + shaderId, entryPoint.asCharSequence(), + null as IntArray?, null as IntArray? + ) + } + SourceType.GLSL -> glShaderSource(shaderId, source.readAsString()) + } + glCompileShader(shaderId) + } + cmdList.summitAndDestroy() + + val compileStatus = glGetShaderi(shaderId, GL_COMPILE_STATUS) + if (compileStatus == GL_FALSE) { + val infoLog = glGetShaderInfoLog(shaderId) + throw Exception("Shader compilation failed: $infoLog") + } + } + + override fun destroy() { + glDeleteShader(shaderId) + } + + companion object { + fun ShaderType.toGl() = when (this) { + ShaderType.VERTEX -> GL_VERTEX_SHADER + ShaderType.FRAGMENT -> GL_FRAGMENT_SHADER + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlShaderSet.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlShaderSet.kt new file mode 100644 index 0000000..3ca9958 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlShaderSet.kt @@ -0,0 +1,29 @@ +package team.exception.sakura.graphics.gl + +import org.lwjgl.opengl.GL41.* +import team.exception.sakura.graphics.geek2.G2ShaderSet + +class GlShaderSet( + override val device: GlDevice, + override val shaders: List, +): G2ShaderSet(device, shaders) { + + val programId: Int = glCreateProgram() + + override fun attachShaders() { + val cmdList = device.createCommandList() + + cmdList.add { + shaders.forEach { shader -> + glAttachShader(programId, shader.shaderId) + } + glLinkProgram(programId) + } + cmdList.summitAndDestroy() + + if (glGetProgrami(programId, GL_LINK_STATUS) == 0) { + throw Exception("Error linking program: " + glGetProgramInfoLog(programId)) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/graphics/gl/GlVertexInput.kt b/src/main/kotlin/team/exception/sakura/graphics/gl/GlVertexInput.kt new file mode 100644 index 0000000..9b3da42 --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/graphics/gl/GlVertexInput.kt @@ -0,0 +1,86 @@ +package team.exception.sakura.graphics.gl + +import org.lwjgl.opengl.GL33.* +import team.exception.sakura.graphics.geek2.G2VertexInput + +class GlVertexInput: G2VertexInput() { + + private val vaoId: Int = glGenVertexArrays() + + private var offset = 0L + + var stride: Int = 0 + + fun bind() { + glBindVertexArray(vaoId) + } + + fun unbind() { + glBindVertexArray(0) + } + + override fun int(index: Int) { + glEnableVertexAttribArray(index) + glVertexAttribIPointer(index, 1, GL_INT, stride, offset) + offset += Int.SIZE_BYTES + } + + override fun float(index: Int) { + glEnableVertexAttribArray(index) + glVertexAttribPointer(index, 1, GL_FLOAT, false, stride, offset) + offset += Float.SIZE_BYTES + } + + override fun vec2(index: Int) { + glEnableVertexAttribArray(index) + glVertexAttribPointer(index, 2, GL_FLOAT, false, stride, offset) + offset += 2 * Float.SIZE_BYTES + } + + override fun vec3(index: Int) { + glEnableVertexAttribArray(index) + glVertexAttribPointer(index, 3, GL_FLOAT, false, stride, offset) + offset += 3 * Float.SIZE_BYTES + } + + override fun vec4(index: Int) { + glEnableVertexAttribArray(index) + glVertexAttribPointer(index, 4, GL_FLOAT, false, stride, offset) + offset += 4 * Float.SIZE_BYTES + } + + override fun mat2(index: Int) { + for (i in 0 until 2) { + glEnableVertexAttribArray(index + i) + glVertexAttribPointer(index + i, 2, GL_FLOAT, false, stride, offset + i * 2 * Float.SIZE_BYTES) + glVertexAttribDivisor(index + i, 1) + } + offset += 4 * Float.SIZE_BYTES + } + + override fun mat3(index: Int) { + for (i in 0 until 3) { + glEnableVertexAttribArray(index + i) + glVertexAttribPointer(index + i, 3, GL_FLOAT, false, stride, offset + i * 3 * Float.SIZE_BYTES) + glVertexAttribDivisor(index + i, 1) + } + offset += 9 * Float.SIZE_BYTES + } + + /** + * Enables a mat4 attribute (4 columns of vec4). + * Each column is treated as a separate attribute. + */ + override fun mat4(index: Int) { + for (i in 0 until 4) { + glEnableVertexAttribArray(index + i) + glVertexAttribPointer(index + i, 4, GL_FLOAT, false, stride, offset + i * 4 * Float.SIZE_BYTES) + glVertexAttribDivisor(index + i, 1) + } + offset += 16 * Float.SIZE_BYTES + } + + override fun destroy() { + glDeleteVertexArrays(vaoId) + } +} diff --git a/src/main/kotlin/team/exception/sakura/utils/extension/ByteBufferUtils.kt b/src/main/kotlin/team/exception/sakura/utils/extension/ByteBufferUtils.kt new file mode 100644 index 0000000..1136e2c --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/utils/extension/ByteBufferUtils.kt @@ -0,0 +1,13 @@ +package team.exception.sakura.utils.extension + +import java.nio.ByteBuffer + +object ByteBufferUtils { + + fun ByteBuffer.readAsString(): String { + val bytes = ByteArray(remaining()) + get(bytes) + return String(bytes) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/team/exception/sakura/utils/extension/StringUtils.kt b/src/main/kotlin/team/exception/sakura/utils/extension/StringUtils.kt new file mode 100644 index 0000000..6495edf --- /dev/null +++ b/src/main/kotlin/team/exception/sakura/utils/extension/StringUtils.kt @@ -0,0 +1,7 @@ +package team.exception.sakura.utils.extension + +object StringUtils { + + fun String.asCharSequence(): CharSequence = this + +} \ No newline at end of file