diff --git a/.gitignore b/.gitignore index c3f6db6..c8a615f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ cmake/ *.flac *.aac *.opus +external/nanovg \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4845363 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/nanovg"] + path = external/nanovg + url = https://github.com/memononen/nanovg.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..a7e045e --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc-14", + "cStandard": "c11", + "cppStandard": "c++20", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b586e5c..2413b2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,9 +57,9 @@ if(stb_ADDED) add_library(stb INTERFACE) target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) endif() - # ── Local dependencies ─────────────────────────────────────────────────────── add_subdirectory(external/glad) +add_subdirectory(external/nanovg) find_package(OpenGL REQUIRED) find_package(X11 REQUIRED) @@ -90,6 +90,7 @@ target_link_libraries(${PROJECT_NAME} Lua::Library sol2 stb + nanovg glad PkgConfig::GST ) diff --git a/external/nanovg b/external/nanovg new file mode 160000 index 0000000..ce3bf74 --- /dev/null +++ b/external/nanovg @@ -0,0 +1 @@ +Subproject commit ce3bf745eb2d2dbc14a50bf2446783f691ac4353 diff --git a/samples/font.otf b/samples/font.otf new file mode 100644 index 0000000..35daee4 Binary files /dev/null and b/samples/font.otf differ diff --git a/samples/test.lua b/samples/test.lua index 682669d..16702d1 100644 --- a/samples/test.lua +++ b/samples/test.lua @@ -1,5 +1,11 @@ effect = nil video = nil +ctx = nil + +timeDisplayText = "" +dateDisplayText = "" + +timer = 0.0 function _create() video = Texture.FromGStreamer("filesrc location=" .. Resolve("video.mkv")) @@ -21,10 +27,28 @@ function _create() } ]] effect = Effect.new(fxSrc) + + ctx = GraphicsContext.new() + + local fontPath = Resolve("font.otf") + ctx:CreateFont("font", fontPath) + + UpdateTimeAndDate() +end + +function UpdateTimeAndDate() + local now = os.date("*t") + timeDisplayText = string.format("%02d:%02d", now.hour, now.min) + dateDisplayText = os.date("%A, %B %d, %Y") end function _update(dt) + timer = timer + dt + if timer >= 0.5 then + timer = 0.0 + UpdateTimeAndDate() + end end function _render() @@ -32,4 +56,23 @@ function _render() effect:Use() effect:SetTexture("uTexture", video, 0) effect:Render() + + ctx:BeginFrame(DesktopSize.x, DesktopSize.y, DesktopSize.x / DesktopSize.y) + + local xPos = 80.0 + local yPos = 120.0 + local timeFontSize = 80 + local dateFontSize = 28 + + ctx:FontSize(timeFontSize) + ctx:FontFace("font") + ctx:FillColor(RGBAf(1, 1, 1, 0.75)) + ctx:TextAlign(Align.RIGHT | Align.BOTTOM) + ctx:Text(Displays[1].x + Displays[1].z - xPos, Displays[1].y + Displays[1].w - yPos, timeDisplayText) + + ctx:FontSize(dateFontSize) + ctx:FillColor(RGBAf(1, 1, 1, 0.45)) + ctx:Text(Displays[1].x + Displays[1].z - xPos, Displays[1].y + Displays[1].w - (yPos + timeFontSize + 8), dateDisplayText) + + ctx:EndFrame() end \ No newline at end of file diff --git a/src/lua.cpp b/src/lua.cpp index c600671..649a4ea 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -8,6 +8,7 @@ #include "framebuffer.h" #include "effect.h" #include "opengl.h" +#include "nanovg_cpp.h" #include "globals.h" static void RegisterTMath(sol::state& lua) { @@ -403,13 +404,234 @@ static void RegisterOpenGL(sol::state& lua) { gl["ONE_MINUS_DST_ALPHA"] = GL_ONE_MINUS_DST_ALPHA; } +#pragma region NanoVG bindings + +static void RegisterNanoVG(sol::state& lua) { + // ── NVGcolor ───────────────────────────────────────────────────────── + lua.new_usertype("Color", + sol::constructors<>(), + "r", &NVGcolor::r, + "g", &NVGcolor::g, + "b", &NVGcolor::b, + "a", &NVGcolor::a, + sol::meta_function::to_string, [](const NVGcolor& c) { + return "Color(" + std::to_string(c.r) + ", " + std::to_string(c.g) + ", " + std::to_string(c.b) + ", " + std::to_string(c.a) + ")"; + } + ); + + // ── NVGpaint ──────────────────────────────────────────────────────── + lua.new_usertype("Paint", + sol::constructors<>(), + "innerColor", &NVGpaint::innerColor, + "outerColor", &NVGpaint::outerColor, + "image", &NVGpaint::image, + "radius", &NVGpaint::radius, + "feather", &NVGpaint::feather + ); + + // ── NVG Enums ─────────────────────────────────────────────────────── + lua.new_enum("Winding", { + { "CCW", NVG_CCW }, + { "CW", NVG_CW } + }); + + lua.new_enum("Solidity", { + { "SOLID", NVG_SOLID }, + { "HOLE", NVG_HOLE } + }); + + lua.new_enum("LineCap", { + { "BUTT", NVG_BUTT }, + { "ROUND", NVG_ROUND }, + { "SQUARE", NVG_SQUARE }, + { "BEVEL", NVG_BEVEL }, + { "MITER", NVG_MITER } + }); + + lua.new_enum("Align", { + { "LEFT", NVG_ALIGN_LEFT }, + { "CENTER", NVG_ALIGN_CENTER }, + { "RIGHT", NVG_ALIGN_RIGHT }, + { "TOP", NVG_ALIGN_TOP }, + { "MIDDLE", NVG_ALIGN_MIDDLE }, + { "BOTTOM", NVG_ALIGN_BOTTOM }, + { "BASELINE", NVG_ALIGN_BASELINE } + }); + + lua.new_enum("CompositeOperation", { + { "SOURCE_OVER", NVG_SOURCE_OVER }, + { "SOURCE_IN", NVG_SOURCE_IN }, + { "SOURCE_OUT", NVG_SOURCE_OUT }, + { "ATOP", NVG_ATOP }, + { "DESTINATION_OVER", NVG_DESTINATION_OVER }, + { "DESTINATION_IN", NVG_DESTINATION_IN }, + { "DESTINATION_OUT", NVG_DESTINATION_OUT }, + { "DESTINATION_ATOP", NVG_DESTINATION_ATOP }, + { "LIGHTER", NVG_LIGHTER }, + { "COPY", NVG_COPY }, + { "XOR", NVG_XOR } + }); + + lua.create_named_table("ImageFlags", + "GENERATE_MIPMAPS", NVG_IMAGE_GENERATE_MIPMAPS, + "REPEATX", NVG_IMAGE_REPEATX, + "REPEATY", NVG_IMAGE_REPEATY, + "FLIPY", NVG_IMAGE_FLIPY, + "PREMULTIPLIED", NVG_IMAGE_PREMULTIPLIED, + "NEAREST", NVG_IMAGE_NEAREST + ); + + // ── GraphicsContext ───────────────────────────────────────────────── + lua.new_usertype("GraphicsContext", + sol::constructors(), + + // Frame management + "BeginFrame", &GraphicsContext::BeginFrame, + "CancelFrame", &GraphicsContext::CancelFrame, + "EndFrame", &GraphicsContext::EndFrame, + + // Composite operations + "GlobalCompositeOperation", &GraphicsContext::GlobalCompositeOperation, + "GlobalCompositeBlendFunc", &GraphicsContext::GlobalCompositeBlendFunc, + "GlobalCompositeBlendFuncSeparate", &GraphicsContext::GlobalCompositeBlendFuncSeparate, + + // State handling + "Save", &GraphicsContext::Save, + "Restore", &GraphicsContext::Restore, + "Reset", &GraphicsContext::Reset, + + // Render styles + "ShapeAntiAlias", &GraphicsContext::ShapeAntiAlias, + "StrokeColor", &GraphicsContext::StrokeColor, + "StrokePaint", &GraphicsContext::StrokePaint, + "FillColor", &GraphicsContext::FillColor, + "FillPaint", &GraphicsContext::FillPaint, + "MiterLimit", &GraphicsContext::MiterLimit, + "StrokeWidth", &GraphicsContext::StrokeWidth, + "LineCap", &GraphicsContext::LineCap, + "LineJoin", &GraphicsContext::LineJoin, + "GlobalAlpha", &GraphicsContext::GlobalAlpha, + + // Transforms + "ResetTransform", &GraphicsContext::ResetTransform, + "Transform", &GraphicsContext::Transform, + "Translate", &GraphicsContext::Translate, + "Rotate", &GraphicsContext::Rotate, + "SkewX", &GraphicsContext::SkewX, + "SkewY", &GraphicsContext::SkewY, + "Scale", &GraphicsContext::Scale, + "DegToRad", &GraphicsContext::DegToRad, + "RadToDeg", &GraphicsContext::RadToDeg, + + // Images + "CreateImage", &GraphicsContext::CreateImage, + "CreateImageRGBA", &GraphicsContext::CreateImageRGBA, + "ImageSize", [](GraphicsContext& g, int image) -> std::pair { + return g.ImageSize(image); + }, + "DeleteImage", &GraphicsContext::DeleteImage, + + // Paints (gradients & patterns) + "LinearGradient", &GraphicsContext::LinearGradient, + "BoxGradient", &GraphicsContext::BoxGradient, + "RadialGradient", &GraphicsContext::RadialGradient, + "ImagePattern", &GraphicsContext::ImagePattern, + + // Scissoring + "Scissor", &GraphicsContext::Scissor, + "IntersectScissor", &GraphicsContext::IntersectScissor, + "ResetScissor", &GraphicsContext::ResetScissor, + + // Paths + "BeginPath", &GraphicsContext::BeginPath, + "MoveTo", &GraphicsContext::MoveTo, + "LineTo", &GraphicsContext::LineTo, + "BezierTo", &GraphicsContext::BezierTo, + "QuadTo", &GraphicsContext::QuadTo, + "ArcTo", &GraphicsContext::ArcTo, + "ClosePath", &GraphicsContext::ClosePath, + "PathWinding", &GraphicsContext::PathWinding, + "Arc", &GraphicsContext::Arc, + "Rect", &GraphicsContext::Rect, + "RoundedRect", &GraphicsContext::RoundedRect, + "RoundedRectVarying", &GraphicsContext::RoundedRectVarying, + "Ellipse", &GraphicsContext::Ellipse, + "Circle", &GraphicsContext::Circle, + "Fill", &GraphicsContext::Fill, + "Stroke", &GraphicsContext::Stroke, + + // Fonts + "CreateFont", &GraphicsContext::CreateFont, + "CreateFontAtIndex", &GraphicsContext::CreateFontAtIndex, + "FindFont", &GraphicsContext::FindFont, + "AddFallbackFontId", &GraphicsContext::AddFallbackFontId, + "AddFallbackFont", &GraphicsContext::AddFallbackFont, + "ResetFallbackFontsId", &GraphicsContext::ResetFallbackFontsId, + "ResetFallbackFonts", &GraphicsContext::ResetFallbackFonts, + + // Text styling + "FontSize", &GraphicsContext::FontSize, + "FontBlur", &GraphicsContext::FontBlur, + "TextLetterSpacing", &GraphicsContext::TextLetterSpacing, + "TextLineHeight", &GraphicsContext::TextLineHeight, + "TextAlign", &GraphicsContext::TextAlign, + "FontFaceId", &GraphicsContext::FontFaceId, + "FontFace", &GraphicsContext::FontFace, + + // Text rendering & measurement + "Text", [](GraphicsContext& g, float x, float y, const std::string& str) -> float { + return g.Text(x, y, str.c_str()); + }, + "TextBox", [](GraphicsContext& g, float x, float y, float breakRowWidth, const std::string& str) { + g.TextBox(x, y, breakRowWidth, str.c_str()); + }, + "TextBounds", [](GraphicsContext& g, float x, float y, const std::string& str) -> std::tuple { + float bounds[4]; + float advance = g.TextBounds(x, y, str.c_str(), nullptr, bounds); + return { advance, bounds[0], bounds[1], bounds[2], bounds[3] }; + }, + "TextBoxBounds", [](GraphicsContext& g, float x, float y, float breakRowWidth, const std::string& str) -> std::tuple { + float bounds[4]; + g.TextBoxBounds(x, y, breakRowWidth, str.c_str(), nullptr, bounds); + return { bounds[0], bounds[1], bounds[2], bounds[3] }; + }, + "TextMetrics", [](GraphicsContext& g) -> std::tuple { + return g.TextMetrics(); + } + ); + + // Loose functions + lua.set_function("RGB", &GraphicsContext::RGB); + lua.set_function("RGBf", &GraphicsContext::RGBf); + lua.set_function("RGBA", &GraphicsContext::RGBA); + lua.set_function("RGBAf", &GraphicsContext::RGBAf); + lua.set_function("LerpRGBA", &GraphicsContext::LerpRGBA); + lua.set_function("TransRGBA", &GraphicsContext::TransRGBA); + lua.set_function("TransRGBAf", &GraphicsContext::TransRGBAf); + lua.set_function("HSL", &GraphicsContext::HSL); + lua.set_function("HSLA", &GraphicsContext::HSLA); + + // Matrices/utils + lua.set_function("DegToRad", &GraphicsContext::DegToRad); + lua.set_function("RadToDeg", &GraphicsContext::RadToDeg); + + lua.set_function("Rotate", &GraphicsContext::Rotate); + lua.set_function("Translate", &GraphicsContext::Translate); + lua.set_function("Scale", &GraphicsContext::Scale); +} + +#pragma endregion + void RegisterLuaBindings(sol::state& lua) { lua.open_libraries( sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table, - sol::lib::package + sol::lib::package, + sol::lib::os, + sol::lib::io, + sol::lib::coroutine ); RegisterTMath(lua); @@ -417,6 +639,7 @@ void RegisterLuaBindings(sol::state& lua) { RegisterFrameBuffer(lua); RegisterEffect(lua); RegisterOpenGL(lua); + RegisterNanoVG(lua); // Globals lua["DesktopSize"] = g_DesktopSize; diff --git a/src/nanovg_cpp.cpp b/src/nanovg_cpp.cpp new file mode 100644 index 0000000..b7de398 --- /dev/null +++ b/src/nanovg_cpp.cpp @@ -0,0 +1,171 @@ +#include "nanovg_cpp.h" + +GraphicsContext::GraphicsContext() +{ + ctx = nvgCreateGL46(NVG_ANTIALIAS | NVG_STENCIL_STROKES); +} + +GraphicsContext::~GraphicsContext() +{ + if (ctx) { + nvgDeleteGL46(ctx); + ctx = nullptr; + } +} + +// Frame management + +void GraphicsContext::BeginFrame(float width, float height, float devicePixelRatio) +{ + nvgBeginFrame(ctx, width, height, devicePixelRatio); +} + +void GraphicsContext::CancelFrame() { nvgCancelFrame(ctx); } + +void GraphicsContext::EndFrame() +{ + nvgEndFrame(ctx); + glBindVertexArray(0); + glDisable(GL_STENCIL_TEST); + glDisable(GL_CULL_FACE); + glUseProgram(0); +} + +// Composite operations + +void GraphicsContext::GlobalCompositeOperation(int op) { nvgGlobalCompositeOperation(ctx, op); } +void GraphicsContext::GlobalCompositeBlendFunc(int sfactor, int dfactor) { nvgGlobalCompositeBlendFunc(ctx, sfactor, dfactor); } +void GraphicsContext::GlobalCompositeBlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) { nvgGlobalCompositeBlendFuncSeparate(ctx, srcRGB, dstRGB, srcAlpha, dstAlpha); } + +// Color utilities + +NVGcolor GraphicsContext::RGB(unsigned char r, unsigned char g, unsigned char b) { return nvgRGB(r, g, b); } +NVGcolor GraphicsContext::RGBf(float r, float g, float b) { return nvgRGBf(r, g, b); } +NVGcolor GraphicsContext::RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { return nvgRGBA(r, g, b, a); } +NVGcolor GraphicsContext::RGBAf(float r, float g, float b, float a) { return nvgRGBAf(r, g, b, a); } +NVGcolor GraphicsContext::LerpRGBA(NVGcolor c0, NVGcolor c1, float u) { return nvgLerpRGBA(c0, c1, u); } +NVGcolor GraphicsContext::TransRGBA(NVGcolor c0, unsigned char a) { return nvgTransRGBA(c0, a); } +NVGcolor GraphicsContext::TransRGBAf(NVGcolor c0, float a) { return nvgTransRGBAf(c0, a); } +NVGcolor GraphicsContext::HSL(float h, float s, float l) { return nvgHSL(h, s, l); } +NVGcolor GraphicsContext::HSLA(float h, float s, float l, unsigned char a) { return nvgHSLA(h, s, l, a); } + +// State handling + +void GraphicsContext::Save() { nvgSave(ctx); } +void GraphicsContext::Restore() { nvgRestore(ctx); } +void GraphicsContext::Reset() { nvgReset(ctx); } + +// Render styles + +void GraphicsContext::ShapeAntiAlias(int enabled) { nvgShapeAntiAlias(ctx, enabled); } +void GraphicsContext::StrokeColor(NVGcolor color) { nvgStrokeColor(ctx, color); } +void GraphicsContext::StrokePaint(NVGpaint paint) { nvgStrokePaint(ctx, paint); } +void GraphicsContext::FillColor(NVGcolor color) { nvgFillColor(ctx, color); } +void GraphicsContext::FillPaint(NVGpaint paint) { nvgFillPaint(ctx, paint); } +void GraphicsContext::MiterLimit(float limit) { nvgMiterLimit(ctx, limit); } +void GraphicsContext::StrokeWidth(float size) { nvgStrokeWidth(ctx, size); } +void GraphicsContext::LineCap(int cap) { nvgLineCap(ctx, cap); } +void GraphicsContext::LineJoin(int join) { nvgLineJoin(ctx, join); } +void GraphicsContext::GlobalAlpha(float alpha) { nvgGlobalAlpha(ctx, alpha); } + +// Transforms + +void GraphicsContext::ResetTransform() { nvgResetTransform(ctx); } +void GraphicsContext::Transform(float a, float b, float c, float d, float e, float f) { nvgTransform(ctx, a, b, c, d, e, f); } +void GraphicsContext::Translate(float x, float y) { nvgTranslate(ctx, x, y); } +void GraphicsContext::Rotate(float angle) { nvgRotate(ctx, angle); } +void GraphicsContext::SkewX(float angle) { nvgSkewX(ctx, angle); } +void GraphicsContext::SkewY(float angle) { nvgSkewY(ctx, angle); } +void GraphicsContext::Scale(float x, float y) { nvgScale(ctx, x, y); } +void GraphicsContext::CurrentTransform(float* xform) { nvgCurrentTransform(ctx, xform); } + +// Transform matrix utilities + +void GraphicsContext::TransformIdentity(float* dst) { nvgTransformIdentity(dst); } +void GraphicsContext::TransformTranslate(float* dst, float tx, float ty) { nvgTransformTranslate(dst, tx, ty); } +void GraphicsContext::TransformScale(float* dst, float sx, float sy) { nvgTransformScale(dst, sx, sy); } +void GraphicsContext::TransformRotate(float* dst, float a) { nvgTransformRotate(dst, a); } +void GraphicsContext::TransformSkewX(float* dst, float a) { nvgTransformSkewX(dst, a); } +void GraphicsContext::TransformSkewY(float* dst, float a) { nvgTransformSkewY(dst, a); } +void GraphicsContext::TransformMultiply(float* dst, const float* src) { nvgTransformMultiply(dst, src); } +void GraphicsContext::TransformPremultiply(float* dst, const float* src) { nvgTransformPremultiply(dst, src); } +int GraphicsContext::TransformInverse(float* dst, const float* src) { return nvgTransformInverse(dst, src); } +void GraphicsContext::TransformPoint(float* dstx, float* dsty, const float* xform, float srcx, float srcy) { nvgTransformPoint(dstx, dsty, xform, srcx, srcy); } + +float GraphicsContext::DegToRad(float deg) { return nvgDegToRad(deg); } +float GraphicsContext::RadToDeg(float rad) { return nvgRadToDeg(rad); } + +// Images + +int GraphicsContext::CreateImage(const char* filename, int imageFlags) { return nvgCreateImage(ctx, filename, imageFlags); } +int GraphicsContext::CreateImageMem(int imageFlags, unsigned char* data, int ndata) { return nvgCreateImageMem(ctx, imageFlags, data, ndata); } +int GraphicsContext::CreateImageRGBA(int w, int h, int imageFlags, const unsigned char* data) { return nvgCreateImageRGBA(ctx, w, h, imageFlags, data); } +void GraphicsContext::UpdateImage(int image, const unsigned char* data) { nvgUpdateImage(ctx, image, data); } +void GraphicsContext::ImageSize(int image, int* w, int* h) { nvgImageSize(ctx, image, w, h); } +std::pair GraphicsContext::ImageSize(int image) { int w, h; nvgImageSize(ctx, image, &w, &h); return {w, h}; } +void GraphicsContext::DeleteImage(int image) { nvgDeleteImage(ctx, image); } + +// Paints (gradients & patterns) + +NVGpaint GraphicsContext::LinearGradient(float sx, float sy, float ex, float ey, NVGcolor icol, NVGcolor ocol) { return nvgLinearGradient(ctx, sx, sy, ex, ey, icol, ocol); } +NVGpaint GraphicsContext::BoxGradient(float x, float y, float w, float h, float r, float f, NVGcolor icol, NVGcolor ocol) { return nvgBoxGradient(ctx, x, y, w, h, r, f, icol, ocol); } +NVGpaint GraphicsContext::RadialGradient(float cx, float cy, float inr, float outr, NVGcolor icol, NVGcolor ocol) { return nvgRadialGradient(ctx, cx, cy, inr, outr, icol, ocol); } +NVGpaint GraphicsContext::ImagePattern(float ox, float oy, float ex, float ey, float angle, int image, float alpha) { return nvgImagePattern(ctx, ox, oy, ex, ey, angle, image, alpha); } + +// Scissoring + +void GraphicsContext::Scissor(float x, float y, float w, float h) { nvgScissor(ctx, x, y, w, h); } +void GraphicsContext::IntersectScissor(float x, float y, float w, float h) { nvgIntersectScissor(ctx, x, y, w, h); } +void GraphicsContext::ResetScissor() { nvgResetScissor(ctx); } + +// Paths + +void GraphicsContext::BeginPath() { nvgBeginPath(ctx); } +void GraphicsContext::MoveTo(float x, float y) { nvgMoveTo(ctx, x, y); } +void GraphicsContext::LineTo(float x, float y) { nvgLineTo(ctx, x, y); } +void GraphicsContext::BezierTo(float c1x, float c1y, float c2x, float c2y, float x, float y) { nvgBezierTo(ctx, c1x, c1y, c2x, c2y, x, y); } +void GraphicsContext::QuadTo(float cx, float cy, float x, float y) { nvgQuadTo(ctx, cx, cy, x, y); } +void GraphicsContext::ArcTo(float x1, float y1, float x2, float y2, float radius) { nvgArcTo(ctx, x1, y1, x2, y2, radius); } +void GraphicsContext::ClosePath() { nvgClosePath(ctx); } +void GraphicsContext::PathWinding(int dir) { nvgPathWinding(ctx, dir); } +void GraphicsContext::Arc(float cx, float cy, float r, float a0, float a1, int dir) { nvgArc(ctx, cx, cy, r, a0, a1, dir); } +void GraphicsContext::Rect(float x, float y, float w, float h) { nvgRect(ctx, x, y, w, h); } +void GraphicsContext::RoundedRect(float x, float y, float w, float h, float r) { nvgRoundedRect(ctx, x, y, w, h, r); } +void GraphicsContext::RoundedRectVarying(float x, float y, float w, float h, float radTopLeft, float radTopRight, float radBottomRight, float radBottomLeft) { nvgRoundedRectVarying(ctx, x, y, w, h, radTopLeft, radTopRight, radBottomRight, radBottomLeft); } +void GraphicsContext::Ellipse(float cx, float cy, float rx, float ry) { nvgEllipse(ctx, cx, cy, rx, ry); } +void GraphicsContext::Circle(float cx, float cy, float r) { nvgCircle(ctx, cx, cy, r); } +void GraphicsContext::Fill() { nvgFill(ctx); } +void GraphicsContext::Stroke() { nvgStroke(ctx); } + +// Fonts + +int GraphicsContext::CreateFont(const char* name, const char* filename) { return nvgCreateFont(ctx, name, filename); } +int GraphicsContext::CreateFontAtIndex(const char* name, const char* filename, int fontIndex) { return nvgCreateFontAtIndex(ctx, name, filename, fontIndex); } +int GraphicsContext::CreateFontMem(const char* name, unsigned char* data, int ndata, int freeData) { return nvgCreateFontMem(ctx, name, data, ndata, freeData); } +int GraphicsContext::CreateFontMemAtIndex(const char* name, unsigned char* data, int ndata, int freeData, int fontIndex) { return nvgCreateFontMemAtIndex(ctx, name, data, ndata, freeData, fontIndex); } +int GraphicsContext::FindFont(const char* name) { return nvgFindFont(ctx, name); } +int GraphicsContext::AddFallbackFontId(int baseFont, int fallbackFont) { return nvgAddFallbackFontId(ctx, baseFont, fallbackFont); } +int GraphicsContext::AddFallbackFont(const char* baseFont, const char* fallbackFont) { return nvgAddFallbackFont(ctx, baseFont, fallbackFont); } +void GraphicsContext::ResetFallbackFontsId(int baseFont) { nvgResetFallbackFontsId(ctx, baseFont); } +void GraphicsContext::ResetFallbackFonts(const char* baseFont) { nvgResetFallbackFonts(ctx, baseFont); } + +// Text styling + +void GraphicsContext::FontSize(float size) { nvgFontSize(ctx, size); } +void GraphicsContext::FontBlur(float blur) { nvgFontBlur(ctx, blur); } +void GraphicsContext::TextLetterSpacing(float spacing) { nvgTextLetterSpacing(ctx, spacing); } +void GraphicsContext::TextLineHeight(float lineHeight) { nvgTextLineHeight(ctx, lineHeight); } +void GraphicsContext::TextAlign(int align) { nvgTextAlign(ctx, align); } +void GraphicsContext::FontFaceId(int font) { nvgFontFaceId(ctx, font); } +void GraphicsContext::FontFace(const char* font) { nvgFontFace(ctx, font); } + +// Text rendering & measurement + +float GraphicsContext::Text(float x, float y, const char* string, const char* end) { return nvgText(ctx, x, y, string, end); } +void GraphicsContext::TextBox(float x, float y, float breakRowWidth, const char* string, const char* end) { nvgTextBox(ctx, x, y, breakRowWidth, string, end); } +float GraphicsContext::TextBounds(float x, float y, const char* string, const char* end, float* bounds) { return nvgTextBounds(ctx, x, y, string, end, bounds); } +void GraphicsContext::TextBoxBounds(float x, float y, float breakRowWidth, const char* string, const char* end, float* bounds) { nvgTextBoxBounds(ctx, x, y, breakRowWidth, string, end, bounds); } +int GraphicsContext::TextGlyphPositions(float x, float y, const char* string, const char* end, NVGglyphPosition* positions, int maxPositions) { return nvgTextGlyphPositions(ctx, x, y, string, end, positions, maxPositions); } +void GraphicsContext::TextMetrics(float* ascender, float* descender, float* lineh) { nvgTextMetrics(ctx, ascender, descender, lineh); } +std::tuple GraphicsContext::TextMetrics() { float a, d, l; nvgTextMetrics(ctx, &a, &d, &l); return {a, d, l}; } +int GraphicsContext::TextBreakLines(const char* string, const char* end, float breakRowWidth, NVGtextRow* rows, int maxRows) { return nvgTextBreakLines(ctx, string, end, breakRowWidth, rows, maxRows); } diff --git a/src/nanovg_cpp.h b/src/nanovg_cpp.h new file mode 100644 index 0000000..67af1ad --- /dev/null +++ b/src/nanovg_cpp.h @@ -0,0 +1,149 @@ +#pragma once + +#include "nanovg.h" +#include "nanovg_gl46.h" + +#include +#include + +class GraphicsContext { +public: + GraphicsContext(); + ~GraphicsContext(); + + NVGcontext* GetNVG() const { return ctx; } + + // Frame management + void BeginFrame(float width, float height, float devicePixelRatio); + void CancelFrame(); + void EndFrame(); + + // Composite operations + void GlobalCompositeOperation(int op); + void GlobalCompositeBlendFunc(int sfactor, int dfactor); + void GlobalCompositeBlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha); + + // Color utilities (static) + static NVGcolor RGB(unsigned char r, unsigned char g, unsigned char b); + static NVGcolor RGBf(float r, float g, float b); + static NVGcolor RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + static NVGcolor RGBAf(float r, float g, float b, float a); + static NVGcolor LerpRGBA(NVGcolor c0, NVGcolor c1, float u); + static NVGcolor TransRGBA(NVGcolor c0, unsigned char a); + static NVGcolor TransRGBAf(NVGcolor c0, float a); + static NVGcolor HSL(float h, float s, float l); + static NVGcolor HSLA(float h, float s, float l, unsigned char a); + + // State handling + void Save(); + void Restore(); + void Reset(); + + // Render styles + void ShapeAntiAlias(int enabled); + void StrokeColor(NVGcolor color); + void StrokePaint(NVGpaint paint); + void FillColor(NVGcolor color); + void FillPaint(NVGpaint paint); + void MiterLimit(float limit); + void StrokeWidth(float size); + void LineCap(int cap); + void LineJoin(int join); + void GlobalAlpha(float alpha); + + // Transforms + void ResetTransform(); + void Transform(float a, float b, float c, float d, float e, float f); + void Translate(float x, float y); + void Rotate(float angle); + void SkewX(float angle); + void SkewY(float angle); + void Scale(float x, float y); + void CurrentTransform(float* xform); + + // Transform matrix utilities (static) + static void TransformIdentity(float* dst); + static void TransformTranslate(float* dst, float tx, float ty); + static void TransformScale(float* dst, float sx, float sy); + static void TransformRotate(float* dst, float a); + static void TransformSkewX(float* dst, float a); + static void TransformSkewY(float* dst, float a); + static void TransformMultiply(float* dst, const float* src); + static void TransformPremultiply(float* dst, const float* src); + static int TransformInverse(float* dst, const float* src); + static void TransformPoint(float* dstx, float* dsty, const float* xform, float srcx, float srcy); + + static float DegToRad(float deg); + static float RadToDeg(float rad); + + // Images + int CreateImage(const char* filename, int imageFlags); + int CreateImageMem(int imageFlags, unsigned char* data, int ndata); + int CreateImageRGBA(int w, int h, int imageFlags, const unsigned char* data); + void UpdateImage(int image, const unsigned char* data); + void ImageSize(int image, int* w, int* h); + std::pair ImageSize(int image); + void DeleteImage(int image); + + // Paints (gradients & patterns) + NVGpaint LinearGradient(float sx, float sy, float ex, float ey, NVGcolor icol, NVGcolor ocol); + NVGpaint BoxGradient(float x, float y, float w, float h, float r, float f, NVGcolor icol, NVGcolor ocol); + NVGpaint RadialGradient(float cx, float cy, float inr, float outr, NVGcolor icol, NVGcolor ocol); + NVGpaint ImagePattern(float ox, float oy, float ex, float ey, float angle, int image, float alpha); + + // Scissoring + void Scissor(float x, float y, float w, float h); + void IntersectScissor(float x, float y, float w, float h); + void ResetScissor(); + + // Paths + void BeginPath(); + void MoveTo(float x, float y); + void LineTo(float x, float y); + void BezierTo(float c1x, float c1y, float c2x, float c2y, float x, float y); + void QuadTo(float cx, float cy, float x, float y); + void ArcTo(float x1, float y1, float x2, float y2, float radius); + void ClosePath(); + void PathWinding(int dir); + void Arc(float cx, float cy, float r, float a0, float a1, int dir); + void Rect(float x, float y, float w, float h); + void RoundedRect(float x, float y, float w, float h, float r); + void RoundedRectVarying(float x, float y, float w, float h, float radTopLeft, float radTopRight, float radBottomRight, float radBottomLeft); + void Ellipse(float cx, float cy, float rx, float ry); + void Circle(float cx, float cy, float r); + void Fill(); + void Stroke(); + + // Fonts + int CreateFont(const char* name, const char* filename); + int CreateFontAtIndex(const char* name, const char* filename, int fontIndex); + int CreateFontMem(const char* name, unsigned char* data, int ndata, int freeData); + int CreateFontMemAtIndex(const char* name, unsigned char* data, int ndata, int freeData, int fontIndex); + int FindFont(const char* name); + int AddFallbackFontId(int baseFont, int fallbackFont); + int AddFallbackFont(const char* baseFont, const char* fallbackFont); + void ResetFallbackFontsId(int baseFont); + void ResetFallbackFonts(const char* baseFont); + + // Text styling + void FontSize(float size); + void FontBlur(float blur); + void TextLetterSpacing(float spacing); + void TextLineHeight(float lineHeight); + void TextAlign(int align); + void FontFaceId(int font); + void FontFace(const char* font); + + // Text rendering & measurement + float Text(float x, float y, const char* string, const char* end = nullptr); + void TextBox(float x, float y, float breakRowWidth, const char* string, const char* end = nullptr); + float TextBounds(float x, float y, const char* string, const char* end, float* bounds); + void TextBoxBounds(float x, float y, float breakRowWidth, const char* string, const char* end, float* bounds); + int TextGlyphPositions(float x, float y, const char* string, const char* end, NVGglyphPosition* positions, int maxPositions); + void TextMetrics(float* ascender, float* descender, float* lineh); + std::tuple TextMetrics(); + int TextBreakLines(const char* string, const char* end, float breakRowWidth, NVGtextRow* rows, int maxRows); + +private: + NVGcontext* ctx = nullptr; +}; diff --git a/src/nanovg_gl46.cpp b/src/nanovg_gl46.cpp new file mode 100644 index 0000000..5d03df6 --- /dev/null +++ b/src/nanovg_gl46.cpp @@ -0,0 +1,1208 @@ +// +// NanoVG OpenGL 4.6 Core Profile backend — implementation +// +// Based on nanovg_gl.h by Mikko Mononen (Copyright (c) 2009-2013) +// Adapted to use OpenGL 4.6 Direct State Access (DSA) functions, +// immutable texture storage, explicit layout qualifiers in GLSL 460, +// and named buffer/vertex-array operations. +// +#include "nanovg_gl46.h" +#include "nanovg.h" + +#include +#include +#include +#include + +// ── Constants ─────────────────────────────────────────────────────────────── + +enum GLNVGshaderType { + NSVG_SHADER_FILLGRAD, + NSVG_SHADER_FILLIMG, + NSVG_SHADER_SIMPLE, + NSVG_SHADER_IMG +}; + +enum GLNVGuniformBindings { + GLNVG_FRAG_BINDING = 0, +}; + +enum GLNVGcallType { + GLNVG_NONE = 0, + GLNVG_FILL, + GLNVG_CONVEXFILL, + GLNVG_STROKE, + GLNVG_TRIANGLES, +}; + +// viewSize uniform is at explicit location 0 in the shader +static constexpr GLint LOC_VIEWSIZE = 0; + +// ── Internal types ────────────────────────────────────────────────────────── + +struct GLNVGshader { + GLuint prog; + GLuint frag; + GLuint vert; +}; + +struct GLNVGtexture { + int id; + GLuint tex; + int width, height; + int type; + int flags; +}; + +struct GLNVGblend { + GLenum srcRGB; + GLenum dstRGB; + GLenum srcAlpha; + GLenum dstAlpha; +}; + +struct GLNVGcall { + int type; + int image; + int pathOffset; + int pathCount; + int triangleOffset; + int triangleCount; + int uniformOffset; + GLNVGblend blendFunc; +}; + +struct GLNVGpath { + int fillOffset; + int fillCount; + int strokeOffset; + int strokeCount; +}; + +struct GLNVGfragUniforms { + float scissorMat[12]; // mat3 stored as 3 vec4s (std140) + float paintMat[12]; + NVGcolor innerCol; + NVGcolor outerCol; + float scissorExt[2]; + float scissorScale[2]; + float extent[2]; + float radius; + float feather; + float strokeMult; + float strokeThr; + int texType; + int type; +}; + +struct GLNVGcontext { + GLNVGshader shader; + GLNVGtexture* textures; + float view[2]; + int ntextures; + int ctextures; + int textureId; + GLuint vertBuf; + GLuint vertArr; + GLuint fragBuf; + int fragSize; + int flags; + + // Per-frame buffers + GLNVGcall* calls; + int ccalls; + int ncalls; + GLNVGpath* paths; + int cpaths; + int npaths; + NVGvertex* verts; + int cverts; + int nverts; + unsigned char* uniforms; + int cuniforms; + int nuniforms; + + // Cached state + GLuint boundTexture; + GLuint stencilMask; + GLenum stencilFunc; + GLint stencilFuncRef; + GLuint stencilFuncMask; + GLNVGblend blendFunc; + + int dummyTex; +}; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +static int glnvg__maxi(int a, int b) { return a > b ? a : b; } + +static void glnvg__bindTexture(GLNVGcontext* gl, GLuint tex) +{ + if (gl->boundTexture != tex) { + gl->boundTexture = tex; + glBindTextureUnit(0, tex); + } +} + +static void glnvg__stencilMask(GLNVGcontext* gl, GLuint mask) +{ + if (gl->stencilMask != mask) { + gl->stencilMask = mask; + glStencilMask(mask); + } +} + +static void glnvg__stencilFunc(GLNVGcontext* gl, GLenum func, GLint ref, GLuint mask) +{ + if ((gl->stencilFunc != func) || + (gl->stencilFuncRef != ref) || + (gl->stencilFuncMask != mask)) { + gl->stencilFunc = func; + gl->stencilFuncRef = ref; + gl->stencilFuncMask = mask; + glStencilFunc(func, ref, mask); + } +} + +static void glnvg__blendFuncSeparate(GLNVGcontext* gl, const GLNVGblend* blend) +{ + if ((gl->blendFunc.srcRGB != blend->srcRGB) || + (gl->blendFunc.dstRGB != blend->dstRGB) || + (gl->blendFunc.srcAlpha != blend->srcAlpha) || + (gl->blendFunc.dstAlpha != blend->dstAlpha)) { + gl->blendFunc = *blend; + glBlendFuncSeparate(blend->srcRGB, blend->dstRGB, blend->srcAlpha, blend->dstAlpha); + } +} + +// ── Texture management ────────────────────────────────────────────────────── + +static GLNVGtexture* glnvg__allocTexture(GLNVGcontext* gl) +{ + GLNVGtexture* tex = nullptr; + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].id == 0) { + tex = &gl->textures[i]; + break; + } + } + if (tex == nullptr) { + if (gl->ntextures + 1 > gl->ctextures) { + int ctextures = glnvg__maxi(gl->ntextures + 1, 4) + gl->ctextures / 2; + GLNVGtexture* textures = (GLNVGtexture*)realloc(gl->textures, sizeof(GLNVGtexture) * ctextures); + if (textures == nullptr) return nullptr; + gl->textures = textures; + gl->ctextures = ctextures; + } + tex = &gl->textures[gl->ntextures++]; + } + memset(tex, 0, sizeof(*tex)); + tex->id = ++gl->textureId; + return tex; +} + +static GLNVGtexture* glnvg__findTexture(GLNVGcontext* gl, int id) +{ + for (int i = 0; i < gl->ntextures; i++) + if (gl->textures[i].id == id) + return &gl->textures[i]; + return nullptr; +} + +static int glnvg__deleteTexture(GLNVGcontext* gl, int id) +{ + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].id == id) { + if (gl->textures[i].tex != 0 && (gl->textures[i].flags & NVG_IMAGE_NODELETE) == 0) + glDeleteTextures(1, &gl->textures[i].tex); + memset(&gl->textures[i], 0, sizeof(gl->textures[i])); + return 1; + } + } + return 0; +} + +// ── Shader ────────────────────────────────────────────────────────────────── + +static void glnvg__dumpShaderError(GLuint shader, const char* name, const char* type) +{ + GLchar str[512 + 1]; + GLsizei len = 0; + glGetShaderInfoLog(shader, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + printf("Shader %s/%s error:\n%s\n", name, type, str); +} + +static void glnvg__dumpProgramError(GLuint prog, const char* name) +{ + GLchar str[512 + 1]; + GLsizei len = 0; + glGetProgramInfoLog(prog, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + printf("Program %s error:\n%s\n", name, str); +} + +static void glnvg__checkError(GLNVGcontext* gl, const char* str) +{ + if ((gl->flags & NVG_DEBUG) == 0) return; + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + printf("Error %08x after %s\n", err, str); + } +} + +static int glnvg__createShader(GLNVGshader* shader, const char* name, + const char* header, const char* opts, + const char* vshader, const char* fshader) +{ + GLint status; + const char* str[3]; + str[0] = header; + str[1] = opts != nullptr ? opts : ""; + + memset(shader, 0, sizeof(*shader)); + + GLuint prog = glCreateProgram(); + GLuint vert = glCreateShader(GL_VERTEX_SHADER); + GLuint frag = glCreateShader(GL_FRAGMENT_SHADER); + + str[2] = vshader; + glShaderSource(vert, 3, str, 0); + str[2] = fshader; + glShaderSource(frag, 3, str, 0); + + glCompileShader(vert); + glGetShaderiv(vert, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpShaderError(vert, name, "vert"); + return 0; + } + + glCompileShader(frag); + glGetShaderiv(frag, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpShaderError(frag, name, "frag"); + return 0; + } + + glAttachShader(prog, vert); + glAttachShader(prog, frag); + + glLinkProgram(prog); + glGetProgramiv(prog, GL_LINK_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpProgramError(prog, name); + return 0; + } + + shader->prog = prog; + shader->vert = vert; + shader->frag = frag; + return 1; +} + +static void glnvg__deleteShader(GLNVGshader* shader) +{ + if (shader->prog != 0) glDeleteProgram(shader->prog); + if (shader->vert != 0) glDeleteShader(shader->vert); + if (shader->frag != 0) glDeleteShader(shader->frag); +} + +// ── Render callbacks ──────────────────────────────────────────────────────── + +static int glnvg__renderCreateTexture(void* uptr, int type, int w, int h, int imageFlags, const unsigned char* data); + +static int glnvg__renderCreate(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + int align = 4; + + static const char* shaderHeader = + "#version 460 core\n" + "\n"; + + static const char* fillVertShader = + "layout(location = 0) in vec2 vertex;\n" + "layout(location = 1) in vec2 tcoord;\n" + "layout(location = 0) uniform vec2 viewSize;\n" + "out vec2 ftcoord;\n" + "out vec2 fpos;\n" + "void main(void) {\n" + " ftcoord = tcoord;\n" + " fpos = vertex;\n" + " gl_Position = vec4(2.0*vertex.x/viewSize.x - 1.0, 1.0 - 2.0*vertex.y/viewSize.y, 0, 1);\n" + "}\n"; + + static const char* fillFragShader = + "layout(std140, binding = 0) uniform frag {\n" + " mat3 scissorMat;\n" + " mat3 paintMat;\n" + " vec4 innerCol;\n" + " vec4 outerCol;\n" + " vec2 scissorExt;\n" + " vec2 scissorScale;\n" + " vec2 extent;\n" + " float radius;\n" + " float feather;\n" + " float strokeMult;\n" + " float strokeThr;\n" + " int texType;\n" + " int type;\n" + "};\n" + "layout(binding = 0) uniform sampler2D tex;\n" + "in vec2 ftcoord;\n" + "in vec2 fpos;\n" + "out vec4 outColor;\n" + "\n" + "float sdroundrect(vec2 pt, vec2 ext, float rad) {\n" + " vec2 ext2 = ext - vec2(rad,rad);\n" + " vec2 d = abs(pt) - ext2;\n" + " return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad;\n" + "}\n" + "\n" + "float scissorMask(vec2 p) {\n" + " vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);\n" + " sc = vec2(0.5,0.5) - sc * scissorScale;\n" + " return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);\n" + "}\n" + "#ifdef EDGE_AA\n" + "float strokeMask() {\n" + " return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);\n" + "}\n" + "#endif\n" + "\n" + "void main(void) {\n" + " vec4 result;\n" + " float scissor = scissorMask(fpos);\n" + "#ifdef EDGE_AA\n" + " float strokeAlpha = strokeMask();\n" + " if (strokeAlpha < strokeThr) discard;\n" + "#else\n" + " float strokeAlpha = 1.0;\n" + "#endif\n" + " if (type == 0) {\n" + " vec2 pt = (paintMat * vec3(fpos,1.0)).xy;\n" + " float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);\n" + " vec4 color = mix(innerCol,outerCol,d);\n" + " color *= strokeAlpha * scissor;\n" + " result = color;\n" + " } else if (type == 1) {\n" + " vec2 pt = (paintMat * vec3(fpos,1.0)).xy / extent;\n" + " vec4 color = texture(tex, pt);\n" + " if (texType == 1) color = vec4(color.xyz*color.w,color.w);\n" + " if (texType == 2) color = vec4(color.x);\n" + " color *= innerCol;\n" + " color *= strokeAlpha * scissor;\n" + " result = color;\n" + " } else if (type == 2) {\n" + " result = vec4(1,1,1,1);\n" + " } else if (type == 3) {\n" + " vec4 color = texture(tex, ftcoord);\n" + " if (texType == 1) color = vec4(color.xyz*color.w,color.w);\n" + " if (texType == 2) color = vec4(color.x);\n" + " color *= scissor;\n" + " result = color * innerCol;\n" + " }\n" + " outColor = result;\n" + "}\n"; + + glnvg__checkError(gl, "init"); + + if (gl->flags & NVG_ANTIALIAS) { + if (glnvg__createShader(&gl->shader, "shader", shaderHeader, "#define EDGE_AA 1\n", fillVertShader, fillFragShader) == 0) + return 0; + } else { + if (glnvg__createShader(&gl->shader, "shader", shaderHeader, nullptr, fillVertShader, fillFragShader) == 0) + return 0; + } + + glnvg__checkError(gl, "uniform locations"); + + // Create VAO with DSA + glCreateVertexArrays(1, &gl->vertArr); + glCreateBuffers(1, &gl->vertBuf); + + // Set up vertex format (persistent — does not change per frame) + glEnableVertexArrayAttrib(gl->vertArr, 0); + glEnableVertexArrayAttrib(gl->vertArr, 1); + glVertexArrayAttribFormat(gl->vertArr, 0, 2, GL_FLOAT, GL_FALSE, 0); + glVertexArrayAttribFormat(gl->vertArr, 1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float)); + glVertexArrayAttribBinding(gl->vertArr, 0, 0); + glVertexArrayAttribBinding(gl->vertArr, 1, 0); + + // Create UBO + glCreateBuffers(1, &gl->fragBuf); + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align); + gl->fragSize = sizeof(GLNVGfragUniforms) + align - sizeof(GLNVGfragUniforms) % align; + + // Dummy texture so sampling never hits an unbound unit + gl->dummyTex = glnvg__renderCreateTexture(gl, NVG_TEXTURE_ALPHA, 1, 1, 0, nullptr); + + glnvg__checkError(gl, "create done"); + + glFinish(); + return 1; +} + +static int glnvg__renderCreateTexture(void* uptr, int type, int w, int h, int imageFlags, const unsigned char* data) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__allocTexture(gl); + if (tex == nullptr) return 0; + + // Calculate mip levels + int levels = 1; + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) + levels = (int)floor(log2(fmax(w, h))) + 1; + + GLenum internalFormat = (type == NVG_TEXTURE_RGBA) ? GL_RGBA8 : GL_R8; + + // DSA texture creation with immutable storage + glCreateTextures(GL_TEXTURE_2D, 1, &tex->tex); + glTextureStorage2D(tex->tex, levels, internalFormat, w, h); + + tex->width = w; + tex->height = h; + tex->type = type; + tex->flags = imageFlags; + + // Upload initial data if provided + if (data != nullptr) { + GLenum format = (type == NVG_TEXTURE_RGBA) ? GL_RGBA : GL_RED; + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, tex->width); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glTextureSubImage2D(tex->tex, 0, 0, 0, w, h, format, GL_UNSIGNED_BYTE, data); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } + + // Filter + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) { + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + } else { + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + } + + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Wrap + glTextureParameteri(tex->tex, GL_TEXTURE_WRAP_S, + (imageFlags & NVG_IMAGE_REPEATX) ? GL_REPEAT : GL_CLAMP_TO_EDGE); + glTextureParameteri(tex->tex, GL_TEXTURE_WRAP_T, + (imageFlags & NVG_IMAGE_REPEATY) ? GL_REPEAT : GL_CLAMP_TO_EDGE); + + // Generate mipmaps + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) + glGenerateTextureMipmap(tex->tex); + + glnvg__checkError(gl, "create tex"); + return tex->id; +} + +static int glnvg__renderDeleteTexture(void* uptr, int image) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + return glnvg__deleteTexture(gl, image); +} + +static int glnvg__renderUpdateTexture(void* uptr, int image, int x, int y, int w, int h, const unsigned char* data) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + if (tex == nullptr) return 0; + + GLenum format = (tex->type == NVG_TEXTURE_RGBA) ? GL_RGBA : GL_RED; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, tex->width); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, x); + glPixelStorei(GL_UNPACK_SKIP_ROWS, y); + + // DSA sub-image update — no need to bind the texture + glTextureSubImage2D(tex->tex, 0, x, y, w, h, format, GL_UNSIGNED_BYTE, data); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + + return 1; +} + +static int glnvg__renderGetTextureSize(void* uptr, int image, int* w, int* h) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + if (tex == nullptr) return 0; + *w = tex->width; + *h = tex->height; + return 1; +} + +// ── Paint / uniform conversion ────────────────────────────────────────────── + +static void glnvg__xformToMat3x4(float* m3, float* t) +{ + m3[ 0] = t[0]; m3[ 1] = t[1]; m3[ 2] = 0.0f; m3[ 3] = 0.0f; + m3[ 4] = t[2]; m3[ 5] = t[3]; m3[ 6] = 0.0f; m3[ 7] = 0.0f; + m3[ 8] = t[4]; m3[ 9] = t[5]; m3[10] = 1.0f; m3[11] = 0.0f; +} + +static NVGcolor glnvg__premulColor(NVGcolor c) +{ + c.r *= c.a; + c.g *= c.a; + c.b *= c.a; + return c; +} + +static int glnvg__convertPaint(GLNVGcontext* gl, GLNVGfragUniforms* frag, NVGpaint* paint, + NVGscissor* scissor, float width, float fringe, float strokeThr) +{ + GLNVGtexture* tex = nullptr; + float invxform[6]; + + memset(frag, 0, sizeof(*frag)); + + frag->innerCol = glnvg__premulColor(paint->innerColor); + frag->outerCol = glnvg__premulColor(paint->outerColor); + + if (scissor->extent[0] < -0.5f || scissor->extent[1] < -0.5f) { + memset(frag->scissorMat, 0, sizeof(frag->scissorMat)); + frag->scissorExt[0] = 1.0f; + frag->scissorExt[1] = 1.0f; + frag->scissorScale[0] = 1.0f; + frag->scissorScale[1] = 1.0f; + } else { + nvgTransformInverse(invxform, scissor->xform); + glnvg__xformToMat3x4(frag->scissorMat, invxform); + frag->scissorExt[0] = scissor->extent[0]; + frag->scissorExt[1] = scissor->extent[1]; + frag->scissorScale[0] = sqrtf(scissor->xform[0] * scissor->xform[0] + scissor->xform[2] * scissor->xform[2]) / fringe; + frag->scissorScale[1] = sqrtf(scissor->xform[1] * scissor->xform[1] + scissor->xform[3] * scissor->xform[3]) / fringe; + } + + memcpy(frag->extent, paint->extent, sizeof(frag->extent)); + frag->strokeMult = (width * 0.5f + fringe * 0.5f) / fringe; + frag->strokeThr = strokeThr; + + if (paint->image != 0) { + tex = glnvg__findTexture(gl, paint->image); + if (tex == nullptr) return 0; + if ((tex->flags & NVG_IMAGE_FLIPY) != 0) { + float m1[6], m2[6]; + nvgTransformTranslate(m1, 0.0f, frag->extent[1] * 0.5f); + nvgTransformMultiply(m1, paint->xform); + nvgTransformScale(m2, 1.0f, -1.0f); + nvgTransformMultiply(m2, m1); + nvgTransformTranslate(m1, 0.0f, -frag->extent[1] * 0.5f); + nvgTransformMultiply(m1, m2); + nvgTransformInverse(invxform, m1); + } else { + nvgTransformInverse(invxform, paint->xform); + } + frag->type = NSVG_SHADER_FILLIMG; + + if (tex->type == NVG_TEXTURE_RGBA) + frag->texType = (tex->flags & NVG_IMAGE_PREMULTIPLIED) ? 0 : 1; + else + frag->texType = 2; + } else { + frag->type = NSVG_SHADER_FILLGRAD; + frag->radius = paint->radius; + frag->feather = paint->feather; + nvgTransformInverse(invxform, paint->xform); + } + + glnvg__xformToMat3x4(frag->paintMat, invxform); + return 1; +} + +static GLNVGfragUniforms* nvg__fragUniformPtr(GLNVGcontext* gl, int i) +{ + return (GLNVGfragUniforms*)&gl->uniforms[i]; +} + +static void glnvg__setUniforms(GLNVGcontext* gl, int uniformOffset, int image) +{ + GLNVGtexture* tex = nullptr; + glBindBufferRange(GL_UNIFORM_BUFFER, GLNVG_FRAG_BINDING, gl->fragBuf, uniformOffset, sizeof(GLNVGfragUniforms)); + + if (image != 0) + tex = glnvg__findTexture(gl, image); + if (tex == nullptr) + tex = glnvg__findTexture(gl, gl->dummyTex); + + glnvg__bindTexture(gl, tex != nullptr ? tex->tex : 0); + glnvg__checkError(gl, "tex paint tex"); +} + +// ── Viewport ──────────────────────────────────────────────────────────────── + +static void glnvg__renderViewport(void* uptr, float width, float height, float devicePixelRatio) +{ + NVG_NOTUSED(devicePixelRatio); + GLNVGcontext* gl = (GLNVGcontext*)uptr; + gl->view[0] = width; + gl->view[1] = height; +} + +// ── Draw commands ─────────────────────────────────────────────────────────── + +static void glnvg__fill(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + glEnable(GL_STENCIL_TEST); + glnvg__stencilMask(gl, 0xff); + glnvg__stencilFunc(gl, GL_ALWAYS, 0, 0xff); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + + glnvg__setUniforms(gl, call->uniformOffset, 0); + glnvg__checkError(gl, "fill simple"); + + glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP); + glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP); + glDisable(GL_CULL_FACE); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_FAN, paths[i].fillOffset, paths[i].fillCount); + glEnable(GL_CULL_FACE); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glnvg__setUniforms(gl, call->uniformOffset + gl->fragSize, call->image); + glnvg__checkError(gl, "fill fill"); + + if (gl->flags & NVG_ANTIALIAS) { + glnvg__stencilFunc(gl, GL_EQUAL, 0x00, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } + + glnvg__stencilFunc(gl, GL_NOTEQUAL, 0x0, 0xff); + glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO); + glDrawArrays(GL_TRIANGLE_STRIP, call->triangleOffset, call->triangleCount); + + glDisable(GL_STENCIL_TEST); +} + +static void glnvg__convexFill(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "convex fill"); + + for (int i = 0; i < npaths; i++) { + glDrawArrays(GL_TRIANGLE_FAN, paths[i].fillOffset, paths[i].fillCount); + if (paths[i].strokeCount > 0) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } +} + +static void glnvg__stroke(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + if (gl->flags & NVG_STENCIL_STROKES) { + glEnable(GL_STENCIL_TEST); + glnvg__stencilMask(gl, 0xff); + + glnvg__stencilFunc(gl, GL_EQUAL, 0x0, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); + glnvg__setUniforms(gl, call->uniformOffset + gl->fragSize, call->image); + glnvg__checkError(gl, "stroke fill 0"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__stencilFunc(gl, GL_EQUAL, 0x00, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glnvg__stencilFunc(gl, GL_ALWAYS, 0x0, 0xff); + glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO); + glnvg__checkError(gl, "stroke fill 1"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glDisable(GL_STENCIL_TEST); + } else { + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "stroke fill"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } +} + +static void glnvg__triangles(GLNVGcontext* gl, GLNVGcall* call) +{ + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "triangles fill"); + glDrawArrays(GL_TRIANGLES, call->triangleOffset, call->triangleCount); +} + +// ── Frame management ──────────────────────────────────────────────────────── + +static void glnvg__renderCancel(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + gl->nverts = 0; + gl->npaths = 0; + gl->ncalls = 0; + gl->nuniforms = 0; +} + +static GLenum glnvg_convertBlendFuncFactor(int factor) +{ + if (factor == NVG_ZERO) return GL_ZERO; + if (factor == NVG_ONE) return GL_ONE; + if (factor == NVG_SRC_COLOR) return GL_SRC_COLOR; + if (factor == NVG_ONE_MINUS_SRC_COLOR) return GL_ONE_MINUS_SRC_COLOR; + if (factor == NVG_DST_COLOR) return GL_DST_COLOR; + if (factor == NVG_ONE_MINUS_DST_COLOR) return GL_ONE_MINUS_DST_COLOR; + if (factor == NVG_SRC_ALPHA) return GL_SRC_ALPHA; + if (factor == NVG_ONE_MINUS_SRC_ALPHA) return GL_ONE_MINUS_SRC_ALPHA; + if (factor == NVG_DST_ALPHA) return GL_DST_ALPHA; + if (factor == NVG_ONE_MINUS_DST_ALPHA) return GL_ONE_MINUS_DST_ALPHA; + if (factor == NVG_SRC_ALPHA_SATURATE) return GL_SRC_ALPHA_SATURATE; + return GL_INVALID_ENUM; +} + +static GLNVGblend glnvg__blendCompositeOperation(NVGcompositeOperationState op) +{ + GLNVGblend blend; + blend.srcRGB = glnvg_convertBlendFuncFactor(op.srcRGB); + blend.dstRGB = glnvg_convertBlendFuncFactor(op.dstRGB); + blend.srcAlpha = glnvg_convertBlendFuncFactor(op.srcAlpha); + blend.dstAlpha = glnvg_convertBlendFuncFactor(op.dstAlpha); + if (blend.srcRGB == GL_INVALID_ENUM || blend.dstRGB == GL_INVALID_ENUM || + blend.srcAlpha == GL_INVALID_ENUM || blend.dstAlpha == GL_INVALID_ENUM) { + blend.srcRGB = GL_ONE; + blend.dstRGB = GL_ONE_MINUS_SRC_ALPHA; + blend.srcAlpha = GL_ONE; + blend.dstAlpha = GL_ONE_MINUS_SRC_ALPHA; + } + return blend; +} + +static void glnvg__renderFlush(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + + if (gl->ncalls > 0) { + glUseProgram(gl->shader.prog); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + glEnable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glStencilMask(0xffffffff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilFunc(GL_ALWAYS, 0, 0xffffffff); + glBindTextureUnit(0, 0); + + // Reset cached state + gl->boundTexture = 0; + gl->stencilMask = 0xffffffff; + gl->stencilFunc = GL_ALWAYS; + gl->stencilFuncRef = 0; + gl->stencilFuncMask = 0xffffffff; + gl->blendFunc.srcRGB = GL_INVALID_ENUM; + gl->blendFunc.srcAlpha = GL_INVALID_ENUM; + gl->blendFunc.dstRGB = GL_INVALID_ENUM; + gl->blendFunc.dstAlpha = GL_INVALID_ENUM; + + // Upload UBO data + glNamedBufferData(gl->fragBuf, gl->nuniforms * gl->fragSize, gl->uniforms, GL_STREAM_DRAW); + + // Upload vertex data + glNamedBufferData(gl->vertBuf, gl->nverts * sizeof(NVGvertex), gl->verts, GL_STREAM_DRAW); + + // Bind VAO and associate the VBO + glBindVertexArray(gl->vertArr); + glVertexArrayVertexBuffer(gl->vertArr, 0, gl->vertBuf, 0, sizeof(NVGvertex)); + + // Set viewSize uniform (layout location = 0) + glUniform2fv(LOC_VIEWSIZE, 1, gl->view); + + // Dispatch draw calls + for (int i = 0; i < gl->ncalls; i++) { + GLNVGcall* call = &gl->calls[i]; + glnvg__blendFuncSeparate(gl, &call->blendFunc); + if (call->type == GLNVG_FILL) + glnvg__fill(gl, call); + else if (call->type == GLNVG_CONVEXFILL) + glnvg__convexFill(gl, call); + else if (call->type == GLNVG_STROKE) + glnvg__stroke(gl, call); + else if (call->type == GLNVG_TRIANGLES) + glnvg__triangles(gl, call); + } + + glBindVertexArray(0); + glDisable(GL_CULL_FACE); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glUseProgram(0); + glnvg__bindTexture(gl, 0); + } + + // Reset per-frame state + gl->nverts = 0; + gl->npaths = 0; + gl->ncalls = 0; + gl->nuniforms = 0; +} + +// ── Allocators ────────────────────────────────────────────────────────────── + +static int glnvg__maxVertCount(const NVGpath* paths, int npaths) +{ + int count = 0; + for (int i = 0; i < npaths; i++) { + count += paths[i].nfill; + count += paths[i].nstroke; + } + return count; +} + +static GLNVGcall* glnvg__allocCall(GLNVGcontext* gl) +{ + if (gl->ncalls + 1 > gl->ccalls) { + int ccalls = glnvg__maxi(gl->ncalls + 1, 128) + gl->ccalls / 2; + GLNVGcall* calls = (GLNVGcall*)realloc(gl->calls, sizeof(GLNVGcall) * ccalls); + if (calls == nullptr) return nullptr; + gl->calls = calls; + gl->ccalls = ccalls; + } + GLNVGcall* ret = &gl->calls[gl->ncalls++]; + memset(ret, 0, sizeof(GLNVGcall)); + return ret; +} + +static int glnvg__allocPaths(GLNVGcontext* gl, int n) +{ + if (gl->npaths + n > gl->cpaths) { + int cpaths = glnvg__maxi(gl->npaths + n, 128) + gl->cpaths / 2; + GLNVGpath* paths = (GLNVGpath*)realloc(gl->paths, sizeof(GLNVGpath) * cpaths); + if (paths == nullptr) return -1; + gl->paths = paths; + gl->cpaths = cpaths; + } + int ret = gl->npaths; + gl->npaths += n; + return ret; +} + +static int glnvg__allocVerts(GLNVGcontext* gl, int n) +{ + if (gl->nverts + n > gl->cverts) { + int cverts = glnvg__maxi(gl->nverts + n, 4096) + gl->cverts / 2; + NVGvertex* verts = (NVGvertex*)realloc(gl->verts, sizeof(NVGvertex) * cverts); + if (verts == nullptr) return -1; + gl->verts = verts; + gl->cverts = cverts; + } + int ret = gl->nverts; + gl->nverts += n; + return ret; +} + +static int glnvg__allocFragUniforms(GLNVGcontext* gl, int n) +{ + int structSize = gl->fragSize; + if (gl->nuniforms + n > gl->cuniforms) { + int cuniforms = glnvg__maxi(gl->nuniforms + n, 128) + gl->cuniforms / 2; + unsigned char* uniforms = (unsigned char*)realloc(gl->uniforms, structSize * cuniforms); + if (uniforms == nullptr) return -1; + gl->uniforms = uniforms; + gl->cuniforms = cuniforms; + } + int ret = gl->nuniforms * structSize; + gl->nuniforms += n; + return ret; +} + +static void glnvg__vset(NVGvertex* vtx, float x, float y, float u, float v) +{ + vtx->x = x; + vtx->y = y; + vtx->u = u; + vtx->v = v; +} + +// ── Render path recording ─────────────────────────────────────────────────── + +static void glnvg__renderFill(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, float fringe, const float* bounds, + const NVGpath* paths, int npaths) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + NVGvertex* quad; + GLNVGfragUniforms* frag; + int maxverts, offset; + + if (call == nullptr) return; + + call->type = GLNVG_FILL; + call->triangleCount = 4; + call->pathOffset = glnvg__allocPaths(gl, npaths); + if (call->pathOffset == -1) goto error; + call->pathCount = npaths; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + if (npaths == 1 && paths[0].convex) { + call->type = GLNVG_CONVEXFILL; + call->triangleCount = 0; + } + + maxverts = glnvg__maxVertCount(paths, npaths) + call->triangleCount; + offset = glnvg__allocVerts(gl, maxverts); + if (offset == -1) goto error; + + for (int i = 0; i < npaths; i++) { + GLNVGpath* copy = &gl->paths[call->pathOffset + i]; + const NVGpath* path = &paths[i]; + memset(copy, 0, sizeof(GLNVGpath)); + if (path->nfill > 0) { + copy->fillOffset = offset; + copy->fillCount = path->nfill; + memcpy(&gl->verts[offset], path->fill, sizeof(NVGvertex) * path->nfill); + offset += path->nfill; + } + if (path->nstroke > 0) { + copy->strokeOffset = offset; + copy->strokeCount = path->nstroke; + memcpy(&gl->verts[offset], path->stroke, sizeof(NVGvertex) * path->nstroke); + offset += path->nstroke; + } + } + + if (call->type == GLNVG_FILL) { + call->triangleOffset = offset; + quad = &gl->verts[call->triangleOffset]; + glnvg__vset(&quad[0], bounds[2], bounds[3], 0.5f, 1.0f); + glnvg__vset(&quad[1], bounds[2], bounds[1], 0.5f, 1.0f); + glnvg__vset(&quad[2], bounds[0], bounds[3], 0.5f, 1.0f); + glnvg__vset(&quad[3], bounds[0], bounds[1], 0.5f, 1.0f); + + call->uniformOffset = glnvg__allocFragUniforms(gl, 2); + if (call->uniformOffset == -1) goto error; + frag = nvg__fragUniformPtr(gl, call->uniformOffset); + memset(frag, 0, sizeof(*frag)); + frag->strokeThr = -1.0f; + frag->type = NSVG_SHADER_SIMPLE; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset + gl->fragSize), paint, scissor, fringe, fringe, -1.0f); + } else { + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, fringe, fringe, -1.0f); + } + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +static void glnvg__renderStroke(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, float fringe, float strokeWidth, + const NVGpath* paths, int npaths) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + int maxverts, offset; + + if (call == nullptr) return; + + call->type = GLNVG_STROKE; + call->pathOffset = glnvg__allocPaths(gl, npaths); + if (call->pathOffset == -1) goto error; + call->pathCount = npaths; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + maxverts = glnvg__maxVertCount(paths, npaths); + offset = glnvg__allocVerts(gl, maxverts); + if (offset == -1) goto error; + + for (int i = 0; i < npaths; i++) { + GLNVGpath* copy = &gl->paths[call->pathOffset + i]; + const NVGpath* path = &paths[i]; + memset(copy, 0, sizeof(GLNVGpath)); + if (path->nstroke) { + copy->strokeOffset = offset; + copy->strokeCount = path->nstroke; + memcpy(&gl->verts[offset], path->stroke, sizeof(NVGvertex) * path->nstroke); + offset += path->nstroke; + } + } + + if (gl->flags & NVG_STENCIL_STROKES) { + call->uniformOffset = glnvg__allocFragUniforms(gl, 2); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, strokeWidth, fringe, -1.0f); + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset + gl->fragSize), paint, scissor, strokeWidth, fringe, 1.0f - 0.5f / 255.0f); + } else { + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, strokeWidth, fringe, -1.0f); + } + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +static void glnvg__renderTriangles(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, const NVGvertex* verts, int nverts, float fringe) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + GLNVGfragUniforms* frag; + + if (call == nullptr) return; + + call->type = GLNVG_TRIANGLES; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + call->triangleOffset = glnvg__allocVerts(gl, nverts); + if (call->triangleOffset == -1) goto error; + call->triangleCount = nverts; + + memcpy(&gl->verts[call->triangleOffset], verts, sizeof(NVGvertex) * nverts); + + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + frag = nvg__fragUniformPtr(gl, call->uniformOffset); + glnvg__convertPaint(gl, frag, paint, scissor, 1.0f, fringe, -1.0f); + frag->type = NSVG_SHADER_IMG; + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +// ── Cleanup ───────────────────────────────────────────────────────────────── + +static void glnvg__renderDelete(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + if (gl == nullptr) return; + + glnvg__deleteShader(&gl->shader); + + if (gl->fragBuf != 0) + glDeleteBuffers(1, &gl->fragBuf); + if (gl->vertArr != 0) + glDeleteVertexArrays(1, &gl->vertArr); + if (gl->vertBuf != 0) + glDeleteBuffers(1, &gl->vertBuf); + + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].tex != 0 && (gl->textures[i].flags & NVG_IMAGE_NODELETE) == 0) + glDeleteTextures(1, &gl->textures[i].tex); + } + free(gl->textures); + + free(gl->paths); + free(gl->verts); + free(gl->uniforms); + free(gl->calls); + + free(gl); +} + +// ── Public API ────────────────────────────────────────────────────────────── + +NVGcontext* nvgCreateGL46(int flags) +{ + NVGparams params; + NVGcontext* ctx = nullptr; + GLNVGcontext* gl = (GLNVGcontext*)malloc(sizeof(GLNVGcontext)); + if (gl == nullptr) goto error; + memset(gl, 0, sizeof(GLNVGcontext)); + + memset(¶ms, 0, sizeof(params)); + params.renderCreate = glnvg__renderCreate; + params.renderCreateTexture = glnvg__renderCreateTexture; + params.renderDeleteTexture = glnvg__renderDeleteTexture; + params.renderUpdateTexture = glnvg__renderUpdateTexture; + params.renderGetTextureSize = glnvg__renderGetTextureSize; + params.renderViewport = glnvg__renderViewport; + params.renderCancel = glnvg__renderCancel; + params.renderFlush = glnvg__renderFlush; + params.renderFill = glnvg__renderFill; + params.renderStroke = glnvg__renderStroke; + params.renderTriangles = glnvg__renderTriangles; + params.renderDelete = glnvg__renderDelete; + params.userPtr = gl; + params.edgeAntiAlias = flags & NVG_ANTIALIAS ? 1 : 0; + + gl->flags = flags; + + ctx = nvgCreateInternal(¶ms); + if (ctx == nullptr) goto error; + + return ctx; + +error: + if (ctx != nullptr) nvgDeleteInternal(ctx); + return nullptr; +} + +void nvgDeleteGL46(NVGcontext* ctx) +{ + nvgDeleteInternal(ctx); +} + +int nvglCreateImageFromHandleGL46(NVGcontext* ctx, GLuint textureId, int w, int h, int imageFlags) +{ + GLNVGcontext* gl = (GLNVGcontext*)nvgInternalParams(ctx)->userPtr; + GLNVGtexture* tex = glnvg__allocTexture(gl); + if (tex == nullptr) return 0; + + tex->type = NVG_TEXTURE_RGBA; + tex->tex = textureId; + tex->flags = imageFlags; + tex->width = w; + tex->height = h; + + return tex->id; +} + +GLuint nvglImageHandleGL46(NVGcontext* ctx, int image) +{ + GLNVGcontext* gl = (GLNVGcontext*)nvgInternalParams(ctx)->userPtr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + return tex->tex; +} diff --git a/src/nanovg_gl46.h b/src/nanovg_gl46.h new file mode 100644 index 0000000..d69889e --- /dev/null +++ b/src/nanovg_gl46.h @@ -0,0 +1,31 @@ +// +// NanoVG OpenGL 4.6 Core Profile backend +// +// Based on nanovg_gl.h by Mikko Mononen (Copyright (c) 2009-2013) +// Adapted to use OpenGL 4.6 Direct State Access (DSA) functions, +// immutable texture storage, and explicit layout qualifiers. +// +#pragma once + +#include "glad/gl.h" + +struct NVGcontext; + +// Create flags (same as nanovg_gl.h, guarded to avoid redefinition) +#ifndef NANOVG_GL_H +enum NVGcreateFlags { + NVG_ANTIALIAS = 1 << 0, + NVG_STENCIL_STROKES = 1 << 1, + NVG_DEBUG = 1 << 2, +}; + +enum NVGimageFlagsGL { + NVG_IMAGE_NODELETE = 1 << 16, +}; +#endif + +NVGcontext* nvgCreateGL46(int flags); +void nvgDeleteGL46(NVGcontext* ctx); + +int nvglCreateImageFromHandleGL46(NVGcontext* ctx, GLuint textureId, int w, int h, int flags); +GLuint nvglImageHandleGL46(NVGcontext* ctx, int image);