From 2bec7a633a2af3455d4d8ddd6ad75988d0445c94 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 16 May 2023 18:06:22 +0200 Subject: [PATCH] [win32] reflected changes to canvas surface interface on GL backend. But implementation still uses the (slower) Loop-Blinn + triangle fan method. --- examples/canvas/main.c | 419 +++--- src/gl_canvas.c | 1648 +++++++++++++++++++++- src/graphics_surface.h | 312 +++-- src/mtl_renderer.m | 2950 ++++++++++++++++++++-------------------- 4 files changed, 3443 insertions(+), 1886 deletions(-) diff --git a/examples/canvas/main.c b/examples/canvas/main.c index 9cb0d8b..ddc8a4e 100644 --- a/examples/canvas/main.c +++ b/examples/canvas/main.c @@ -1,209 +1,210 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#include -#include - -#define _USE_MATH_DEFINES //NOTE: necessary for MSVC -#include - -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - - -mg_font create_font() -{ - //NOTE(martin): create font - str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); - char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); - - FILE* fontFile = fopen(fontPathCString, "r"); - if(!fontFile) - { - log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); - return(mg_font_nil()); - } - unsigned char* fontData = 0; - fseek(fontFile, 0, SEEK_END); - u32 fontDataSize = ftell(fontFile); - rewind(fontFile); - fontData = (unsigned char*)malloc(fontDataSize); - fread(fontData, 1, fontDataSize, fontFile); - fclose(fontFile); - - unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, - UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - UNICODE_RANGE_LATIN_EXTENDED_A, - UNICODE_RANGE_LATIN_EXTENDED_B, - UNICODE_RANGE_SPECIALS}; - - mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); - free(fontData); - - return(font); -} - -int main() -{ - mp_init(); - mp_clock_init(); //TODO put that in mp_init()? - - mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; - mp_window window = mp_window_create(windowRect, "test", 0); - - mp_rect contentRect = mp_window_get_content_rect(window); - - //NOTE: create surface - mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); - mg_surface_swap_interval(surface, 0); - - mg_canvas canvas = mg_canvas_create(); - - if(mg_canvas_is_nil(canvas)) - { - printf("Error: couldn't create canvas\n"); - return(-1); - } - - mg_font font = create_font(); - - // start app - mp_window_bring_to_front(window); - mp_window_focus(window); - - f32 x = 400, y = 300; - f32 speed = 0; - f32 dx = speed, dy = speed; - f64 frameTime = 0; - - while(!mp_should_quit()) - { - f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); - - mp_pump_events(0); - mp_event* event = 0; - while((event = mp_next_event(mem_scratch())) != 0) - { - switch(event->type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - case MP_EVENT_KEYBOARD_KEY: - { - if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) - { - f32 factor = (event->key.mods & MP_KEYMOD_SHIFT) ? 10 : 1; - - if(event->key.code == MP_KEY_LEFT) - { - x-=0.3*factor; - } - else if(event->key.code == MP_KEY_RIGHT) - { - x+=0.3*factor; - } - else if(event->key.code == MP_KEY_UP) - { - y-=0.3*factor; - } - else if(event->key.code == MP_KEY_DOWN) - { - y+=0.3*factor; - } - } - } break; - - default: - break; - } - } - - if(x-200 < 0) - { - x = 200; - dx = speed; - } - if(x+200 > contentRect.w) - { - x = contentRect.w - 200; - dx = -speed; - } - if(y-200 < 0) - { - y = 200; - dy = speed; - } - if(y+200 > contentRect.h) - { - y = contentRect.h - 200; - dy = -speed; - } - x += dx; - y += dy; - - // background - mg_set_color_rgba(0, 1, 1, 1); - mg_clear(); - - // head - mg_set_color_rgba(1, 1, 0, 1); - - mg_circle_fill(x, y, 200); - - // smile - f32 frown = frameTime > 0.033 ? -100 : 0; - - mg_set_color_rgba(0, 0, 0, 1); - mg_set_width(20); - mg_move_to(x-100, y+100); - mg_cubic_to(x-50, y+150+frown, x+50, y+150+frown, x+100, y+100); - mg_stroke(); - - // eyes - mg_ellipse_fill(x-70, y-50, 30, 50); - mg_ellipse_fill(x+70, y-50, 30, 50); - - // text - mg_set_color_rgba(0, 0, 1, 1); - mg_set_font(font); - mg_set_font_size(12); - mg_move_to(50, 600-50); - - str8 text = str8_pushf(mem_scratch(), - "Milepost vector graphics test program (frame time = %fs, fps = %f)...", - frameTime, - 1./frameTime); - mg_text_outlines(text); - mg_fill(); - - printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", - frameTime, - 1./frameTime); - - mg_surface_prepare(surface); - mg_render(surface, canvas); - mg_surface_present(surface); - - mem_arena_clear(mem_scratch()); - frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; - } - - mg_font_destroy(font); - mg_canvas_destroy(canvas); - mg_surface_destroy(surface); - mp_window_destroy(window); - - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include +#include +#include + +#define _USE_MATH_DEFINES //NOTE: necessary for MSVC +#include + +#include"milepost.h" + +#define LOG_SUBSYSTEM "Main" + + +mg_font create_font() +{ + //NOTE(martin): create font + str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); + char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); + + FILE* fontFile = fopen(fontPathCString, "r"); + if(!fontFile) + { + log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); + return(mg_font_nil()); + } + unsigned char* fontData = 0; + fseek(fontFile, 0, SEEK_END); + u32 fontDataSize = ftell(fontFile); + rewind(fontFile); + fontData = (unsigned char*)malloc(fontDataSize); + fread(fontData, 1, fontDataSize, fontFile); + fclose(fontFile); + + unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, + UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + UNICODE_RANGE_LATIN_EXTENDED_A, + UNICODE_RANGE_LATIN_EXTENDED_B, + UNICODE_RANGE_SPECIALS}; + + mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); + free(fontData); + + return(font); +} + +int main() +{ + mp_init(); + mp_clock_init(); //TODO put that in mp_init()? + + mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; + mp_window window = mp_window_create(windowRect, "test", 0); + + mp_rect contentRect = mp_window_get_content_rect(window); + + //NOTE: create surface + mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); + mg_surface_swap_interval(surface, 0); + + mg_canvas canvas = mg_canvas_create(); + + if(mg_canvas_is_nil(canvas)) + { + printf("Error: couldn't create canvas\n"); + return(-1); + } + + mg_font font = create_font(); + + // start app + mp_window_bring_to_front(window); + mp_window_focus(window); + + f32 x = 400, y = 300; + f32 speed = 0; + f32 dx = speed, dy = speed; + f64 frameTime = 0; + + while(!mp_should_quit()) + { + f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); + + mp_pump_events(0); + mp_event* event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + case MP_EVENT_KEYBOARD_KEY: + { + if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) + { + f32 factor = (event->key.mods & MP_KEYMOD_SHIFT) ? 10 : 1; + + if(event->key.code == MP_KEY_LEFT) + { + x-=0.3*factor; + } + else if(event->key.code == MP_KEY_RIGHT) + { + x+=0.3*factor; + } + else if(event->key.code == MP_KEY_UP) + { + y-=0.3*factor; + } + else if(event->key.code == MP_KEY_DOWN) + { + y+=0.3*factor; + } + } + } break; + + default: + break; + } + } + + if(x-200 < 0) + { + x = 200; + dx = speed; + } + if(x+200 > contentRect.w) + { + x = contentRect.w - 200; + dx = -speed; + } + if(y-200 < 0) + { + y = 200; + dy = speed; + } + if(y+200 > contentRect.h) + { + y = contentRect.h - 200; + dy = -speed; + } + x += dx; + y += dy; + + // background + mg_set_color_rgba(0, 1, 1, 1); + mg_clear(); + + // head + mg_set_color_rgba(1, 1, 0, 1); + + mg_circle_fill(x, y, 200); + + // smile + f32 frown = frameTime > 0.033 ? -100 : 0; + + mg_set_color_rgba(0, 0, 0, 1); + mg_set_width(20); + mg_move_to(x-100, y+100); + mg_cubic_to(x-50, y+150+frown, x+50, y+150+frown, x+100, y+100); + mg_stroke(); + + // eyes + mg_ellipse_fill(x-70, y-50, 30, 50); + mg_ellipse_fill(x+70, y-50, 30, 50); + + // text + mg_set_color_rgba(0, 0, 1, 1); + mg_set_font(font); + mg_set_font_size(12); + mg_move_to(50, 600-50); + + str8 text = str8_pushf(mem_scratch(), + "Milepost vector graphics test program (frame time = %fs, fps = %f)...", + frameTime, + 1./frameTime); + mg_text_outlines(text); + mg_fill(); + + printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", + frameTime, + 1./frameTime); + + mg_surface_prepare(surface); + mg_render(surface, canvas); + mg_surface_present(surface); + + mem_arena_clear(mem_scratch()); + frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; + } + + mg_font_destroy(font); + mg_canvas_destroy(canvas); + mg_surface_destroy(surface); + mp_window_destroy(window); + + mp_terminate(); + + return(0); +} diff --git a/src/gl_canvas.c b/src/gl_canvas.c index 908c08a..7cba07c 100644 --- a/src/gl_canvas.c +++ b/src/gl_canvas.c @@ -14,7 +14,22 @@ typedef struct mg_gl_canvas_backend { mg_canvas_backend interface; - mg_surface surface; + mg_wgl_surface* surface; + + mp_rect clip; + mg_mat2x3 transform; + mg_image image; + mp_rect srcRegion; + mg_color clearColor; + + u32 nextShapeIndex; + u32 vertexCount; + u32 indexCount; + + vec4 shapeExtents; + vec4 shapeScreenExtents; + + mg_vertex_layout vertexLayout; GLuint vao; GLuint dummyVertexBuffer; @@ -61,6 +76,1435 @@ typedef struct debug_shape u8 pad[8]; } debug_shape; + +//-------------------------------------------------------------------- +// Primitives encoding +//-------------------------------------------------------------------- +void mg_reset_shape_index(mg_gl_canvas_backend* backend) +{ + backend->nextShapeIndex = 0; + backend->shapeExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; +} + +void mg_finalize_shape(mg_gl_canvas_backend* backend) +{ + if(backend->nextShapeIndex) + { + //NOTE: set shape's uv transform for the _current_ shape + vec2 texSize = mg_image_size(backend->image); + + mp_rect srcRegion = backend->srcRegion; + + mp_rect destRegion = {backend->shapeExtents.x, + backend->shapeExtents.y, + backend->shapeExtents.z - backend->shapeExtents.x, + backend->shapeExtents.w - backend->shapeExtents.y}; + + mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, + 0, 1/texSize.y, srcRegion.y/texSize.y}; + mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, + 0, srcRegion.h/destRegion.h, 0}; + mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, + 0, 1, -destRegion.y}; + + mg_mat2x3 screenToUser = mg_mat2x3_inv(backend->transform); + + mg_mat2x3 uvTransform = srcRegionToImage; + uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); + + int index = backend->nextShapeIndex-1; + mg_vertex_layout* layout = &backend->vertexLayout; + *(mg_mat2x3*)(layout->uvTransformBuffer + index*layout->uvTransformStride) = uvTransform; + + //TODO: transform extents before clipping + mp_rect clip = {maximum(backend->clip.x, backend->shapeScreenExtents.x), + maximum(backend->clip.y, backend->shapeScreenExtents.y), + minimum(backend->clip.x + backend->clip.w, backend->shapeScreenExtents.z), + minimum(backend->clip.y + backend->clip.h, backend->shapeScreenExtents.w)}; + + *(mp_rect*)(((char*)layout->clipBuffer) + index*layout->clipStride) = clip; + } +} + +u32 mg_next_shape(mg_gl_canvas_backend* backend, mg_attributes* attributes) +{ + mg_finalize_shape(backend); + + backend->clip = attributes->clip; + backend->transform = attributes->transform; + backend->srcRegion = attributes->srcRegion; + backend->shapeExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + backend->shapeScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + mg_vertex_layout* layout = &backend->vertexLayout; + int index = backend->nextShapeIndex; + backend->nextShapeIndex++; + + *(mg_color*)(((char*)layout->colorBuffer) + index*layout->colorStride) = attributes->color; + + return(index); +} + +//TODO(martin): rename with something more explicit +u32 mg_vertices_base_index(mg_gl_canvas_backend* backend) +{ + return(backend->vertexCount); +} + +int* mg_reserve_indices(mg_gl_canvas_backend* backend, u32 indexCount) +{ + mg_vertex_layout* layout = &backend->vertexLayout; + + //TODO: do something here... + ASSERT(backend->indexCount + indexCount < layout->maxIndexCount); + + int* base = ((int*)layout->indexBuffer) + backend->indexCount; + backend->indexCount += indexCount; + return(base); +} + +void mg_push_vertex_cubic(mg_gl_canvas_backend* backend, vec2 pos, vec4 cubic) +{ + backend->shapeExtents.x = minimum(backend->shapeExtents.x, pos.x); + backend->shapeExtents.y = minimum(backend->shapeExtents.y, pos.y); + backend->shapeExtents.z = maximum(backend->shapeExtents.z, pos.x); + backend->shapeExtents.w = maximum(backend->shapeExtents.w, pos.y); + + vec2 screenPos = mg_mat2x3_mul(backend->transform, pos); + + backend->shapeScreenExtents.x = minimum(backend->shapeScreenExtents.x, screenPos.x); + backend->shapeScreenExtents.y = minimum(backend->shapeScreenExtents.y, screenPos.y); + backend->shapeScreenExtents.z = maximum(backend->shapeScreenExtents.z, screenPos.x); + backend->shapeScreenExtents.w = maximum(backend->shapeScreenExtents.w, screenPos.y); + + mg_vertex_layout* layout = &backend->vertexLayout; + ASSERT(backend->vertexCount < layout->maxVertexCount); + ASSERT(backend->nextShapeIndex > 0); + + int shapeIndex = maximum(0, backend->nextShapeIndex-1); + u32 index = backend->vertexCount; + backend->vertexCount++; + + *(vec2*)(((char*)layout->posBuffer) + index*layout->posStride) = screenPos; + *(vec4*)(((char*)layout->cubicBuffer) + index*layout->cubicStride) = cubic; + *(u32*)(((char*)layout->shapeIndexBuffer) + index*layout->shapeIndexStride) = shapeIndex; +} + +void mg_push_vertex(mg_gl_canvas_backend* backend, vec2 pos) +{ + mg_push_vertex_cubic(backend, pos, (vec4){1, 1, 1, 1}); +} +//----------------------------------------------------------------------------------------------------------- +// Path Filling +//----------------------------------------------------------------------------------------------------------- +//NOTE(martin): forward declarations +void mg_render_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4]); + +//NOTE(martin): quadratics filling + +void mg_render_fill_quadratic(mg_gl_canvas_backend* backend, vec2 p[3]) +{ + u32 baseIndex = mg_vertices_base_index(backend); + + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex_cubic(backend, (vec2){p[0].x, p[0].y}, (vec4){0, 0, 0, 1}); + mg_push_vertex_cubic(backend, (vec2){p[1].x, p[1].y}, (vec4){0.5, 0, 0.5, 1}); + mg_push_vertex_cubic(backend, (vec2){p[2].x, p[2].y}, (vec4){1, 1, 1, 1}); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; +} + +//NOTE(martin): cubic filling + +void mg_split_and_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4], f32 tSplit) +{ + int subVertexCount = 0; + int subIndexCount = 0; + + f32 OneMinusTSplit = 1-tSplit; + + vec2 q0 = {OneMinusTSplit*p[0].x + tSplit*p[1].x, + OneMinusTSplit*p[0].y + tSplit*p[1].y}; + + vec2 q1 = {OneMinusTSplit*p[1].x + tSplit*p[2].x, + OneMinusTSplit*p[1].y + tSplit*p[2].y}; + + vec2 q2 = {OneMinusTSplit*p[2].x + tSplit*p[3].x, + OneMinusTSplit*p[2].y + tSplit*p[3].y}; + + vec2 r0 = {OneMinusTSplit*q0.x + tSplit*q1.x, + OneMinusTSplit*q0.y + tSplit*q1.y}; + + vec2 r1 = {OneMinusTSplit*q1.x + tSplit*q2.x, + OneMinusTSplit*q1.y + tSplit*q2.y}; + + vec2 split = {OneMinusTSplit*r0.x + tSplit*r1.x, + OneMinusTSplit*r0.y + tSplit*r1.y};; + + vec2 subPointsLow[4] = {p[0], q0, r0, split}; + vec2 subPointsHigh[4] = {split, r1, q2, p[3]}; + + //NOTE(martin): add base triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, (vec2){p[0].x, p[0].y}); + mg_push_vertex(backend, (vec2){split.x, split.y}); + mg_push_vertex(backend, (vec2){p[3].x, p[3].y}); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + + mg_render_fill_cubic(backend, subPointsLow); + mg_render_fill_cubic(backend, subPointsHigh); +} + +int mg_cubic_outside_test(vec4 c) +{ + int res = (c.x*c.x*c.x - c.y*c.z < 0) ? -1 : 1; + return(res); +} + +void mg_render_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4]) +{ + vec4 testCoords[4]; + + /*NOTE(martin): first convert the control points to power basis, multiplying by M3 + + | 1 0 0 0| + M3 = |-3 3 0 0| + | 3 -6 3 0| + |-1 3 -3 1| + ie: + c0 = p0 + c1 = -3*p0 + 3*p1 + c2 = 3*p0 - 6*p1 + 3*p2 + c3 = -p0 + 3*p1 - 3*p2 + p3 + */ + f32 c1x = 3.0*p[1].x - 3.0*p[0].x; + f32 c1y = 3.0*p[1].y - 3.0*p[0].y; + + f32 c2x = 3.0*p[0].x + 3.0*p[2].x - 6.0*p[1].x; + f32 c2y = 3.0*p[0].y + 3.0*p[2].y - 6.0*p[1].y; + + f32 c3x = 3.0*p[1].x - 3.0*p[2].x + p[3].x - p[0].x; + f32 c3y = 3.0*p[1].y - 3.0*p[2].y + p[3].y - p[0].y; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //TODO(martin): we shouldn't need scaling here since now we're doing our shader math in fixed point? + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + c1x /= 10; + c1y /= 10; + c2x /= 10; + c2y /= 10; + c3x /= 10; + c3y /= 10; + + /*NOTE(martin): + now, compute determinants d0, d1, d2, d3, which gives the coefficients of the + inflection points polynomial: + + I(t, s) = d0*t^3 - 3*d1*t^2*s + 3*d2*t*s^2 - d3*s^3 + + The roots of this polynomial are the inflection points of the parametric curve, in homogeneous + coordinates (ie we can have an inflection point at inifinity with s=0). + + |x3 y3 w3| |x3 y3 w3| |x3 y3 w3| |x2 y2 w2| + d0 = det |x2 y2 w2| d1 = -det |x2 y2 w2| d2 = det |x1 y1 w1| d3 = -det |x1 y1 w1| + |x1 y1 w1| |x0 y0 w0| |x0 y0 w0| |x0 y0 w0| + + In our case, the pi.w equal 1 (no point at infinity), so _in_the_power_basis_, w1 = w2 = w3 = 0 and w0 = 1 + (which also means d0 = 0) + */ + + f32 d1 = c3y*c2x - c3x*c2y; + f32 d2 = c3x*c1y - c3y*c1x; + f32 d3 = c2y*c1x - c2x*c1y; + + //NOTE(martin): compute the second factor of the discriminant discr(I) = d1^2*(3*d2^2 - 4*d3*d1) + f32 discrFactor2 = 3.0*Square(d2) - 4.0*d3*d1; + + //NOTE(martin): each following case gives the number of roots, hence the category of the parametric curve + if(fabs(d1) < 0.1 && fabs(d2) < 0.1 && d3 != 0) + { + //NOTE(martin): quadratic degenerate case + //NOTE(martin): compute quadratic curve control point, which is at p0 + 1.5*(p1-p0) = 1.5*p1 - 0.5*p0 + vec2 quadControlPoints[3] = { p[0], + {1.5*p[1].x - 0.5*p[0].x, 1.5*p[1].y - 0.5*p[0].y}, + p[3]}; + + mg_render_fill_quadratic(backend, quadControlPoints); + return; + } + else if( (discrFactor2 > 0 && d1 != 0) + ||(discrFactor2 == 0 && d1 != 0)) + { + //NOTE(martin): serpentine curve or cusp with inflection at infinity + // (these two cases are handled the same way). + //NOTE(martin): compute the solutions (tl, sl), (tm, sm), and (tn, sn) of the inflection point equation + f32 tl = d2 + sqrt(discrFactor2/3); + f32 sl = 2*d1; + f32 tm = d2 - sqrt(discrFactor2/3); + f32 sm = sl; + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | tl*tm tl^3 tm^3 1 | + | -sm*tl - sl*tm -3sl*tl^2 -3*sm*tm^2 0 | + | sl*sm 3*sl^2*tl 3*sm^2*tm 0 | + | 0 -sl^3 -sm^3 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + testCoords[0].x = tl*tm; + testCoords[0].y = Cube(tl); + testCoords[0].z = Cube(tm); + + testCoords[1].x = tl*tm - (sm*tl + sl*tm)/3; + testCoords[1].y = Cube(tl) - sl*Square(tl); + testCoords[1].z = Cube(tm) - sm*Square(tm); + + testCoords[2].x = tl*tm - (sm*tl + sl*tm)*2/3 + sl*sm/3; + testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; + testCoords[2].z = Cube(tm) - 2*sm*Square(tm) + Square(sm)*tm; + + testCoords[3].x = tl*tm - (sm*tl + sl*tm) + sl*sm; + testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); + testCoords[3].z = Cube(tm) - 3*sm*Square(tm) + 3*Square(sm)*tm - Cube(sm); + } + else if(discrFactor2 < 0 && d1 != 0) + { + //NOTE(martin): loop curve + f32 td = d2 + sqrt(-discrFactor2); + f32 sd = 2*d1; + f32 te = d2 - sqrt(-discrFactor2); + f32 se = sd; + + //NOTE(martin): if one of the parameters (td/sd) or (te/se) is in the interval [0,1], the double point + // is inside the control points convex hull and would cause a shading anomaly. If this is + // the case, subdivide the curve at that point + + //TODO: study edge case where td/sd ~ 1 or 0 (which causes an infinite recursion in split and fill). + // quick fix for now is adding a little slop in the check... + + if(sd != 0 && td/sd < 0.99 && td/sd > 0.01) + { + mg_split_and_fill_cubic(backend, p, td/sd); + return; + } + if(se != 0 && te/se < 0.99 && te/se > 0.01) + { + mg_split_and_fill_cubic(backend, p, te/se); + return; + } + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | td*te td^2*te td*te^2 1 | + | -se*td - sd*te -se*td^2 - 2sd*te*td -sd*te^2 - 2*se*td*te 0 | + | sd*se te*sd^2 + 2*se*td*sd td*se^2 + 2*sd*te*se 0 | + | 0 -sd^2*se -sd*se^2 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + testCoords[0].x = td*te; + testCoords[0].y = Square(td)*te; + testCoords[0].z = td*Square(te); + + testCoords[1].x = td*te - (se*td + sd*te)/3.0; + testCoords[1].y = Square(td)*te - (se*Square(td) + 2.*sd*te*td)/3.0; + testCoords[1].z = td*Square(te) - (sd*Square(te) + 2*se*td*te)/3.0; + + testCoords[2].x = td*te - 2.0*(se*td + sd*te)/3.0 + sd*se/3.0; + testCoords[2].y = Square(td)*te - 2.0*(se*Square(td) + 2.0*sd*te*td)/3.0 + (te*Square(sd) + 2.0*se*td*sd)/3.0; + testCoords[2].z = td*Square(te) - 2.0*(sd*Square(te) + 2.0*se*td*te)/3.0 + (td*Square(se) + 2.0*sd*te*se)/3.0; + + testCoords[3].x = td*te - (se*td + sd*te) + sd*se; + testCoords[3].y = Square(td)*te - (se*Square(td) + 2.0*sd*te*td) + (te*Square(sd) + 2.0*se*td*sd) - Square(sd)*se; + testCoords[3].z = td*Square(te) - (sd*Square(te) + 2.0*se*td*te) + (td*Square(se) + 2.0*sd*te*se) - sd*Square(se); + } + else if(d1 == 0 && d2 != 0) + { + //NOTE(martin): cusp with cusp at infinity + + f32 tl = d3; + f32 sl = 3*d2; + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | tl tl^3 1 1 | + | -sl -3sl*tl^2 0 0 | + | 0 3*sl^2*tl 0 0 | + | 0 -sl^3 0 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + + testCoords[0].x = tl; + testCoords[0].y = Cube(tl); + testCoords[0].z = 1; + + testCoords[1].x = tl - sl/3; + testCoords[1].y = Cube(tl) - sl*Square(tl); + testCoords[1].z = 1; + + testCoords[2].x = tl - sl*2/3; + testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; + testCoords[2].z = 1; + + testCoords[3].x = tl - sl; + testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); + testCoords[3].z = 1; + } + else if(d1 == 0 && d2 == 0 && d3 == 0) + { + //NOTE(martin): line or point degenerate case, ignored + return; + } + else + { + //TODO(martin): handle error ? put some epsilon slack on the conditions ? + ASSERT(0, "not implemented yet !"); + return; + } + + //NOTE(martin): compute convex hull indices using Gift wrapping / Jarvis' march algorithm + int convexHullIndices[4]; + int leftMostPointIndex = 0; + + for(int i=0; i<4; i++) + { + if(p[i].x < p[leftMostPointIndex].x) + { + leftMostPointIndex = i; + } + } + int currentPointIndex = leftMostPointIndex; + int i=0; + int convexHullCount = 0; + + do + { + convexHullIndices[i] = currentPointIndex; + convexHullCount++; + int bestGuessIndex = 0; + + for(int j=0; j<4; j++) + { + vec2 bestGuessEdge = {.x = p[bestGuessIndex].x - p[currentPointIndex].x, + .y = p[bestGuessIndex].y - p[currentPointIndex].y}; + + vec2 nextGuessEdge = {.x = p[j].x - p[currentPointIndex].x, + .y = p[j].y - p[currentPointIndex].y}; + + //NOTE(martin): if control point j is on the right of current edge, it is a best guess + // (in case of colinearity we choose the point which is farthest from the current point) + + f32 crossProduct = bestGuessEdge.x*nextGuessEdge.y - bestGuessEdge.y*nextGuessEdge.x; + + if( bestGuessIndex == currentPointIndex + || crossProduct < 0) + { + bestGuessIndex = j; + } + else if(crossProduct == 0) + { + + //NOTE(martin): if vectors v1, v2 are colinear and distinct, and ||v1|| > ||v2||, + // either abs(v1.x) > abs(v2.x) or abs(v1.y) > abs(v2.y) + // so we don't actually need to compute their norm to select the greatest + // (and if v1 and v2 are equal we don't have to update our best guess.) + + //TODO(martin): in case of colinearity we should rather select the edge that has the greatest dot product with last edge ?? + + if(fabs(nextGuessEdge.x) > fabs(bestGuessEdge.x) + || fabs(nextGuessEdge.y) > fabs(bestGuessEdge.y)) + { + bestGuessIndex = j; + } + } + } + i++; + currentPointIndex = bestGuessIndex; + + } while(currentPointIndex != leftMostPointIndex && i<4); + + //NOTE(martin): triangulation and inside/outside tests. In the shader, the outside is defined by s*(k^3 - lm) > 0 + // ie the 4th coordinate s flips the inside/outside test. + // We affect s such that the covered are is between the curve and the line joining p0 and p3. + + //TODO: quick fix, maybe later cull degenerate hulls beforehand + if(convexHullCount <= 2) + { + //NOTE(martin): if convex hull has only two point, we have a degenerate cubic that displays nothing. + return; + } + else if(convexHullCount == 3) + { + /*NOTE(martin): + We have 3 case here: + 1) Endpoints are coincidents. We push on triangle, and test an intermediate point for orientation. + 2) The point not on the hull is an endpoint. We push two triangle (p0, p3, p1) and (p0, p3, p2). We test the intermediate + points to know if we must flip the orientation of the curve. + 3) The point not on the hull is an intermediate point: we emit one triangle. We test the intermediate point on the hull + to know if we must flip the orientation of the curve. + */ + if( p[0].x == p[3].x + && p[0].y == p[3].y) + { + //NOTE: case 1: endpoints are coincidents + int outsideTest = mg_cubic_outside_test(testCoords[1]); + + //NOTE: push triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest}); + mg_push_vertex_cubic(backend, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest}); + mg_push_vertex_cubic(backend, p[2], (vec4){vec4_expand_xyz(testCoords[2]), outsideTest}); + + for(int i=0; i<3; i++) + { + indices[i] = baseIndex + i; + } + } + else + { + //NOTE: find point not on the hull + int insidePointIndex = -1; + { + bool present[4] = {0}; + for(int i=0; i<3; i++) + { + present[convexHullIndices[i]] = true; + } + for(int i=0; i<4; i++) + { + if(!present[i]) + { + insidePointIndex = i; + break; + } + } + } + DEBUG_ASSERT(insidePointIndex >= 0 && insidePointIndex < 4); + + if(insidePointIndex == 0 || insidePointIndex == 3) + { + //NOTE: case 2: the point inside the hull is an endpoint + + int outsideTest0 = mg_cubic_outside_test(testCoords[1]); + int outsideTest1 = mg_cubic_outside_test(testCoords[2]); + + //NOTE: push triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest0}); + mg_push_vertex_cubic(backend, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest0}); + mg_push_vertex_cubic(backend, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest0}); + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest1}); + mg_push_vertex_cubic(backend, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest1}); + mg_push_vertex_cubic(backend, p[2], (vec4){vec4_expand_xyz(testCoords[2]), outsideTest1}); + + for(int i=0; i<6; i++) + { + indices[i] = baseIndex + i; + } + } + else + { + int testIndex = (insidePointIndex == 1) ? 2 : 1; + int outsideTest = mg_cubic_outside_test(testCoords[testIndex]); + + //NOTE: push triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, + p[convexHullIndices[i]], + (vec4){vec4_expand_xyz(testCoords[convexHullIndices[i]]), + outsideTest}); + } + + for(int i=0; i<3; i++) + { + indices[i] = baseIndex + i; + } + } + } + } + else + { + DEBUG_ASSERT(convexHullCount == 4); + /*NOTE(martin): + We build a fan from the hull, starting from an endpoint. For each triangle, we test the vertex that is an intermediate + control point for orientation + */ + int endPointIndex = -1; + for(int i=0; i<4; i++) + { + if(convexHullIndices[i] == 0 || convexHullIndices[i] == 3) + { + endPointIndex = i; + break; + } + } + ASSERT(endPointIndex >= 0); + + int fanIndices[6] = {convexHullIndices[endPointIndex], + convexHullIndices[(endPointIndex + 1)%4], + convexHullIndices[(endPointIndex + 2)%4], + convexHullIndices[endPointIndex], + convexHullIndices[(endPointIndex + 2)%4], + convexHullIndices[(endPointIndex + 3)%4]}; + + //NOTE: fan indices on the hull are (0,1,2)(0,2,3). So if the 3rd vertex of the hull is an intermediate point it works + // as a test vertex for both triangles. Otherwise, the test vertices on the fan are 1 and 5. + int outsideTest0 = 1; + int outsideTest1 = 1; + + if( fanIndices[2] == 1 + ||fanIndices[2] == 2) + { + outsideTest0 = outsideTest1 = mg_cubic_outside_test(testCoords[fanIndices[2]]); + } + else + { + DEBUG_ASSERT(fanIndices[1] == 1 || fanIndices[1] == 2); + DEBUG_ASSERT(fanIndices[5] == 1 || fanIndices[5] == 2); + + outsideTest0 = mg_cubic_outside_test(testCoords[fanIndices[1]]); + outsideTest1 = mg_cubic_outside_test(testCoords[fanIndices[5]]); + } + + //NOTE: push triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, p[fanIndices[i]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i]]), outsideTest0}); + } + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, p[fanIndices[i+3]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i+3]]), outsideTest1}); + } + + for(int i=0; i<6; i++) + { + indices[i] = baseIndex + i; + } + } +} + +//NOTE(martin): global path fill + +void mg_render_fill(mg_gl_canvas_backend* backend, mg_path_elt* elements, mg_path_descriptor* path) +{ + u32 eltCount = path->count; + vec2 startPoint = path->startPoint; + vec2 endPoint = path->startPoint; + vec2 currentPoint = path->startPoint; + + for(int eltIndex=0; eltIndexp[0], elt->p[1], elt->p[2]}; + + switch(elt->type) + { + case MG_PATH_MOVE: + { + startPoint = elt->p[0]; + endPoint = elt->p[0]; + currentPoint = endPoint; + continue; + } break; + + case MG_PATH_LINE: + { + endPoint = controlPoints[1]; + } break; + + case MG_PATH_QUADRATIC: + { + mg_render_fill_quadratic(backend, controlPoints); + endPoint = controlPoints[2]; + + } break; + + case MG_PATH_CUBIC: + { + mg_render_fill_cubic(backend, controlPoints); + endPoint = controlPoints[3]; + } break; + } + + //NOTE(martin): now fill interior triangle + u32 baseIndex = mg_vertices_base_index(backend); + int* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, startPoint); + mg_push_vertex(backend, currentPoint); + mg_push_vertex(backend, endPoint); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + + currentPoint = endPoint; + } +} + +//----------------------------------------------------------------------------------------------------------- +// Path Stroking +//----------------------------------------------------------------------------------------------------------- + +void mg_render_stroke_line(mg_gl_canvas_backend* backend, vec2 p[2], mg_attributes* attributes) +{ + //NOTE(martin): get normals multiplied by halfWidth + f32 halfW = attributes->width/2; + + vec2 n0 = {p[0].y - p[1].y, + p[1].x - p[0].x}; + f32 norm0 = sqrt(n0.x*n0.x + n0.y*n0.y); + n0.x *= halfW/norm0; + n0.y *= halfW/norm0; + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, (vec2){p[0].x + n0.x, p[0].y + n0.y}); + mg_push_vertex(backend, (vec2){p[1].x + n0.x, p[1].y + n0.y}); + mg_push_vertex(backend, (vec2){p[1].x - n0.x, p[1].y - n0.y}); + mg_push_vertex(backend, (vec2){p[0].x - n0.x, p[0].y - n0.y}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; +} + +bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) +{ + /*NOTE: check intersection of lines (p0-p1) and (p2-p3) + + P = p0 + u(p1-p0) + P = p2 + w(p3-p2) + */ + bool found = false; + + f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); + if(fabs(den) > 0.0001) + { + f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; + f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; + + intersection->x = p0.x + u*(p1.x - p0.x); + intersection->y = p0.y + u*(p1.y - p0.y); + found = true; + } + return(found); +} + +bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) +{ + //NOTE: we should have no more than two coincident points here. This means the leg between + // those two points can't be offset, but we can set a double point at the start of first leg, + // end of first leg, or we can join the first and last leg to create a missing middle one + + vec2 legs[3][2] = {0}; + bool valid[3] = {0}; + + for(int i=0; i= 1e-6) + { + n = vec2_mul(offset/norm, n); + legs[i][0] = vec2_add(p[i], n); + legs[i][1] = vec2_add(p[i+1], n); + valid[i] = true; + } + } + + //NOTE: now we find intersections + + // first point is either the start of the first or second leg + if(valid[0]) + { + result[0] = legs[0][0]; + } + else + { + ASSERT(valid[1]); + result[0] = legs[1][0]; + } + + for(int i=1; iwidth) + ||!mg_offset_hull(3, p, negativeOffsetHull, -0.5 * attributes->width)) + { + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, 0.5, splitLeft, splitRight); + mg_render_stroke_quadratic(backend, splitLeft, attributes); + mg_render_stroke_quadratic(backend, splitRight, attributes); + } + else + { + //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance + // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: + // + // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 + // + // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter + + //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot + f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); + f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); + f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_render_stroke_quadratic(backend, splitLeft, attributes); + mg_render_stroke_quadratic(backend, splitRight, attributes); + } + else + { + //NOTE(martin): push the actual fill commands for the offset contour + + mg_next_shape(backend, attributes); + + mg_render_fill_quadratic(backend, positiveOffsetHull); + mg_render_fill_quadratic(backend, negativeOffsetHull); + + //NOTE(martin): add base triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, positiveOffsetHull[0]); + mg_push_vertex(backend, positiveOffsetHull[2]); + mg_push_vertex(backend, negativeOffsetHull[2]); + mg_push_vertex(backend, negativeOffsetHull[0]); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex + 0; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + } + #undef CHECK_SAMPLE_COUNT +} + +vec2 mg_cubic_get_point(vec2 p[4], f32 t) +{ + vec2 r; + + f32 oneMt = 1-t; + f32 oneMt2 = Square(oneMt); + f32 oneMt3 = oneMt2*oneMt; + f32 t2 = Square(t); + f32 t3 = t2*t; + + r.x = oneMt3*p[0].x + 3*oneMt2*t*p[1].x + 3*oneMt*t2*p[2].x + t3*p[3].x; + r.y = oneMt3*p[0].y + 3*oneMt2*t*p[1].y + 3*oneMt*t2*p[2].y + t3*p[3].y; + + return(r); +} + +void mg_cubic_split(vec2 p[4], f32 t, vec2 outLeft[4], vec2 outRight[4]) +{ + //NOTE(martin): split bezier curve p at parameter t, using De Casteljau's algorithm + // the q_n are the points along the hull's segments at parameter t + // the r_n are the points along the (q_n, q_n+1) segments at parameter t + // s is the split point. + + vec2 q0 = {(1-t)*p[0].x + t*p[1].x, + (1-t)*p[0].y + t*p[1].y}; + + vec2 q1 = {(1-t)*p[1].x + t*p[2].x, + (1-t)*p[1].y + t*p[2].y}; + + vec2 q2 = {(1-t)*p[2].x + t*p[3].x, + (1-t)*p[2].y + t*p[3].y}; + + vec2 r0 = {(1-t)*q0.x + t*q1.x, + (1-t)*q0.y + t*q1.y}; + + vec2 r1 = {(1-t)*q1.x + t*q2.x, + (1-t)*q1.y + t*q2.y}; + + vec2 s = {(1-t)*r0.x + t*r1.x, + (1-t)*r0.y + t*r1.y};; + + outLeft[0] = p[0]; + outLeft[1] = q0; + outLeft[2] = r0; + outLeft[3] = s; + + outRight[0] = s; + outRight[1] = r1; + outRight[2] = q2; + outRight[3] = p[3]; +} + +void mg_render_stroke_cubic(mg_gl_canvas_backend* backend, vec2 p[4], mg_attributes* attributes) +{ + //NOTE: check degenerate line cases + f32 equalEps = 1e-3; + + if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) + ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) + ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) + { + vec2 line[2] = {p[0], p[3]}; + mg_render_stroke_line(backend, line, attributes); + return; + } + else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; + mg_render_stroke_line(backend, line, attributes); + return; + } + else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; + mg_render_stroke_line(backend, line, attributes); + return; + } + + #define CHECK_SAMPLE_COUNT 5 + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + vec2 positiveOffsetHull[4]; + vec2 negativeOffsetHull[4]; + + if( !mg_offset_hull(4, p, positiveOffsetHull, 0.5 * attributes->width) + || !mg_offset_hull(4, p, negativeOffsetHull, -0.5 * attributes->width)) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, 0.5, splitLeft, splitRight); + mg_render_stroke_cubic(backend, splitLeft, attributes); + mg_render_stroke_cubic(backend, splitRight, attributes); + return; + } + + //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance + // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: + // + // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 + // + // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter + + //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot + f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); + f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); + f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_render_stroke_cubic(backend, splitLeft, attributes); + mg_render_stroke_cubic(backend, splitRight, attributes); + + //TODO: render joint between the split curves + } + else + { + //NOTE(martin): push the actual fill commands for the offset contour + mg_next_shape(backend, attributes); + + mg_render_fill_cubic(backend, positiveOffsetHull); + mg_render_fill_cubic(backend, negativeOffsetHull); + + //NOTE(martin): add base triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, positiveOffsetHull[0]); + mg_push_vertex(backend, positiveOffsetHull[3]); + mg_push_vertex(backend, negativeOffsetHull[3]); + mg_push_vertex(backend, negativeOffsetHull[0]); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex + 0; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + #undef CHECK_SAMPLE_COUNT +} + +void mg_stroke_cap(mg_gl_canvas_backend* backend, vec2 p0, vec2 direction, mg_attributes* attributes) +{ + //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point + + f32 dn = sqrt(Square(direction.x) + Square(direction.y)); + f32 alpha = 0.5 * attributes->width/dn; + + vec2 n0 = {-alpha*direction.y, + alpha*direction.x}; + + vec2 m0 = {alpha*direction.x, + alpha*direction.y}; + + mg_next_shape(backend, attributes); + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, (vec2){p0.x + n0.x, p0.y + n0.y}); + mg_push_vertex(backend, (vec2){p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}); + mg_push_vertex(backend, (vec2){p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}); + mg_push_vertex(backend, (vec2){p0.x - n0.x, p0.y - n0.y}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; +} + +void mg_stroke_joint(mg_gl_canvas_backend* backend, + vec2 p0, + vec2 t0, + vec2 t1, + mg_attributes* attributes) +{ + //NOTE(martin): compute the normals at the joint point + f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); + f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); + + vec2 n0 = {-t0.y, t0.x}; + n0.x /= norm_t0; + n0.y /= norm_t0; + + vec2 n1 = {-t1.y, t1.x}; + n1.x /= norm_t1; + n1.y /= norm_t1; + + //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. + // we flip them to face outwards if needed + f32 crossZ = n0.x*n1.y - n0.y*n1.x; + if(crossZ > 0) + { + n0.x *= -1; + n0.y *= -1; + n1.x *= -1; + n1.y *= -1; + } + + mg_next_shape(backend, attributes); + + //NOTE(martin): use the same code as hull offset to find mitter point... + /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 + then v = u * (2*offset / norm(u)^2) + (this can be derived from writing the pythagoras theorems in the triangles of the joint) + */ + f32 halfW = 0.5 * attributes->width; + vec2 u = {n0.x + n1.x, n0.y + n1.y}; + f32 uNormSquare = u.x*u.x + u.y*u.y; + f32 alpha = attributes->width / uNormSquare; + vec2 v = {u.x * alpha, u.y * alpha}; + + f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); + + if( attributes->joint == MG_JOINT_MITER + && excursionSquare <= Square(attributes->maxJointExcursion)) + { + vec2 mitterPoint = {p0.x + v.x, p0.y + v.y}; + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, p0); + mg_push_vertex(backend, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); + mg_push_vertex(backend, mitterPoint); + mg_push_vertex(backend, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + else + { + //NOTE(martin): add a bevel joint + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, p0); + mg_push_vertex(backend, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); + mg_push_vertex(backend, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); + + DEBUG_ASSERT(!isnan(n0.x) && !isnan(n0.y) && !isnan(n1.x) && !isnan(n1.y)); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + } +} + +void mg_render_stroke_element(mg_gl_canvas_backend* backend, + mg_path_elt* element, + mg_attributes* attributes, + vec2 currentPoint, + vec2* startTangent, + vec2* endTangent, + vec2* endPoint) +{ + vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; + int endPointIndex = 0; + mg_next_shape(backend, attributes); + + switch(element->type) + { + case MG_PATH_LINE: + mg_render_stroke_line(backend, controlPoints, attributes); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_render_stroke_quadratic(backend, controlPoints, attributes); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_render_stroke_cubic(backend, controlPoints, attributes); + endPointIndex = 3; + break; + + case MG_PATH_MOVE: + ASSERT(0, "should be unreachable"); + break; + } + + //NOTE: ensure tangents are properly computed even in presence of coincident points + //TODO: see if we can do this in a less hacky way + + for(int i=1; i<4; i++) + { + if( controlPoints[i].x != controlPoints[0].x + || controlPoints[i].y != controlPoints[0].y) + { + *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, + .y = controlPoints[i].y - controlPoints[0].y}; + break; + } + } + *endPoint = controlPoints[endPointIndex]; + + for(int i=endPointIndex-1; i>=0; i++) + { + if( controlPoints[i].x != endPoint->x + || controlPoints[i].y != endPoint->y) + { + *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, + .y = endPoint->y - controlPoints[i].y}; + break; + } + } + + DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); +} + +u32 mg_render_stroke_subpath(mg_gl_canvas_backend* backend, + mg_path_elt* elements, + mg_path_descriptor* path, + mg_attributes* attributes, + u32 startIndex, + vec2 startPoint) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(startIndex < eltCount); + + vec2 currentPoint = startPoint; + vec2 endPoint = {0, 0}; + vec2 previousEndTangent = {0, 0}; + vec2 firstTangent = {0, 0}; + vec2 startTangent = {0, 0}; + vec2 endTangent = {0, 0}; + + //NOTE(martin): render first element and compute first tangent + mg_render_stroke_element(backend, elements + startIndex, attributes, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): render subsequent elements along with their joints + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_stroke_joint(backend, currentPoint, previousEndTangent, startTangent, attributes); + } + previousEndTangent = endTangent; + currentPoint = endPoint; + } + u32 subPathEltCount = eltIndex - (startIndex+1); + + //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint + if( subPathEltCount > 1 + && startPoint.x == endPoint.x + && startPoint.y == endPoint.y) + { + if(attributes->joint != MG_JOINT_NONE) + { + //NOTE(martin): add a closing joint if the path is closed + mg_stroke_joint(backend, endPoint, endTangent, firstTangent, attributes); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}, attributes); + mg_stroke_cap(backend, endPoint, startTangent, attributes); + } + + return(eltIndex); +} + + +void mg_render_stroke(mg_gl_canvas_backend* backend, + mg_path_elt* elements, + mg_path_descriptor* path, + mg_attributes* attributes) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(eltCount); + + vec2 startPoint = path->startPoint; + u32 startIndex = 0; + + while(startIndex < eltCount) + { + //NOTE(martin): eliminate leading moves + while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) + { + startPoint = elements[startIndex].p[0]; + startIndex++; + } + + if(startIndex < eltCount) + { + startIndex = mg_render_stroke_subpath(backend, elements, path, attributes, startIndex, startPoint); + } + } +} + +//-------------------------------------------------------------------- +// GL dispatch +//-------------------------------------------------------------------- #define LayoutNext(prevName, prevType, nextType) \ AlignUpOnPow2(_cat3_(LAYOUT_, prevName, _OFFSET)+_cat3_(LAYOUT_, prevType, _SIZE), _cat3_(LAYOUT_, nextType, _ALIGN)) @@ -101,7 +1545,7 @@ enum { void mg_gl_canvas_update_vertex_layout(mg_gl_canvas_backend* backend) { - backend->interface.vertexLayout = (mg_vertex_layout){ + backend->vertexLayout = (mg_vertex_layout){ .maxVertexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, .maxIndexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, .posBuffer = backend->vertexMapping + LAYOUT_POS_OFFSET, @@ -134,14 +1578,16 @@ void mg_gl_send_buffers(mg_gl_canvas_backend* backend, int shapeCount, int verte glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_INT_SIZE*indexCount, backend->indexMapping, GL_STREAM_DRAW); } -void mg_gl_canvas_begin(mg_canvas_backend* interface) +void mg_gl_canvas_begin(mg_gl_canvas_backend* backend) { - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glClearColor(backend->clearColor.r, backend->clearColor.g, backend->clearColor.b, backend->clearColor.a); + glClear(GL_COLOR_BUFFER_BIT); } -void mg_gl_canvas_end(mg_canvas_backend* interface) +void mg_gl_canvas_end(mg_gl_canvas_backend* backend) { //NOTE: nothing to do here... } @@ -154,9 +1600,9 @@ void mg_gl_canvas_clear(mg_canvas_backend* interface, mg_color clearColor) glClear(GL_COLOR_BUFFER_BIT); } -void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageInterface, u32 shapeCount, u32 vertexCount, u32 indexCount) +void mg_gl_canvas_draw_batch(mg_gl_canvas_backend* backend, mg_image_data* imageInterface, u32 shapeCount, u32 vertexCount, u32 indexCount) { - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + mg_finalize_shape(backend); /*NOTE: if we want debug_vertex while debugging, the following ensures the struct def doesn't get stripped away debug_vertex vertex; @@ -165,8 +1611,10 @@ void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageI //*/ mg_gl_send_buffers(backend, shapeCount, vertexCount, indexCount); - mp_rect frame = mg_surface_get_frame(backend->surface); - vec2 contentsScaling = mg_surface_contents_scaling(backend->surface); + mg_wgl_surface* surface = backend->surface; + + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); const int tileSize = 16; const int tileCountX = (frame.w*contentsScaling.x + tileSize - 1)/tileSize; @@ -245,29 +1693,100 @@ void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageI glDrawArrays(GL_TRIANGLES, 0, 6); mg_gl_canvas_update_vertex_layout(backend); + + mg_reset_shape_index(backend); + + backend->vertexCount = 0; + backend->indexCount = 0; } -void mg_gl_canvas_destroy(mg_canvas_backend* interface) + +void mg_gl_canvas_render(mg_canvas_backend* interface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements) { mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; - glDeleteTextures(1, &backend->outTexture); + u32 nextIndex = 0; - glDeleteBuffers(1, &backend->dummyVertexBuffer); - glDeleteBuffers(1, &backend->vertexBuffer); - glDeleteBuffers(1, &backend->shapeBuffer); - glDeleteBuffers(1, &backend->indexBuffer); - glDeleteBuffers(1, &backend->tileCounterBuffer); - glDeleteBuffers(1, &backend->tileArrayBuffer); + mg_reset_shape_index(backend); - glDeleteVertexArrays(1, &backend->vao); + backend->clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; + backend->image = mg_image_nil(); + backend->clearColor = clearColor; + mg_gl_canvas_begin(backend); - free(backend->shapeMapping); - free(backend->vertexMapping); - free(backend->indexMapping); - free(backend); + for(int i=0; i= primitiveCount) + { + log_error("invalid location '%i' in graphics command buffer would cause an overrun\n", nextIndex); + break; + } + mg_primitive* primitive = &(primitives[nextIndex]); + nextIndex++; + + if(i && primitive->attributes.image.h != backend->image.h) + { + mg_image_data* imageData = mg_image_data_from_handle(backend->image); + mg_gl_canvas_draw_batch(backend, imageData, backend->nextShapeIndex, backend->vertexCount, backend->indexCount); + backend->image = primitive->attributes.image; + } + + switch(primitive->cmd) + { + case MG_CMD_FILL: + { + mg_next_shape(backend, &primitive->attributes); + mg_render_fill(backend, + pathElements + primitive->path.startIndex, + &primitive->path); + } break; + + case MG_CMD_STROKE: + { + mg_render_stroke(backend, + pathElements + primitive->path.startIndex, + &primitive->path, + &primitive->attributes); + } break; + + case MG_CMD_JUMP: + { + if(primitive->jump == ~0) + { + //NOTE(martin): normal end of stream marker + goto exit_command_loop; + } + else if(primitive->jump >= primitiveCount) + { + log_error("invalid jump location '%i' in graphics command buffer\n", primitive->jump); + goto exit_command_loop; + } + else + { + nextIndex = primitive->jump; + } + } break; + } + } + exit_command_loop: ; + + mg_image_data* imageData = mg_image_data_from_handle(backend->image); + mg_gl_canvas_draw_batch(backend, imageData, backend->nextShapeIndex, backend->vertexCount, backend->indexCount); + + mg_gl_canvas_end(backend); + + //NOTE(martin): clear buffers + backend->vertexCount = 0; + backend->indexCount = 0; } - +//-------------------------------------------------------------------- +// Image API +//-------------------------------------------------------------------- mg_image_data* mg_gl_canvas_image_create(mg_canvas_backend* interface, vec2 size) { mg_gl_image* image = 0; @@ -305,6 +1824,31 @@ void mg_gl_canvas_image_upload_region(mg_canvas_backend* interface, glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, region.w, region.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); } +//-------------------------------------------------------------------- +// Canvas setup / destroy +//-------------------------------------------------------------------- + +void mg_gl_canvas_destroy(mg_canvas_backend* interface) +{ + mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + + glDeleteTextures(1, &backend->outTexture); + + glDeleteBuffers(1, &backend->dummyVertexBuffer); + glDeleteBuffers(1, &backend->vertexBuffer); + glDeleteBuffers(1, &backend->shapeBuffer); + glDeleteBuffers(1, &backend->indexBuffer); + glDeleteBuffers(1, &backend->tileCounterBuffer); + glDeleteBuffers(1, &backend->tileArrayBuffer); + + glDeleteVertexArrays(1, &backend->vao); + + free(backend->shapeMapping); + free(backend->vertexMapping); + free(backend->indexMapping); + free(backend); +} + static int mg_gl_compile_shader(const char* name, GLuint shader, const char* source) { int res = 0; @@ -408,34 +1952,33 @@ int mg_gl_canvas_compile_render_program_named(const char* progName, #define mg_gl_canvas_compile_render_program(progName, shaderSrc, vertexSrc, out) \ mg_gl_canvas_compile_render_program_named(progName, #shaderSrc, #vertexSrc, shaderSrc, vertexSrc, out) -mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) +mg_canvas_backend* gl_canvas_backend_create(mg_wgl_surface* surface) { - mg_wgl_surface* surface = (mg_wgl_surface*)mg_wgl_surface_create_for_window(window); - - -/* - mg_gl_canvas_backend* backend = 0; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - - int err = 0; - - if(surfaceData && surfaceData->api == MG_GL) + mg_gl_canvas_backend* backend = malloc_type(mg_gl_canvas_backend); + if(backend) { - backend = malloc_type(mg_gl_canvas_backend); + ////////////////////////////////////////////////////// + //TODO + ////////////////////////////////////////////////////// memset(backend, 0, sizeof(mg_gl_canvas_backend)); backend->surface = surface; //NOTE(martin): setup interface functions backend->interface.destroy = mg_gl_canvas_destroy; - backend->interface.begin = mg_gl_canvas_begin; - backend->interface.end = mg_gl_canvas_end; - backend->interface.clear = mg_gl_canvas_clear; - backend->interface.drawBatch = mg_gl_canvas_draw_batch; + backend->interface.render = mg_gl_canvas_render; backend->interface.imageCreate = mg_gl_canvas_image_create; backend->interface.imageDestroy = mg_gl_canvas_image_destroy; backend->interface.imageUploadRegion = mg_gl_canvas_image_upload_region; - mg_surface_prepare(surface); + /* + backend->interface.destroy = mg_gl_canvas_destroy; + backend->interface.begin = mg_gl_canvas_begin; + backend->interface.end = mg_gl_canvas_end; + backend->interface.clear = mg_gl_canvas_clear; + backend->interface.drawBatch = mg_gl_canvas_draw_batch; + */ + + surface->interface.prepare((mg_surface_data*)surface); glGenVertexArrays(1, &backend->vao); glBindVertexArray(backend->vao); @@ -455,8 +1998,8 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileArrayBuffer); glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_CANVAS_TILE_ARRAY_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - mp_rect frame = mg_surface_get_frame(backend->surface); - vec2 contentsScaling = mg_surface_contents_scaling(backend->surface); + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); glGenTextures(1, &backend->outTexture); glBindTexture(GL_TEXTURE_2D, backend->outTexture); @@ -465,6 +2008,7 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //NOTE: create programs + int err = 0; err |= mg_gl_canvas_compile_compute_program(glsl_clear_counters, &backend->clearCounterProgram); err |= mg_gl_canvas_compile_compute_program(glsl_tile, &backend->tileProgram); err |= mg_gl_canvas_compile_compute_program(glsl_sort, &backend->sortProgram); @@ -497,9 +2041,25 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) mg_gl_canvas_update_vertex_layout(backend); } } - return((mg_canvas_backend*)backend); -*/ +} +mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) +{ + mg_wgl_surface* surface = (mg_wgl_surface*)mg_wgl_surface_create_for_window(window); + + if(surface) + { + surface->interface.backend = gl_canvas_backend_create(surface); + if(surface->interface.backend) + { + surface->interface.api = MG_CANVAS; + } + else + { + surface->interface.destroy((mg_surface_data*)surface); + surface = 0; + } + } return((mg_surface_data*)surface); } diff --git a/src/graphics_surface.h b/src/graphics_surface.h index 22218cc..89e1e5c 100644 --- a/src/graphics_surface.h +++ b/src/graphics_surface.h @@ -1,158 +1,154 @@ -/************************************************************//** -* -* @file: graphics_surface.h -* @author: Martin Fouilleul -* @date: 26/04/2023 -* -*****************************************************************/ -#ifndef __GRAPHICS_SURFACE_H_ -#define __GRAPHICS_SURFACE_H_ - -#include"graphics_common.h" -#include"mp_app_internal.h" - -#ifdef __cplusplus -extern "C" { -#endif - -//--------------------------------------------------------------- -// surface interface -//--------------------------------------------------------------- -typedef struct mg_surface_data mg_surface_data; -typedef struct mg_canvas_backend mg_canvas_backend; - -typedef void (*mg_surface_destroy_proc)(mg_surface_data* surface); -typedef void (*mg_surface_prepare_proc)(mg_surface_data* surface); -typedef void (*mg_surface_present_proc)(mg_surface_data* surface); -typedef void (*mg_surface_swap_interval_proc)(mg_surface_data* surface, int swap); -typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); -typedef mp_rect (*mg_surface_get_frame_proc)(mg_surface_data* surface); -typedef void (*mg_surface_set_frame_proc)(mg_surface_data* surface, mp_rect frame); -typedef bool (*mg_surface_get_hidden_proc)(mg_surface_data* surface); -typedef void (*mg_surface_set_hidden_proc)(mg_surface_data* surface, bool hidden); -typedef void* (*mg_surface_native_layer_proc)(mg_surface_data* surface); -typedef mg_surface_id (*mg_surface_remote_id_proc)(mg_surface_data* surface); -typedef void (*mg_surface_host_connect_proc)(mg_surface_data* surface, mg_surface_id remoteId); - -typedef struct mg_surface_data -{ - mg_surface_api api; - mp_layer layer; - - mg_surface_destroy_proc destroy; - mg_surface_prepare_proc prepare; - mg_surface_present_proc present; - mg_surface_swap_interval_proc swapInterval; - mg_surface_contents_scaling_proc contentsScaling; - mg_surface_get_frame_proc getFrame; - mg_surface_set_frame_proc setFrame; - mg_surface_get_hidden_proc getHidden; - mg_surface_set_hidden_proc setHidden; - mg_surface_native_layer_proc nativeLayer; - mg_surface_remote_id_proc remoteID; - mg_surface_host_connect_proc hostConnect; - - mg_canvas_backend* backend; - -} mg_surface_data; - -mg_surface mg_surface_alloc_handle(mg_surface_data* surface); -mg_surface_data* mg_surface_data_from_handle(mg_surface handle); - -void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window); -void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height); -void mg_surface_init_host(mg_surface_data* surface, mp_window_data* window); -void mg_surface_cleanup(mg_surface_data* surface); -void* mg_surface_native_layer(mg_surface surface); - -//--------------------------------------------------------------- -// canvas backend interface -//--------------------------------------------------------------- -typedef struct mg_image_data -{ - list_elt listElt; - u32 generation; - mg_surface surface; - vec2 size; - -} mg_image_data; - -typedef struct mg_vertex_layout -{ - u32 maxVertexCount; - u32 maxIndexCount; - - char* posBuffer; - u32 posStride; - - char* cubicBuffer; - u32 cubicStride; - - char* uvTransformBuffer; - u32 uvTransformStride; - - char* colorBuffer; - u32 colorStride; - - char* texturedBuffer; - u32 texturedStride; - - char* shapeIndexBuffer; - u32 shapeIndexStride; - - char* clipBuffer; - u32 clipStride; - - char* indexBuffer; - u32 indexStride; - -} mg_vertex_layout; - - -typedef void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); -typedef void (*mg_canvas_backend_begin_proc)(mg_canvas_backend* backend, mg_color clearColor); -typedef void (*mg_canvas_backend_end_proc)(mg_canvas_backend* backend); -typedef void (*mg_canvas_backend_draw_batch_proc)(mg_canvas_backend* backend, - mg_image_data* imageData, - u32 vertexCount, - u32 shapeCount, - u32 indexCount); - - -typedef mg_image_data* (*mg_canvas_backend_image_create_proc)(mg_canvas_backend* backend, vec2 size); -typedef void (*mg_canvas_backend_image_destroy_proc)(mg_canvas_backend* backend, mg_image_data* image); -typedef void (*mg_canvas_backend_image_upload_region_proc)(mg_canvas_backend* backend, - mg_image_data* image, - mp_rect region, - u8* pixels); - -typedef void (*mg_canvas_backend_render_proc)(mg_canvas_backend* backend, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* pathElements); - -typedef struct mg_canvas_backend -{ - mg_vertex_layout vertexLayout; - - mg_canvas_backend_destroy_proc destroy; - mg_canvas_backend_begin_proc begin; - mg_canvas_backend_end_proc end; - mg_canvas_backend_draw_batch_proc drawBatch; - - mg_canvas_backend_image_create_proc imageCreate; - mg_canvas_backend_image_destroy_proc imageDestroy; - mg_canvas_backend_image_upload_region_proc imageUploadRegion; - - - mg_canvas_backend_render_proc render; - -} mg_canvas_backend; - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //__GRAPHICS_SURFACE_H_ +/************************************************************//** +* +* @file: graphics_surface.h +* @author: Martin Fouilleul +* @date: 26/04/2023 +* +*****************************************************************/ +#ifndef __GRAPHICS_SURFACE_H_ +#define __GRAPHICS_SURFACE_H_ + +#include"graphics_common.h" +#include"mp_app_internal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------- +// surface interface +//--------------------------------------------------------------- +typedef struct mg_surface_data mg_surface_data; +typedef struct mg_canvas_backend mg_canvas_backend; + +typedef void (*mg_surface_destroy_proc)(mg_surface_data* surface); +typedef void (*mg_surface_prepare_proc)(mg_surface_data* surface); +typedef void (*mg_surface_present_proc)(mg_surface_data* surface); +typedef void (*mg_surface_swap_interval_proc)(mg_surface_data* surface, int swap); +typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); +typedef mp_rect (*mg_surface_get_frame_proc)(mg_surface_data* surface); +typedef void (*mg_surface_set_frame_proc)(mg_surface_data* surface, mp_rect frame); +typedef bool (*mg_surface_get_hidden_proc)(mg_surface_data* surface); +typedef void (*mg_surface_set_hidden_proc)(mg_surface_data* surface, bool hidden); +typedef void* (*mg_surface_native_layer_proc)(mg_surface_data* surface); +typedef mg_surface_id (*mg_surface_remote_id_proc)(mg_surface_data* surface); +typedef void (*mg_surface_host_connect_proc)(mg_surface_data* surface, mg_surface_id remoteId); + +typedef struct mg_surface_data +{ + mg_surface_api api; + mp_layer layer; + + mg_surface_destroy_proc destroy; + mg_surface_prepare_proc prepare; + mg_surface_present_proc present; + mg_surface_swap_interval_proc swapInterval; + mg_surface_contents_scaling_proc contentsScaling; + mg_surface_get_frame_proc getFrame; + mg_surface_set_frame_proc setFrame; + mg_surface_get_hidden_proc getHidden; + mg_surface_set_hidden_proc setHidden; + mg_surface_native_layer_proc nativeLayer; + mg_surface_remote_id_proc remoteID; + mg_surface_host_connect_proc hostConnect; + + mg_canvas_backend* backend; + +} mg_surface_data; + +mg_surface mg_surface_alloc_handle(mg_surface_data* surface); +mg_surface_data* mg_surface_data_from_handle(mg_surface handle); + +void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window); +void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height); +void mg_surface_init_host(mg_surface_data* surface, mp_window_data* window); +void mg_surface_cleanup(mg_surface_data* surface); +void* mg_surface_native_layer(mg_surface surface); + +//--------------------------------------------------------------- +// canvas backend interface +//--------------------------------------------------------------- +typedef struct mg_image_data +{ + list_elt listElt; + u32 generation; + mg_surface surface; + vec2 size; + +} mg_image_data; + +typedef struct mg_vertex_layout +{ + u32 maxVertexCount; + u32 maxIndexCount; + + char* posBuffer; + u32 posStride; + + char* cubicBuffer; + u32 cubicStride; + + char* shapeIndexBuffer; + u32 shapeIndexStride; + + char* colorBuffer; + u32 colorStride; + + char* clipBuffer; + u32 clipStride; + + char* uvTransformBuffer; + u32 uvTransformStride; + + char* indexBuffer; + u32 indexStride; + +} mg_vertex_layout; + +typedef void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); +typedef void (*mg_canvas_backend_begin_proc)(mg_canvas_backend* backend, mg_color clearColor); +typedef void (*mg_canvas_backend_end_proc)(mg_canvas_backend* backend); +typedef void (*mg_canvas_backend_draw_batch_proc)(mg_canvas_backend* backend, + mg_image_data* imageData, + u32 vertexCount, + u32 shapeCount, + u32 indexCount); + + +typedef mg_image_data* (*mg_canvas_backend_image_create_proc)(mg_canvas_backend* backend, vec2 size); +typedef void (*mg_canvas_backend_image_destroy_proc)(mg_canvas_backend* backend, mg_image_data* image); +typedef void (*mg_canvas_backend_image_upload_region_proc)(mg_canvas_backend* backend, + mg_image_data* image, + mp_rect region, + u8* pixels); + +typedef void (*mg_canvas_backend_render_proc)(mg_canvas_backend* backend, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements); + +typedef struct mg_canvas_backend +{ +// mg_vertex_layout vertexLayout; + + mg_canvas_backend_destroy_proc destroy; + mg_canvas_backend_begin_proc begin; + mg_canvas_backend_end_proc end; + mg_canvas_backend_draw_batch_proc drawBatch; + + mg_canvas_backend_image_create_proc imageCreate; + mg_canvas_backend_image_destroy_proc imageDestroy; + mg_canvas_backend_image_upload_region_proc imageUploadRegion; + + + mg_canvas_backend_render_proc render; + +} mg_canvas_backend; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //__GRAPHICS_SURFACE_H_ diff --git a/src/mtl_renderer.m b/src/mtl_renderer.m index f17de0b..3c45b73 100644 --- a/src/mtl_renderer.m +++ b/src/mtl_renderer.m @@ -1,1475 +1,1475 @@ -/************************************************************//** -* -* @file: mtl_canvas.m -* @author: Martin Fouilleul -* @date: 12/07/2020 -* @revision: 24/01/2023 -* -*****************************************************************/ -#import -#import -#include - -#include"graphics_surface.h" -#include"macro_helpers.h" -#include"osx_app.h" - -#include"mtl_renderer.h" - -const int MG_MTL_INPUT_BUFFERS_COUNT = 3, - MG_MTL_TILE_SIZE = 16, - MG_MTL_MSAA_COUNT = 8; - -typedef struct mg_mtl_canvas_backend -{ - mg_canvas_backend interface; - mg_mtl_surface* surface; - - id pathPipeline; - id segmentPipeline; - id backpropPipeline; - id mergePipeline; - id rasterPipeline; - id blitPipeline; - - id outTexture; - - int pathBufferOffset; - int elementBufferOffset; - int bufferIndex; - dispatch_semaphore_t bufferSemaphore; - - id pathBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id elementBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id logBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id logOffsetBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - - id segmentCountBuffer; - id segmentBuffer; - id pathQueueBuffer; - id tileQueueBuffer; - id tileQueueCountBuffer; - id tileOpBuffer; - id tileOpCountBuffer; - id screenTilesBuffer; - - int msaaCount; - vec2 frameSize; - -} mg_mtl_canvas_backend; - -typedef struct mg_mtl_image_data -{ - mg_image_data interface; - id texture; -} mg_mtl_image_data; - -void mg_mtl_print_log(int bufferIndex, id logBuffer, id logOffsetBuffer) -{ - char* log = [logBuffer contents]; - int size = *(int*)[logOffsetBuffer contents]; - - if(size) - { - log_info("Log from buffer %i:\n", bufferIndex); - - int index = 0; - while(index < size) - { - int len = strlen(log+index); - printf("%s", log+index); - index += (len+1); - } - } -} - - -typedef struct mg_mtl_encoding_context -{ - int mtlEltCount; - mg_mtl_path* pathBufferData; - mg_mtl_path_elt* elementBufferData; - int pathIndex; - int localEltIndex; - mg_primitive* primitive; - vec4 pathScreenExtents; - vec4 pathUserExtents; - -} mg_mtl_encoding_context; - -static void mg_update_path_extents(vec4* extents, vec2 p) -{ - extents->x = minimum(extents->x, p.x); - extents->y = minimum(extents->y, p.y); - extents->z = maximum(extents->z, p.x); - extents->w = maximum(extents->w, p.y); -} - -void mg_mtl_canvas_encode_element(mg_mtl_encoding_context* context, mg_path_elt_type kind, vec2* p) -{ - mg_mtl_path_elt* mtlElt = &context->elementBufferData[context->mtlEltCount]; - context->mtlEltCount++; - - mtlElt->pathIndex = context->pathIndex; - int count = 0; - switch(kind) - { - case MG_PATH_LINE: - mtlElt->kind = MG_MTL_LINE; - count = 2; - break; - - case MG_PATH_QUADRATIC: - mtlElt->kind = MG_MTL_QUADRATIC; - count = 3; - break; - - case MG_PATH_CUBIC: - mtlElt->kind = MG_MTL_CUBIC; - count = 4; - break; - - default: - break; - } - - mtlElt->localEltIndex = context->localEltIndex; - - for(int i=0; ipathUserExtents, p[i]); - - vec2 screenP = mg_mat2x3_mul(context->primitive->attributes.transform, p[i]); - mtlElt->p[i] = (vector_float2){screenP.x, screenP.y}; - - mg_update_path_extents(&context->pathScreenExtents, screenP); - } -} - - -bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) -{ - /*NOTE: check intersection of lines (p0-p1) and (p2-p3) - - P = p0 + u(p1-p0) - P = p2 + w(p3-p2) - */ - bool found = false; - - f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); - if(fabs(den) > 0.0001) - { - f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; - f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; - - intersection->x = p0.x + u*(p1.x - p0.x); - intersection->y = p0.y + u*(p1.y - p0.y); - found = true; - } - return(found); -} - -bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) -{ - //NOTE: we should have no more than two coincident points here. This means the leg between - // those two points can't be offset, but we can set a double point at the start of first leg, - // end of first leg, or we can join the first and last leg to create a missing middle one - - vec2 legs[3][2] = {0}; - bool valid[3] = {0}; - - for(int i=0; i= 1e-6) - { - n = vec2_mul(offset/norm, n); - legs[i][0] = vec2_add(p[i], n); - legs[i][1] = vec2_add(p[i+1], n); - valid[i] = true; - } - } - - //NOTE: now we find intersections - - // first point is either the start of the first or second leg - if(valid[0]) - { - result[0] = legs[0][0]; - } - else - { - ASSERT(valid[1]); - result[0] = legs[1][0]; - } - - for(int i=1; iprimitive->attributes.width; - - vec2 v = {p[1].x-p[0].x, p[1].y-p[0].y}; - vec2 n = {v.y, -v.x}; - f32 norm = sqrt(n.x*n.x + n.y*n.y); - vec2 offset = vec2_mul(0.5*width/norm, n); - - vec2 left[2] = {vec2_add(p[0], offset), vec2_add(p[1], offset)}; - vec2 right[2] = {vec2_add(p[1], vec2_mul(-1, offset)), vec2_add(p[0], vec2_mul(-1, offset))}; - vec2 joint0[2] = {vec2_add(p[0], vec2_mul(-1, offset)), vec2_add(p[0], offset)}; - vec2 joint1[2] = {vec2_add(p[1], offset), vec2_add(p[1], vec2_mul(-1, offset))}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, right); - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, left); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); -} - -void mg_mtl_render_stroke_quadratic(mg_mtl_encoding_context* context, vec2* p) -{ - f32 width = context->primitive->attributes.width; - f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); - - //NOTE: check for degenerate line case - const f32 equalEps = 1e-3; - if(vec2_close(p[0], p[1], equalEps)) - { - mg_mtl_render_stroke_line(context, p+1); - return; - } - else if(vec2_close(p[1], p[2], equalEps)) - { - mg_mtl_render_stroke_line(context, p); - return; - } - - vec2 leftHull[3]; - vec2 rightHull[3]; - - if( !mg_offset_hull(3, p, leftHull, width/2) - || !mg_offset_hull(3, p, rightHull, -width/2)) - { - //TODO split and recurse - //NOTE: offsetting the hull failed, split the curve - vec2 splitLeft[3]; - vec2 splitRight[3]; - mg_quadratic_split(p, 0.5, splitLeft, splitRight); - mg_mtl_render_stroke_quadratic(context, splitLeft); - mg_mtl_render_stroke_quadratic(context, splitRight); - } - else - { - const int CHECK_SAMPLE_COUNT = 5; - f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; - - f32 d2LowBound = Square(0.5 * width - tolerance); - f32 d2HighBound = Square(0.5 * width + tolerance); - - f32 maxOvershoot = 0; - f32 maxOvershootParameter = 0; - - for(int i=0; i maxOvershoot) - { - maxOvershoot = overshoot; - maxOvershootParameter = t; - } - } - - if(maxOvershoot > 0) - { - vec2 splitLeft[3]; - vec2 splitRight[3]; - mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); - mg_mtl_render_stroke_quadratic(context, splitLeft); - mg_mtl_render_stroke_quadratic(context, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[2]; - leftHull[2] = tmp; - - mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, rightHull); - mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, leftHull); - - vec2 joint0[2] = {rightHull[2], leftHull[0]}; - vec2 joint1[2] = {leftHull[2], rightHull[0]}; - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_cubic(mg_mtl_encoding_context* context, vec2* p) -{ - f32 width = context->primitive->attributes.width; - f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); - - //NOTE: check degenerate line cases - f32 equalEps = 1e-3; - - if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) - ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) - ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) - { - vec2 line[2] = {p[0], p[3]}; - mg_mtl_render_stroke_line(context, line); - return; - } - else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) - { - vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; - mg_mtl_render_stroke_line(context, line); - return; - } - else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) - { - vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; - mg_mtl_render_stroke_line(context, line); - return; - } - - vec2 leftHull[4]; - vec2 rightHull[4]; - - if( !mg_offset_hull(4, p, leftHull, width/2) - || !mg_offset_hull(4, p, rightHull, -width/2)) - { - //TODO split and recurse - //NOTE: offsetting the hull failed, split the curve - vec2 splitLeft[4]; - vec2 splitRight[4]; - mg_cubic_split(p, 0.5, splitLeft, splitRight); - mg_mtl_render_stroke_cubic(context, splitLeft); - mg_mtl_render_stroke_cubic(context, splitRight); - } - else - { - const int CHECK_SAMPLE_COUNT = 5; - f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; - - f32 d2LowBound = Square(0.5 * width - tolerance); - f32 d2HighBound = Square(0.5 * width + tolerance); - - f32 maxOvershoot = 0; - f32 maxOvershootParameter = 0; - - for(int i=0; i maxOvershoot) - { - maxOvershoot = overshoot; - maxOvershootParameter = t; - } - } - - if(maxOvershoot > 0) - { - vec2 splitLeft[4]; - vec2 splitRight[4]; - mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); - mg_mtl_render_stroke_cubic(context, splitLeft); - mg_mtl_render_stroke_cubic(context, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[3]; - leftHull[3] = tmp; - tmp = leftHull[1]; - leftHull[1] = leftHull[2]; - leftHull[2] = tmp; - - mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, rightHull); - mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, leftHull); - - vec2 joint0[2] = {rightHull[3], leftHull[0]}; - vec2 joint1[2] = {leftHull[3], rightHull[0]}; - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_element(mg_mtl_encoding_context* context, - mg_path_elt* element, - vec2 currentPoint, - vec2* startTangent, - vec2* endTangent, - vec2* endPoint) -{ - vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; - int endPointIndex = 0; - - switch(element->type) - { - case MG_PATH_LINE: - mg_mtl_render_stroke_line(context, controlPoints); - endPointIndex = 1; - break; - - case MG_PATH_QUADRATIC: - mg_mtl_render_stroke_quadratic(context, controlPoints); - endPointIndex = 2; - break; - - case MG_PATH_CUBIC: - mg_mtl_render_stroke_cubic(context, controlPoints); - endPointIndex = 3; - break; - - case MG_PATH_MOVE: - ASSERT(0, "should be unreachable"); - break; - } - - //NOTE: ensure tangents are properly computed even in presence of coincident points - //TODO: see if we can do this in a less hacky way - - for(int i=1; i<4; i++) - { - if( controlPoints[i].x != controlPoints[0].x - || controlPoints[i].y != controlPoints[0].y) - { - *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, - .y = controlPoints[i].y - controlPoints[0].y}; - break; - } - } - *endPoint = controlPoints[endPointIndex]; - - for(int i=endPointIndex-1; i>=0; i++) - { - if( controlPoints[i].x != endPoint->x - || controlPoints[i].y != endPoint->y) - { - *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, - .y = endPoint->y - controlPoints[i].y}; - break; - } - } - DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); -} - -void mg_mtl_stroke_cap(mg_mtl_encoding_context* context, - vec2 p0, - vec2 direction) -{ - mg_attributes* attributes = &context->primitive->attributes; - - //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point - f32 dn = sqrt(Square(direction.x) + Square(direction.y)); - f32 alpha = 0.5 * attributes->width/dn; - - vec2 n0 = {-alpha*direction.y, - alpha*direction.x}; - - vec2 m0 = {alpha*direction.x, - alpha*direction.y}; - - vec2 points[] = {{p0.x + n0.x, p0.y + n0.y}, - {p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}, - {p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}, - {p0.x - n0.x, p0.y - n0.y}, - {p0.x + n0.x, p0.y + n0.y}}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); -} - -void mg_mtl_stroke_joint(mg_mtl_encoding_context* context, - vec2 p0, - vec2 t0, - vec2 t1) -{ - mg_attributes* attributes = &context->primitive->attributes; - - //NOTE(martin): compute the normals at the joint point - f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); - f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); - - vec2 n0 = {-t0.y, t0.x}; - n0.x /= norm_t0; - n0.y /= norm_t0; - - vec2 n1 = {-t1.y, t1.x}; - n1.x /= norm_t1; - n1.y /= norm_t1; - - //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. - // we flip them to face outwards if needed - f32 crossZ = n0.x*n1.y - n0.y*n1.x; - if(crossZ > 0) - { - n0.x *= -1; - n0.y *= -1; - n1.x *= -1; - n1.y *= -1; - } - - //NOTE(martin): use the same code as hull offset to find mitter point... - /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 - then v = u * (2*offset / norm(u)^2) - (this can be derived from writing the pythagoras theorems in the triangles of the joint) - */ - f32 halfW = 0.5 * attributes->width; - vec2 u = {n0.x + n1.x, n0.y + n1.y}; - f32 uNormSquare = u.x*u.x + u.y*u.y; - f32 alpha = attributes->width / uNormSquare; - vec2 v = {u.x * alpha, u.y * alpha}; - - f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); - - if( attributes->joint == MG_JOINT_MITER - && excursionSquare <= Square(attributes->maxJointExcursion)) - { - //NOTE(martin): add a mitter joint - vec2 points[] = {p0, - {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, - {p0.x + v.x, p0.y + v.y}, - {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, - p0}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); - } - else - { - //NOTE(martin): add a bevel joint - vec2 points[] = {p0, - {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, - {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, - p0}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - } -} - -u32 mg_mtl_render_stroke_subpath(mg_mtl_encoding_context* context, - mg_path_elt* elements, - mg_path_descriptor* path, - u32 startIndex, - vec2 startPoint) -{ - u32 eltCount = path->count; - DEBUG_ASSERT(startIndex < eltCount); - - vec2 currentPoint = startPoint; - vec2 endPoint = {0, 0}; - vec2 previousEndTangent = {0, 0}; - vec2 firstTangent = {0, 0}; - vec2 startTangent = {0, 0}; - vec2 endTangent = {0, 0}; - - //NOTE(martin): render first element and compute first tangent - mg_mtl_render_stroke_element(context, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); - - firstTangent = startTangent; - previousEndTangent = endTangent; - currentPoint = endPoint; - - //NOTE(martin): render subsequent elements along with their joints - - mg_attributes* attributes = &context->primitive->attributes; - - u32 eltIndex = startIndex + 1; - for(; - eltIndexjoint != MG_JOINT_NONE) - { - mg_mtl_stroke_joint(context, currentPoint, previousEndTangent, startTangent); - } - previousEndTangent = endTangent; - currentPoint = endPoint; - } - u32 subPathEltCount = eltIndex - startIndex; - - //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint - if( subPathEltCount > 1 - && startPoint.x == endPoint.x - && startPoint.y == endPoint.y) - { - if(attributes->joint != MG_JOINT_NONE) - { - //NOTE(martin): add a closing joint if the path is closed - mg_mtl_stroke_joint(context, endPoint, endTangent, firstTangent); - } - } - else if(attributes->cap == MG_CAP_SQUARE) - { - //NOTE(martin): add start and end cap - mg_mtl_stroke_cap(context, startPoint, (vec2){-startTangent.x, -startTangent.y}); - mg_mtl_stroke_cap(context, endPoint, endTangent); - } - return(eltIndex); -} - -void mg_mtl_render_stroke(mg_mtl_encoding_context* context, - mg_path_elt* elements, - mg_path_descriptor* path) -{ - u32 eltCount = path->count; - DEBUG_ASSERT(eltCount); - - vec2 startPoint = path->startPoint; - u32 startIndex = 0; - - while(startIndex < eltCount) - { - //NOTE(martin): eliminate leading moves - while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) - { - startPoint = elements[startIndex].p[0]; - startIndex++; - } - if(startIndex < eltCount) - { - startIndex = mg_mtl_render_stroke_subpath(context, elements, path, startIndex, startPoint); - } - } -} - - -void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, - mg_mtl_surface* surface, - int pathCount, - int eltCount, - mg_image_data* image, - int tileSize, - int nTilesX, - int nTilesY, - vec2 viewportSize, - f32 scale) -{ - //NOTE: encode GPU commands - @autoreleasepool - { - //NOTE: clear counters - id blitEncoder = [surface->commandBuffer blitCommandEncoder]; - blitEncoder.label = @"clear counters"; - [blitEncoder fillBuffer: backend->segmentCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder fillBuffer: backend->tileQueueCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder fillBuffer: backend->tileOpCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder endEncoding]; - - //NOTE: path setup pass - id pathEncoder = [surface->commandBuffer computeCommandEncoder]; - pathEncoder.label = @"path pass"; - [pathEncoder setComputePipelineState: backend->pathPipeline]; - - [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; - [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; - [pathEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; - [pathEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; - [pathEncoder setBuffer:backend->tileQueueCountBuffer offset:0 atIndex:4]; - [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:5]; - [pathEncoder setBytes:&scale length:sizeof(int) atIndex:6]; - - MTLSize pathGridSize = MTLSizeMake(pathCount, 1, 1); - MTLSize pathGroupSize = MTLSizeMake([backend->pathPipeline maxTotalThreadsPerThreadgroup], 1, 1); - - [pathEncoder dispatchThreads: pathGridSize threadsPerThreadgroup: pathGroupSize]; - [pathEncoder endEncoding]; - - //NOTE: segment setup pass - id segmentEncoder = [surface->commandBuffer computeCommandEncoder]; - segmentEncoder.label = @"segment pass"; - [segmentEncoder setComputePipelineState: backend->segmentPipeline]; - - [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; - [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset:backend->elementBufferOffset atIndex:1]; - [segmentEncoder setBuffer:backend->segmentCountBuffer offset:0 atIndex:2]; - [segmentEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; - [segmentEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:4]; - [segmentEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:5]; - [segmentEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:6]; - [segmentEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:7]; - [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:8]; - [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:9]; - [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:10]; - [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:11]; - - MTLSize segmentGridSize = MTLSizeMake(eltCount, 1, 1); - MTLSize segmentGroupSize = MTLSizeMake([backend->segmentPipeline maxTotalThreadsPerThreadgroup], 1, 1); - - [segmentEncoder dispatchThreads: segmentGridSize threadsPerThreadgroup: segmentGroupSize]; - [segmentEncoder endEncoding]; - - //NOTE: backprop pass - id backpropEncoder = [surface->commandBuffer computeCommandEncoder]; - backpropEncoder.label = @"backprop pass"; - [backpropEncoder setComputePipelineState: backend->backpropPipeline]; - - [backpropEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:0]; - [backpropEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:1]; - [backpropEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:2]; - [backpropEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:3]; - - MTLSize backpropGroupSize = MTLSizeMake([backend->backpropPipeline maxTotalThreadsPerThreadgroup], 1, 1); - MTLSize backpropGridSize = MTLSizeMake(pathCount*backpropGroupSize.width, 1, 1); - - [backpropEncoder dispatchThreads: backpropGridSize threadsPerThreadgroup: backpropGroupSize]; - [backpropEncoder endEncoding]; - - //NOTE: merge pass - id mergeEncoder = [surface->commandBuffer computeCommandEncoder]; - mergeEncoder.label = @"merge pass"; - [mergeEncoder setComputePipelineState: backend->mergePipeline]; - - [mergeEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; - [mergeEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; - [mergeEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; - [mergeEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; - [mergeEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:4]; - [mergeEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:5]; - [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:6]; - [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:7]; - [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:8]; - [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:9]; - [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:10]; - - MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); - MTLSize mergeGroupSize = MTLSizeMake(16, 16, 1); - - [mergeEncoder dispatchThreads: mergeGridSize threadsPerThreadgroup: mergeGroupSize]; - [mergeEncoder endEncoding]; - - //NOTE: raster pass - id rasterEncoder = [surface->commandBuffer computeCommandEncoder]; - rasterEncoder.label = @"raster pass"; - [rasterEncoder setComputePipelineState: backend->rasterPipeline]; - - [rasterEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:0]; - [rasterEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:1]; - [rasterEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:2]; - [rasterEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; - [rasterEncoder setBytes:&tileSize length:sizeof(int) atIndex:4]; - [rasterEncoder setBytes:&scale length:sizeof(float) atIndex:5]; - [rasterEncoder setBytes:&backend->msaaCount length:sizeof(int) atIndex:6]; - [rasterEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:7]; - [rasterEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:8]; - - [rasterEncoder setTexture:backend->outTexture atIndex:0]; - - int useTexture = 0; - if(image) - { - mg_mtl_image_data* mtlImage = (mg_mtl_image_data*)image; - [rasterEncoder setTexture: mtlImage->texture atIndex: 1]; - useTexture = 1; - } - [rasterEncoder setBytes: &useTexture length:sizeof(int) atIndex: 9]; - - MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); - MTLSize rasterGroupSize = MTLSizeMake(16, 16, 1); - [rasterEncoder dispatchThreads: rasterGridSize threadsPerThreadgroup: rasterGroupSize]; - - [rasterEncoder endEncoding]; - - //NOTE: blit pass - MTLViewport viewport = {0, 0, viewportSize.x, viewportSize.y, 0, 1}; - - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; - renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; - renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - - id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - renderEncoder.label = @"blit pass"; - [renderEncoder setViewport: viewport]; - [renderEncoder setRenderPipelineState: backend->blitPipeline]; - [renderEncoder setFragmentTexture: backend->outTexture atIndex: 0]; - [renderEncoder drawPrimitives: MTLPrimitiveTypeTriangle - vertexStart: 0 - vertexCount: 3 ]; - [renderEncoder endEncoding]; - } -} - -void mg_mtl_canvas_resize(mg_mtl_canvas_backend* backend, vec2 size) -{ - @autoreleasepool - { - if(backend->screenTilesBuffer) - { - [backend->screenTilesBuffer release]; - backend->screenTilesBuffer = nil; - } - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(size.x + tileSize - 1)/tileSize; - int nTilesY = (int)(size.y + tileSize - 1)/tileSize; - MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; - backend->screenTilesBuffer = [backend->surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) - options: bufferOptions]; - - if(backend->outTexture) - { - [backend->outTexture release]; - backend->outTexture = nil; - } - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModePrivate; - texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = size.x; - texDesc.height = size.y; - - backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; - - backend->frameSize = size; - } -} - -void mg_mtl_canvas_render(mg_canvas_backend* interface, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* pathElements) -{ - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - - //NOTE: update rolling buffers - dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); - backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; - - mg_mtl_path_elt* elementBufferData = (mg_mtl_path_elt*)[backend->elementBuffer[backend->bufferIndex] contents]; - mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; - - ///////////////////////////////////////////////////////////////////////////////////// - //TODO: ensure screen tiles buffer is correct size - ///////////////////////////////////////////////////////////////////////////////////// - - //NOTE: prepare rendering - mg_mtl_surface* surface = backend->surface; - - mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); - - f32 scale = surface->mtlLayer.contentsScale; - vec2 viewportSize = {frame.w * scale, frame.h * scale}; - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; - int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; - - if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) - { - mg_mtl_canvas_resize(backend, viewportSize); - } - - mg_mtl_surface_acquire_command_buffer(surface); - mg_mtl_surface_acquire_drawable(surface); - - @autoreleasepool - { - //NOTE: clear log counter - id blitEncoder = [surface->commandBuffer blitCommandEncoder]; - blitEncoder.label = @"clear log counter"; - [blitEncoder fillBuffer: backend->logOffsetBuffer[backend->bufferIndex] range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder endEncoding]; - - //NOTE: clear screen - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; - renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clearColor.r, clearColor.g, clearColor.b, clearColor.a); - renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - - id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - renderEncoder.label = @"clear pass"; - [renderEncoder endEncoding]; - } - backend->pathBufferOffset = 0; - backend->elementBufferOffset = 0; - - //NOTE: encode and render batches - int pathCount = 0; - vec2 currentPos = {0}; - - mg_image currentImage = mg_image_nil(); - mg_mtl_encoding_context context = {.mtlEltCount = 0, - .elementBufferData = elementBufferData, - .pathBufferData = pathBufferData}; - - for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) - { - mg_primitive* primitive = &primitives[primitiveIndex]; - - if(primitiveIndex && (primitive->attributes.image.h != currentImage.h)) - { - mg_image_data* imageData = mg_image_data_from_handle(currentImage); - - mg_mtl_render_batch(backend, - surface, - pathCount, - context.mtlEltCount, - imageData, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - backend->pathBufferOffset += pathCount * sizeof(mg_mtl_path); - backend->elementBufferOffset += context.mtlEltCount * sizeof(mg_mtl_path_elt); - pathCount = 0; - context.mtlEltCount = 0; - context.elementBufferData = (mg_mtl_path_elt*)((char*)elementBufferData + backend->elementBufferOffset); - context.pathBufferData = (mg_mtl_path*)((char*)pathBufferData + backend->pathBufferOffset); - } - currentImage = primitive->attributes.image; - - if(primitive->path.count) - { - context.primitive = primitive; - context.pathIndex = pathCount; - context.pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - context.pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - - if(primitive->cmd == MG_CMD_STROKE) - { - mg_mtl_render_stroke(&context, pathElements + primitive->path.startIndex, &primitive->path); - } - else - { - int segCount = 0; - for(int eltIndex = 0; - (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); - eltIndex++) - { - context.localEltIndex = segCount; - - mg_path_elt* elt = &pathElements[primitive->path.startIndex + eltIndex]; - - if(elt->type != MG_PATH_MOVE) - { - vec2 p[4] = {currentPos, elt->p[0], elt->p[1], elt->p[2]}; - mg_mtl_canvas_encode_element(&context, elt->type, p); - segCount++; - } - switch(elt->type) - { - case MG_PATH_MOVE: - currentPos = elt->p[0]; - break; - - case MG_PATH_LINE: - currentPos = elt->p[0]; - break; - - case MG_PATH_QUADRATIC: - currentPos = elt->p[1]; - break; - - case MG_PATH_CUBIC: - currentPos = elt->p[2]; - break; - } - } - } - //NOTE: push path - mg_mtl_path* path = &context.pathBufferData[pathCount]; - pathCount++; - - path->cmd = (mg_mtl_cmd)primitive->cmd; - - path->box = (vector_float4){context.pathScreenExtents.x, - context.pathScreenExtents.y, - context.pathScreenExtents.z, - context.pathScreenExtents.w}; - - path->clip = (vector_float4){primitive->attributes.clip.x, - primitive->attributes.clip.y, - primitive->attributes.clip.x + primitive->attributes.clip.w, - primitive->attributes.clip.y + primitive->attributes.clip.h}; - - path->color = (vector_float4){primitive->attributes.color.r, - primitive->attributes.color.g, - primitive->attributes.color.b, - primitive->attributes.color.a}; - - mp_rect srcRegion = primitive->attributes.srcRegion; - - mp_rect destRegion = {context.pathUserExtents.x, - context.pathUserExtents.y, - context.pathUserExtents.z - context.pathUserExtents.x, - context.pathUserExtents.w - context.pathUserExtents.y}; - - if(!mg_image_is_nil(primitive->attributes.image)) - { - vec2 texSize = mg_image_size(primitive->attributes.image); - - mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, - 0, 1/texSize.y, srcRegion.y/texSize.y}; - - mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, - 0, srcRegion.h/destRegion.h, 0}; - - mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, - 0, 1, -destRegion.y}; - - mg_mat2x3 screenToUser = mg_mat2x3_inv(primitive->attributes.transform); - - mg_mat2x3 uvTransform = srcRegionToImage; - uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); - uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); - uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); - - path->uvTransform = simd_matrix(simd_make_float3(uvTransform.m[0]/scale, uvTransform.m[3]/scale, 0), - simd_make_float3(uvTransform.m[1]/scale, uvTransform.m[4]/scale, 0), - simd_make_float3(uvTransform.m[2], uvTransform.m[5], 1)); - } - } - } - - mg_image_data* imageData = mg_image_data_from_handle(currentImage); - mg_mtl_render_batch(backend, - surface, - pathCount, - context.mtlEltCount, - imageData, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - @autoreleasepool - { - //NOTE: finalize - [surface->commandBuffer addCompletedHandler:^(id commandBuffer) - { - mg_mtl_print_log(backend->bufferIndex, backend->logBuffer[backend->bufferIndex], backend->logOffsetBuffer[backend->bufferIndex]); - dispatch_semaphore_signal(backend->bufferSemaphore); - }]; - } -} - -void mg_mtl_canvas_destroy(mg_canvas_backend* interface) -{ - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - - @autoreleasepool - { - [backend->pathPipeline release]; - [backend->segmentPipeline release]; - [backend->backpropPipeline release]; - [backend->mergePipeline release]; - [backend->rasterPipeline release]; - [backend->blitPipeline release]; - - for(int i=0; ipathBuffer[i] release]; - [backend->elementBuffer[i] release]; - [backend->logBuffer[i] release]; - [backend->logOffsetBuffer[i] release]; - } - [backend->segmentCountBuffer release]; - [backend->segmentBuffer release]; - [backend->tileQueueBuffer release]; - [backend->tileQueueCountBuffer release]; - [backend->tileOpBuffer release]; - [backend->tileOpCountBuffer release]; - [backend->screenTilesBuffer release]; - } - - free(backend); -} - -mg_image_data* mg_mtl_canvas_image_create(mg_canvas_backend* interface, vec2 size) -{ - mg_mtl_image_data* image = 0; - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - mg_mtl_surface* surface = backend->surface; - - @autoreleasepool - { - image = malloc_type(mg_mtl_image_data); - if(image) - { - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModeManaged; - texDesc.usage = MTLTextureUsageShaderRead; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = size.x; - texDesc.height = size.y; - - image->texture = [surface->device newTextureWithDescriptor:texDesc]; - if(image->texture != nil) - { - [image->texture retain]; - image->interface.size = size; - } - else - { - free(image); - image = 0; - } - } - } - return((mg_image_data*)image); -} - -void mg_mtl_canvas_image_destroy(mg_canvas_backend* backendInterface, mg_image_data* imageInterface) -{ - mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; - @autoreleasepool - { - [image->texture release]; - free(image); - } -} - -void mg_mtl_canvas_image_upload_region(mg_canvas_backend* backendInterface, mg_image_data* imageInterface, mp_rect region, u8* pixels) -{@autoreleasepool{ - mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; - MTLRegion mtlRegion = MTLRegionMake2D(region.x, region.y, region.w, region.h); - [image->texture replaceRegion:mtlRegion - mipmapLevel:0 - withBytes:(void*)pixels - bytesPerRow: 4 * region.w]; -}} - -const u32 MG_MTL_PATH_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path), - MG_MTL_ELEMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_elt), - MG_MTL_SEGMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_segment), - MG_MTL_PATH_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_queue), - MG_MTL_TILE_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_queue), - MG_MTL_TILE_OP_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_op); - -mg_canvas_backend* mtl_canvas_backend_create(mg_mtl_surface* surface) -{ - mg_mtl_canvas_backend* backend = 0; - - backend = malloc_type(mg_mtl_canvas_backend); - memset(backend, 0, sizeof(mg_mtl_canvas_backend)); - - backend->msaaCount = MG_MTL_MSAA_COUNT; - backend->surface = surface; - - //NOTE(martin): setup interface functions - backend->interface.destroy = mg_mtl_canvas_destroy; - backend->interface.render = mg_mtl_canvas_render; - backend->interface.imageCreate = mg_mtl_canvas_image_create; - backend->interface.imageDestroy = mg_mtl_canvas_image_destroy; - backend->interface.imageUploadRegion = mg_mtl_canvas_image_upload_region; - - @autoreleasepool{ - //NOTE: load metal library - str8 shaderPath = mp_app_get_resource_path(mem_scratch(), "mtl_renderer.metallib"); - NSString* metalFileName = [[NSString alloc] initWithBytes: shaderPath.ptr length:shaderPath.len encoding: NSUTF8StringEncoding]; - NSError* err = 0; - id library = [surface->device newLibraryWithFile: metalFileName error:&err]; - if(err != nil) - { - const char* errStr = [[err localizedDescription] UTF8String]; - log_error("error : %s\n", errStr); - return(0); - } - id pathFunction = [library newFunctionWithName:@"mtl_path_setup"]; - id segmentFunction = [library newFunctionWithName:@"mtl_segment_setup"]; - id backpropFunction = [library newFunctionWithName:@"mtl_backprop"]; - id mergeFunction = [library newFunctionWithName:@"mtl_merge"]; - id rasterFunction = [library newFunctionWithName:@"mtl_raster"]; - id vertexFunction = [library newFunctionWithName:@"mtl_vertex_shader"]; - id fragmentFunction = [library newFunctionWithName:@"mtl_fragment_shader"]; - - //NOTE: create pipelines - NSError* error = NULL; - - backend->pathPipeline = [surface->device newComputePipelineStateWithFunction: pathFunction - error:&error]; - - backend->segmentPipeline = [surface->device newComputePipelineStateWithFunction: segmentFunction - error:&error]; - - backend->backpropPipeline = [surface->device newComputePipelineStateWithFunction: backpropFunction - error:&error]; - - backend->mergePipeline = [surface->device newComputePipelineStateWithFunction: mergeFunction - error:&error]; - - backend->rasterPipeline = [surface->device newComputePipelineStateWithFunction: rasterFunction - error:&error]; - - MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; - pipelineStateDescriptor.label = @"blit pipeline"; - pipelineStateDescriptor.vertexFunction = vertexFunction; - pipelineStateDescriptor.fragmentFunction = fragmentFunction; - pipelineStateDescriptor.colorAttachments[0].pixelFormat = surface->mtlLayer.pixelFormat; - pipelineStateDescriptor.colorAttachments[0].blendingEnabled = YES; - pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; - pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; - pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; - pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; - pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - - backend->blitPipeline = [surface->device newRenderPipelineStateWithDescriptor: pipelineStateDescriptor error:&err]; - - //NOTE: create textures - mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); - f32 scale = surface->mtlLayer.contentsScale; - - backend->frameSize = (vec2){frame.w*scale, frame.h*scale}; - - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModePrivate; - texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = backend->frameSize.x; - texDesc.height = backend->frameSize.y; - - backend->outTexture = [surface->device newTextureWithDescriptor:texDesc]; - - //NOTE: create buffers - - backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_INPUT_BUFFERS_COUNT); - backend->bufferIndex = 0; - - MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined - | MTLResourceStorageModeShared; - - for(int i=0; ipathBuffer[i] = [surface->device newBufferWithLength: MG_MTL_PATH_BUFFER_SIZE - options: bufferOptions]; - - backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_ELEMENT_BUFFER_SIZE - options: bufferOptions]; - } - - bufferOptions = MTLResourceStorageModePrivate; - backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_SEGMENT_BUFFER_SIZE - options: bufferOptions]; - - backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_PATH_QUEUE_BUFFER_SIZE - options: bufferOptions]; - - backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_QUEUE_BUFFER_SIZE - options: bufferOptions]; - - backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_OP_BUFFER_SIZE - options: bufferOptions]; - - backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; - int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; - backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) - options: bufferOptions]; - - bufferOptions = MTLResourceStorageModeShared; - for(int i=0; ilogBuffer[i] = [surface->device newBufferWithLength: 1<<20 - options: bufferOptions]; - - backend->logOffsetBuffer[i] = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - } - } - return((mg_canvas_backend*)backend); -} - -mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window) -{ - mg_mtl_surface* surface = (mg_mtl_surface*)mg_mtl_surface_create_for_window(window); - - if(surface) - { - surface->interface.backend = mtl_canvas_backend_create(surface); - if(surface->interface.backend) - { - surface->interface.api = MG_CANVAS; - } - else - { - surface->interface.destroy((mg_surface_data*)surface); - surface = 0; - } - } - return((mg_surface_data*)surface); -} +/************************************************************//** +* +* @file: mtl_canvas.m +* @author: Martin Fouilleul +* @date: 12/07/2020 +* @revision: 24/01/2023 +* +*****************************************************************/ +#import +#import +#include + +#include"graphics_surface.h" +#include"macro_helpers.h" +#include"osx_app.h" + +#include"mtl_renderer.h" + +const int MG_MTL_INPUT_BUFFERS_COUNT = 3, + MG_MTL_TILE_SIZE = 16, + MG_MTL_MSAA_COUNT = 8; + +typedef struct mg_mtl_canvas_backend +{ + mg_canvas_backend interface; + mg_mtl_surface* surface; + + id pathPipeline; + id segmentPipeline; + id backpropPipeline; + id mergePipeline; + id rasterPipeline; + id blitPipeline; + + id outTexture; + + int pathBufferOffset; + int elementBufferOffset; + int bufferIndex; + dispatch_semaphore_t bufferSemaphore; + + id pathBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id elementBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id logBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id logOffsetBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + + id segmentCountBuffer; + id segmentBuffer; + id pathQueueBuffer; + id tileQueueBuffer; + id tileQueueCountBuffer; + id tileOpBuffer; + id tileOpCountBuffer; + id screenTilesBuffer; + + int msaaCount; + vec2 frameSize; + +} mg_mtl_canvas_backend; + +typedef struct mg_mtl_image_data +{ + mg_image_data interface; + id texture; +} mg_mtl_image_data; + +void mg_mtl_print_log(int bufferIndex, id logBuffer, id logOffsetBuffer) +{ + char* log = [logBuffer contents]; + int size = *(int*)[logOffsetBuffer contents]; + + if(size) + { + log_info("Log from buffer %i:\n", bufferIndex); + + int index = 0; + while(index < size) + { + int len = strlen(log+index); + printf("%s", log+index); + index += (len+1); + } + } +} + + +typedef struct mg_mtl_encoding_context +{ + int mtlEltCount; + mg_mtl_path* pathBufferData; + mg_mtl_path_elt* elementBufferData; + int pathIndex; + int localEltIndex; + mg_primitive* primitive; + vec4 pathScreenExtents; + vec4 pathUserExtents; + +} mg_mtl_encoding_context; + +static void mg_update_path_extents(vec4* extents, vec2 p) +{ + extents->x = minimum(extents->x, p.x); + extents->y = minimum(extents->y, p.y); + extents->z = maximum(extents->z, p.x); + extents->w = maximum(extents->w, p.y); +} + +void mg_mtl_canvas_encode_element(mg_mtl_encoding_context* context, mg_path_elt_type kind, vec2* p) +{ + mg_mtl_path_elt* mtlElt = &context->elementBufferData[context->mtlEltCount]; + context->mtlEltCount++; + + mtlElt->pathIndex = context->pathIndex; + int count = 0; + switch(kind) + { + case MG_PATH_LINE: + mtlElt->kind = MG_MTL_LINE; + count = 2; + break; + + case MG_PATH_QUADRATIC: + mtlElt->kind = MG_MTL_QUADRATIC; + count = 3; + break; + + case MG_PATH_CUBIC: + mtlElt->kind = MG_MTL_CUBIC; + count = 4; + break; + + default: + break; + } + + mtlElt->localEltIndex = context->localEltIndex; + + for(int i=0; ipathUserExtents, p[i]); + + vec2 screenP = mg_mat2x3_mul(context->primitive->attributes.transform, p[i]); + mtlElt->p[i] = (vector_float2){screenP.x, screenP.y}; + + mg_update_path_extents(&context->pathScreenExtents, screenP); + } +} + + +bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) +{ + /*NOTE: check intersection of lines (p0-p1) and (p2-p3) + + P = p0 + u(p1-p0) + P = p2 + w(p3-p2) + */ + bool found = false; + + f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); + if(fabs(den) > 0.0001) + { + f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; + f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; + + intersection->x = p0.x + u*(p1.x - p0.x); + intersection->y = p0.y + u*(p1.y - p0.y); + found = true; + } + return(found); +} + +bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) +{ + //NOTE: we should have no more than two coincident points here. This means the leg between + // those two points can't be offset, but we can set a double point at the start of first leg, + // end of first leg, or we can join the first and last leg to create a missing middle one + + vec2 legs[3][2] = {0}; + bool valid[3] = {0}; + + for(int i=0; i= 1e-6) + { + n = vec2_mul(offset/norm, n); + legs[i][0] = vec2_add(p[i], n); + legs[i][1] = vec2_add(p[i+1], n); + valid[i] = true; + } + } + + //NOTE: now we find intersections + + // first point is either the start of the first or second leg + if(valid[0]) + { + result[0] = legs[0][0]; + } + else + { + ASSERT(valid[1]); + result[0] = legs[1][0]; + } + + for(int i=1; iprimitive->attributes.width; + + vec2 v = {p[1].x-p[0].x, p[1].y-p[0].y}; + vec2 n = {v.y, -v.x}; + f32 norm = sqrt(n.x*n.x + n.y*n.y); + vec2 offset = vec2_mul(0.5*width/norm, n); + + vec2 left[2] = {vec2_add(p[0], offset), vec2_add(p[1], offset)}; + vec2 right[2] = {vec2_add(p[1], vec2_mul(-1, offset)), vec2_add(p[0], vec2_mul(-1, offset))}; + vec2 joint0[2] = {vec2_add(p[0], vec2_mul(-1, offset)), vec2_add(p[0], offset)}; + vec2 joint1[2] = {vec2_add(p[1], offset), vec2_add(p[1], vec2_mul(-1, offset))}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, right); + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, left); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); +} + +void mg_mtl_render_stroke_quadratic(mg_mtl_encoding_context* context, vec2* p) +{ + f32 width = context->primitive->attributes.width; + f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); + + //NOTE: check for degenerate line case + const f32 equalEps = 1e-3; + if(vec2_close(p[0], p[1], equalEps)) + { + mg_mtl_render_stroke_line(context, p+1); + return; + } + else if(vec2_close(p[1], p[2], equalEps)) + { + mg_mtl_render_stroke_line(context, p); + return; + } + + vec2 leftHull[3]; + vec2 rightHull[3]; + + if( !mg_offset_hull(3, p, leftHull, width/2) + || !mg_offset_hull(3, p, rightHull, -width/2)) + { + //TODO split and recurse + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, 0.5, splitLeft, splitRight); + mg_mtl_render_stroke_quadratic(context, splitLeft); + mg_mtl_render_stroke_quadratic(context, splitRight); + } + else + { + const int CHECK_SAMPLE_COUNT = 5; + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + f32 d2LowBound = Square(0.5 * width - tolerance); + f32 d2HighBound = Square(0.5 * width + tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_mtl_render_stroke_quadratic(context, splitLeft); + mg_mtl_render_stroke_quadratic(context, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[2]; + leftHull[2] = tmp; + + mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, rightHull); + mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, leftHull); + + vec2 joint0[2] = {rightHull[2], leftHull[0]}; + vec2 joint1[2] = {leftHull[2], rightHull[0]}; + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_cubic(mg_mtl_encoding_context* context, vec2* p) +{ + f32 width = context->primitive->attributes.width; + f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); + + //NOTE: check degenerate line cases + f32 equalEps = 1e-3; + + if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) + ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) + ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) + { + vec2 line[2] = {p[0], p[3]}; + mg_mtl_render_stroke_line(context, line); + return; + } + else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; + mg_mtl_render_stroke_line(context, line); + return; + } + else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; + mg_mtl_render_stroke_line(context, line); + return; + } + + vec2 leftHull[4]; + vec2 rightHull[4]; + + if( !mg_offset_hull(4, p, leftHull, width/2) + || !mg_offset_hull(4, p, rightHull, -width/2)) + { + //TODO split and recurse + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, 0.5, splitLeft, splitRight); + mg_mtl_render_stroke_cubic(context, splitLeft); + mg_mtl_render_stroke_cubic(context, splitRight); + } + else + { + const int CHECK_SAMPLE_COUNT = 5; + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + f32 d2LowBound = Square(0.5 * width - tolerance); + f32 d2HighBound = Square(0.5 * width + tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_mtl_render_stroke_cubic(context, splitLeft); + mg_mtl_render_stroke_cubic(context, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[3]; + leftHull[3] = tmp; + tmp = leftHull[1]; + leftHull[1] = leftHull[2]; + leftHull[2] = tmp; + + mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, rightHull); + mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, leftHull); + + vec2 joint0[2] = {rightHull[3], leftHull[0]}; + vec2 joint1[2] = {leftHull[3], rightHull[0]}; + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_element(mg_mtl_encoding_context* context, + mg_path_elt* element, + vec2 currentPoint, + vec2* startTangent, + vec2* endTangent, + vec2* endPoint) +{ + vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; + int endPointIndex = 0; + + switch(element->type) + { + case MG_PATH_LINE: + mg_mtl_render_stroke_line(context, controlPoints); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_mtl_render_stroke_quadratic(context, controlPoints); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_mtl_render_stroke_cubic(context, controlPoints); + endPointIndex = 3; + break; + + case MG_PATH_MOVE: + ASSERT(0, "should be unreachable"); + break; + } + + //NOTE: ensure tangents are properly computed even in presence of coincident points + //TODO: see if we can do this in a less hacky way + + for(int i=1; i<4; i++) + { + if( controlPoints[i].x != controlPoints[0].x + || controlPoints[i].y != controlPoints[0].y) + { + *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, + .y = controlPoints[i].y - controlPoints[0].y}; + break; + } + } + *endPoint = controlPoints[endPointIndex]; + + for(int i=endPointIndex-1; i>=0; i++) + { + if( controlPoints[i].x != endPoint->x + || controlPoints[i].y != endPoint->y) + { + *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, + .y = endPoint->y - controlPoints[i].y}; + break; + } + } + DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); +} + +void mg_mtl_stroke_cap(mg_mtl_encoding_context* context, + vec2 p0, + vec2 direction) +{ + mg_attributes* attributes = &context->primitive->attributes; + + //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point + f32 dn = sqrt(Square(direction.x) + Square(direction.y)); + f32 alpha = 0.5 * attributes->width/dn; + + vec2 n0 = {-alpha*direction.y, + alpha*direction.x}; + + vec2 m0 = {alpha*direction.x, + alpha*direction.y}; + + vec2 points[] = {{p0.x + n0.x, p0.y + n0.y}, + {p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}, + {p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}, + {p0.x - n0.x, p0.y - n0.y}, + {p0.x + n0.x, p0.y + n0.y}}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); +} + +void mg_mtl_stroke_joint(mg_mtl_encoding_context* context, + vec2 p0, + vec2 t0, + vec2 t1) +{ + mg_attributes* attributes = &context->primitive->attributes; + + //NOTE(martin): compute the normals at the joint point + f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); + f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); + + vec2 n0 = {-t0.y, t0.x}; + n0.x /= norm_t0; + n0.y /= norm_t0; + + vec2 n1 = {-t1.y, t1.x}; + n1.x /= norm_t1; + n1.y /= norm_t1; + + //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. + // we flip them to face outwards if needed + f32 crossZ = n0.x*n1.y - n0.y*n1.x; + if(crossZ > 0) + { + n0.x *= -1; + n0.y *= -1; + n1.x *= -1; + n1.y *= -1; + } + + //NOTE(martin): use the same code as hull offset to find mitter point... + /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 + then v = u * (2*offset / norm(u)^2) + (this can be derived from writing the pythagoras theorems in the triangles of the joint) + */ + f32 halfW = 0.5 * attributes->width; + vec2 u = {n0.x + n1.x, n0.y + n1.y}; + f32 uNormSquare = u.x*u.x + u.y*u.y; + f32 alpha = attributes->width / uNormSquare; + vec2 v = {u.x * alpha, u.y * alpha}; + + f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); + + if( attributes->joint == MG_JOINT_MITER + && excursionSquare <= Square(attributes->maxJointExcursion)) + { + //NOTE(martin): add a mitter joint + vec2 points[] = {p0, + {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, + {p0.x + v.x, p0.y + v.y}, + {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, + p0}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); + } + else + { + //NOTE(martin): add a bevel joint + vec2 points[] = {p0, + {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, + {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, + p0}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + } +} + +u32 mg_mtl_render_stroke_subpath(mg_mtl_encoding_context* context, + mg_path_elt* elements, + mg_path_descriptor* path, + u32 startIndex, + vec2 startPoint) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(startIndex < eltCount); + + vec2 currentPoint = startPoint; + vec2 endPoint = {0, 0}; + vec2 previousEndTangent = {0, 0}; + vec2 firstTangent = {0, 0}; + vec2 startTangent = {0, 0}; + vec2 endTangent = {0, 0}; + + //NOTE(martin): render first element and compute first tangent + mg_mtl_render_stroke_element(context, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): render subsequent elements along with their joints + + mg_attributes* attributes = &context->primitive->attributes; + + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_mtl_stroke_joint(context, currentPoint, previousEndTangent, startTangent); + } + previousEndTangent = endTangent; + currentPoint = endPoint; + } + u32 subPathEltCount = eltIndex - startIndex; + + //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint + if( subPathEltCount > 1 + && startPoint.x == endPoint.x + && startPoint.y == endPoint.y) + { + if(attributes->joint != MG_JOINT_NONE) + { + //NOTE(martin): add a closing joint if the path is closed + mg_mtl_stroke_joint(context, endPoint, endTangent, firstTangent); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_mtl_stroke_cap(context, startPoint, (vec2){-startTangent.x, -startTangent.y}); + mg_mtl_stroke_cap(context, endPoint, endTangent); + } + return(eltIndex); +} + +void mg_mtl_render_stroke(mg_mtl_encoding_context* context, + mg_path_elt* elements, + mg_path_descriptor* path) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(eltCount); + + vec2 startPoint = path->startPoint; + u32 startIndex = 0; + + while(startIndex < eltCount) + { + //NOTE(martin): eliminate leading moves + while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) + { + startPoint = elements[startIndex].p[0]; + startIndex++; + } + if(startIndex < eltCount) + { + startIndex = mg_mtl_render_stroke_subpath(context, elements, path, startIndex, startPoint); + } + } +} + + +void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, + mg_mtl_surface* surface, + int pathCount, + int eltCount, + mg_image_data* image, + int tileSize, + int nTilesX, + int nTilesY, + vec2 viewportSize, + f32 scale) +{ + //NOTE: encode GPU commands + @autoreleasepool + { + //NOTE: clear counters + id blitEncoder = [surface->commandBuffer blitCommandEncoder]; + blitEncoder.label = @"clear counters"; + [blitEncoder fillBuffer: backend->segmentCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder fillBuffer: backend->tileQueueCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder fillBuffer: backend->tileOpCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder endEncoding]; + + //NOTE: path setup pass + id pathEncoder = [surface->commandBuffer computeCommandEncoder]; + pathEncoder.label = @"path pass"; + [pathEncoder setComputePipelineState: backend->pathPipeline]; + + [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; + [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; + [pathEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; + [pathEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; + [pathEncoder setBuffer:backend->tileQueueCountBuffer offset:0 atIndex:4]; + [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:5]; + [pathEncoder setBytes:&scale length:sizeof(int) atIndex:6]; + + MTLSize pathGridSize = MTLSizeMake(pathCount, 1, 1); + MTLSize pathGroupSize = MTLSizeMake([backend->pathPipeline maxTotalThreadsPerThreadgroup], 1, 1); + + [pathEncoder dispatchThreads: pathGridSize threadsPerThreadgroup: pathGroupSize]; + [pathEncoder endEncoding]; + + //NOTE: segment setup pass + id segmentEncoder = [surface->commandBuffer computeCommandEncoder]; + segmentEncoder.label = @"segment pass"; + [segmentEncoder setComputePipelineState: backend->segmentPipeline]; + + [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; + [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset:backend->elementBufferOffset atIndex:1]; + [segmentEncoder setBuffer:backend->segmentCountBuffer offset:0 atIndex:2]; + [segmentEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; + [segmentEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:4]; + [segmentEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:5]; + [segmentEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:6]; + [segmentEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:7]; + [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:8]; + [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:9]; + [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:10]; + [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:11]; + + MTLSize segmentGridSize = MTLSizeMake(eltCount, 1, 1); + MTLSize segmentGroupSize = MTLSizeMake([backend->segmentPipeline maxTotalThreadsPerThreadgroup], 1, 1); + + [segmentEncoder dispatchThreads: segmentGridSize threadsPerThreadgroup: segmentGroupSize]; + [segmentEncoder endEncoding]; + + //NOTE: backprop pass + id backpropEncoder = [surface->commandBuffer computeCommandEncoder]; + backpropEncoder.label = @"backprop pass"; + [backpropEncoder setComputePipelineState: backend->backpropPipeline]; + + [backpropEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:0]; + [backpropEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:1]; + [backpropEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:2]; + [backpropEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:3]; + + MTLSize backpropGroupSize = MTLSizeMake([backend->backpropPipeline maxTotalThreadsPerThreadgroup], 1, 1); + MTLSize backpropGridSize = MTLSizeMake(pathCount*backpropGroupSize.width, 1, 1); + + [backpropEncoder dispatchThreads: backpropGridSize threadsPerThreadgroup: backpropGroupSize]; + [backpropEncoder endEncoding]; + + //NOTE: merge pass + id mergeEncoder = [surface->commandBuffer computeCommandEncoder]; + mergeEncoder.label = @"merge pass"; + [mergeEncoder setComputePipelineState: backend->mergePipeline]; + + [mergeEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; + [mergeEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; + [mergeEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; + [mergeEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; + [mergeEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:4]; + [mergeEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:5]; + [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:6]; + [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:7]; + [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:8]; + [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:9]; + [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:10]; + + MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); + MTLSize mergeGroupSize = MTLSizeMake(16, 16, 1); + + [mergeEncoder dispatchThreads: mergeGridSize threadsPerThreadgroup: mergeGroupSize]; + [mergeEncoder endEncoding]; + + //NOTE: raster pass + id rasterEncoder = [surface->commandBuffer computeCommandEncoder]; + rasterEncoder.label = @"raster pass"; + [rasterEncoder setComputePipelineState: backend->rasterPipeline]; + + [rasterEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:0]; + [rasterEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:1]; + [rasterEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:2]; + [rasterEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; + [rasterEncoder setBytes:&tileSize length:sizeof(int) atIndex:4]; + [rasterEncoder setBytes:&scale length:sizeof(float) atIndex:5]; + [rasterEncoder setBytes:&backend->msaaCount length:sizeof(int) atIndex:6]; + [rasterEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:7]; + [rasterEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:8]; + + [rasterEncoder setTexture:backend->outTexture atIndex:0]; + + int useTexture = 0; + if(image) + { + mg_mtl_image_data* mtlImage = (mg_mtl_image_data*)image; + [rasterEncoder setTexture: mtlImage->texture atIndex: 1]; + useTexture = 1; + } + [rasterEncoder setBytes: &useTexture length:sizeof(int) atIndex: 9]; + + MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); + MTLSize rasterGroupSize = MTLSizeMake(16, 16, 1); + [rasterEncoder dispatchThreads: rasterGridSize threadsPerThreadgroup: rasterGroupSize]; + + [rasterEncoder endEncoding]; + + //NOTE: blit pass + MTLViewport viewport = {0, 0, viewportSize.x, viewportSize.y, 0, 1}; + + MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + renderEncoder.label = @"blit pass"; + [renderEncoder setViewport: viewport]; + [renderEncoder setRenderPipelineState: backend->blitPipeline]; + [renderEncoder setFragmentTexture: backend->outTexture atIndex: 0]; + [renderEncoder drawPrimitives: MTLPrimitiveTypeTriangle + vertexStart: 0 + vertexCount: 3 ]; + [renderEncoder endEncoding]; + } +} + +void mg_mtl_canvas_resize(mg_mtl_canvas_backend* backend, vec2 size) +{ + @autoreleasepool + { + if(backend->screenTilesBuffer) + { + [backend->screenTilesBuffer release]; + backend->screenTilesBuffer = nil; + } + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(size.x + tileSize - 1)/tileSize; + int nTilesY = (int)(size.y + tileSize - 1)/tileSize; + MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; + backend->screenTilesBuffer = [backend->surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) + options: bufferOptions]; + + if(backend->outTexture) + { + [backend->outTexture release]; + backend->outTexture = nil; + } + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModePrivate; + texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = size.x; + texDesc.height = size.y; + + backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; + + backend->frameSize = size; + } +} + +void mg_mtl_canvas_render(mg_canvas_backend* interface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements) +{ + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + + //NOTE: update rolling buffers + dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); + backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; + + mg_mtl_path_elt* elementBufferData = (mg_mtl_path_elt*)[backend->elementBuffer[backend->bufferIndex] contents]; + mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; + + ///////////////////////////////////////////////////////////////////////////////////// + //TODO: ensure screen tiles buffer is correct size + ///////////////////////////////////////////////////////////////////////////////////// + + //NOTE: prepare rendering + mg_mtl_surface* surface = backend->surface; + + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + + f32 scale = surface->mtlLayer.contentsScale; + vec2 viewportSize = {frame.w * scale, frame.h * scale}; + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; + int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; + + if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) + { + mg_mtl_canvas_resize(backend, viewportSize); + } + + mg_mtl_surface_acquire_command_buffer(surface); + mg_mtl_surface_acquire_drawable(surface); + + @autoreleasepool + { + //NOTE: clear log counter + id blitEncoder = [surface->commandBuffer blitCommandEncoder]; + blitEncoder.label = @"clear log counter"; + [blitEncoder fillBuffer: backend->logOffsetBuffer[backend->bufferIndex] range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder endEncoding]; + + //NOTE: clear screen + MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clearColor.r, clearColor.g, clearColor.b, clearColor.a); + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + renderEncoder.label = @"clear pass"; + [renderEncoder endEncoding]; + } + backend->pathBufferOffset = 0; + backend->elementBufferOffset = 0; + + //NOTE: encode and render batches + int pathCount = 0; + vec2 currentPos = {0}; + + mg_image currentImage = mg_image_nil(); + mg_mtl_encoding_context context = {.mtlEltCount = 0, + .elementBufferData = elementBufferData, + .pathBufferData = pathBufferData}; + + for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) + { + mg_primitive* primitive = &primitives[primitiveIndex]; + + if(primitiveIndex && (primitive->attributes.image.h != currentImage.h)) + { + mg_image_data* imageData = mg_image_data_from_handle(currentImage); + + mg_mtl_render_batch(backend, + surface, + pathCount, + context.mtlEltCount, + imageData, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + backend->pathBufferOffset += pathCount * sizeof(mg_mtl_path); + backend->elementBufferOffset += context.mtlEltCount * sizeof(mg_mtl_path_elt); + pathCount = 0; + context.mtlEltCount = 0; + context.elementBufferData = (mg_mtl_path_elt*)((char*)elementBufferData + backend->elementBufferOffset); + context.pathBufferData = (mg_mtl_path*)((char*)pathBufferData + backend->pathBufferOffset); + } + currentImage = primitive->attributes.image; + + if(primitive->path.count) + { + context.primitive = primitive; + context.pathIndex = pathCount; + context.pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + context.pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + if(primitive->cmd == MG_CMD_STROKE) + { + mg_mtl_render_stroke(&context, pathElements + primitive->path.startIndex, &primitive->path); + } + else + { + int segCount = 0; + for(int eltIndex = 0; + (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); + eltIndex++) + { + context.localEltIndex = segCount; + + mg_path_elt* elt = &pathElements[primitive->path.startIndex + eltIndex]; + + if(elt->type != MG_PATH_MOVE) + { + vec2 p[4] = {currentPos, elt->p[0], elt->p[1], elt->p[2]}; + mg_mtl_canvas_encode_element(&context, elt->type, p); + segCount++; + } + switch(elt->type) + { + case MG_PATH_MOVE: + currentPos = elt->p[0]; + break; + + case MG_PATH_LINE: + currentPos = elt->p[0]; + break; + + case MG_PATH_QUADRATIC: + currentPos = elt->p[1]; + break; + + case MG_PATH_CUBIC: + currentPos = elt->p[2]; + break; + } + } + } + //NOTE: push path + mg_mtl_path* path = &context.pathBufferData[pathCount]; + pathCount++; + + path->cmd = (mg_mtl_cmd)primitive->cmd; + + path->box = (vector_float4){context.pathScreenExtents.x, + context.pathScreenExtents.y, + context.pathScreenExtents.z, + context.pathScreenExtents.w}; + + path->clip = (vector_float4){primitive->attributes.clip.x, + primitive->attributes.clip.y, + primitive->attributes.clip.x + primitive->attributes.clip.w, + primitive->attributes.clip.y + primitive->attributes.clip.h}; + + path->color = (vector_float4){primitive->attributes.color.r, + primitive->attributes.color.g, + primitive->attributes.color.b, + primitive->attributes.color.a}; + + mp_rect srcRegion = primitive->attributes.srcRegion; + + mp_rect destRegion = {context.pathUserExtents.x, + context.pathUserExtents.y, + context.pathUserExtents.z - context.pathUserExtents.x, + context.pathUserExtents.w - context.pathUserExtents.y}; + + if(!mg_image_is_nil(primitive->attributes.image)) + { + vec2 texSize = mg_image_size(primitive->attributes.image); + + mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, + 0, 1/texSize.y, srcRegion.y/texSize.y}; + + mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, + 0, srcRegion.h/destRegion.h, 0}; + + mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, + 0, 1, -destRegion.y}; + + mg_mat2x3 screenToUser = mg_mat2x3_inv(primitive->attributes.transform); + + mg_mat2x3 uvTransform = srcRegionToImage; + uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); + + path->uvTransform = simd_matrix(simd_make_float3(uvTransform.m[0]/scale, uvTransform.m[3]/scale, 0), + simd_make_float3(uvTransform.m[1]/scale, uvTransform.m[4]/scale, 0), + simd_make_float3(uvTransform.m[2], uvTransform.m[5], 1)); + } + } + } + + mg_image_data* imageData = mg_image_data_from_handle(currentImage); + mg_mtl_render_batch(backend, + surface, + pathCount, + context.mtlEltCount, + imageData, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + @autoreleasepool + { + //NOTE: finalize + [surface->commandBuffer addCompletedHandler:^(id commandBuffer) + { + mg_mtl_print_log(backend->bufferIndex, backend->logBuffer[backend->bufferIndex], backend->logOffsetBuffer[backend->bufferIndex]); + dispatch_semaphore_signal(backend->bufferSemaphore); + }]; + } +} + +void mg_mtl_canvas_destroy(mg_canvas_backend* interface) +{ + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + + @autoreleasepool + { + [backend->pathPipeline release]; + [backend->segmentPipeline release]; + [backend->backpropPipeline release]; + [backend->mergePipeline release]; + [backend->rasterPipeline release]; + [backend->blitPipeline release]; + + for(int i=0; ipathBuffer[i] release]; + [backend->elementBuffer[i] release]; + [backend->logBuffer[i] release]; + [backend->logOffsetBuffer[i] release]; + } + [backend->segmentCountBuffer release]; + [backend->segmentBuffer release]; + [backend->tileQueueBuffer release]; + [backend->tileQueueCountBuffer release]; + [backend->tileOpBuffer release]; + [backend->tileOpCountBuffer release]; + [backend->screenTilesBuffer release]; + } + + free(backend); +} + +mg_image_data* mg_mtl_canvas_image_create(mg_canvas_backend* interface, vec2 size) +{ + mg_mtl_image_data* image = 0; + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + mg_mtl_surface* surface = backend->surface; + + @autoreleasepool + { + image = malloc_type(mg_mtl_image_data); + if(image) + { + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModeManaged; + texDesc.usage = MTLTextureUsageShaderRead; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = size.x; + texDesc.height = size.y; + + image->texture = [surface->device newTextureWithDescriptor:texDesc]; + if(image->texture != nil) + { + [image->texture retain]; + image->interface.size = size; + } + else + { + free(image); + image = 0; + } + } + } + return((mg_image_data*)image); +} + +void mg_mtl_canvas_image_destroy(mg_canvas_backend* backendInterface, mg_image_data* imageInterface) +{ + mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; + @autoreleasepool + { + [image->texture release]; + free(image); + } +} + +void mg_mtl_canvas_image_upload_region(mg_canvas_backend* backendInterface, mg_image_data* imageInterface, mp_rect region, u8* pixels) +{@autoreleasepool{ + mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; + MTLRegion mtlRegion = MTLRegionMake2D(region.x, region.y, region.w, region.h); + [image->texture replaceRegion:mtlRegion + mipmapLevel:0 + withBytes:(void*)pixels + bytesPerRow: 4 * region.w]; +}} + +const u32 MG_MTL_PATH_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path), + MG_MTL_ELEMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_elt), + MG_MTL_SEGMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_segment), + MG_MTL_PATH_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_queue), + MG_MTL_TILE_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_queue), + MG_MTL_TILE_OP_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_op); + +mg_canvas_backend* mtl_canvas_backend_create(mg_mtl_surface* surface) +{ + mg_mtl_canvas_backend* backend = 0; + + backend = malloc_type(mg_mtl_canvas_backend); + memset(backend, 0, sizeof(mg_mtl_canvas_backend)); + + backend->msaaCount = MG_MTL_MSAA_COUNT; + backend->surface = surface; + + //NOTE(martin): setup interface functions + backend->interface.destroy = mg_mtl_canvas_destroy; + backend->interface.render = mg_mtl_canvas_render; + backend->interface.imageCreate = mg_mtl_canvas_image_create; + backend->interface.imageDestroy = mg_mtl_canvas_image_destroy; + backend->interface.imageUploadRegion = mg_mtl_canvas_image_upload_region; + + @autoreleasepool{ + //NOTE: load metal library + str8 shaderPath = mp_app_get_resource_path(mem_scratch(), "mtl_renderer.metallib"); + NSString* metalFileName = [[NSString alloc] initWithBytes: shaderPath.ptr length:shaderPath.len encoding: NSUTF8StringEncoding]; + NSError* err = 0; + id library = [surface->device newLibraryWithFile: metalFileName error:&err]; + if(err != nil) + { + const char* errStr = [[err localizedDescription] UTF8String]; + log_error("error : %s\n", errStr); + return(0); + } + id pathFunction = [library newFunctionWithName:@"mtl_path_setup"]; + id segmentFunction = [library newFunctionWithName:@"mtl_segment_setup"]; + id backpropFunction = [library newFunctionWithName:@"mtl_backprop"]; + id mergeFunction = [library newFunctionWithName:@"mtl_merge"]; + id rasterFunction = [library newFunctionWithName:@"mtl_raster"]; + id vertexFunction = [library newFunctionWithName:@"mtl_vertex_shader"]; + id fragmentFunction = [library newFunctionWithName:@"mtl_fragment_shader"]; + + //NOTE: create pipelines + NSError* error = NULL; + + backend->pathPipeline = [surface->device newComputePipelineStateWithFunction: pathFunction + error:&error]; + + backend->segmentPipeline = [surface->device newComputePipelineStateWithFunction: segmentFunction + error:&error]; + + backend->backpropPipeline = [surface->device newComputePipelineStateWithFunction: backpropFunction + error:&error]; + + backend->mergePipeline = [surface->device newComputePipelineStateWithFunction: mergeFunction + error:&error]; + + backend->rasterPipeline = [surface->device newComputePipelineStateWithFunction: rasterFunction + error:&error]; + + MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineStateDescriptor.label = @"blit pipeline"; + pipelineStateDescriptor.vertexFunction = vertexFunction; + pipelineStateDescriptor.fragmentFunction = fragmentFunction; + pipelineStateDescriptor.colorAttachments[0].pixelFormat = surface->mtlLayer.pixelFormat; + pipelineStateDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + backend->blitPipeline = [surface->device newRenderPipelineStateWithDescriptor: pipelineStateDescriptor error:&err]; + + //NOTE: create textures + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + f32 scale = surface->mtlLayer.contentsScale; + + backend->frameSize = (vec2){frame.w*scale, frame.h*scale}; + + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModePrivate; + texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = backend->frameSize.x; + texDesc.height = backend->frameSize.y; + + backend->outTexture = [surface->device newTextureWithDescriptor:texDesc]; + + //NOTE: create buffers + + backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_INPUT_BUFFERS_COUNT); + backend->bufferIndex = 0; + + MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined + | MTLResourceStorageModeShared; + + for(int i=0; ipathBuffer[i] = [surface->device newBufferWithLength: MG_MTL_PATH_BUFFER_SIZE + options: bufferOptions]; + + backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_ELEMENT_BUFFER_SIZE + options: bufferOptions]; + } + + bufferOptions = MTLResourceStorageModePrivate; + backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_SEGMENT_BUFFER_SIZE + options: bufferOptions]; + + backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_PATH_QUEUE_BUFFER_SIZE + options: bufferOptions]; + + backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_QUEUE_BUFFER_SIZE + options: bufferOptions]; + + backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_OP_BUFFER_SIZE + options: bufferOptions]; + + backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; + int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; + backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) + options: bufferOptions]; + + bufferOptions = MTLResourceStorageModeShared; + for(int i=0; ilogBuffer[i] = [surface->device newBufferWithLength: 1<<20 + options: bufferOptions]; + + backend->logOffsetBuffer[i] = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + } + } + return((mg_canvas_backend*)backend); +} + +mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window) +{ + mg_mtl_surface* surface = (mg_mtl_surface*)mg_mtl_surface_create_for_window(window); + + if(surface) + { + surface->interface.backend = mtl_canvas_backend_create(surface); + if(surface->interface.backend) + { + surface->interface.api = MG_CANVAS; + } + else + { + surface->interface.destroy((mg_surface_data*)surface); + surface = 0; + } + } + return((mg_surface_data*)surface); +}