#include "graphics/graphics.h" #include "graphics/buffer.h" #include "graphics/canvas.h" #include "graphics/material.h" #include "graphics/mesh.h" #include "graphics/shader.h" #include "graphics/texture.h" #include "resources/shaders.h" #include "data/blob.h" #include "data/modelData.h" #include "math/math.h" #include #include #include #include #include #ifdef LOVR_WEBGL #include #include #include #include #else #include "lib/glad/glad.h" #endif // Types #define MAX_TEXTURES 16 #define MAX_IMAGES 8 #define MAX_BLOCK_BUFFERS 8 #define LOVR_SHADER_POSITION 0 #define LOVR_SHADER_NORMAL 1 #define LOVR_SHADER_TEX_COORD 2 #define LOVR_SHADER_VERTEX_COLOR 3 #define LOVR_SHADER_TANGENT 4 #define LOVR_SHADER_BONES 5 #define LOVR_SHADER_BONE_WEIGHTS 6 #define LOVR_SHADER_DRAW_ID 7 struct Buffer { uint32_t ref; uint32_t id; void* data; size_t size; size_t flushFrom; size_t flushTo; BufferType type; BufferUsage usage; bool mapped; bool readable; uint8_t incoherent; }; struct Texture { uint32_t ref; GLuint id; GLuint msaaId; GLenum target; TextureType type; TextureFormat format; uint32_t width; uint32_t height; uint32_t depth; uint32_t mipmapCount; CompareMode compareMode; TextureFilter filter; TextureWrap wrap; uint32_t msaa; bool srgb; bool mipmaps; bool allocated; bool native; uint8_t incoherent; }; struct Canvas { uint32_t ref; uint32_t framebuffer; uint32_t resolveBuffer; uint32_t depthBuffer; uint32_t width; uint32_t height; CanvasFlags flags; Attachment attachments[MAX_CANVAS_ATTACHMENTS]; Attachment depth; uint32_t attachmentCount; bool needsAttach; bool needsResolve; bool immortal; }; struct ShaderBlock { uint32_t ref; BlockType type; arr_uniform_t uniforms; map_t uniformMap; struct Buffer* buffer; }; struct Shader { uint32_t ref; uint32_t program; ShaderType type; arr_uniform_t uniforms; arr_block_t blocks[2]; map_t attributes; map_t uniformMap; map_t blockMap; bool multiview; }; struct Mesh { uint32_t ref; uint32_t vao; uint32_t ibo; DrawMode mode; char attributeNames[MAX_ATTRIBUTES][MAX_ATTRIBUTE_NAME_LENGTH]; MeshAttribute attributes[MAX_ATTRIBUTES]; uint8_t locations[MAX_ATTRIBUTES]; uint16_t enabledLocations; uint16_t divisors[MAX_ATTRIBUTES]; map_t attributeMap; uint32_t attributeCount; struct Buffer* vertexBuffer; struct Buffer* indexBuffer; uint32_t vertexCount; uint32_t indexCount; size_t indexSize; size_t indexOffset; uint32_t drawStart; uint32_t drawCount; struct Material* material; }; typedef enum { BARRIER_BLOCK, BARRIER_UNIFORM_TEXTURE, BARRIER_UNIFORM_IMAGE, BARRIER_TEXTURE, BARRIER_CANVAS, MAX_BARRIERS } Barrier; typedef struct { uint32_t buffer; size_t offset; size_t size; } BlockBuffer; typedef struct { GLuint* queries; uint32_t* chain; uint32_t next; uint32_t count; } QueryPool; typedef struct { uint32_t head; uint32_t tail; uint64_t nanoseconds; } Timer; static struct { Texture* defaultTexture; enum { NONE, INSTANCED_STEREO, MULTIVIEW } singlepass; bool alphaToCoverage; bool blendEnabled; BlendMode blendMode; BlendAlphaMode blendAlphaMode; uint8_t colorMask; bool culling; bool depthEnabled; CompareMode depthTest; bool depthWrite; float lineWidth; uint32_t primitiveRestart; bool stencilEnabled; CompareMode stencilMode; int stencilValue; bool stencilWriting; Winding winding; bool wireframe; uint32_t framebuffer; uint32_t program; Mesh* vertexArray; uint32_t buffers[MAX_BUFFER_TYPES]; BlockBuffer blockBuffers[2][MAX_BLOCK_BUFFERS]; int activeTexture; Texture* textures[MAX_TEXTURES]; StorageImage images[MAX_IMAGES]; float viewports[2][4]; uint32_t viewportCount; arr_t(void*) incoherents[MAX_BARRIERS]; QueryPool queryPool; arr_t(Timer) timers; uint32_t activeTimer; map_t timerMap; GpuFeatures features; GpuLimits limits; GpuStats stats; bool amd; } state; // Helper functions static GLenum convertCompareMode(CompareMode mode) { switch (mode) { case COMPARE_NONE: return GL_ALWAYS; case COMPARE_EQUAL: return GL_EQUAL; case COMPARE_NEQUAL: return GL_NOTEQUAL; case COMPARE_LESS: return GL_LESS; case COMPARE_LEQUAL: return GL_LEQUAL; case COMPARE_GREATER: return GL_GREATER; case COMPARE_GEQUAL: return GL_GEQUAL; default: lovrThrow("Unreachable"); } } static GLenum convertWrapMode(WrapMode mode) { switch (mode) { case WRAP_CLAMP: return GL_CLAMP_TO_EDGE; case WRAP_REPEAT: return GL_REPEAT; case WRAP_MIRRORED_REPEAT: return GL_MIRRORED_REPEAT; default: lovrThrow("Unreachable"); } } static GLenum convertTextureTarget(TextureType type) { switch (type) { case TEXTURE_2D: return GL_TEXTURE_2D; break; case TEXTURE_ARRAY: return GL_TEXTURE_2D_ARRAY; break; case TEXTURE_CUBE: return GL_TEXTURE_CUBE_MAP; break; case TEXTURE_VOLUME: return GL_TEXTURE_3D; break; default: lovrThrow("Unreachable"); } } static GLenum convertTextureFormat(TextureFormat format) { switch (format) { case FORMAT_RGB: return GL_RGB; case FORMAT_RGBA: return GL_RGBA; case FORMAT_RGBA4: return GL_RGBA; case FORMAT_R16: return GL_RED; case FORMAT_RG16: return GL_RG; case FORMAT_RGBA16: return GL_RGBA; case FORMAT_RGBA16F: return GL_RGBA; case FORMAT_RGBA32F: return GL_RGBA; case FORMAT_R16F: return GL_RED; case FORMAT_R32F: return GL_RED; case FORMAT_RG16F: return GL_RG; case FORMAT_RG32F: return GL_RG; case FORMAT_RGB5A1: return GL_RGBA; case FORMAT_RGB10A2: return GL_RGBA; case FORMAT_RG11B10F: return GL_RGB; case FORMAT_D16: return GL_DEPTH_COMPONENT; case FORMAT_D32F: return GL_DEPTH_COMPONENT; case FORMAT_D24S8: return GL_DEPTH_STENCIL; case FORMAT_DXT1: return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; case FORMAT_DXT3: return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; case FORMAT_DXT5: return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; case FORMAT_ASTC_4x4: case FORMAT_ASTC_5x4: case FORMAT_ASTC_5x5: case FORMAT_ASTC_6x5: case FORMAT_ASTC_6x6: case FORMAT_ASTC_8x5: case FORMAT_ASTC_8x6: case FORMAT_ASTC_8x8: case FORMAT_ASTC_10x5: case FORMAT_ASTC_10x6: case FORMAT_ASTC_10x8: case FORMAT_ASTC_10x10: case FORMAT_ASTC_12x10: case FORMAT_ASTC_12x12: return GL_RGBA; default: lovrThrow("Unreachable"); } } static GLenum convertTextureFormatInternal(TextureFormat format, bool srgb) { switch (format) { case FORMAT_RGB: return srgb ? GL_SRGB8 : GL_RGB8; case FORMAT_RGBA: return srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8; case FORMAT_RGBA4: return GL_RGBA4; case FORMAT_R16: return GL_R16; case FORMAT_RG16: return GL_RG16; case FORMAT_RGBA16: return GL_RGBA16; case FORMAT_RGBA16F: return GL_RGBA16F; case FORMAT_RGBA32F: return GL_RGBA32F; case FORMAT_R16F: return GL_R16F; case FORMAT_R32F: return GL_R32F; case FORMAT_RG16F: return GL_RG16F; case FORMAT_RG32F: return GL_RG32F; case FORMAT_RGB5A1: return GL_RGB5_A1; case FORMAT_RGB10A2: return GL_RGB10_A2; case FORMAT_RG11B10F: return GL_R11F_G11F_B10F; case FORMAT_D16: return GL_DEPTH_COMPONENT16; case FORMAT_D32F: return GL_DEPTH_COMPONENT32F; case FORMAT_D24S8: return GL_DEPTH24_STENCIL8; case FORMAT_DXT1: return srgb ? GL_COMPRESSED_SRGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT; case FORMAT_DXT3: return srgb ? GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT : GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; case FORMAT_DXT5: return srgb ? GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; #ifdef LOVR_WEBGL case FORMAT_ASTC_4x4: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : GL_COMPRESSED_RGBA_ASTC_4x4_KHR; case FORMAT_ASTC_5x4: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : GL_COMPRESSED_RGBA_ASTC_5x4_KHR; case FORMAT_ASTC_5x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : GL_COMPRESSED_RGBA_ASTC_5x5_KHR; case FORMAT_ASTC_6x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : GL_COMPRESSED_RGBA_ASTC_6x5_KHR; case FORMAT_ASTC_6x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : GL_COMPRESSED_RGBA_ASTC_6x6_KHR; case FORMAT_ASTC_8x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : GL_COMPRESSED_RGBA_ASTC_8x5_KHR; case FORMAT_ASTC_8x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : GL_COMPRESSED_RGBA_ASTC_8x6_KHR; case FORMAT_ASTC_8x8: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : GL_COMPRESSED_RGBA_ASTC_8x8_KHR; case FORMAT_ASTC_10x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : GL_COMPRESSED_RGBA_ASTC_10x5_KHR; case FORMAT_ASTC_10x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : GL_COMPRESSED_RGBA_ASTC_10x6_KHR; case FORMAT_ASTC_10x8: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : GL_COMPRESSED_RGBA_ASTC_10x8_KHR; case FORMAT_ASTC_10x10: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : GL_COMPRESSED_RGBA_ASTC_10x10_KHR; case FORMAT_ASTC_12x10: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : GL_COMPRESSED_RGBA_ASTC_12x10_KHR; case FORMAT_ASTC_12x12: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : GL_COMPRESSED_RGBA_ASTC_12x12_KHR; #else case FORMAT_ASTC_4x4: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4 : GL_COMPRESSED_RGBA_ASTC_4x4; case FORMAT_ASTC_5x4: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4 : GL_COMPRESSED_RGBA_ASTC_5x4; case FORMAT_ASTC_5x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5 : GL_COMPRESSED_RGBA_ASTC_5x5; case FORMAT_ASTC_6x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5 : GL_COMPRESSED_RGBA_ASTC_6x5; case FORMAT_ASTC_6x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6 : GL_COMPRESSED_RGBA_ASTC_6x6; case FORMAT_ASTC_8x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5 : GL_COMPRESSED_RGBA_ASTC_8x5; case FORMAT_ASTC_8x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6 : GL_COMPRESSED_RGBA_ASTC_8x6; case FORMAT_ASTC_8x8: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8 : GL_COMPRESSED_RGBA_ASTC_8x8; case FORMAT_ASTC_10x5: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5 : GL_COMPRESSED_RGBA_ASTC_10x5; case FORMAT_ASTC_10x6: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6 : GL_COMPRESSED_RGBA_ASTC_10x6; case FORMAT_ASTC_10x8: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8 : GL_COMPRESSED_RGBA_ASTC_10x8; case FORMAT_ASTC_10x10: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10 : GL_COMPRESSED_RGBA_ASTC_10x10; case FORMAT_ASTC_12x10: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10 : GL_COMPRESSED_RGBA_ASTC_12x10; case FORMAT_ASTC_12x12: return srgb ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12 : GL_COMPRESSED_RGBA_ASTC_12x12; #endif default: lovrThrow("Unreachable"); } } static GLenum convertTextureFormatType(TextureFormat format) { switch (format) { case FORMAT_RGB: return GL_UNSIGNED_BYTE; case FORMAT_RGBA: return GL_UNSIGNED_BYTE; case FORMAT_RGBA4: return GL_UNSIGNED_SHORT_4_4_4_4; case FORMAT_R16: return GL_UNSIGNED_SHORT; case FORMAT_RG16: return GL_UNSIGNED_SHORT; case FORMAT_RGBA16: return GL_UNSIGNED_SHORT; case FORMAT_RGBA16F: return GL_HALF_FLOAT; case FORMAT_RGBA32F: return GL_FLOAT; case FORMAT_R16F: return GL_HALF_FLOAT; case FORMAT_R32F: return GL_FLOAT; case FORMAT_RG16F: return GL_HALF_FLOAT; case FORMAT_RG32F: return GL_FLOAT; case FORMAT_RGB5A1: return GL_UNSIGNED_SHORT_5_5_5_1; case FORMAT_RGB10A2: return GL_UNSIGNED_INT_2_10_10_10_REV; case FORMAT_RG11B10F: return GL_UNSIGNED_INT_10F_11F_11F_REV; case FORMAT_D16: return GL_UNSIGNED_SHORT; case FORMAT_D32F: return GL_UNSIGNED_INT; case FORMAT_D24S8: return GL_UNSIGNED_INT_24_8; default: lovrThrow("Unreachable"); } } static bool isTextureFormatCompressed(TextureFormat format) { switch (format) { case FORMAT_DXT1: case FORMAT_DXT3: case FORMAT_DXT5: case FORMAT_ASTC_4x4: case FORMAT_ASTC_5x4: case FORMAT_ASTC_5x5: case FORMAT_ASTC_6x5: case FORMAT_ASTC_6x6: case FORMAT_ASTC_8x5: case FORMAT_ASTC_8x6: case FORMAT_ASTC_8x8: case FORMAT_ASTC_10x5: case FORMAT_ASTC_10x6: case FORMAT_ASTC_10x8: case FORMAT_ASTC_10x10: case FORMAT_ASTC_12x10: case FORMAT_ASTC_12x12: return true; default: return false; } } static bool isTextureFormatDepth(TextureFormat format) { switch (format) { case FORMAT_D16: case FORMAT_D32F: case FORMAT_D24S8: return true; default: return false; } } static uint64_t getTextureMemorySize(Texture* texture) { if (texture->native) return 0; float size = 0.f; float bitrate; switch (texture->format) { case FORMAT_RGB: bitrate = 24.f; break; case FORMAT_RGBA: bitrate = 32.f; break; case FORMAT_RGBA4: bitrate = 16.f; break; case FORMAT_R16: bitrate = 16.f; break; case FORMAT_RG16: bitrate = 32.f; break; case FORMAT_RGBA16: bitrate = 64.f; break; case FORMAT_RGBA16F: bitrate = 64.f; break; case FORMAT_RGBA32F: bitrate = 128.f; break; case FORMAT_R16F: bitrate = 16.f; break; case FORMAT_R32F: bitrate = 32.f; break; case FORMAT_RG16F: bitrate = 32.f; break; case FORMAT_RG32F: bitrate = 64.f; break; case FORMAT_RGB5A1: bitrate = 16.f; break; case FORMAT_RGB10A2: bitrate = 32.f; break; case FORMAT_RG11B10F: bitrate = 32.f; break; case FORMAT_D16: bitrate = 16.f; break; case FORMAT_D32F: bitrate = 32.f; break; case FORMAT_D24S8: bitrate = 32.f; break; case FORMAT_DXT1: bitrate = 4.f; break; case FORMAT_DXT3: bitrate = 8.f; break; case FORMAT_DXT5: bitrate = 8.f; break; // Divide fixed-size 128-bit blocks by block size: case FORMAT_ASTC_4x4: bitrate = 8.00f; break; case FORMAT_ASTC_5x4: bitrate = 6.40f; break; case FORMAT_ASTC_5x5: bitrate = 5.12f; break; case FORMAT_ASTC_6x5: bitrate = 4.27f; break; case FORMAT_ASTC_6x6: bitrate = 3.56f; break; case FORMAT_ASTC_8x5: bitrate = 3.20f; break; case FORMAT_ASTC_8x6: bitrate = 2.67f; break; case FORMAT_ASTC_8x8: bitrate = 2.00f; break; case FORMAT_ASTC_10x5: bitrate = 2.56f; break; case FORMAT_ASTC_10x6: bitrate = 2.13f; break; case FORMAT_ASTC_10x8: bitrate = 1.60f; break; case FORMAT_ASTC_10x10: bitrate = 1.28f; break; case FORMAT_ASTC_12x10: bitrate = 1.07f; break; case FORMAT_ASTC_12x12: bitrate = 0.89f; break; default: lovrThrow("Unreachable"); } size = texture->width * texture->height * texture->depth * (bitrate / 8.f) * (texture->mipmaps ? 1.33f : 1.f); size += texture->msaa > 1 ? (texture->width * texture->height * texture->msaa * (bitrate / 8.f)) : 0.f; return (uint64_t) (size + .5f); } static GLenum convertAttributeType(AttributeType type) { switch (type) { case I8: return GL_BYTE; case U8: return GL_UNSIGNED_BYTE; case I16: return GL_SHORT; case U16: return GL_UNSIGNED_SHORT; case I32: return GL_INT; case U32: return GL_UNSIGNED_INT; case F32: return GL_FLOAT; default: lovrThrow("Unreachable"); } } static GLenum convertBufferType(BufferType type) { switch (type) { case BUFFER_VERTEX: return GL_ARRAY_BUFFER; case BUFFER_INDEX: return GL_ELEMENT_ARRAY_BUFFER; case BUFFER_UNIFORM: return GL_UNIFORM_BUFFER; case BUFFER_SHADER_STORAGE: return GL_SHADER_STORAGE_BUFFER; case BUFFER_GENERIC: return GL_COPY_WRITE_BUFFER; default: lovrThrow("Unreachable"); } } static GLenum convertBufferUsage(BufferUsage usage) { switch (usage) { case USAGE_STATIC: return GL_STATIC_DRAW; case USAGE_DYNAMIC: return GL_DYNAMIC_DRAW; case USAGE_STREAM: return GL_STREAM_DRAW; default: lovrThrow("Unreachable"); } } #ifndef LOVR_WEBGL static GLenum convertAccess(UniformAccess access) { switch (access) { case ACCESS_READ: return GL_READ_ONLY; case ACCESS_WRITE: return GL_WRITE_ONLY; case ACCESS_READ_WRITE: return GL_READ_WRITE; default: lovrThrow("Unreachable"); } } #endif static GLenum convertTopology(DrawMode topology) { switch (topology) { case DRAW_POINTS: return GL_POINTS; case DRAW_LINES: return GL_LINES; case DRAW_LINE_STRIP: return GL_LINE_STRIP; case DRAW_LINE_LOOP: return GL_LINE_LOOP; case DRAW_TRIANGLE_STRIP: return GL_TRIANGLE_STRIP; case DRAW_TRIANGLES: return GL_TRIANGLES; case DRAW_TRIANGLE_FAN: return GL_TRIANGLE_FAN; default: lovrThrow("Unreachable"); } } static bool isAttributeTypeInteger(GLenum type) { switch (type) { case GL_INT: case GL_INT_VEC2: case GL_INT_VEC3: case GL_INT_VEC4: case GL_UNSIGNED_INT: case GL_UNSIGNED_INT_VEC2: case GL_UNSIGNED_INT_VEC3: case GL_UNSIGNED_INT_VEC4: return true; default: return false; } } static UniformType getUniformType(GLenum type, const char* debug) { switch (type) { case GL_FLOAT: case GL_FLOAT_VEC2: case GL_FLOAT_VEC3: case GL_FLOAT_VEC4: return UNIFORM_FLOAT; case GL_INT: case GL_INT_VEC2: case GL_INT_VEC3: case GL_INT_VEC4: return UNIFORM_INT; case GL_FLOAT_MAT2: case GL_FLOAT_MAT3: case GL_FLOAT_MAT4: return UNIFORM_MATRIX; case GL_SAMPLER_2D: case GL_SAMPLER_3D: case GL_SAMPLER_CUBE: case GL_SAMPLER_2D_ARRAY: case GL_SAMPLER_2D_SHADOW: return UNIFORM_SAMPLER; #ifdef GL_ARB_shader_image_load_store case GL_IMAGE_2D: case GL_IMAGE_3D: case GL_IMAGE_CUBE: case GL_IMAGE_2D_ARRAY: return UNIFORM_IMAGE; #endif default: lovrThrow("Unsupported uniform type for uniform '%s'", debug); return UNIFORM_FLOAT; } } static int getUniformComponents(GLenum type) { switch (type) { case GL_FLOAT_VEC2: case GL_INT_VEC2: case GL_FLOAT_MAT2: return 2; case GL_FLOAT_VEC3: case GL_INT_VEC3: case GL_FLOAT_MAT3: return 3; case GL_FLOAT_VEC4: case GL_INT_VEC4: case GL_FLOAT_MAT4: return 4; default: return 1; } } static TextureType getUniformTextureType(GLenum type) { switch (type) { case GL_SAMPLER_2D: return TEXTURE_2D; case GL_SAMPLER_3D: return TEXTURE_VOLUME; case GL_SAMPLER_CUBE: return TEXTURE_CUBE; case GL_SAMPLER_2D_ARRAY: return TEXTURE_ARRAY; case GL_SAMPLER_2D_SHADOW: return TEXTURE_2D; #ifdef GL_ARB_shader_image_load_store case GL_IMAGE_2D: return TEXTURE_2D; case GL_IMAGE_3D: return TEXTURE_VOLUME; case GL_IMAGE_CUBE: return TEXTURE_CUBE; case GL_IMAGE_2D_ARRAY: return TEXTURE_ARRAY; #endif default: return -1; } } static size_t getUniformTypeLength(const Uniform* uniform) { size_t size = 0; if (uniform->count > 1) { size += 2 + floor(log10(uniform->count)) + 1; // "[count]" } switch (uniform->type) { case UNIFORM_MATRIX: size += 4; break; case UNIFORM_FLOAT: size += uniform->components == 1 ? 5 : 4; break; case UNIFORM_INT: size += uniform->components == 1 ? 3 : 5; break; default: break; } return size; } static const char* getUniformTypeName(const Uniform* uniform) { switch (uniform->type) { case UNIFORM_FLOAT: switch (uniform->components) { case 1: return "float"; case 2: return "vec2"; case 3: return "vec3"; case 4: return "vec4"; } break; case UNIFORM_INT: switch (uniform->components) { case 1: return "int"; case 2: return "ivec2"; case 3: return "ivec3"; case 4: return "ivec4"; } break; case UNIFORM_MATRIX: switch (uniform->components) { case 2: return "mat2"; case 3: return "mat3"; case 4: return "mat4"; } break; default: break; } lovrThrow("Unreachable"); return ""; } // Syncing resources is only relevant for compute shaders #ifndef LOVR_WEBGL static void lovrGpuSync(uint8_t flags) { if (!flags) { return; } GLbitfield bits = 0; for (int i = 0; i < MAX_BARRIERS; i++) { if (!((flags >> i) & 1)) { continue; } if (state.incoherents[i].length == 0) { flags &= ~(1 << i); continue; } if (i == BARRIER_BLOCK) { for (size_t j = 0; j < state.incoherents[i].length; j++) { Buffer* buffer = state.incoherents[i].data[j]; buffer->incoherent &= ~(1 << i); } } else { for (size_t j = 0; j < state.incoherents[i].length; j++) { Texture* texture = state.incoherents[i].data[j]; texture->incoherent &= ~(1 << i); } } arr_clear(&state.incoherents[i]); switch (i) { case BARRIER_BLOCK: bits |= GL_SHADER_STORAGE_BARRIER_BIT; break; case BARRIER_UNIFORM_IMAGE: bits |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT; break; case BARRIER_UNIFORM_TEXTURE: bits |= GL_TEXTURE_FETCH_BARRIER_BIT; break; case BARRIER_TEXTURE: bits |= GL_TEXTURE_UPDATE_BARRIER_BIT; break; case BARRIER_CANVAS: bits |= GL_FRAMEBUFFER_BARRIER_BIT; break; } } if (bits) { glMemoryBarrier(bits); } } #endif static void lovrGpuDestroySyncResource(void* resource, uint8_t incoherent) { if (!incoherent) { return; } for (uint32_t i = 0; i < MAX_BARRIERS; i++) { if (incoherent & (1 << i)) { for (size_t j = 0; j < state.incoherents[i].length; j++) { if (state.incoherents[i].data[j] == resource) { arr_splice(&state.incoherents[i], j, 1); break; } } } } } static void lovrGpuBindFramebuffer(uint32_t framebuffer) { if (state.framebuffer != framebuffer) { state.framebuffer = framebuffer; glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); state.stats.renderPasses++; } } static void lovrGpuUseProgram(uint32_t program) { if (state.program != program) { state.program = program; glUseProgram(program); state.stats.shaderSwitches++; } } static void lovrGpuBindVertexArray(Mesh* vertexArray) { if (state.vertexArray != vertexArray) { state.vertexArray = vertexArray; glBindVertexArray(vertexArray->vao); } } static void lovrGpuBindBuffer(BufferType type, uint32_t buffer) { if (type == BUFFER_INDEX && state.vertexArray) { if (buffer != state.vertexArray->ibo) { state.vertexArray->ibo = buffer; glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); } } else { if (state.buffers[type] != buffer) { state.buffers[type] = buffer; glBindBuffer(convertBufferType(type), buffer); } } } static void lovrGpuBindBlockBuffer(BlockType type, uint32_t buffer, int slot, size_t offset, size_t size) { lovrAssert(offset % state.limits.blockAlign == 0, "Block buffer offset must be aligned to %d", state.limits.blockAlign); #ifdef LOVR_WEBGL lovrAssert(type == BLOCK_UNIFORM, "Compute blocks are not supported on this system"); GLenum target = GL_UNIFORM_BUFFER; #else GLenum target = type == BLOCK_UNIFORM ? GL_UNIFORM_BUFFER : GL_SHADER_STORAGE_BUFFER; #endif BlockBuffer* block = &state.blockBuffers[type][slot]; if (block->buffer != buffer || block->offset != offset || block->size != size) { block->buffer = buffer; block->offset = offset; block->size = size; glBindBufferRange(target, slot, buffer, offset, size); // Binding to an indexed target also binds to the generic target BufferType bufferType = type == BLOCK_UNIFORM ? BUFFER_UNIFORM : BUFFER_SHADER_STORAGE; state.buffers[bufferType] = buffer; } } static void lovrGpuBindTexture(Texture* texture, int slot) { lovrAssert(slot >= 0 && slot < MAX_TEXTURES, "Invalid texture slot %d", slot); texture = texture ? texture : state.defaultTexture; if (texture != state.textures[slot]) { lovrRetain(texture); lovrRelease(state.textures[slot], lovrTextureDestroy); state.textures[slot] = texture; if (state.activeTexture != slot) { glActiveTexture(GL_TEXTURE0 + slot); state.activeTexture = slot; } glBindTexture(texture->target, texture->id); } } #ifndef LOVR_WEBGL static void lovrGpuBindImage(StorageImage* image, int slot, const char* name) { lovrAssert(slot >= 0 && slot < MAX_IMAGES, "Invalid image slot %d", slot); // This is a risky way to compare the two structs if (memcmp(state.images + slot, image, sizeof(StorageImage))) { Texture* texture = image->texture; lovrAssert(texture, "No Texture bound to image uniform '%s'", name); lovrAssert(texture->format != FORMAT_RGBA || !texture->srgb, "Attempt to bind sRGB texture to image uniform '%s'", name); lovrAssert(!isTextureFormatCompressed(texture->format), "Attempt to bind compressed texture to image uniform '%s'", name); lovrAssert(texture->format != FORMAT_RGB && texture->format != FORMAT_RGBA4 && texture->format != FORMAT_RGB5A1, "Unsupported texture format for image uniform '%s'", name); lovrAssert(image->mipmap < (int) texture->mipmapCount, "Invalid mipmap level '%d' for image uniform '%s'", image->mipmap, name); lovrAssert(image->slice < (int) texture->depth, "Invalid texture slice '%d' for image uniform '%s'", image->slice, name); GLenum glAccess = convertAccess(image->access); GLenum glFormat = convertTextureFormatInternal(texture->format, false); bool layered = image->slice == -1; int slice = layered ? 0 : image->slice; lovrRetain(texture); lovrRelease(state.images[slot].texture, lovrTextureDestroy); glBindImageTexture(slot, texture->id, image->mipmap, layered, slice, glAccess, glFormat); memcpy(state.images + slot, image, sizeof(StorageImage)); } } #endif static void lovrGpuBindMesh(Mesh* mesh, Shader* shader, int baseDivisor) { lovrGpuBindVertexArray(mesh); if (mesh->indexBuffer && mesh->indexCount > 0) { lovrGpuBindBuffer(BUFFER_INDEX, mesh->indexBuffer->id); lovrBufferUnmap(mesh->indexBuffer); #ifdef LOVR_GL uint32_t primitiveRestart = mesh->indexSize == 4 ? 0xffffffff : 0xffff; if (state.primitiveRestart != primitiveRestart) { state.primitiveRestart = primitiveRestart; glPrimitiveRestartIndex(primitiveRestart); } #endif } uint16_t enabledLocations = 0; for (uint32_t i = 0; i < mesh->attributeCount; i++) { MeshAttribute* attribute; int location; bool integer; if ((attribute = &mesh->attributes[i])->disabled) { continue; } if ((location = lovrShaderGetAttributeLocation(shader, mesh->attributeNames[i], &integer)) < 0) { continue; } lovrBufferUnmap(attribute->buffer); enabledLocations |= (1 << location); uint16_t divisor = attribute->divisor * baseDivisor; if (mesh->divisors[location] != divisor) { glVertexAttribDivisor(location, divisor); mesh->divisors[location] = divisor; } if (mesh->locations[location] == i) { continue; } mesh->locations[location] = i; lovrGpuBindBuffer(BUFFER_VERTEX, attribute->buffer->id); GLenum type = convertAttributeType(attribute->type); GLvoid* offset = (GLvoid*) (intptr_t) attribute->offset; if (integer) { glVertexAttribIPointer(location, attribute->components, type, attribute->stride, offset); } else { glVertexAttribPointer(location, attribute->components, type, attribute->normalized, attribute->stride, offset); } } uint16_t diff = enabledLocations ^ mesh->enabledLocations; if (diff != 0) { for (uint32_t i = 0; i < MAX_ATTRIBUTES; i++) { if (diff & (1 << i)) { if (enabledLocations & (1 << i)) { glEnableVertexAttribArray(i); } else { glDisableVertexAttribArray(i); } } } mesh->enabledLocations = enabledLocations; } } static void lovrGpuBindCanvas(Canvas* canvas, bool willDraw) { lovrGpuBindFramebuffer(canvas->framebuffer); if (canvas->framebuffer == 0) { return; } canvas->needsResolve = willDraw; if (!canvas->needsAttach) { return; } // We need to synchronize if any of the Canvas attachments have pending writes on them #ifndef LOVR_WEBGL for (uint32_t i = 0; i < canvas->attachmentCount; i++) { Texture* texture = canvas->attachments[i].texture; if (texture->incoherent && (texture->incoherent >> BARRIER_CANVAS) & 1) { lovrGpuSync(1 << BARRIER_CANVAS); break; } } #endif GLenum buffers[MAX_CANVAS_ATTACHMENTS] = { GL_NONE }; for (uint32_t i = 0; i < canvas->attachmentCount; i++) { GLenum drawBuffer = buffers[i] = GL_COLOR_ATTACHMENT0 + i; Attachment* attachment = &canvas->attachments[i]; Texture* texture = attachment->texture; uint32_t slice = attachment->slice; uint32_t level = attachment->level; if (canvas->flags.stereo && state.singlepass == MULTIVIEW) { #ifdef LOVR_WEBGL lovrThrow("Unreachable"); #else glFramebufferTextureMultisampleMultiviewOVR(GL_FRAMEBUFFER, drawBuffer, texture->id, level, canvas->flags.msaa, slice, 2); #endif } else { if (canvas->flags.msaa) { glFramebufferRenderbuffer(GL_FRAMEBUFFER, drawBuffer, GL_RENDERBUFFER, texture->msaaId); glBindFramebuffer(GL_READ_FRAMEBUFFER, canvas->resolveBuffer); } switch (texture->type) { case TEXTURE_2D: glFramebufferTexture2D(GL_READ_FRAMEBUFFER, drawBuffer, GL_TEXTURE_2D, texture->id, level); break; case TEXTURE_CUBE: glFramebufferTexture2D(GL_READ_FRAMEBUFFER, drawBuffer, GL_TEXTURE_CUBE_MAP_POSITIVE_X + slice, texture->id, level); break; case TEXTURE_ARRAY: glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, drawBuffer, texture->id, level, slice); break; case TEXTURE_VOLUME: glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, drawBuffer, texture->id, level, slice); break; } } } glDrawBuffers(canvas->attachmentCount, buffers); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); switch (status) { case GL_FRAMEBUFFER_COMPLETE: break; case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: lovrThrow("Unable to set Canvas (MSAA settings)"); break; case GL_FRAMEBUFFER_UNSUPPORTED: lovrThrow("Unable to set Canvas (Texture formats)"); break; default: lovrThrow("Unable to set Canvas (reason unknown)"); break; } canvas->needsAttach = false; } static void lovrGpuBindPipeline(Pipeline* pipeline) { // Alpha Coverage if (state.alphaToCoverage != pipeline->alphaSampling) { state.alphaToCoverage = pipeline->alphaSampling; if (state.alphaToCoverage) { glEnable(GL_SAMPLE_ALPHA_TO_COVERAGE); } else { glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); } } // Blend mode if (state.blendMode != pipeline->blendMode || state.blendAlphaMode != pipeline->blendAlphaMode) { state.blendMode = pipeline->blendMode; state.blendAlphaMode = pipeline->blendAlphaMode; if (state.blendMode == BLEND_NONE) { if (state.blendEnabled) { state.blendEnabled = false; glDisable(GL_BLEND); } } else { if (!state.blendEnabled) { state.blendEnabled = true; glEnable(GL_BLEND); } GLenum srcRGB = state.blendMode == BLEND_MULTIPLY ? GL_DST_COLOR : GL_ONE; if (srcRGB == GL_ONE && state.blendAlphaMode == BLEND_ALPHA_MULTIPLY) { srcRGB = GL_SRC_ALPHA; } switch (state.blendMode) { case BLEND_ALPHA: glBlendEquation(GL_FUNC_ADD); glBlendFuncSeparate(srcRGB, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); break; case BLEND_ADD: glBlendEquation(GL_FUNC_ADD); glBlendFuncSeparate(srcRGB, GL_ONE, GL_ZERO, GL_ONE); break; case BLEND_SUBTRACT: glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); glBlendFuncSeparate(srcRGB, GL_ONE, GL_ZERO, GL_ONE); break; case BLEND_MULTIPLY: glBlendEquation(GL_FUNC_ADD); glBlendFuncSeparate(srcRGB, GL_ZERO, GL_DST_COLOR, GL_ZERO); break; case BLEND_LIGHTEN: glBlendEquation(GL_MAX); glBlendFuncSeparate(srcRGB, GL_ZERO, GL_ONE, GL_ZERO); break; case BLEND_DARKEN: glBlendEquation(GL_MIN); glBlendFuncSeparate(srcRGB, GL_ZERO, GL_ONE, GL_ZERO); break; case BLEND_SCREEN: glBlendEquation(GL_FUNC_ADD); glBlendFuncSeparate(srcRGB, GL_ONE_MINUS_SRC_COLOR, GL_ONE, GL_ONE_MINUS_SRC_COLOR); break; case BLEND_NONE: lovrThrow("Unreachable"); break; } } } // Color mask if (state.colorMask != pipeline->colorMask) { state.colorMask = pipeline->colorMask; glColorMask(state.colorMask & 0x8, state.colorMask & 0x4, state.colorMask & 0x2, state.colorMask & 0x1); } // Culling if (state.culling != pipeline->culling) { state.culling = pipeline->culling; if (state.culling) { glEnable(GL_CULL_FACE); } else { glDisable(GL_CULL_FACE); } } // Depth test and depth write bool updateDepthTest = pipeline->depthTest != state.depthTest; bool updateDepthWrite = state.depthWrite != (pipeline->depthWrite && !state.stencilWriting); if (updateDepthTest || updateDepthWrite) { bool enable = pipeline->depthTest != COMPARE_NONE || pipeline->depthWrite; if (enable && !state.depthEnabled) { glEnable(GL_DEPTH_TEST); } else if (!enable && state.depthEnabled) { glDisable(GL_DEPTH_TEST); } state.depthEnabled = enable; state.depthTest = pipeline->depthTest; if (enable && updateDepthTest) { glDepthFunc(convertCompareMode(state.depthTest)); } if (enable && updateDepthWrite) { state.depthWrite = pipeline->depthWrite && !state.stencilWriting; glDepthMask(state.depthWrite); } } // Line width if (state.lineWidth != pipeline->lineWidth) { state.lineWidth = pipeline->lineWidth; glLineWidth(state.lineWidth); } // Stencil mode if (!state.stencilWriting && (state.stencilMode != pipeline->stencilMode || state.stencilValue != pipeline->stencilValue)) { state.stencilMode = pipeline->stencilMode; state.stencilValue = pipeline->stencilValue; if (state.stencilMode != COMPARE_NONE) { if (!state.stencilEnabled) { state.stencilEnabled = true; glEnable(GL_STENCIL_TEST); } GLenum glMode = GL_ALWAYS; switch (state.stencilMode) { case COMPARE_EQUAL: glMode = GL_EQUAL; break; case COMPARE_NEQUAL: glMode = GL_NOTEQUAL; break; case COMPARE_LESS: glMode = GL_GREATER; break; case COMPARE_LEQUAL: glMode = GL_GEQUAL; break; case COMPARE_GREATER: glMode = GL_LESS; break; case COMPARE_GEQUAL: glMode = GL_LEQUAL; break; default: break; } glStencilFunc(glMode, state.stencilValue, 0xff); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); } else if (state.stencilEnabled) { state.stencilEnabled = false; glDisable(GL_STENCIL_TEST); } } // Winding if (state.winding != pipeline->winding) { state.winding = pipeline->winding; glFrontFace(state.winding == WINDING_CLOCKWISE ? GL_CW : GL_CCW); } // Wireframe #ifdef LOVR_GL if (state.wireframe != pipeline->wireframe) { state.wireframe = pipeline->wireframe; glPolygonMode(GL_FRONT_AND_BACK, state.wireframe ? GL_LINE : GL_FILL); } #endif } static void lovrGpuBindShader(Shader* shader) { lovrGpuUseProgram(shader->program); // Figure out if we need to wait for pending writes on resources to complete #ifndef LOVR_WEBGL uint8_t flags = 0; for (size_t i = 0; i < shader->blocks[BLOCK_COMPUTE].length; i++) { UniformBlock* block = &shader->blocks[BLOCK_COMPUTE].data[i]; if (block->source && (block->source->incoherent >> BARRIER_BLOCK) & 1) { flags |= 1 << BARRIER_BLOCK; break; } } for (size_t i = 0; i < shader->uniforms.length; i++) { Uniform* uniform = &shader->uniforms.data[i]; if (uniform->type == UNIFORM_SAMPLER) { for (int j = 0; j < uniform->count; j++) { Texture* texture = uniform->value.textures[j]; if (texture && texture->incoherent && (texture->incoherent >> BARRIER_UNIFORM_TEXTURE) & 1) { flags |= 1 << BARRIER_UNIFORM_TEXTURE; if (flags & (1 << BARRIER_UNIFORM_IMAGE)) { break; } } } } else if (uniform->type == UNIFORM_IMAGE) { for (int j = 0; j < uniform->count; j++) { Texture* texture = uniform->value.images[j].texture; if (texture && texture->incoherent && (texture->incoherent >> BARRIER_UNIFORM_IMAGE) & 1) { flags |= 1 << BARRIER_UNIFORM_IMAGE; if (flags & (1 << BARRIER_UNIFORM_TEXTURE)) { break; } } } } } lovrGpuSync(flags); #endif // Bind uniforms for (size_t i = 0; i < shader->uniforms.length; i++) { Uniform* uniform = &shader->uniforms.data[i]; if (uniform->type != UNIFORM_SAMPLER && uniform->type != UNIFORM_IMAGE && !uniform->dirty) { continue; } uniform->dirty = false; int count = uniform->count; void* data = uniform->value.data; switch (uniform->type) { case UNIFORM_FLOAT: switch (uniform->components) { case 1: glUniform1fv(uniform->location, count, data); break; case 2: glUniform2fv(uniform->location, count, data); break; case 3: glUniform3fv(uniform->location, count, data); break; case 4: glUniform4fv(uniform->location, count, data); break; } break; case UNIFORM_INT: switch (uniform->components) { case 1: glUniform1iv(uniform->location, count, data); break; case 2: glUniform2iv(uniform->location, count, data); break; case 3: glUniform3iv(uniform->location, count, data); break; case 4: glUniform4iv(uniform->location, count, data); break; } break; case UNIFORM_MATRIX: switch (uniform->components) { case 2: glUniformMatrix2fv(uniform->location, count, GL_FALSE, data); break; case 3: glUniformMatrix3fv(uniform->location, count, GL_FALSE, data); break; case 4: glUniformMatrix4fv(uniform->location, count, GL_FALSE, data); break; } break; case UNIFORM_IMAGE: #ifndef LOVR_WEBGL for (int j = 0; j < count; j++) { StorageImage* image = &uniform->value.images[j]; Texture* texture = image->texture; lovrAssert(!texture || texture->type == uniform->textureType, "Uniform texture type mismatch for uniform '%s'", uniform->name); // If the Shader can write to the texture, mark it as incoherent if (texture && image->access != ACCESS_READ) { for (Barrier barrier = BARRIER_BLOCK + 1; barrier < MAX_BARRIERS; barrier++) { texture->incoherent |= 1 << barrier; arr_push(&state.incoherents[barrier], texture); } } lovrGpuBindImage(image, uniform->baseSlot + j, uniform->name); } #endif break; case UNIFORM_SAMPLER: for (int j = 0; j < count; j++) { Texture* texture = uniform->value.textures[j]; lovrAssert(!texture || texture->type == uniform->textureType, "Uniform texture type mismatch for uniform '%s'", uniform->name); lovrAssert(!texture || (uniform->shadow == (texture->compareMode != COMPARE_NONE)), "Uniform '%s' requires a Texture with%s a compare mode", uniform->name, uniform->shadow ? "" : "out"); lovrGpuBindTexture(texture, uniform->baseSlot + j); } break; } } // Bind uniform blocks for (BlockType type = BLOCK_UNIFORM; type <= BLOCK_COMPUTE; type++) { for (size_t i = 0; i < shader->blocks[type].length; i++) { UniformBlock* block = &shader->blocks[type].data[i]; if (block->source) { if (type == BLOCK_COMPUTE && block->access != ACCESS_READ) { block->source->incoherent |= (1 << BARRIER_BLOCK); arr_push(&state.incoherents[BARRIER_BLOCK], block->source); } lovrBufferUnmap(block->source); lovrGpuBindBlockBuffer(type, block->source->id, block->slot, block->offset, block->size); } else { lovrGpuBindBlockBuffer(type, 0, block->slot, 0, 0); } } } } static void lovrGpuSetViewports(float* viewport, uint32_t count) { if (state.viewportCount != count || memcmp(state.viewports, viewport, count * 4 * sizeof(float))) { memcpy(state.viewports, viewport, count * 4 * sizeof(float)); state.viewportCount = count; #ifndef LOVR_WEBGL if (count > 1) { glViewportArrayv(0, count, viewport); } else { #endif glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); } #ifndef LOVR_WEBGL } #endif } // GPU #ifndef LOVR_WEBGL static void GLAPIENTRY onMessage(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userdata) { int level; switch (severity) { case GL_DEBUG_SEVERITY_HIGH: level = LOG_ERROR; break; case GL_DEBUG_SEVERITY_MEDIUM: level = LOG_WARN; break; case GL_DEBUG_SEVERITY_LOW: level = LOG_INFO; break; default: level = LOG_DEBUG; break; } lovrLog(level, "GL", message); } #endif void lovrGpuInit(void (*getProcAddress(const char*))(void), bool debug) { #ifdef LOVR_GL gladLoadGLLoader((GLADloadproc) getProcAddress); #elif defined(LOVR_GLES) gladLoadGLES2Loader((GLADloadproc) getProcAddress); #endif #ifndef LOVR_WEBGL if (debug && (GLAD_GL_KHR_debug || GLAD_GL_ES_VERSION_3_2)) { glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(onMessage, NULL); } const char* vendor = (const char*) glGetString(GL_VENDOR); state.amd = vendor && (strstr(vendor, "ATI Technologies") || strstr(vendor, "AMD") || strstr(vendor, "Advanced Micro Devices")); state.features.astc = GLAD_GL_ES_VERSION_3_2; state.features.compute = GLAD_GL_ES_VERSION_3_1 || (GLAD_GL_ARB_compute_shader && GLAD_GL_ARB_shader_storage_buffer_object && GLAD_GL_ARB_shader_image_load_store); state.features.dxt = GLAD_GL_EXT_texture_compression_s3tc; state.features.instancedStereo = GLAD_GL_ARB_viewport_array && GLAD_GL_AMD_vertex_shader_viewport_index && GLAD_GL_ARB_fragment_layer_viewport; state.features.multiview = GLAD_GL_ES_VERSION_3_0 && GLAD_GL_OVR_multiview2 && GLAD_GL_OVR_multiview_multisampled_render_to_texture; state.features.timers = GLAD_GL_VERSION_3_3; #ifdef LOVR_GL glEnable(GL_LINE_SMOOTH); glEnable(GL_PROGRAM_POINT_SIZE); glEnable(GL_FRAMEBUFFER_SRGB); glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); #endif glGetFloatv(GL_POINT_SIZE_RANGE, state.limits.pointSizes); if (state.features.compute) { glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &state.limits.compute[0]); glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &state.limits.compute[1]); glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &state.limits.compute[2]); } if (state.features.multiview) { state.singlepass = MULTIVIEW; } else if (state.features.instancedStereo) { state.singlepass = INSTANCED_STEREO; } else { state.singlepass = NONE; } #else glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, state.limits.pointSizes); #endif glGetIntegerv(GL_MAX_TEXTURE_SIZE, &state.limits.textureSize); glGetIntegerv(GL_MAX_SAMPLES, &state.limits.textureMSAA); glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &state.limits.blockSize); glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &state.limits.blockAlign); glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &state.limits.textureAnisotropy); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); #ifdef LOVR_GLES glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); #elif defined(LOVR_GL) glEnable(GL_PRIMITIVE_RESTART); state.primitiveRestart = 0xffffffff; glPrimitiveRestartIndex(state.primitiveRestart); #endif state.activeTexture = 0; glActiveTexture(GL_TEXTURE0 + state.activeTexture); state.alphaToCoverage = false; glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); state.blendEnabled = true; state.blendMode = BLEND_ALPHA; state.blendAlphaMode = BLEND_ALPHA_MULTIPLY; glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); state.colorMask = 0xf; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); state.culling = false; glDisable(GL_CULL_FACE); state.depthEnabled = true; state.depthTest = COMPARE_LEQUAL; state.depthWrite = true; glEnable(GL_DEPTH_TEST); glDepthFunc(convertCompareMode(state.depthTest)); glDepthMask(state.depthWrite); state.lineWidth = 1.f; glLineWidth(state.lineWidth); state.stencilEnabled = false; state.stencilMode = COMPARE_NONE; state.stencilValue = 0; state.stencilWriting = false; glDisable(GL_STENCIL_TEST); state.winding = WINDING_COUNTERCLOCKWISE; glFrontFace(GL_CCW); state.wireframe = false; #ifdef LOVR_GL glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #endif for (int i = 0; i < MAX_BARRIERS; i++) { arr_init(&state.incoherents[i], arr_alloc); } Image* image = lovrImageCreate(1, 1, NULL, 0xff, FORMAT_RGBA); state.defaultTexture = lovrTextureCreate(TEXTURE_2D, &image, 1, true, false, 0); lovrTextureSetFilter(state.defaultTexture, (TextureFilter) { .mode = FILTER_NEAREST }); lovrTextureSetWrap(state.defaultTexture, (TextureWrap) { WRAP_CLAMP, WRAP_CLAMP, WRAP_CLAMP }); lovrRelease(image, lovrImageDestroy); map_init(&state.timerMap, 4); state.queryPool.next = ~0u; state.activeTimer = ~0u; } void lovrGpuDestroy() { lovrRelease(state.defaultTexture, lovrTextureDestroy); for (int i = 0; i < MAX_TEXTURES; i++) { lovrRelease(state.textures[i], lovrTextureDestroy); } for (int i = 0; i < MAX_IMAGES; i++) { lovrRelease(state.images[i].texture, lovrTextureDestroy); } for (int i = 0; i < MAX_BARRIERS; i++) { arr_free(&state.incoherents[i]); } glDeleteQueries(state.queryPool.count, state.queryPool.queries); free(state.queryPool.queries); arr_free(&state.timers); map_free(&state.timerMap); memset(&state, 0, sizeof(state)); } void lovrGpuClear(Canvas* canvas, Color* color, float* depth, int* stencil) { lovrGpuBindCanvas(canvas, true); if (color) { int count = canvas ? canvas->attachmentCount : 1; for (int i = 0; i < count; i++) { glClearBufferfv(GL_COLOR, i, (float[]) { color->r, color->g, color->b, color->a }); } } if (depth && !state.depthWrite) { state.depthWrite = true; glDepthMask(state.depthWrite); } if (depth && stencil) { glClearBufferfi(GL_DEPTH_STENCIL, 0, *depth, *stencil); } else if (depth) { glClearBufferfv(GL_DEPTH, 0, depth); } else if (stencil) { glClearBufferiv(GL_STENCIL, 0, stencil); } } void lovrGpuCompute(Shader* shader, int x, int y, int z) { #ifdef LOVR_WEBGL lovrThrow("Compute shaders are not supported on this system"); #else lovrAssert(state.features.compute, "Compute shaders are not supported on this system"); lovrAssert(shader->type == SHADER_COMPUTE, "Attempt to use a non-compute shader for a compute operation"); lovrAssert(x <= state.limits.compute[0], "Compute x size %d exceeds the maximum of %d", state.limits.compute[0]); lovrAssert(y <= state.limits.compute[1], "Compute y size %d exceeds the maximum of %d", state.limits.compute[1]); lovrAssert(z <= state.limits.compute[2], "Compute z size %d exceeds the maximum of %d", state.limits.compute[2]); lovrGraphicsFlush(); lovrGpuBindShader(shader); glDispatchCompute(x, y, z); #endif } void lovrGpuDiscard(Canvas* canvas, bool color, bool depth, bool stencil) { #ifndef LOVR_GL lovrGpuBindCanvas(canvas, false); GLenum attachments[MAX_CANVAS_ATTACHMENTS + 1] = { 0 }; int count = 0; if (color) { int n = MAX(canvas->attachmentCount, 1); for (int i = 0; i < n; i++) { attachments[count++] = GL_COLOR_ATTACHMENT0 + i; } } if (depth) { attachments[count++] = GL_DEPTH_ATTACHMENT; } if (stencil) { attachments[count++] = GL_STENCIL_ATTACHMENT; } glInvalidateFramebuffer(GL_FRAMEBUFFER, count, attachments); #endif } void lovrGpuDraw(DrawCommand* draw) { lovrAssert(state.singlepass != MULTIVIEW || draw->shader->multiview == draw->canvas->flags.stereo, "Shader and Canvas multiview settings must match!"); uint32_t viewportCount = (draw->canvas->flags.stereo && state.singlepass != MULTIVIEW) ? 2 : 1; uint32_t drawCount = state.singlepass == NONE ? viewportCount : 1; uint32_t instanceMultiplier = state.singlepass == INSTANCED_STEREO ? viewportCount : 1; uint32_t viewportsPerDraw = instanceMultiplier; uint32_t instances = MAX(draw->instances, 1) * instanceMultiplier; float w = state.singlepass == MULTIVIEW ? draw->canvas->width : draw->canvas->width / (float) viewportCount; float h = draw->canvas->height; float viewports[2][4] = { { 0.f, 0.f, w, h }, { w, 0.f, w, h } }; lovrShaderSetInts(draw->shader, "lovrViewportCount", &(int) { viewportCount }, 0, 1); lovrGpuBindCanvas(draw->canvas, true); lovrGpuBindPipeline(&draw->pipeline); lovrGpuBindMesh(draw->mesh, draw->shader, instanceMultiplier); for (uint32_t i = 0; i < drawCount; i++) { lovrGpuSetViewports(&viewports[i][0], viewportsPerDraw); lovrShaderSetInts(draw->shader, "lovrViewID", &(int) { i }, 0, 1); lovrGpuBindShader(draw->shader); Mesh* mesh = draw->mesh; GLenum topology = convertTopology(draw->topology); if (mesh->indexCount > 0) { GLenum indexType = mesh->indexSize == sizeof(uint16_t) ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT; GLvoid* offset = (GLvoid*) (mesh->indexOffset + draw->rangeStart * mesh->indexSize); if (instances > 1) { glDrawElementsInstanced(topology, draw->rangeCount, indexType, offset, instances); } else { glDrawElements(topology, draw->rangeCount, indexType, offset); } } else { if (instances > 1) { glDrawArraysInstanced(topology, draw->rangeStart, draw->rangeCount, instances); } else { glDrawArrays(topology, draw->rangeStart, draw->rangeCount); } } state.stats.drawCalls++; } } void lovrGpuPresent() { state.stats.shaderSwitches = 0; state.stats.renderPasses = 0; state.stats.drawCalls = 0; } void lovrGpuStencil(StencilAction action, int replaceValue, StencilCallback callback, void* userdata) { lovrGraphicsFlush(); uint8_t lastColorMask = state.colorMask; state.colorMask = 0; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); if (!state.stencilEnabled) { state.stencilEnabled = true; glEnable(GL_STENCIL_TEST); } GLenum glAction; switch (action) { case STENCIL_REPLACE: glAction = GL_REPLACE; break; case STENCIL_INCREMENT: glAction = GL_INCR; break; case STENCIL_DECREMENT: glAction = GL_DECR; break; case STENCIL_INCREMENT_WRAP: glAction = GL_INCR_WRAP; break; case STENCIL_DECREMENT_WRAP: glAction = GL_DECR_WRAP; break; case STENCIL_INVERT: glAction = GL_INVERT; break; default: lovrThrow("Unreachable"); } glStencilFunc(GL_ALWAYS, replaceValue, 0xff); glStencilOp(GL_KEEP, GL_KEEP, glAction); state.stencilWriting = true; callback(userdata); lovrGraphicsFlush(); state.stencilWriting = false; state.stencilMode = ~0; // Dirty state.colorMask = lastColorMask; glColorMask(state.colorMask & 0x8, state.colorMask & 0x4, state.colorMask & 0x2, state.colorMask & 0x1); } void lovrGpuDirtyTexture() { lovrRelease(state.textures[state.activeTexture], lovrTextureDestroy); state.textures[state.activeTexture] = NULL; } void lovrGpuTick(const char* label) { #ifdef LOVR_GL lovrAssert(state.activeTimer == ~0u, "Attempt to start a new GPU timer while one is already active!"); QueryPool* pool = &state.queryPool; uint64_t hash = hash64(label, strlen(label)); uint64_t index = map_get(&state.timerMap, hash); // Create new timer if (index == MAP_NIL) { index = state.timers.length++; map_set(&state.timerMap, hash, index); arr_reserve(&state.timers, state.timers.length); state.timers.data[index].head = ~0u; state.timers.data[index].tail = ~0u; } Timer* timer = &state.timers.data[index]; state.activeTimer = index; // Expand pool if no unused timers are available. // The pool manages one memory allocation split into two chunks. // - The first chunk contains OpenGL query objects (GLuint). // - The second chunk is a linked list of query indices (uint32_t), used for two purposes: // - For inactive queries, pool->chain[query] points to the next inactive query (freelist). // - For active queries, pool->chain[query] points to the next active query for that timer. // When resizing the query pool allocation, the second half of the old allocation needs to be // copied to the second half of the new allocation. if (pool->next == ~0u) { uint32_t n = pool->count; pool->count = n == 0 ? 4 : (n << 1); pool->queries = realloc(pool->queries, pool->count * (sizeof(GLuint) + sizeof(uint32_t))); lovrAssert(pool->queries, "Out of memory"); pool->chain = pool->queries + pool->count; memcpy(pool->chain, pool->queries + n, n * sizeof(uint32_t)); glGenQueries(n ? n : pool->count, pool->queries + n); for (uint32_t i = n; i < pool->count - 1; i++) { pool->chain[i] = i + 1; } pool->chain[pool->count - 1] = ~0u; pool->next = n; } // Start query, update linked list pointers uint32_t query = pool->next; glBeginQuery(GL_TIME_ELAPSED, pool->queries[query]); if (timer->tail != ~0u) { pool->chain[timer->tail] = query; } if (timer->head == ~0u) { timer->head = query; } pool->next = pool->chain[query]; pool->chain[query] = ~0u; timer->tail = query; #endif } double lovrGpuTock(const char* label) { #ifdef LOVR_GL QueryPool* pool = &state.queryPool; uint64_t hash = hash64(label, strlen(label)); uint64_t index = map_get(&state.timerMap, hash); if (index == MAP_NIL) { return 0.; } Timer* timer = &state.timers.data[index]; if (state.activeTimer != index) { return timer->nanoseconds / 1e9; } glEndQuery(GL_TIME_ELAPSED); state.activeTimer = ~0u; // Repeatedly check timer's oldest pending query for completion for (;;) { int query = timer->head; GLuint available; glGetQueryObjectuiv(pool->queries[query], GL_QUERY_RESULT_AVAILABLE, &available); if (!available) { break; } // Update timer result glGetQueryObjectui64v(pool->queries[query], GL_QUERY_RESULT, &timer->nanoseconds); // Update timer's head pointer and return the completed query back to the pool timer->head = pool->chain[query]; pool->chain[query] = pool->next; pool->next = query; if (timer->head == ~0u) { timer->tail = ~0u; break; } } return timer->nanoseconds / 1e9; #endif return 0.; } const GpuFeatures* lovrGpuGetFeatures() { return &state.features; } const GpuLimits* lovrGpuGetLimits() { return &state.limits; } const GpuStats* lovrGpuGetStats() { return &state.stats; } // Texture Texture* lovrTextureCreate(TextureType type, Image** slices, uint32_t sliceCount, bool srgb, bool mipmaps, uint32_t msaa) { Texture* texture = calloc(1, sizeof(Texture)); lovrAssert(texture, "Out of memory"); texture->ref = 1; texture->type = type; texture->srgb = srgb; texture->mipmaps = mipmaps; texture->target = convertTextureTarget(type); texture->compareMode = COMPARE_NONE; state.stats.textureCount++; WrapMode wrap = type == TEXTURE_CUBE ? WRAP_CLAMP : WRAP_REPEAT; glGenTextures(1, &texture->id); lovrGpuBindTexture(texture, 0); lovrTextureSetWrap(texture, (TextureWrap) { .s = wrap, .t = wrap, .r = wrap }); if (msaa > 1) { texture->msaa = msaa; glGenRenderbuffers(1, &texture->msaaId); } if (sliceCount > 0) { lovrTextureAllocate(texture, slices[0]->width, slices[0]->height, sliceCount, slices[0]->format); for (uint32_t i = 0; i < sliceCount; i++) { lovrTextureReplacePixels(texture, slices[i], 0, 0, i, 0); } } return texture; } Texture* lovrTextureCreateFromHandle(uint32_t handle, TextureType type, uint32_t depth, uint32_t msaa) { Texture* texture = calloc(1, sizeof(Texture)); lovrAssert(texture, "Out of memory"); texture->ref = 1; texture->type = type; texture->id = handle; texture->target = convertTextureTarget(type); texture->compareMode = COMPARE_NONE; texture->native = true; state.stats.textureCount++; int width, height; lovrGpuBindTexture(texture, 0); glGetTexLevelParameteriv(texture->target, 0, GL_TEXTURE_WIDTH, &width); glGetTexLevelParameteriv(texture->target, 0, GL_TEXTURE_HEIGHT, &height); texture->width = (uint32_t) width; texture->height = (uint32_t) height; texture->depth = depth; // There isn't an easy way to get depth/layer count, so it's passed in... texture->mipmapCount = 1; if (msaa > 1) { texture->msaa = msaa; GLint internalFormat; glGetTexLevelParameteriv(texture->target, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); glGenRenderbuffers(1, &texture->msaaId); glBindRenderbuffer(GL_RENDERBUFFER, texture->msaaId); glRenderbufferStorageMultisample(GL_RENDERBUFFER, texture->msaa, internalFormat, width, height); } return texture; } void lovrTextureDestroy(void* ref) { Texture* texture = ref; if (!texture->native) glDeleteTextures(1, &texture->id); glDeleteRenderbuffers(1, &texture->msaaId); lovrGpuDestroySyncResource(texture, texture->incoherent); state.stats.textureMemory -= getTextureMemorySize(texture); state.stats.textureCount--; free(texture); } void lovrTextureAllocate(Texture* texture, uint32_t width, uint32_t height, uint32_t depth, TextureFormat format) { uint32_t maxSize = (uint32_t) state.limits.textureSize; lovrAssert(!texture->allocated, "Texture is already allocated"); lovrAssert(texture->type != TEXTURE_CUBE || width == height, "Cubemap images must be square"); lovrAssert(texture->type != TEXTURE_CUBE || depth == 6, "6 images are required for a cube texture"); lovrAssert(texture->type != TEXTURE_2D || depth == 1, "2D textures can only contain a single image"); lovrAssert(width <= maxSize, "Texture width %d exceeds max of %d", width, maxSize); lovrAssert(height <= maxSize, "Texture height %d exceeds max of %d", height, maxSize); lovrAssert(!texture->msaa || texture->type == TEXTURE_2D, "Only 2D textures can be created with MSAA"); texture->allocated = true; texture->width = width; texture->height = height; texture->depth = depth; texture->format = format; if (texture->mipmaps) { uint32_t dimension = texture->type == TEXTURE_VOLUME ? (MAX(MAX(width, height), depth)) : MAX(width, height); texture->mipmapCount = texture->mipmaps ? (log2(dimension) + 1) : 1; } else { texture->mipmapCount = 1; } if (isTextureFormatCompressed(format)) { return; } GLenum internalFormat = convertTextureFormatInternal(format, texture->srgb); #ifdef LOVR_GL if (GLAD_GL_ARB_texture_storage) { #endif if (texture->type == TEXTURE_ARRAY || texture->type == TEXTURE_VOLUME) { glTexStorage3D(texture->target, texture->mipmapCount, internalFormat, width, height, depth); } else { glTexStorage2D(texture->target, texture->mipmapCount, internalFormat, width, height); } #ifdef LOVR_GL } else { GLenum glFormat = convertTextureFormat(format); for (uint32_t i = 0; i < texture->mipmapCount; i++) { switch (texture->type) { case TEXTURE_2D: glTexImage2D(texture->target, i, internalFormat, width, height, 0, glFormat, GL_UNSIGNED_BYTE, NULL); break; case TEXTURE_CUBE: for (uint32_t face = 0; face < 6; face++) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, i, internalFormat, width, height, 0, glFormat, GL_UNSIGNED_BYTE, NULL); } break; case TEXTURE_ARRAY: case TEXTURE_VOLUME: glTexImage3D(texture->target, i, internalFormat, width, height, depth, 0, glFormat, GL_UNSIGNED_BYTE, NULL); break; } width = MAX(width >> 1, 1); height = MAX(height >> 1, 1); depth = texture->type == TEXTURE_VOLUME ? MAX(depth >> 1, 1) : depth; } } #endif if (texture->msaaId) { glBindRenderbuffer(GL_RENDERBUFFER, texture->msaaId); glRenderbufferStorageMultisample(GL_RENDERBUFFER, texture->msaa, internalFormat, width, height); } state.stats.textureMemory += getTextureMemorySize(texture); } void lovrTextureReplacePixels(Texture* texture, Image* image, uint32_t x, uint32_t y, uint32_t slice, uint32_t mipmap) { lovrGraphicsFlush(); lovrAssert(texture->allocated, "Texture is not allocated"); #ifndef LOVR_WEBGL if ((texture->incoherent >> BARRIER_TEXTURE) & 1) { lovrGpuSync(1 << BARRIER_TEXTURE); } #endif uint32_t maxWidth = lovrTextureGetWidth(texture, mipmap); uint32_t maxHeight = lovrTextureGetHeight(texture, mipmap); uint32_t width = image->width; uint32_t height = image->height; bool overflow = (x + width > maxWidth) || (y + height > maxHeight); lovrAssert(!overflow, "Trying to replace pixels outside the texture's bounds"); lovrAssert(mipmap < texture->mipmapCount, "Invalid mipmap level %d", mipmap); GLenum glFormat = convertTextureFormat(image->format); GLenum glInternalFormat = convertTextureFormatInternal(image->format, texture->srgb); GLenum binding = (texture->type == TEXTURE_CUBE) ? GL_TEXTURE_CUBE_MAP_POSITIVE_X + slice : texture->target; lovrGpuBindTexture(texture, 0); if (isTextureFormatCompressed(image->format)) { lovrAssert(width == maxWidth && height == maxHeight, "Compressed texture pixels must be fully replaced"); lovrAssert(mipmap == 0, "Unable to replace a specific mipmap of a compressed texture"); for (uint32_t i = 0; i < image->mipmapCount; i++) { Mipmap* m = image->mipmaps + i; switch (texture->type) { case TEXTURE_2D: case TEXTURE_CUBE: glCompressedTexImage2D(binding, i, glInternalFormat, m->width, m->height, 0, (GLsizei) m->size, m->data); break; case TEXTURE_ARRAY: case TEXTURE_VOLUME: glCompressedTexSubImage3D(binding, i, x, y, slice, m->width, m->height, 1, glInternalFormat, (GLsizei) m->size, m->data); break; } } } else { lovrAssert(image->blob->data, "Trying to replace Texture pixels with empty pixel data"); GLenum glType = convertTextureFormatType(image->format); switch (texture->type) { case TEXTURE_2D: case TEXTURE_CUBE: glTexSubImage2D(binding, mipmap, x, y, width, height, glFormat, glType, image->blob->data); break; case TEXTURE_ARRAY: case TEXTURE_VOLUME: glTexSubImage3D(binding, mipmap, x, y, slice, width, height, 1, glFormat, glType, image->blob->data); break; } if (texture->mipmaps) { #if defined(__APPLE__) || defined(LOVR_WEBGL) // glGenerateMipmap doesn't work on big cubemap textures on macOS if (texture->type != TEXTURE_CUBE || width < 2048) { glGenerateMipmap(texture->target); } else { glTexParameteri(texture->target, GL_TEXTURE_MAX_LEVEL, 0); } #else glGenerateMipmap(texture->target); #endif } } } uint64_t lovrTextureGetId(Texture* texture) { return texture->id; } uint32_t lovrTextureGetWidth(Texture* texture, uint32_t mipmap) { return MAX(texture->width >> mipmap, 1); } uint32_t lovrTextureGetHeight(Texture* texture, uint32_t mipmap) { return MAX(texture->height >> mipmap, 1); } uint32_t lovrTextureGetDepth(Texture* texture, uint32_t mipmap) { return texture->type == TEXTURE_VOLUME ? MAX(texture->depth >> mipmap, 1) : texture->depth; } uint32_t lovrTextureGetMipmapCount(Texture* texture) { return texture->mipmapCount; } uint32_t lovrTextureGetMSAA(Texture* texture) { return texture->msaa; } TextureType lovrTextureGetType(Texture* texture) { return texture->type; } TextureFormat lovrTextureGetFormat(Texture* texture) { return texture->format; } CompareMode lovrTextureGetCompareMode(Texture* texture) { return texture->compareMode; } TextureFilter lovrTextureGetFilter(Texture* texture) { return texture->filter; } TextureWrap lovrTextureGetWrap(Texture* texture) { return texture->wrap; } void lovrTextureSetCompareMode(Texture* texture, CompareMode compareMode) { if (texture->compareMode != compareMode) { lovrGraphicsFlush(); lovrGpuBindTexture(texture, 0); texture->compareMode = compareMode; if (compareMode == COMPARE_NONE) { glTexParameteri(texture->target, GL_TEXTURE_COMPARE_MODE, GL_NONE); } else { lovrAssert(isTextureFormatDepth(texture->format), "Only depth textures can set a compare mode"); glTexParameteri(texture->target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(texture->target, GL_TEXTURE_COMPARE_FUNC, convertCompareMode(compareMode)); } } } void lovrTextureSetFilter(Texture* texture, TextureFilter filter) { lovrGraphicsFlush(); lovrGpuBindTexture(texture, 0); texture->filter = filter; switch (filter.mode) { case FILTER_NEAREST: glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); break; case FILTER_BILINEAR: if (texture->mipmaps) { glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } break; case FILTER_TRILINEAR: if (texture->mipmaps) { glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } break; } glTexParameteri(texture->target, GL_TEXTURE_MAX_ANISOTROPY_EXT, MAX(filter.anisotropy, 1.f)); } void lovrTextureSetWrap(Texture* texture, TextureWrap wrap) { lovrGraphicsFlush(); texture->wrap = wrap; lovrGpuBindTexture(texture, 0); glTexParameteri(texture->target, GL_TEXTURE_WRAP_S, convertWrapMode(wrap.s)); glTexParameteri(texture->target, GL_TEXTURE_WRAP_T, convertWrapMode(wrap.t)); if (texture->type == TEXTURE_CUBE || texture->type == TEXTURE_VOLUME) { glTexParameteri(texture->target, GL_TEXTURE_WRAP_R, convertWrapMode(wrap.r)); } } // Canvas Canvas* lovrCanvasCreate(uint32_t width, uint32_t height, CanvasFlags flags) { Canvas* canvas = calloc(1, sizeof(Canvas)); lovrAssert(canvas, "Out of memory"); canvas->ref = 1; if (flags.stereo && state.singlepass != MULTIVIEW) { width *= 2; } canvas->width = width; canvas->height = height; canvas->flags = flags; glGenFramebuffers(1, &canvas->framebuffer); lovrGpuBindFramebuffer(canvas->framebuffer); if (flags.depth.enabled) { lovrAssert(isTextureFormatDepth(flags.depth.format), "Canvas depth buffer can't use a color TextureFormat"); GLenum attachment = flags.depth.format == FORMAT_D24S8 ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; if (flags.stereo && state.singlepass == MULTIVIEW) { // Zero MSAA is intentional here, we attach it to the Canvas using legacy MSAA technique canvas->depth.texture = lovrTextureCreate(TEXTURE_ARRAY, NULL, 0, false, flags.mipmaps, 0); lovrTextureAllocate(canvas->depth.texture, width, height, 2, flags.depth.format); #ifdef LOVR_WEBGL lovrThrow("Unreachable"); #else glFramebufferTextureMultisampleMultiviewOVR(GL_FRAMEBUFFER, attachment, canvas->depth.texture->id, 0, flags.msaa, 0, 2); #endif } else if (flags.depth.readable) { canvas->depth.texture = lovrTextureCreate(TEXTURE_2D, NULL, 0, false, flags.mipmaps, flags.msaa); lovrTextureAllocate(canvas->depth.texture, width, height, 1, flags.depth.format); glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, canvas->depth.texture->id, 0); } else { GLenum format = convertTextureFormatInternal(flags.depth.format, false); glGenRenderbuffers(1, &canvas->depthBuffer); glBindRenderbuffer(GL_RENDERBUFFER, canvas->depthBuffer); glRenderbufferStorageMultisample(GL_RENDERBUFFER, canvas->flags.msaa, format, width, height); glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, canvas->depthBuffer); } } if (flags.msaa && (!flags.stereo || state.singlepass != MULTIVIEW)) { glGenFramebuffers(1, &canvas->resolveBuffer); } return canvas; } Canvas* lovrCanvasCreateFromHandle(uint32_t width, uint32_t height, CanvasFlags flags, uint32_t framebuffer, uint32_t depthBuffer, uint32_t resolveBuffer, uint32_t attachmentCount, bool immortal) { Canvas* canvas = calloc(1, sizeof(Canvas)); lovrAssert(canvas, "Out of memory"); canvas->ref = 1; canvas->framebuffer = framebuffer; canvas->depthBuffer = depthBuffer; canvas->resolveBuffer = resolveBuffer; canvas->attachmentCount = attachmentCount; canvas->width = width; canvas->height = height; canvas->flags = flags; canvas->immortal = immortal; return canvas; } void lovrCanvasDestroy(void* ref) { Canvas* canvas = ref; lovrGraphicsFlushCanvas(canvas); if (!canvas->immortal) { glDeleteFramebuffers(1, &canvas->framebuffer); glDeleteRenderbuffers(1, &canvas->depthBuffer); glDeleteFramebuffers(1, &canvas->resolveBuffer); } for (uint32_t i = 0; i < canvas->attachmentCount; i++) { lovrRelease(canvas->attachments[i].texture, lovrTextureDestroy); } lovrRelease(canvas->depth.texture, lovrTextureDestroy); free(canvas); } void lovrCanvasResolve(Canvas* canvas) { if (!canvas->needsResolve) { return; } lovrGraphicsFlushCanvas(canvas); // We don't need to resolve a multiview Canvas because it uses the legacy multisampling method in // which the driver does an implicit multisample resolve whenever the canvas textures are read. if (canvas->flags.msaa && (!canvas->flags.stereo || state.singlepass != MULTIVIEW)) { uint32_t w = canvas->width; uint32_t h = canvas->height; glBindFramebuffer(GL_READ_FRAMEBUFFER, canvas->framebuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, canvas->resolveBuffer); state.framebuffer = canvas->resolveBuffer; if (canvas->attachmentCount == 1) { glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); } else { GLenum buffers[MAX_CANVAS_ATTACHMENTS] = { GL_NONE }; for (uint32_t i = 0; i < canvas->attachmentCount; i++) { buffers[i] = GL_COLOR_ATTACHMENT0 + i; glReadBuffer(i); glDrawBuffers(1, &buffers[i]); glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); } glReadBuffer(0); glDrawBuffers(canvas->attachmentCount, buffers); } } if (canvas->flags.mipmaps) { for (uint32_t i = 0; i < canvas->attachmentCount; i++) { Texture* texture = canvas->attachments[i].texture; if (texture->mipmapCount > 1) { lovrGpuBindTexture(texture, 0); glGenerateMipmap(texture->target); } } } canvas->needsResolve = false; } Image* lovrCanvasNewImage(Canvas* canvas, uint32_t index) { lovrGraphicsFlushCanvas(canvas); lovrGpuBindCanvas(canvas, false); if (canvas->flags.msaa) { glBindFramebuffer(GL_READ_FRAMEBUFFER, canvas->resolveBuffer); } Texture* texture = canvas->attachments[index].texture; #ifndef LOVR_WEBGL if ((texture->incoherent >> BARRIER_TEXTURE) & 1) { lovrGpuSync(1 << BARRIER_TEXTURE); } #endif if (index != 0) { glReadBuffer(index); } Image* image = lovrImageCreate(canvas->width, canvas->height, NULL, 0x0, texture->format); GLenum glFormat = convertTextureFormat(texture->format); GLenum glFormatType = convertTextureFormatType(texture->format); glReadPixels(0, 0, canvas->width, canvas->height, glFormat, glFormatType, image->blob->data); if (index != 0) { glReadBuffer(0); } return image; } const Attachment* lovrCanvasGetAttachments(Canvas* canvas, uint32_t* count) { if (count) *count = canvas->attachmentCount; return canvas->attachments; } void lovrCanvasSetAttachments(Canvas* canvas, Attachment* attachments, uint32_t count) { lovrAssert(count > 0, "A Canvas must have at least one attached Texture"); lovrAssert(count <= MAX_CANVAS_ATTACHMENTS, "Only %d textures can be attached to a Canvas, got %d", MAX_CANVAS_ATTACHMENTS, count); if (!canvas->needsAttach && count == canvas->attachmentCount && !memcmp(canvas->attachments, attachments, count * sizeof(Attachment))) { return; } lovrGraphicsFlushCanvas(canvas); for (uint32_t i = 0; i < count; i++) { Texture* texture = attachments[i].texture; uint32_t slice = attachments[i].slice; uint32_t level = attachments[i].level; uint32_t width = lovrTextureGetWidth(texture, level); uint32_t height = lovrTextureGetHeight(texture, level); uint32_t depth = lovrTextureGetDepth(texture, level); uint32_t mipmaps = lovrTextureGetMipmapCount(texture); bool hasDepthBuffer = canvas->flags.depth.enabled; lovrAssert(slice < depth, "Invalid attachment slice (Texture has %d, got %d)", depth, slice + 1); lovrAssert(level < mipmaps, "Invalid attachment mipmap level (Texture has %d, got %d)", mipmaps, level + 1); lovrAssert(!hasDepthBuffer || width == canvas->width, "Texture width of %d does not match Canvas width (%d)", width, canvas->width); lovrAssert(!hasDepthBuffer || height == canvas->height, "Texture height of %d does not match Canvas height (%d)", height, canvas->height); #ifndef __ANDROID__ // On multiview canvases, the multisample settings can be different lovrAssert(lovrTextureGetMSAA(texture) == canvas->flags.msaa, "Texture MSAA does not match Canvas MSAA"); #endif lovrRetain(texture); } for (uint32_t i = 0; i < canvas->attachmentCount; i++) { lovrRelease(canvas->attachments[i].texture, lovrTextureDestroy); } memcpy(canvas->attachments, attachments, count * sizeof(Attachment)); canvas->attachmentCount = count; canvas->needsAttach = true; } bool lovrCanvasIsStereo(Canvas* canvas) { return canvas->flags.stereo; } void lovrCanvasSetStereo(Canvas* canvas, bool stereo) { canvas->flags.stereo = stereo; } uint32_t lovrCanvasGetWidth(Canvas* canvas) { return canvas->width; } uint32_t lovrCanvasGetHeight(Canvas* canvas) { return canvas->height; } void lovrCanvasSetWidth(Canvas* canvas, uint32_t width) { canvas->width = width; } void lovrCanvasSetHeight(Canvas* canvas, uint32_t height) { canvas->height = height; } uint32_t lovrCanvasGetMSAA(Canvas* canvas) { return canvas->flags.msaa; } Texture* lovrCanvasGetDepthTexture(Canvas* canvas) { return canvas->depth.texture; } // Buffer Buffer* lovrBufferCreate(size_t size, void* data, BufferType type, BufferUsage usage, bool readable) { Buffer* buffer = calloc(1, sizeof(Buffer)); lovrAssert(buffer, "Out of memory"); buffer->ref = 1; state.stats.bufferCount++; state.stats.bufferMemory += size; buffer->size = size; buffer->readable = readable; buffer->type = type; buffer->usage = usage; glGenBuffers(1, &buffer->id); lovrGpuBindBuffer(type, buffer->id); GLenum glType = convertBufferType(type); #ifndef LOVR_WEBGL if (state.amd) { #endif buffer->data = malloc(size); lovrAssert(buffer->data, "Out of memory"); glBufferData(glType, size, data, convertBufferUsage(usage)); if (data) { memcpy(buffer->data, data, size); } #ifndef LOVR_WEBGL } else { glBufferData(glType, size, data, convertBufferUsage(usage)); } #endif return buffer; } void lovrBufferDestroy(void* ref) { Buffer* buffer = ref; lovrGpuDestroySyncResource(buffer, buffer->incoherent); glDeleteBuffers(1, &buffer->id); #ifndef LOVR_WEBGL if (state.amd) #endif free(buffer->data); state.stats.bufferMemory -= buffer->size; state.stats.bufferCount--; free(buffer); } size_t lovrBufferGetSize(Buffer* buffer) { return buffer->size; } bool lovrBufferIsReadable(Buffer* buffer) { return buffer->readable; } BufferUsage lovrBufferGetUsage(Buffer* buffer) { return buffer->usage; } void* lovrBufferMap(Buffer* buffer, size_t offset, bool unsynchronized) { #ifndef LOVR_WEBGL if (!state.amd && !buffer->mapped) { buffer->mapped = true; lovrGpuBindBuffer(buffer->type, buffer->id); lovrAssert(!buffer->readable || !unsynchronized, "Readable Buffers must be mapped with synchronization"); GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT; flags |= buffer->readable ? GL_MAP_READ_BIT : 0; flags |= unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0; buffer->data = glMapBufferRange(convertBufferType(buffer->type), 0, buffer->size, flags); return (uint8_t*) buffer->data + offset; } #endif return (uint8_t*) buffer->data + offset; } void lovrBufferFlush(Buffer* buffer, size_t offset, size_t size) { #ifndef LOVR_WEBGL lovrAssert(state.amd || size == 0 || buffer->mapped, "Attempt to flush unmapped Buffer"); #endif buffer->flushFrom = MIN(buffer->flushFrom, offset); buffer->flushTo = MAX(buffer->flushTo, offset + size); } void lovrBufferUnmap(Buffer* buffer) { #ifndef LOVR_WEBGL if (state.amd) { #endif if (buffer->flushTo > buffer->flushFrom) { lovrGpuBindBuffer(buffer->type, buffer->id); void* data = (uint8_t*) buffer->data + buffer->flushFrom; glBufferSubData(convertBufferType(buffer->type), buffer->flushFrom, buffer->flushTo - buffer->flushFrom, data); } #ifndef LOVR_WEBGL } else if (buffer->mapped) { lovrGpuBindBuffer(buffer->type, buffer->id); if (buffer->flushTo > buffer->flushFrom) { glFlushMappedBufferRange(convertBufferType(buffer->type), buffer->flushFrom, buffer->flushTo - buffer->flushFrom); } glUnmapBuffer(convertBufferType(buffer->type)); buffer->mapped = false; } #endif buffer->flushFrom = SIZE_MAX; buffer->flushTo = 0; } void lovrBufferDiscard(Buffer* buffer) { lovrAssert(!buffer->readable, "Readable Buffers can not be discarded"); lovrAssert(!buffer->mapped, "Mapped Buffers can not be discarded"); lovrGpuBindBuffer(buffer->type, buffer->id); GLenum glType = convertBufferType(buffer->type); #ifndef LOVR_WEBGL if (state.amd) { #endif glBufferData(glType, buffer->size, NULL, convertBufferUsage(buffer->usage)); #ifndef LOVR_WEBGL } else { GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT | GL_MAP_INVALIDATE_BUFFER_BIT; buffer->data = glMapBufferRange(glType, 0, buffer->size, flags); buffer->mapped = true; } #endif } // Shader static GLuint compileShader(GLenum type, const char** sources, int* lengths, int count) { GLuint shader = glCreateShader(type); glShaderSource(shader, count, sources, lengths); glCompileShader(shader); int isShaderCompiled; glGetShaderiv(shader, GL_COMPILE_STATUS, &isShaderCompiled); if (!isShaderCompiled) { int logLength; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); char* log = malloc(logLength); lovrAssert(log, "Out of memory"); glGetShaderInfoLog(shader, logLength, &logLength, log); const char* name; switch (type) { case GL_VERTEX_SHADER: name = "vertex shader"; break; case GL_FRAGMENT_SHADER: name = "fragment shader"; break; case GL_COMPUTE_SHADER: name = "compute shader"; break; default: name = "shader"; break; } lovrThrow("Could not compile %s:\n%s", name, log); } return shader; } static GLuint linkProgram(GLuint program) { glLinkProgram(program); int isLinked; glGetProgramiv(program, GL_LINK_STATUS, &isLinked); if (!isLinked) { int logLength; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength); char* log = malloc(logLength); lovrAssert(log, "Out of memory"); glGetProgramInfoLog(program, logLength, &logLength, log); lovrThrow("Could not link shader:\n%s", log); } return program; } static void lovrShaderSetupUniforms(Shader* shader) { uint32_t program = shader->program; lovrGpuUseProgram(program); // TODO necessary? // Uniform blocks int32_t blockCount; glGetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &blockCount); lovrAssert((size_t) blockCount <= MAX_BLOCK_BUFFERS, "Shader has too many uniform blocks (%d) the max is %d", blockCount, MAX_BLOCK_BUFFERS); map_init(&shader->blockMap, blockCount); arr_block_t* uniformBlocks = &shader->blocks[BLOCK_UNIFORM]; arr_init(uniformBlocks, arr_alloc); arr_reserve(uniformBlocks, (size_t) blockCount); for (int i = 0; i < blockCount; i++) { UniformBlock block = { .slot = i, .source = NULL }; glUniformBlockBinding(program, i, block.slot); GLsizei length; char name[LOVR_MAX_UNIFORM_LENGTH]; glGetActiveUniformBlockName(program, i, LOVR_MAX_UNIFORM_LENGTH, &length, name); int blockId = (i << 1) + BLOCK_UNIFORM; map_set(&shader->blockMap, hash64(name, length), blockId); arr_push(uniformBlocks, block); arr_init(&uniformBlocks->data[uniformBlocks->length - 1].uniforms, arr_alloc); } // Shader storage buffers and their buffer variables arr_block_t* computeBlocks = &shader->blocks[BLOCK_COMPUTE]; arr_init(computeBlocks, arr_alloc); #ifndef LOVR_WEBGL if ((GLAD_GL_ARB_shader_storage_buffer_object && GLAD_GL_ARB_program_interface_query) || GLAD_GL_ES_VERSION_3_1) { // Iterate over compute blocks, setting their binding and pushing them onto the block vector int32_t computeBlockCount; glGetProgramInterfaceiv(program, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &computeBlockCount); lovrAssert(computeBlockCount <= MAX_BLOCK_BUFFERS, "Shader has too many compute blocks (%d) the max is %d", computeBlockCount, MAX_BLOCK_BUFFERS); arr_reserve(computeBlocks, (size_t) computeBlockCount); for (int i = 0; i < computeBlockCount; i++) { UniformBlock block = { .slot = i, .source = NULL }; #ifdef LOVR_GLES // GLES can only set the block binding in shader code, so for now we only support one 0-bound block block.slot = 0; #else glShaderStorageBlockBinding(program, i, block.slot); #endif arr_init(&block.uniforms, arr_alloc); GLsizei length; char name[LOVR_MAX_UNIFORM_LENGTH]; glGetProgramResourceName(program, GL_SHADER_STORAGE_BLOCK, i, LOVR_MAX_UNIFORM_LENGTH, &length, name); int blockId = (i << 1) + BLOCK_COMPUTE; map_set(&shader->blockMap, hash64(name, length), blockId); arr_push(computeBlocks, block); } // Iterate over buffer variables, pushing them onto the uniform list of the correct block int bufferVariableCount; glGetProgramInterfaceiv(program, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES, &bufferVariableCount); for (int i = 0; i < bufferVariableCount; i++) { Uniform uniform; enum { blockIndex, offset, glType, count, arrayStride, matrixStride, propCount }; int values[propCount]; GLenum properties[propCount] = { GL_BLOCK_INDEX, GL_OFFSET, GL_TYPE, GL_ARRAY_SIZE, GL_ARRAY_STRIDE, GL_MATRIX_STRIDE }; glGetProgramResourceiv(program, GL_BUFFER_VARIABLE, i, propCount, properties, sizeof(values), NULL, values); glGetProgramResourceName(program, GL_BUFFER_VARIABLE, i, LOVR_MAX_UNIFORM_LENGTH, NULL, uniform.name); uniform.type = getUniformType(values[glType], uniform.name); uniform.components = getUniformComponents(uniform.type); uniform.count = values[count]; uniform.offset = values[offset]; if (uniform.count > 1) { uniform.size = uniform.count * values[arrayStride]; } else if (uniform.type == UNIFORM_MATRIX) { uniform.size = values[matrixStride] * uniform.components; } else { uniform.size = 4 * (uniform.components == 3 ? 4 : uniform.components); } arr_push(&computeBlocks->data[values[blockIndex]].uniforms, uniform); } } #endif // Uniform introspection int32_t uniformCount; int textureSlot = 0; int imageSlot = 0; glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount); map_init(&shader->uniformMap, 0); arr_init(&shader->uniforms, arr_alloc); for (uint32_t i = 0; i < (uint32_t) uniformCount; i++) { Uniform uniform; GLenum glType; GLsizei length; glGetActiveUniform(program, i, LOVR_MAX_UNIFORM_LENGTH, &length, &uniform.count, &glType, uniform.name); char* subscript = strchr(uniform.name, '['); if (subscript) { if (subscript[1] > '0') { continue; } else { *subscript = '\0'; length = subscript - uniform.name; } } uniform.location = glGetUniformLocation(program, uniform.name); uniform.type = getUniformType(glType, uniform.name); uniform.components = getUniformComponents(glType); uniform.shadow = glType == GL_SAMPLER_2D_SHADOW; #ifdef LOVR_WEBGL uniform.image = false; #else uniform.image = glType == GL_IMAGE_2D || glType == GL_IMAGE_3D || glType == GL_IMAGE_CUBE || glType == GL_IMAGE_2D_ARRAY; #endif uniform.textureType = getUniformTextureType(glType); uniform.baseSlot = uniform.type == UNIFORM_SAMPLER ? textureSlot : (uniform.type == UNIFORM_IMAGE ? imageSlot : -1); uniform.dirty = false; int blockIndex; glGetActiveUniformsiv(program, 1, &i, GL_UNIFORM_BLOCK_INDEX, &blockIndex); if (blockIndex != -1) { UniformBlock* block = &shader->blocks[BLOCK_UNIFORM].data[blockIndex]; glGetActiveUniformsiv(program, 1, &i, GL_UNIFORM_OFFSET, &uniform.offset); glGetActiveUniformsiv(program, 1, &i, GL_UNIFORM_SIZE, &uniform.count); if (uniform.count > 1) { int stride; glGetActiveUniformsiv(program, 1, &i, GL_UNIFORM_ARRAY_STRIDE, &stride); uniform.size = stride * uniform.count; } else if (uniform.type == UNIFORM_MATRIX) { int matrixStride; glGetActiveUniformsiv(program, 1, &i, GL_UNIFORM_MATRIX_STRIDE, &matrixStride); uniform.size = uniform.components * matrixStride; } else { uniform.size = 4 * (uniform.components == 3 ? 4 : uniform.components); } arr_push(&block->uniforms, uniform); continue; } else if (uniform.location == -1) { continue; } switch (uniform.type) { case UNIFORM_FLOAT: uniform.size = uniform.components * uniform.count * sizeof(float); uniform.value.data = calloc(1, uniform.size); lovrAssert(uniform.value.data, "Out of memory"); break; case UNIFORM_INT: uniform.size = uniform.components * uniform.count * sizeof(int); uniform.value.data = calloc(1, uniform.size); lovrAssert(uniform.value.data, "Out of memory"); break; case UNIFORM_MATRIX: uniform.size = uniform.components * uniform.components * uniform.count * sizeof(float); uniform.value.data = calloc(1, uniform.size); lovrAssert(uniform.value.data, "Out of memory"); break; case UNIFORM_SAMPLER: case UNIFORM_IMAGE: uniform.size = uniform.count * (uniform.type == UNIFORM_SAMPLER ? sizeof(Texture*) : sizeof(StorageImage)); uniform.value.data = calloc(1, uniform.size); lovrAssert(uniform.value.data, "Out of memory"); // Use the value for ints to bind texture slots, but use the value for textures afterwards. for (int j = 0; j < uniform.count; j++) { uniform.value.ints[j] = uniform.baseSlot + j; } glUniform1iv(uniform.location, uniform.count, uniform.value.ints); memset(uniform.value.data, 0, uniform.size); break; } size_t offset = 0; for (int j = 0; j < uniform.count; j++) { int location = uniform.location; if (uniform.count > 1) { char name[76 /* LOVR_MAX_UNIFORM_LENGTH + 2 + 10 */]; snprintf(name, sizeof(name), "%s[%d]", uniform.name, j); location = glGetUniformLocation(program, name); } switch (uniform.type) { case UNIFORM_FLOAT: glGetUniformfv(program, location, &uniform.value.floats[offset]); break; case UNIFORM_INT: glGetUniformiv(program, location, &uniform.value.ints[offset]); break; case UNIFORM_MATRIX: glGetUniformfv(program, location, &uniform.value.floats[offset]); break; default: break; } offset += uniform.components * (uniform.type == UNIFORM_MATRIX ? uniform.components : 1); } map_set(&shader->uniformMap, hash64(uniform.name, length), shader->uniforms.length); arr_push(&shader->uniforms, uniform); textureSlot += uniform.type == UNIFORM_SAMPLER ? uniform.count : 0; imageSlot += uniform.type == UNIFORM_IMAGE ? uniform.count : 0; } } static char* lovrShaderGetFlagCode(ShaderFlag* flags, uint32_t flagCount) { if (flagCount == 0) { return NULL; } // Figure out how much space we need size_t length = 0; for (uint32_t i = 0; i < flagCount; i++) { if (flags[i].name && !(flags[i].type == FLAG_BOOL && flags[i].value.b32 == false)) { length += strlen("#define FLAG_"); length += strlen(flags[i].name); if (flags[i].type == FLAG_INT) { length += snprintf(NULL, 0, " %d", flags[i].value.i32); } length += strlen("\n"); } } // Generate the string char* code = malloc(length + 1); code[length] = '\0'; char* s = code; for (uint32_t i = 0; i < flagCount; i++) { if (flags[i].name && !(flags[i].type == FLAG_BOOL && flags[i].value.b32 == false)) { s += sprintf(s, "#define FLAG_%s", flags[i].name); if (flags[i].type == FLAG_INT) { s += sprintf(s, " %d", flags[i].value.i32); } *s++ = '\n'; } } return code; } Shader* lovrShaderCreateGraphics(const char* vertexSource, int vertexSourceLength, const char* fragmentSource, int fragmentSourceLength, ShaderFlag* flags, uint32_t flagCount, bool multiview) { Shader* shader = calloc(1, sizeof(Shader)); lovrAssert(shader, "Out of memory"); shader->ref = 1; #if defined(LOVR_WEBGL) || defined(LOVR_GLES) const char* version = "#version 300 es\n"; const char* computeExtensions = ""; #else const char* version = "#version 330\n"; const char* computeExtensions = state.features.compute ? "#extension GL_ARB_shader_storage_buffer_object : enable\n" "#extension GL_ARB_shader_image_load_store : enable\n" : ""; #endif const char* singlepass[2] = { "", "" }; if (multiview && state.singlepass == MULTIVIEW) { singlepass[0] = singlepass[1] = "#extension GL_OVR_multiview2 : require\n#define MULTIVIEW\n"; } else if (state.singlepass == INSTANCED_STEREO) { singlepass[0] = "#extension GL_AMD_vertex_shader_viewport_index : require\n""#define INSTANCED_STEREO\n"; singlepass[1] = "#extension GL_ARB_fragment_layer_viewport : require\n""#define INSTANCED_STEREO\n"; } char* flagSource = lovrShaderGetFlagCode(flags, flagCount); if (!vertexSource) { vertexSource = lovrUnlitVertexShader; vertexSourceLength = -1; } if (!fragmentSource) { fragmentSource = lovrUnlitFragmentShader; fragmentSourceLength = -1; } // Vertex const char* vertexSources[] = { version, computeExtensions, singlepass[0], flagSource ? flagSource : "", lovrShaderVertexPrefix, vertexSource, lovrShaderVertexSuffix }; int vertexSourceLengths[] = { -1, -1, -1, -1, -1, vertexSourceLength, -1 }; int vertexSourceCount = sizeof(vertexSources) / sizeof(vertexSources[0]); GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSources, vertexSourceLengths, vertexSourceCount); // Fragment const char* fragmentSources[] = { version, computeExtensions, singlepass[1], flagSource ? flagSource : "", lovrShaderFragmentPrefix, fragmentSource, lovrShaderFragmentSuffix }; int fragmentSourceLengths[] = { -1, -1, -1, -1, -1, fragmentSourceLength, -1 }; int fragmentSourceCount = sizeof(fragmentSources) / sizeof(fragmentSources[0]); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSources, fragmentSourceLengths, fragmentSourceCount); free(flagSource); // Link uint32_t program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); glBindAttribLocation(program, LOVR_SHADER_POSITION, "lovrPosition"); glBindAttribLocation(program, LOVR_SHADER_NORMAL, "lovrNormal"); glBindAttribLocation(program, LOVR_SHADER_TEX_COORD, "lovrTexCoord"); glBindAttribLocation(program, LOVR_SHADER_VERTEX_COLOR, "lovrVertexColor"); glBindAttribLocation(program, LOVR_SHADER_TANGENT, "lovrTangent"); glBindAttribLocation(program, LOVR_SHADER_BONES, "lovrBones"); glBindAttribLocation(program, LOVR_SHADER_BONE_WEIGHTS, "lovrBoneWeights"); glBindAttribLocation(program, LOVR_SHADER_DRAW_ID, "lovrDrawID"); linkProgram(program); glDetachShader(program, vertexShader); glDeleteShader(vertexShader); glDetachShader(program, fragmentShader); glDeleteShader(fragmentShader); shader->program = program; shader->type = SHADER_GRAPHICS; // Generic attributes lovrGpuUseProgram(program); glVertexAttrib4fv(LOVR_SHADER_VERTEX_COLOR, (float[4]) { 1., 1., 1., 1. }); glVertexAttribI4uiv(LOVR_SHADER_BONES, (uint32_t[4]) { 0., 0., 0., 0. }); glVertexAttrib4fv(LOVR_SHADER_BONE_WEIGHTS, (float[4]) { 1., 0., 0., 0. }); glVertexAttribI4ui(LOVR_SHADER_DRAW_ID, 0, 0, 0, 0); lovrShaderSetupUniforms(shader); // Attribute cache int32_t attributeCount; glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &attributeCount); map_init(&shader->attributes, attributeCount); for (int i = 0; i < attributeCount; i++) { char name[LOVR_MAX_ATTRIBUTE_LENGTH]; GLint size; GLenum type; GLsizei length; glGetActiveAttrib(program, i, LOVR_MAX_ATTRIBUTE_LENGTH, &length, &size, &type, name); int location = glGetAttribLocation(program, name); if (location >= 0) { map_set(&shader->attributes, hash64(name, length), (location << 1) | isAttributeTypeInteger(type)); } } shader->multiview = multiview; return shader; } Shader* lovrShaderCreateDefault(DefaultShader type, ShaderFlag* flags, uint32_t flagCount, bool multiview) { switch (type) { case SHADER_UNLIT: return lovrShaderCreateGraphics(NULL, -1, NULL, -1, flags, flagCount, multiview); case SHADER_STANDARD: return lovrShaderCreateGraphics(lovrStandardVertexShader, -1, lovrStandardFragmentShader, -1, flags, flagCount, multiview); case SHADER_CUBE: return lovrShaderCreateGraphics(lovrCubeVertexShader, -1, lovrCubeFragmentShader, -1, flags, flagCount, multiview); case SHADER_PANO: return lovrShaderCreateGraphics(lovrCubeVertexShader, -1, lovrPanoFragmentShader, -1, flags, flagCount, multiview); case SHADER_FONT: return lovrShaderCreateGraphics(NULL, -1, lovrFontFragmentShader, -1, flags, flagCount, multiview); case SHADER_FILL: return lovrShaderCreateGraphics(lovrFillVertexShader, -1, NULL, -1, flags, flagCount, multiview); default: lovrThrow("Unknown default shader type"); return NULL; } } Shader* lovrShaderCreateCompute(const char* source, int length, ShaderFlag* flags, uint32_t flagCount) { Shader* shader = calloc(1, sizeof(Shader)); lovrAssert(shader, "Out of memory"); shader->ref = 1; #ifdef LOVR_WEBGL lovrThrow("Compute shaders are not supported on this system"); #else lovrAssert(state.features.compute, "Compute shaders are not supported on this system"); char* flagSource = lovrShaderGetFlagCode(flags, flagCount); const char* sources[] = { lovrShaderComputePrefix, flagSource ? flagSource : "", source, lovrShaderComputeSuffix }; int lengths[] = { -1, -1, length, -1 }; int count = sizeof(sources) / sizeof(sources[0]); GLuint computeShader = compileShader(GL_COMPUTE_SHADER, sources, lengths, count); free(flagSource); GLuint program = glCreateProgram(); glAttachShader(program, computeShader); linkProgram(program); glDetachShader(program, computeShader); glDeleteShader(computeShader); shader->program = program; shader->type = SHADER_COMPUTE; lovrShaderSetupUniforms(shader); #endif return shader; } void lovrShaderDestroy(void* ref) { Shader* shader = ref; lovrGraphicsFlushShader(shader); glDeleteProgram(shader->program); for (size_t i = 0; i < shader->uniforms.length; i++) { free(shader->uniforms.data[i].value.data); } for (BlockType type = BLOCK_UNIFORM; type <= BLOCK_COMPUTE; type++) { for (size_t i = 0; i < shader->blocks[type].length; i++) { lovrRelease(shader->blocks[type].data[i].source, lovrBufferDestroy); arr_free(&shader->blocks[type].data[i].uniforms); } } arr_free(&shader->uniforms); arr_free(&shader->blocks[BLOCK_UNIFORM]); arr_free(&shader->blocks[BLOCK_COMPUTE]); map_free(&shader->attributes); map_free(&shader->uniformMap); map_free(&shader->blockMap); free(shader); } ShaderType lovrShaderGetType(Shader* shader) { return shader->type; } int lovrShaderGetAttributeLocation(Shader* shader, const char* name, bool* integer) { uint64_t info = map_get(&shader->attributes, hash64(name, strlen(name))); *integer = info & 1; return info == MAP_NIL ? -1 : (int) (info >> 1); } bool lovrShaderHasUniform(Shader* shader, const char* name) { return map_get(&shader->uniformMap, hash64(name, strlen(name))) != MAP_NIL; } bool lovrShaderHasBlock(Shader* shader, const char* name) { return map_get(&shader->blockMap, hash64(name, strlen(name))) != MAP_NIL; } const Uniform* lovrShaderGetUniform(Shader* shader, const char* name) { uint64_t index = map_get(&shader->uniformMap, hash64(name, strlen(name))); return index == MAP_NIL ? NULL : &shader->uniforms.data[index]; } static void lovrShaderSetUniform(Shader* shader, const char* name, UniformType type, void* data, int start, int count, int size, const char* debug) { uint64_t index = map_get(&shader->uniformMap, hash64(name, strlen(name))); if (index == MAP_NIL) { return; } Uniform* uniform = &shader->uniforms.data[index]; lovrAssert(uniform->type == type, "Unable to send %ss to uniform %s", debug, name); lovrAssert((start + count) * size <= uniform->size, "Too many %ss for uniform %s, maximum is %d", debug, name, uniform->size / size); void* dest = uniform->value.bytes + start * size; if (memcmp(dest, data, count * size)) { lovrGraphicsFlushShader(shader); memcpy(dest, data, count * size); uniform->dirty = true; } } void lovrShaderSetFloats(Shader* shader, const char* name, float* data, int start, int count) { lovrShaderSetUniform(shader, name, UNIFORM_FLOAT, data, start, count, sizeof(float), "float"); } void lovrShaderSetInts(Shader* shader, const char* name, int* data, int start, int count) { lovrShaderSetUniform(shader, name, UNIFORM_INT, data, start, count, sizeof(int), "int"); } void lovrShaderSetMatrices(Shader* shader, const char* name, float* data, int start, int count) { lovrShaderSetUniform(shader, name, UNIFORM_MATRIX, data, start, count, sizeof(float), "float"); } void lovrShaderSetTextures(Shader* shader, const char* name, Texture** data, int start, int count) { lovrShaderSetUniform(shader, name, UNIFORM_SAMPLER, data, start, count, sizeof(Texture*), "texture"); } void lovrShaderSetImages(Shader* shader, const char* name, StorageImage* data, int start, int count) { lovrShaderSetUniform(shader, name, UNIFORM_IMAGE, data, start, count, sizeof(StorageImage), "image"); } void lovrShaderSetColor(Shader* shader, const char* name, Color color) { color.r = lovrMathGammaToLinear(color.r); color.g = lovrMathGammaToLinear(color.g); color.b = lovrMathGammaToLinear(color.b); lovrShaderSetUniform(shader, name, UNIFORM_FLOAT, (float*) &color, 0, 4, sizeof(float), "float"); } void lovrShaderSetBlock(Shader* shader, const char* name, Buffer* buffer, size_t offset, size_t size, UniformAccess access) { uint64_t id = map_get(&shader->blockMap, hash64(name, strlen(name))); if (id == MAP_NIL) return; int type = id & 1; int index = id >> 1; UniformBlock* block = &shader->blocks[type].data[index]; if (block->source != buffer || block->offset != offset || block->size != size) { lovrGraphicsFlushShader(shader); lovrRetain(buffer); lovrRelease(block->source, lovrBufferDestroy); block->access = access; block->source = buffer; block->offset = offset; block->size = size; } } // ShaderBlock // Calculates uniform size and byte offsets using std140 rules, returning the total buffer size size_t lovrShaderComputeUniformLayout(arr_uniform_t* uniforms) { size_t size = 0; for (size_t i = 0; i < uniforms->length; i++) { int align; Uniform* uniform = &uniforms->data[i]; if (uniform->count > 1 || uniform->type == UNIFORM_MATRIX) { align = 16; uniform->size = align * uniform->count * (uniform->type == UNIFORM_MATRIX ? uniform->components : 1); } else { align = (uniform->components + (uniform->components == 3)) * 4; uniform->size = uniform->components * 4; } uniform->offset = (size + (align - 1)) & -align; size = uniform->offset + uniform->size; } return size; } ShaderBlock* lovrShaderBlockCreate(BlockType type, Buffer* buffer, arr_uniform_t* uniforms) { ShaderBlock* block = calloc(1, sizeof(ShaderBlock)); lovrAssert(block, "Out of memory"); block->ref = 1; arr_init(&block->uniforms, arr_alloc); map_init(&block->uniformMap, (uint32_t) uniforms->length); arr_append(&block->uniforms, uniforms->data, uniforms->length); for (size_t i = 0; i < block->uniforms.length; i++) { Uniform* uniform = &block->uniforms.data[i]; map_set(&block->uniformMap, hash64(uniform->name, strlen(uniform->name)), i); } block->type = type; block->buffer = buffer; lovrRetain(buffer); return block; } void lovrShaderBlockDestroy(void* ref) { ShaderBlock* block = ref; lovrRelease(block->buffer, lovrBufferDestroy); arr_free(&block->uniforms); map_free(&block->uniformMap); free(block); } BlockType lovrShaderBlockGetType(ShaderBlock* block) { return block->type; } char* lovrShaderBlockGetShaderCode(ShaderBlock* block, const char* blockName, const char* namespace, size_t* length) { // Calculate size_t size = 0; size_t tab = 2; size += 15; // "layout(std140) " size += block->type == BLOCK_UNIFORM ? 7 : 6; // "uniform" || "buffer" size += 1; // " " size += strlen(blockName); size += 3; // " {\n" for (size_t i = 0; i < block->uniforms.length; i++) { size += tab; size += getUniformTypeLength(&block->uniforms.data[i]); size += 1; // " " size += strlen(block->uniforms.data[i].name); size += 2; // ";\n" } if (namespace) { size += 2; // "} " size += strlen(namespace); size += 2; // ";\n" } else { size += 3; // "};\n" } // Allocate char* code = malloc(size + 1); lovrAssert(code, "Out of memory"); // Concatenate char* s = code; s += sprintf(s, "layout(std140) %s %s {\n", block->type == BLOCK_UNIFORM ? "uniform" : "buffer", blockName); for (size_t i = 0; i < block->uniforms.length; i++) { const Uniform* uniform = &block->uniforms.data[i]; if (uniform->count > 1) { s += sprintf(s, " %s %s[%d];\n", getUniformTypeName(uniform), uniform->name, uniform->count); } else { s += sprintf(s, " %s %s;\n", getUniformTypeName(uniform), uniform->name); } } if (namespace) { s += sprintf(s, "} %s;\n", namespace); } else { s += sprintf(s, "};\n"); } *s = '\0'; *length = size; return code; } const Uniform* lovrShaderBlockGetUniform(ShaderBlock* block, const char* name) { uint64_t index = map_get(&block->uniformMap, hash64(name, strlen(name))); return index == MAP_NIL ? NULL : &block->uniforms.data[index]; } Buffer* lovrShaderBlockGetBuffer(ShaderBlock* block) { return block->buffer; } // Mesh Mesh* lovrMeshCreate(DrawMode mode, Buffer* vertexBuffer, uint32_t vertexCount) { Mesh* mesh = calloc(1, sizeof(Mesh)); lovrAssert(mesh, "Out of memory"); mesh->ref = 1; mesh->mode = mode; mesh->vertexBuffer = vertexBuffer; mesh->vertexCount = vertexCount; lovrRetain(mesh->vertexBuffer); glGenVertexArrays(1, &mesh->vao); map_init(&mesh->attributeMap, MAX_ATTRIBUTES); memset(mesh->locations, 0xff, MAX_ATTRIBUTES * sizeof(uint8_t)); return mesh; } void lovrMeshDestroy(void* ref) { Mesh* mesh = ref; lovrGraphicsFlushMesh(mesh); glDeleteVertexArrays(1, &mesh->vao); for (uint32_t i = 0; i < mesh->attributeCount; i++) { lovrRelease(mesh->attributes[i].buffer, lovrBufferDestroy); } map_free(&mesh->attributeMap); lovrRelease(mesh->vertexBuffer, lovrBufferDestroy); lovrRelease(mesh->indexBuffer, lovrBufferDestroy); lovrRelease(mesh->material, lovrMaterialDestroy); free(mesh); } void lovrMeshSetIndexBuffer(Mesh* mesh, Buffer* buffer, uint32_t indexCount, size_t indexSize, size_t offset) { if (mesh->indexBuffer != buffer || mesh->indexCount != indexCount || mesh->indexSize != indexSize) { lovrGraphicsFlushMesh(mesh); lovrRetain(buffer); lovrRelease(mesh->indexBuffer, lovrBufferDestroy); mesh->indexBuffer = buffer; mesh->indexCount = indexCount; mesh->indexSize = indexSize; mesh->indexOffset = offset; } } Buffer* lovrMeshGetVertexBuffer(Mesh* mesh) { return mesh->vertexBuffer; } Buffer* lovrMeshGetIndexBuffer(Mesh* mesh) { return mesh->indexBuffer; } uint32_t lovrMeshGetVertexCount(Mesh* mesh) { return mesh->vertexCount; } uint32_t lovrMeshGetIndexCount(Mesh* mesh) { return mesh->indexCount; } size_t lovrMeshGetIndexSize(Mesh* mesh) { return mesh->indexSize; } uint32_t lovrMeshGetAttributeCount(Mesh* mesh) { return mesh->attributeCount; } void lovrMeshAttachAttribute(Mesh* mesh, const char* name, MeshAttribute* attribute) { uint64_t hash = hash64(name, strlen(name)); lovrAssert(map_get(&mesh->attributeMap, hash) == MAP_NIL, "Mesh already has an attribute named '%s'", name); lovrAssert(mesh->attributeCount < MAX_ATTRIBUTES, "Mesh already has the max number of attributes (%d)", MAX_ATTRIBUTES); lovrAssert(strlen(name) < MAX_ATTRIBUTE_NAME_LENGTH, "Mesh attribute name '%s' is too long (max is %d)", name, MAX_ATTRIBUTE_NAME_LENGTH); lovrGraphicsFlushMesh(mesh); uint64_t index = mesh->attributeCount++; mesh->attributes[index] = *attribute; strcpy(mesh->attributeNames[index], name); map_set(&mesh->attributeMap, hash, index); lovrRetain(attribute->buffer); } void lovrMeshDetachAttribute(Mesh* mesh, const char* name) { uint64_t hash = hash64(name, strlen(name)); uint64_t index = map_get(&mesh->attributeMap, hash); lovrAssert(index != MAP_NIL, "No attached attribute named '%s' was found", name); MeshAttribute* attribute = &mesh->attributes[index]; lovrGraphicsFlushMesh(mesh); lovrRelease(attribute->buffer, lovrBufferDestroy); map_remove(&mesh->attributeMap, hash); mesh->attributeNames[index][0] = '\0'; memmove(mesh->attributeNames + index, mesh->attributeNames + index + 1, (mesh->attributeCount - index - 1) * MAX_ATTRIBUTE_NAME_LENGTH * sizeof(char)); memmove(mesh->attributes + index, mesh->attributes + index + 1, (mesh->attributeCount - index - 1) * sizeof(MeshAttribute)); mesh->attributeCount--; for (uint32_t i = 0; i < MAX_ATTRIBUTES; i++) { if (mesh->locations[i] > index) { mesh->locations[i]--; } else if (mesh->locations[i] == index) { mesh->locations[i] = 0xff; } } } const MeshAttribute* lovrMeshGetAttribute(Mesh* mesh, uint32_t index) { return index < mesh->attributeCount ? &mesh->attributes[index] : NULL; } uint32_t lovrMeshGetAttributeIndex(Mesh* mesh, const char* name) { uint64_t hash = hash64(name, strlen(name)); uint64_t index = map_get(&mesh->attributeMap, hash); return index == MAP_NIL ? ~0u : index; } const char* lovrMeshGetAttributeName(Mesh* mesh, uint32_t index) { return mesh->attributeNames[index]; } bool lovrMeshIsAttributeEnabled(Mesh* mesh, const char* name) { uint64_t hash = hash64(name, strlen(name)); uint64_t index = map_get(&mesh->attributeMap, hash); lovrAssert(index != MAP_NIL, "Mesh does not have an attribute named '%s'", name); return !mesh->attributes[index].disabled; } void lovrMeshSetAttributeEnabled(Mesh* mesh, const char* name, bool enable) { bool disable = !enable; uint64_t hash = hash64(name, strlen(name)); uint64_t index = map_get(&mesh->attributeMap, hash); lovrAssert(index != MAP_NIL, "Mesh does not have an attribute named '%s'", name); if (mesh->attributes[index].disabled != disable) { lovrGraphicsFlushMesh(mesh); mesh->attributes[index].disabled = disable; } } DrawMode lovrMeshGetDrawMode(Mesh* mesh) { return mesh->mode; } void lovrMeshSetDrawMode(Mesh* mesh, DrawMode mode) { mesh->mode = mode; } void lovrMeshGetDrawRange(Mesh* mesh, uint32_t* start, uint32_t* count) { *start = mesh->drawStart; *count = mesh->drawCount; } void lovrMeshSetDrawRange(Mesh* mesh, uint32_t start, uint32_t count) { uint32_t limit = mesh->indexSize > 0 ? mesh->indexCount : mesh->vertexCount; lovrAssert(start + count <= limit, "Invalid mesh draw range [%d, %d]", start + 1, start + count + 1); mesh->drawStart = start; mesh->drawCount = count; } Material* lovrMeshGetMaterial(Mesh* mesh) { return mesh->material; } void lovrMeshSetMaterial(Mesh* mesh, Material* material) { lovrRetain(material); lovrRelease(mesh->material, lovrMaterialDestroy); mesh->material = material; }