[mtl canvas] testing the metal canvas rendering the ghostscript tiger, and acknowledging it's painfully slow

This commit is contained in:
Martin Fouilleul 2023-03-18 14:35:51 +01:00
parent 1d36088302
commit 92f4909d63
12 changed files with 4792 additions and 313 deletions

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
DEBUG_FLAGS="-g -O1 -DDEBUG -DLOG_COMPILE_DEBUG" DEBUG_FLAGS="-g -DDEBUG -DLOG_COMPILE_DEBUG"
#DEBUG_FLAGS="-O3" #DEBUG_FLAGS="-O3"
#-------------------------------------------------------------- #--------------------------------------------------------------

View File

@ -165,7 +165,7 @@ int main()
mg_circle_fill(x, y, 200); mg_circle_fill(x, y, 200);
// smile // smile
f32 frown = frameTime > 0.033 ? 100 : 0; f32 frown = frameTime > 0.033 ? -100 : 0;
mg_set_color_rgba(0, 0, 0, 1); mg_set_color_rgba(0, 0, 0, 1);
mg_set_width(20); mg_set_width(20);

View File

@ -106,7 +106,7 @@ int main()
//NOTE: create surface, canvas and font //NOTE: create surface, canvas and font
mg_surface surface = mg_surface_create_for_window(window, MG_BACKEND_DEFAULT); mg_surface surface = mg_surface_create_for_window(window, MG_BACKEND_DEFAULT);
mg_surface_swap_interval(surface, 1); mg_surface_swap_interval(surface, 0);
mg_canvas canvas = mg_canvas_create(surface); mg_canvas canvas = mg_canvas_create(surface);
@ -286,9 +286,10 @@ int main()
mg_fill(); mg_fill();
f64 startFlushTime = mp_get_time(MP_CLOCK_MONOTONIC); f64 startFlushTime = mp_get_time(MP_CLOCK_MONOTONIC);
mg_surface_prepare(surface); mg_surface_prepare(surface);
mg_flush(); mg_flush();
f64 startPresentTime = mp_get_time(MP_CLOCK_MONOTONIC); f64 startPresentTime = mp_get_time(MP_CLOCK_MONOTONIC);

4
examples/tiger/build.bat Normal file
View File

@ -0,0 +1,4 @@
set INCLUDES=/I ..\..\src /I ..\..\src\util /I ..\..\src\platform /I ../../ext /I ../../ext/angle_headers
cl /we4013 /Zi /Zc:preprocessor /std:c11 %INCLUDES% main.c /link /LIBPATH:../../bin milepost.dll.lib /out:../../bin/example_canvas.exe

11
examples/tiger/build.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
BINDIR=../../bin
RESDIR=../../resources
SRCDIR=../../src
INCLUDES="-I$SRCDIR -I$SRCDIR/util -I$SRCDIR/platform -I$SRCDIR/app"
LIBS="-L$BINDIR -lmilepost"
FLAGS="-mmacos-version-min=10.15.4 -DDEBUG -DLOG_COMPILE_DEBUG"
clang -g $FLAGS $LIBS $INCLUDES -o $BINDIR/example_tiger main.c

154
examples/tiger/main.c Normal file
View File

@ -0,0 +1,154 @@
/************************************************************//**
*
* @file: main.cpp
* @author: Martin Fouilleul
* @date: 30/07/2022
* @revision:
*
*****************************************************************/
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#define _USE_MATH_DEFINES //NOTE: necessary for MSVC
#include<math.h>
#include"milepost.h"
#define LOG_SUBSYSTEM "Main"
#include"tiger.c"
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()
{
LogLevel(LOG_LEVEL_WARNING);
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_BACKEND_DEFAULT);
mg_surface_swap_interval(surface, 0);
//TODO: create canvas
mg_canvas canvas = mg_canvas_create(surface);
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);
f64 frameTime = 0;
while(!mp_should_quit())
{
f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC);
mp_pump_events(0);
mp_event event = {0};
while(mp_next_event(&event))
{
switch(event.type)
{
case MP_EVENT_WINDOW_CLOSE:
{
mp_request_quit();
} break;
case MP_EVENT_KEYBOARD_KEY:
{
} break;
default:
break;
}
}
mg_surface_prepare(surface);
mg_set_color_rgba(1, 0, 1, 1);
mg_clear();
mg_matrix_push((mg_mat2x3){1, 0, 300,
0, 1, 200});
draw_tiger();
mg_matrix_pop();
// 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_flush();
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);
}

227
examples/tiger/svg2mg.py Normal file
View File

@ -0,0 +1,227 @@
import xml.etree.ElementTree as et
import sys
def expect_char(s, i, c):
if i >= len(s) or c != s[i]:
print("error: expected character " + c + " at index " + str(i))
exit()
return(i+1)
def accept_char(s, i, c):
if i < len(s) and c == s[i]:
i += 1
return(i)
def consume_number(s, i):
sign = 1
if s[i] == '+':
i += 1
if s[i] == '-':
sign = -1
i += 1
res = 0
decimalPos = 0.1
while i < len(s) and s[i].isdigit() :
res *= 10
res += int(s[i])
i += 1
if i < len(s) and s[i] == '.':
i += 1
while i < len(s) and s[i].isdigit() :
res += decimalPos * int(s[i])
decimalPos *= 0.1
i += 1
return(res*sign, i)
def consume_point(s, i):
(x, i) = consume_number(s, i)
i = accept_char(s, i, ',')
(y, i) = consume_number(s, i)
return(x, y, i)
def f2s(x):
return f'{x:.3f}'
class svgContext:
sp = (0, 0)
cp = (0, 0)
rp = (0, 0)
def reset(self):
self.cp = self.sp = self.rp = (0,0)
def move_to(self, rel, x, y):
if rel:
x += self.cp[0]
y += self.cp[1]
print("\tmg_move_to(" + f2s(x) + ", " + f2s(y) + ");")
self.sp = (x, y)
self.cp = (x, y)
self.rp = self.cp
def curve_to(self, rel, x1, y1, x2, y2, x3, y3):
if rel:
x1 += self.cp[0]
y1 += self.cp[1]
x2 += self.cp[0]
y2 += self.cp[1]
x3 += self.cp[0]
y3 += self.cp[1]
print("\tmg_cubic_to(" + f2s(x1) + ", " + f2s(y1) + ", " + f2s(x2) + ", " + f2s(y2) + ", " + f2s(x3) + ", " + f2s(y3) + ");")
self.rp = (x2, y2)
self.cp = (x3, y3)
def smooth_curve_to(self, rel, x2, y2, x3, y3):
if rel:
x2 += self.cp[0]
y2 += self.cp[1]
x3 += self.cp[0]
y3 += self.cp[1]
x1 = 2*self.cp[0] - self.rp[0]
y1 = 2*self.cp[1] - self.rp[1]
print("\tmg_cubic_to(" + f2s(x1) + ", " + f2s(y1) + ", " + f2s(x2) + ", " + f2s(y2) + ", " + f2s(x3) + ", " + f2s(y3) + ");")
self.rp = (x2, y2)
self.cp = (x3, y3)
def line_to(self, rel, x1, y1):
if rel:
x1 += self.cp[0]
y1 += self.cp[1]
print("\tmg_line_to(" + f2s(x1) + ", " + f2s(y1) + ");")
self.cp = (x1, y1)
self.rp = self.cp
def vertical_to(self, rel, y1):
if rel:
y1 += self.cp[1]
x1 = self.cp[0]
print("\tmg_line_to(" + f2s(x1) + ", " + f2s(y1) + ");")
self.cp = (x1, y1)
self.rp = self.cp
def close_path(self):
print("\tmg_close_path();");
self.cp = self.rp = self.sp
def print_path(path, ctx):
# print("path " + path.get('id') + ":")
d = path.get('d')
index = 0
c = d[index]
while index < len(d):
c = d[index]
index += 1
rel = c.islower()
c = c.lower()
if c == 'm':
(x, y, index) = consume_point(d, index)
ctx.move_to(rel, x, y)
while index < len(d) and (d[index] == ',' or d[index] == '+' or d[index] == '-'):
index = accept_char(d, index, ',')
(x, y, index) = consume_point(d, index)
ctx.move_to(rel, x, y)
elif c == 'l':
(x1, y1, index) = consume_point(d, index)
ctx.line_to(rel, x1, y1)
while index < len(d) and (d[index] == ',' or d[index] == '+' or d[index] == '-'):
index = accept_char(d, index, ',')
(x1, y1, index) = consume_point(d, index)
ctx.line_to(rel, x1, y1)
elif c == 'v':
(y1, index) = consume_number(d, index)
ctx.vertical_to(rel, y1)
elif c == 'c':
(x1, y1, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x2, y2, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x3, y3, index) = consume_point(d, index)
ctx.curve_to(rel, x1, y1, x2, y2, x3, y3)
while index < len(d) and (d[index] == ',' or d[index] == '+' or d[index] == '-'):
index = accept_char(d, index, ',')
(x1, y1, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x2, y2, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x3, y3, index) = consume_point(d, index)
ctx.curve_to(rel, x1, y1, x2, y2, x3, y3)
elif c == 's':
(x2, y2, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x3, y3, index) = consume_point(d, index)
ctx.smooth_curve_to(rel, x2, y2, x3, y3)
while index < len(d) and (d[index] == ',' or d[index] == '+' or d[index] == '-'):
index = accept_char(d, index, ',')
(x2, y2, index) = consume_point(d, index)
index = accept_char(d, index, ',')
(x3, y3, index) = consume_point(d, index)
ctx.smooth_curve_to(rel, x2, y2, x3, y3)
elif c == 'z':
ctx.close_path()
index += 1
else:
print('error: unrecognized command')
exit()
def parse_color(s):
s = s.lstrip('#')
if len(s) == 3:
return (int(s[0]+s[0], 16)/255., int(s[1]+s[1], 16)/255., int(s[2]+s[2], 16)/255.)
elif len(s) == 6:
return (int(s[0:2], 16)/255., int(s[2:4], 16)/255., int(s[4:6], 16)/255.)
else:
print("error: unrecognized color: " + s)
exit()
tree = et.parse('./Ghostscript_Tiger.svg')
ctx = svgContext()
print("void draw_tiger()")
print("{")
for g in tree.iter('{http://www.w3.org/2000/svg}g'):
for path in g.findall('{http://www.w3.org/2000/svg}path'):
ctx.reset()
print_path(path, ctx)
fill = g.get('fill')
stroke = g.get('stroke')
stroke_width = g.get('stroke-width')
if fill != None and fill != "none":
(r, g, b) = parse_color(fill)
print("\tmg_set_color_rgba(" + f2s(r) + ", " + f2s(g) + ", " + f2s(b) + ", 1);")
print("\tmg_fill();")
if stroke_width != None:
print("\tmg_set_width(" + stroke_width + ");");
if stroke != None and stroke != "none":
(r, g, b) = parse_color(stroke)
if fill != None:
ctx.reset()
print_path(path, ctx)
print("\tmg_set_color_rgba(" + f2s(r) + ", " + f2s(g) + ", " + f2s(b) + ", 1);")
print("\tmg_stroke();")
if (stroke == None or stroke == 'none') and (fill == None or fill == ''):
print("error, group " + g.get("id") + " has no command")
print("")
print("}")

3900
examples/tiger/tiger.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -720,6 +720,7 @@ void mg_push_command(mg_canvas_data* canvas, mg_primitive primitive)
{ {
//NOTE(martin): push primitive and updates current stream, eventually patching a pending jump. //NOTE(martin): push primitive and updates current stream, eventually patching a pending jump.
ASSERT(canvas->primitiveCount < MG_MAX_PRIMITIVE_COUNT); ASSERT(canvas->primitiveCount < MG_MAX_PRIMITIVE_COUNT);
canvas->primitives[canvas->primitiveCount] = primitive; canvas->primitives[canvas->primitiveCount] = primitive;
canvas->primitives[canvas->primitiveCount].attributes = canvas->attributes; canvas->primitives[canvas->primitiveCount].attributes = canvas->attributes;
canvas->primitives[canvas->primitiveCount].attributes.transform = mg_matrix_stack_top(canvas); canvas->primitives[canvas->primitiveCount].attributes.transform = mg_matrix_stack_top(canvas);
@ -736,9 +737,11 @@ void mg_new_path(mg_canvas_data* canvas)
void mg_path_push_elements(mg_canvas_data* canvas, u32 count, mg_path_elt* elements) void mg_path_push_elements(mg_canvas_data* canvas, u32 count, mg_path_elt* elements)
{ {
ASSERT(canvas->path.count + canvas->path.startIndex + count <= MG_MAX_PATH_ELEMENT_COUNT); ASSERT(canvas->path.count + canvas->path.startIndex + count < MG_MAX_PATH_ELEMENT_COUNT);
memcpy(canvas->pathElements + canvas->path.startIndex + canvas->path.count, elements, count*sizeof(mg_path_elt)); memcpy(canvas->pathElements + canvas->path.startIndex + canvas->path.count, elements, count*sizeof(mg_path_elt));
canvas->path.count += count; canvas->path.count += count;
ASSERT(canvas->path.count < MG_MAX_PATH_ELEMENT_COUNT);
} }
void mg_path_push_element(mg_canvas_data* canvas, mg_path_elt elt) void mg_path_push_element(mg_canvas_data* canvas, mg_path_elt elt)
@ -840,8 +843,8 @@ void mg_push_vertex_cubic(mg_canvas_data* canvas, vec2 pos, vec4 cubic)
vec2 screenPos = mg_mat2x3_mul(canvas->transform, pos); vec2 screenPos = mg_mat2x3_mul(canvas->transform, pos);
mg_vertex_layout* layout = &canvas->backend->vertexLayout; mg_vertex_layout* layout = &canvas->backend->vertexLayout;
DEBUG_ASSERT(canvas->vertexCount < layout->maxVertexCount); ASSERT(canvas->vertexCount < layout->maxVertexCount);
DEBUG_ASSERT(canvas->nextShapeIndex > 0); ASSERT(canvas->nextShapeIndex > 0);
int shapeIndex = maximum(0, canvas->nextShapeIndex-1); int shapeIndex = maximum(0, canvas->nextShapeIndex-1);
u32 index = canvas->vertexCount; u32 index = canvas->vertexCount;
@ -925,6 +928,12 @@ void mg_split_and_fill_cubic(mg_canvas_data* canvas, vec2 p[4], f32 tSplit)
mg_render_fill_cubic(canvas, subPointsHigh); mg_render_fill_cubic(canvas, 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_canvas_data* canvas, vec2 p[4]) void mg_render_fill_cubic(mg_canvas_data* canvas, vec2 p[4])
{ {
LOG_DEBUG("graphics render fill cubic\n"); LOG_DEBUG("graphics render fill cubic\n");
@ -953,7 +962,7 @@ void mg_render_fill_cubic(mg_canvas_data* canvas, vec2 p[4])
f32 c3y = 3.0*p[1].y - 3.0*p[2].y + p[3].y - p[0].y; f32 c3y = 3.0*p[1].y - 3.0*p[2].y + p[3].y - p[0].y;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//TODO(martin): we should do the tex coords computations in f64 and scale them to avoid f32 precision/range glitches in shader //TODO(martin): we shouldn't need scaling here since now we're doing our shader math in fixed point?
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
c1x /= 10; c1x /= 10;
c1y /= 10; c1y /= 10;
@ -1226,143 +1235,174 @@ void mg_render_fill_cubic(mg_canvas_data* canvas, vec2 p[4])
} while(currentPointIndex != leftMostPointIndex && i<4); } 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 //TODO: quick fix, maybe later cull degenerate hulls beforehand
if(convexHullCount <= 2) if(convexHullCount <= 2)
{ {
//NOTE(martin): if convex hull has only two point, we have a degenerate cubic that displays nothing. //NOTE(martin): if convex hull has only two point, we have a degenerate cubic that displays nothing.
return; return;
} }
else if(convexHullCount == 3)
//NOTE(martin): rearrange convex hull to put p0 first
int startIndex = -1;
int orderedHullIndices[4];
for(int i=0; i<convexHullCount; i++)
{ {
if(convexHullIndices[i] == 0) /*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)
{ {
startIndex = i; //NOTE: case 1: endpoints are coincidents
int outsideTest = mg_cubic_outside_test(testCoords[1]);
//NOTE: push triangle
u32 baseIndex = mg_vertices_base_index(canvas);
i32* indices = mg_reserve_indices(canvas, 3);
mg_push_vertex_cubic(canvas, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest});
mg_push_vertex_cubic(canvas, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest});
mg_push_vertex_cubic(canvas, p[2], (vec4){vec4_expand_xyz(testCoords[2]), outsideTest});
for(int i=0; i<3; i++)
{
indices[i] = baseIndex + i;
}
} }
if(startIndex >= 0) else
{ {
orderedHullIndices[i-startIndex] = convexHullIndices[i]; //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(canvas);
i32* indices = mg_reserve_indices(canvas, 6);
mg_push_vertex_cubic(canvas, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest0});
mg_push_vertex_cubic(canvas, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest0});
mg_push_vertex_cubic(canvas, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest0});
mg_push_vertex_cubic(canvas, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest1});
mg_push_vertex_cubic(canvas, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest1});
mg_push_vertex_cubic(canvas, 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(canvas);
i32* indices = mg_reserve_indices(canvas, 3);
for(int i=0; i<3; i++)
{
mg_push_vertex_cubic(canvas,
p[convexHullIndices[i]],
(vec4){vec4_expand_xyz(testCoords[convexHullIndices[i]]),
outsideTest});
}
for(int i=0; i<3; i++)
{
indices[i] = baseIndex + i;
}
}
} }
} }
for(int i=0; i<startIndex; i++)
{
orderedHullIndices[convexHullCount-startIndex+i] = convexHullIndices[i];
}
//NOTE(martin): inside/outside tests for the two triangles. 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 control points p1 and p2 are always outside the covered area.
if(convexHullCount <= 3)
{
//NOTE(martin): the convex hull is a triangle
//NOTE(martin): when we degenerate from 4 control points to a 3 points convex hull, this means one of the
// control points p1 or p2 could be inside the covered area. We want to compute the test for the
// control point which belongs to the convex hull, as we know it should be outside the covered area.
//
// Since there are 3 points in the hull and p0 is the first, and p3 belongs to the hull, this means we
// must select the point of the convex hull which is neither the first, nor p3.
int testPointIndex = orderedHullIndices[1] == 3 ? orderedHullIndices[2] : orderedHullIndices[1];
int outsideTest = 1;
if(Cube(testCoords[testPointIndex].x)-testCoords[testPointIndex].y*testCoords[testPointIndex].z < 0)
{
outsideTest = -1;
}
u32 baseIndex = mg_vertices_base_index(canvas);
i32* indices = mg_reserve_indices(canvas, 3);
for(int i=0; i<3; i++)
{
vec4 cubic = testCoords[orderedHullIndices[i]];
cubic.w = outsideTest;
mg_push_vertex_cubic(canvas, p[orderedHullIndices[i]], cubic);
indices[i] = baseIndex + i;
}
}
else if(orderedHullIndices[2] == 3)
{
//NOTE(martin): p1 and p2 are not on the same side of (p0,p3). The outside test can be different for
// the two triangles
int outsideTest1 = 1;
int outsideTest2 = 1;
int testIndex = orderedHullIndices[1];
if(Cube(testCoords[testIndex].x)-testCoords[testIndex].y*testCoords[testIndex].z < 0)
{
outsideTest1 = -1;
}
testIndex = orderedHullIndices[3];
if(Cube(testCoords[testIndex].x)-testCoords[testIndex].y*testCoords[testIndex].z < 0)
{
outsideTest2 = -1;
}
u32 baseIndex = mg_vertices_base_index(canvas);
i32* indices = mg_reserve_indices(canvas, 6);
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[0]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[0]]), outsideTest1});
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[1]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[1]]), outsideTest1});
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[2]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[2]]), outsideTest1});
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[0]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[0]]), outsideTest2});
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[2]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[2]]), outsideTest2});
mg_push_vertex_cubic(canvas,
p[orderedHullIndices[3]],
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[3]]), outsideTest2});
indices[0] = baseIndex + 0;
indices[1] = baseIndex + 1;
indices[2] = baseIndex + 2;
indices[3] = baseIndex + 3;
indices[4] = baseIndex + 4;
indices[5] = baseIndex + 5;
}
else else
{ {
//NOTE(martin): if p1 and p2 are on the same side of (p0,p3), the outside test is the same for both triangles DEBUG_ASSERT(convexHullCount == 4);
int outsideTest = 1; /*NOTE(martin):
if(Cube(testCoords[1].x)-testCoords[1].y*testCoords[1].z < 0) 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++)
{ {
outsideTest = -1; 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(canvas); u32 baseIndex = mg_vertices_base_index(canvas);
i32* indices = mg_reserve_indices(canvas, 6); i32* indices = mg_reserve_indices(canvas, 6);
for(int i=0; i<4; i++) for(int i=0; i<3; i++)
{ {
mg_push_vertex_cubic(canvas, mg_push_vertex_cubic(canvas, p[fanIndices[i]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i]]), outsideTest0});
p[orderedHullIndices[i]], }
(vec4){vec4_expand_xyz(testCoords[orderedHullIndices[i]]), outsideTest}); for(int i=0; i<3; i++)
{
mg_push_vertex_cubic(canvas, p[fanIndices[i+3]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i+3]]), outsideTest1});
} }
indices[0] = baseIndex + 0; for(int i=0; i<6; i++)
indices[1] = baseIndex + 1; {
indices[2] = baseIndex + 2; indices[i] = baseIndex + i;
indices[3] = baseIndex + 0; }
indices[4] = baseIndex + 2;
indices[5] = baseIndex + 3;
} }
} }
@ -1457,79 +1497,122 @@ void mg_render_stroke_line(mg_canvas_data* canvas, vec2 p[2], mg_attributes* att
indices[5] = baseIndex + 3; indices[5] = baseIndex + 3;
} }
void mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) //TODO put these elsewhere
bool vec2_equal(vec2 v0, vec2 v1)
{ {
////////////////////////////////////////////////////////////////////////////////////// return(v0.x == v1.x && v0.y == v1.y);
//WARN: quick fix for coincident middle control points }
if(count == 4 && (fabs(p[1].x - p[2].x) < 0.01) && (fabs(p[1].y - p[2].y) < 0.01))
bool vec2_close(vec2 p0, vec2 p1, f32 tolerance)
{
f32 norm2 = (p1.x - p0.x)*(p1.x - p0.x) + (p1.y - p0.y)*(p1.y - p0.y);
return(fabs(norm2) < tolerance);
}
vec2 vec2_mul(f32 f, vec2 v)
{
return((vec2){f*v.x, f*v.y});
}
vec2 vec2_add(vec2 v0, vec2 v1)
{
return((vec2){v0.x + v1.x, v0.y + v1.y});
}
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)
{ {
vec2 hull3[3] = {p[0], p[1], p[3]}; f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den;
vec2 result3[3]; f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den;
mg_offset_hull(3, hull3, result3, offset);
result[0] = result3[0]; intersection->x = p0.x + u*(p1.x - p0.x);
result[1] = result3[1]; intersection->y = p0.y + u*(p1.y - p0.y);
result[2] = result3[1]; found = true;
result[3] = result3[2];
return;
} }
/////////////////////////////////////////////////////////////////////////////////////: return(found);
}
//TODO(martin): review edge cases (coincident points ? colinear points ? control points pointing outward end point?) bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset)
//NOTE(martin): first offset control point is just the offset of first control point {
vec2 n = {p[0].y - p[1].y, //NOTE: we should have no more than two coincident points here. This means the leg between
p[1].x - p[0].x}; // those two points can't be offset, but we can set a double point at the start of first leg,
f32 norm = sqrt(n.x*n.x + n.y*n.y); // end of first leg, or we can join the first and last leg to create a missing middle one
n.x *= offset/norm;
n.y *= offset/norm;
result[0].x = p[0].x + n.x; vec2 legs[3][2] = {0};
result[0].y = p[0].y + n.y; bool valid[3] = {0};
for(int i=0; i<count-1; i++)
{
vec2 n = {p[i].y - p[i+1].y,
p[i+1].x - p[i].x};
f32 norm = sqrt(n.x*n.x + n.y*n.y);
if(norm >= 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];
}
//NOTE(martin): subsequent offset control points are the intersection of offset control lines
for(int i=1; i<count-1; i++) for(int i=1; i<count-1; i++)
{ {
vec2 p0 = p[i-1]; //NOTE: we're computing the control point i, at the end of leg (i-1)
vec2 p1 = p[i];
vec2 p2 = p[i+1];
//NOTE(martin): get normals if(!valid[i-1])
vec2 n0 = {p0.y - p1.y, {
p1.x - p0.x}; ASSERT(valid[i]);
f32 norm0 = sqrt(n0.x*n0.x + n0.y*n0.y); result[i] = legs[i][0];
n0.x /= norm0; }
n0.y /= norm0; else if(!valid[i])
{
vec2 n1 = {p1.y - p2.y, ASSERT(valid[i-1]);
p2.x - p1.x}; result[i] = legs[i-1][0];
f32 norm1 = sqrt(n1.x*n1.x + n1.y*n1.y); }
n1.x /= norm1; else
n1.y /= norm1; {
if(!mg_intersect_hull_legs(legs[i-1][0], legs[i-1][1], legs[i][0], legs[i][1], &result[i]))
/*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 {
then v = u * (2*offset / norm(u)^2) // legs don't intersect.
(this can be derived from writing the pythagoras theorems in the triangles of the joint) return(false);
*/ }
vec2 u = {n0.x + n1.x, n0.y + n1.y}; }
f32 uNormSquare = u.x*u.x + u.y*u.y;
f32 alpha = 2*offset / uNormSquare;
vec2 v = {u.x * alpha, u.y * alpha};
result[i].x = p1.x + v.x;
result[i].y = p1.y + v.y;
ASSERT(!isnan(result[i].x));
ASSERT(!isnan(result[i].y));
} }
//NOTE(martin): last offset control point is just the offset of last control point if(valid[count-2])
n = (vec2){p[count-2].y - p[count-1].y, {
p[count-1].x - p[count-2].x}; result[count-1] = legs[count-2][1];
norm = sqrt(n.x*n.x + n.y*n.y); }
n.x *= offset/norm; else
n.y *= offset/norm; {
ASSERT(valid[count-3]);
result[count-1] = legs[count-3][1];
}
result[count-1].x = p[count-1].x + n.x; return(true);
result[count-1].y = p[count-1].y + n.y;
} }
vec2 mg_quadratic_get_point(vec2 p[3], f32 t) vec2 mg_quadratic_get_point(vec2 p[3], f32 t)
@ -1573,87 +1656,110 @@ void mg_quadratic_split(vec2 p[3], f32 t, vec2 outLeft[3], vec2 outRight[3])
} }
void mg_render_stroke_quadratic(mg_canvas_data* canvas, vec2 p[4], mg_attributes* attributes) void mg_render_stroke_quadratic(mg_canvas_data* canvas, vec2 p[3], mg_attributes* attributes)
{ {
//NOTE: check for degenerate line case
const f32 equalEps = 1e-3;
if(vec2_close(p[0], p[1], equalEps))
{
mg_render_stroke_line(canvas, p+1, attributes);
return;
}
else if(vec2_close(p[1], p[2], equalEps))
{
mg_render_stroke_line(canvas, p, attributes);
return;
}
#define CHECK_SAMPLE_COUNT 5 #define CHECK_SAMPLE_COUNT 5
f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6};
vec2 positiveOffsetHull[3]; vec2 positiveOffsetHull[3];
vec2 negativeOffsetHull[3]; vec2 negativeOffsetHull[3];
mg_offset_hull(3, p, positiveOffsetHull, 0.5 * attributes->width); if( !mg_offset_hull(3, p, positiveOffsetHull, 0.5 * attributes->width)
mg_offset_hull(3, p, negativeOffsetHull, -0.5 * attributes->width); ||!mg_offset_hull(3, p, negativeOffsetHull, -0.5 * attributes->width))
//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<CHECK_SAMPLE_COUNT; i++)
{
f32 t = checkSamples[i];
vec2 c = mg_quadratic_get_point(p, t);
vec2 cp = mg_quadratic_get_point(positiveOffsetHull, t);
vec2 cn = mg_quadratic_get_point(negativeOffsetHull, t);
f32 positiveDistSquare = Square(c.x - cp.x) + Square(c.y - cp.y);
f32 negativeDistSquare = Square(c.x - cn.x) + Square(c.y - cn.y);
f32 positiveOvershoot = maximum(positiveDistSquare - d2HighBound, d2LowBound - positiveDistSquare);
f32 negativeOvershoot = maximum(negativeDistSquare - d2HighBound, d2LowBound - negativeDistSquare);
f32 overshoot = maximum(positiveOvershoot, negativeOvershoot);
if(overshoot > maxOvershoot)
{
maxOvershoot = overshoot;
maxOvershootParameter = t;
}
}
if(maxOvershoot > 0)
{ {
//NOTE: offsetting the hull failed, split the curve
vec2 splitLeft[3]; vec2 splitLeft[3];
vec2 splitRight[3]; vec2 splitRight[3];
mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); mg_quadratic_split(p, 0.5, splitLeft, splitRight);
mg_render_stroke_quadratic(canvas, splitLeft, attributes); mg_render_stroke_quadratic(canvas, splitLeft, attributes);
mg_render_stroke_quadratic(canvas, splitRight, attributes); mg_render_stroke_quadratic(canvas, splitRight, attributes);
} }
else else
{ {
//NOTE(martin): push the actual fill commands for the offset contour //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
mg_next_shape(canvas, attributes); //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);
mg_render_fill_quadratic(canvas, positiveOffsetHull); f32 maxOvershoot = 0;
mg_render_fill_quadratic(canvas, negativeOffsetHull); f32 maxOvershootParameter = 0;
//NOTE(martin): add base triangles for(int i=0; i<CHECK_SAMPLE_COUNT; i++)
u32 baseIndex = mg_vertices_base_index(canvas); {
i32* indices = mg_reserve_indices(canvas, 6); f32 t = checkSamples[i];
mg_push_vertex(canvas, positiveOffsetHull[0]); vec2 c = mg_quadratic_get_point(p, t);
mg_push_vertex(canvas, positiveOffsetHull[2]); vec2 cp = mg_quadratic_get_point(positiveOffsetHull, t);
mg_push_vertex(canvas, negativeOffsetHull[2]); vec2 cn = mg_quadratic_get_point(negativeOffsetHull, t);
mg_push_vertex(canvas, negativeOffsetHull[0]);
indices[0] = baseIndex + 0; f32 positiveDistSquare = Square(c.x - cp.x) + Square(c.y - cp.y);
indices[1] = baseIndex + 1; f32 negativeDistSquare = Square(c.x - cn.x) + Square(c.y - cn.y);
indices[2] = baseIndex + 2;
indices[3] = baseIndex + 0; f32 positiveOvershoot = maximum(positiveDistSquare - d2HighBound, d2LowBound - positiveDistSquare);
indices[4] = baseIndex + 2; f32 negativeOvershoot = maximum(negativeDistSquare - d2HighBound, d2LowBound - negativeDistSquare);
indices[5] = baseIndex + 3;
f32 overshoot = maximum(positiveOvershoot, negativeOvershoot);
if(overshoot > 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(canvas, splitLeft, attributes);
mg_render_stroke_quadratic(canvas, splitRight, attributes);
}
else
{
//NOTE(martin): push the actual fill commands for the offset contour
mg_next_shape(canvas, attributes);
mg_render_fill_quadratic(canvas, positiveOffsetHull);
mg_render_fill_quadratic(canvas, negativeOffsetHull);
//NOTE(martin): add base triangles
u32 baseIndex = mg_vertices_base_index(canvas);
i32* indices = mg_reserve_indices(canvas, 6);
mg_push_vertex(canvas, positiveOffsetHull[0]);
mg_push_vertex(canvas, positiveOffsetHull[2]);
mg_push_vertex(canvas, negativeOffsetHull[2]);
mg_push_vertex(canvas, 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 #undef CHECK_SAMPLE_COUNT
} }
@ -1681,25 +1787,23 @@ void mg_cubic_split(vec2 p[4], f32 t, vec2 outLeft[4], vec2 outRight[4])
// the r_n are the points along the (q_n, q_n+1) 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. // s is the split point.
f32 oneMt = 1-t; vec2 q0 = {(1-t)*p[0].x + t*p[1].x,
(1-t)*p[0].y + t*p[1].y};
vec2 q0 = {oneMt*p[0].x + t*p[1].x, vec2 q1 = {(1-t)*p[1].x + t*p[2].x,
oneMt*p[0].y + t*p[1].y}; (1-t)*p[1].y + t*p[2].y};
vec2 q1 = {oneMt*p[1].x + t*p[2].x, vec2 q2 = {(1-t)*p[2].x + t*p[3].x,
oneMt*p[1].y + t*p[2].y}; (1-t)*p[2].y + t*p[3].y};
vec2 q2 = {oneMt*p[2].x + t*p[3].x, vec2 r0 = {(1-t)*q0.x + t*q1.x,
oneMt*p[2].y + t*p[3].y}; (1-t)*q0.y + t*q1.y};
vec2 r0 = {oneMt*q0.x + t*q1.x, vec2 r1 = {(1-t)*q1.x + t*q2.x,
oneMt*q0.y + t*q1.y}; (1-t)*q1.y + t*q2.y};
vec2 r1 = {oneMt*q1.x + t*q2.x, vec2 s = {(1-t)*r0.x + t*r1.x,
oneMt*q1.y + t*q2.y}; (1-t)*r0.y + t*r1.y};;
vec2 s = {oneMt*r0.x + t*r1.x,
oneMt*r0.y + t*r1.y};;
outLeft[0] = p[0]; outLeft[0] = p[0];
outLeft[1] = q0; outLeft[1] = q0;
@ -1714,14 +1818,46 @@ void mg_cubic_split(vec2 p[4], f32 t, vec2 outLeft[4], vec2 outRight[4])
void mg_render_stroke_cubic(mg_canvas_data* canvas, vec2 p[4], mg_attributes* attributes) void mg_render_stroke_cubic(mg_canvas_data* canvas, 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(canvas, 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(canvas, 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(canvas, line, attributes);
return;
}
#define CHECK_SAMPLE_COUNT 5 #define CHECK_SAMPLE_COUNT 5
f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6};
vec2 positiveOffsetHull[4]; vec2 positiveOffsetHull[4];
vec2 negativeOffsetHull[4]; vec2 negativeOffsetHull[4];
mg_offset_hull(4, p, positiveOffsetHull, 0.5 * attributes->width); if( !mg_offset_hull(4, p, positiveOffsetHull, 0.5 * attributes->width)
mg_offset_hull(4, p, negativeOffsetHull, -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(canvas, splitLeft, attributes);
mg_render_stroke_cubic(canvas, 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 //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: // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this:
@ -1945,14 +2081,33 @@ void mg_render_stroke_element(mg_canvas_data* canvas,
break; break;
} }
*startTangent = (vec2){.x = controlPoints[1].x - controlPoints[0].x, //NOTE: ensure tangents are properly computed even in presence of coincident points
.y = controlPoints[1].y - controlPoints[0].y}; //TODO: see if we can do this in a less hacky way
*endTangent = (vec2){controlPoints[endPointIndex].x - controlPoints[endPointIndex-1].x,
controlPoints[endPointIndex].y - controlPoints[endPointIndex-1].y};
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]; *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_canvas_data* canvas, u32 mg_render_stroke_subpath(mg_canvas_data* canvas,
@ -2744,10 +2899,7 @@ mg_canvas mg_canvas_create(mg_surface surface)
canvas->attributes.clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; canvas->attributes.clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX};
canvasHandle = mg_canvas_handle_alloc(canvas); canvasHandle = mg_canvas_handle_alloc(canvas);
mg_canvas_set_current(canvasHandle); mg_canvas_prepare(canvasHandle);
//TODO: move that in mg_canvas_set_current() if needed?
mg_surface_prepare(surface);
} }
} }
return(canvasHandle); return(canvasHandle);
@ -2773,13 +2925,17 @@ void mg_canvas_destroy(mg_canvas handle)
} }
} }
mg_canvas mg_canvas_set_current(mg_canvas canvas) mg_canvas mg_canvas_prepare(mg_canvas canvas)
{ {
mg_canvas old = __mgCurrentCanvasHandle; mg_canvas old = __mgCurrentCanvasHandle;
__mgCurrentCanvasHandle = canvas; __mgCurrentCanvasHandle = canvas;
__mgCurrentCanvas = mg_canvas_data_from_handle(canvas); __mgCurrentCanvas = mg_canvas_data_from_handle(canvas);
if(__mgCurrentCanvas)
{
mg_surface_prepare(__mgCurrentCanvas->surface);
}
return(old); return(old);
} }
@ -2998,16 +3154,27 @@ void mg_flush_commands(int primitiveCount, mg_primitive* primitives, mg_path_elt
void mg_flush() void mg_flush()
{ {
mg_canvas_data* canvas = __mgCurrentCanvas; mg_canvas_data* canvas = __mgCurrentCanvas;
if(!canvas) if(canvas)
{ {
return; mg_flush_commands(canvas->primitiveCount, canvas->primitives, canvas->pathElements);
canvas->primitiveCount = 0;
canvas->path.startIndex = 0;
canvas->path.count = 0;
} }
}
mg_flush_commands(canvas->primitiveCount, canvas->primitives, canvas->pathElements); void mg_present()
{
mg_canvas_data* canvas = __mgCurrentCanvas;
if(canvas)
{
mg_flush_commands(canvas->primitiveCount, canvas->primitives, canvas->pathElements);
canvas->primitiveCount = 0;
canvas->path.startIndex = 0;
canvas->path.count = 0;
canvas->primitiveCount = 0; mg_surface_present(canvas->surface);
canvas->path.startIndex = 0; }
canvas->path.count = 0;
} }
//------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------

View File

@ -174,9 +174,10 @@ MP_API bool mg_canvas_is_nil(mg_canvas canvas);
MP_API mg_canvas mg_canvas_create(mg_surface surface); MP_API mg_canvas mg_canvas_create(mg_surface surface);
MP_API void mg_canvas_destroy(mg_canvas canvas); MP_API void mg_canvas_destroy(mg_canvas canvas);
MP_API mg_canvas mg_canvas_set_current(mg_canvas canvas);
MP_API mg_canvas mg_canvas_prepare(mg_canvas canvas);
MP_API void mg_flush(void); MP_API void mg_flush(void);
MP_API void mg_present(void);
MP_API vec2 mg_canvas_size(void); MP_API vec2 mg_canvas_size(void);
//------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------

View File

@ -20,7 +20,7 @@
static const int MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH = 4<<20; static const int MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH = 4<<20;
static const int MG_MTL_MAX_BUFFERS_IN_FLIGHT = 3; static const int MG_MTL_MAX_BUFFER_AVAILABLE = 3;
typedef struct mg_mtl_canvas_backend typedef struct mg_mtl_canvas_backend
{ {
@ -49,9 +49,9 @@ typedef struct mg_mtl_canvas_backend
id<MTLTexture> backbuffer; id<MTLTexture> backbuffer;
id<MTLTexture> outTexture; id<MTLTexture> outTexture;
id<MTLBuffer> shapeBuffer[MG_MTL_MAX_BUFFERS_IN_FLIGHT]; id<MTLBuffer> shapeBuffer[MG_MTL_MAX_BUFFER_AVAILABLE];
id<MTLBuffer> vertexBuffer[MG_MTL_MAX_BUFFERS_IN_FLIGHT]; id<MTLBuffer> vertexBuffer[MG_MTL_MAX_BUFFER_AVAILABLE];
id<MTLBuffer> indexBuffer[MG_MTL_MAX_BUFFERS_IN_FLIGHT]; id<MTLBuffer> indexBuffer[MG_MTL_MAX_BUFFER_AVAILABLE];
id<MTLBuffer> tileCounters; id<MTLBuffer> tileCounters;
id<MTLBuffer> tileArrayBuffer; id<MTLBuffer> tileArrayBuffer;
id<MTLBuffer> triangleArray; id<MTLBuffer> triangleArray;
@ -82,9 +82,12 @@ void mg_mtl_canvas_update_vertex_layout(mg_mtl_canvas_backend* backend)
char* shapeBase = (char*)[backend->shapeBuffer[backend->bufferIndex] contents] + backend->shapeBufferOffset; char* shapeBase = (char*)[backend->shapeBuffer[backend->bufferIndex] contents] + backend->shapeBufferOffset;
char* indexBase = (char*)[backend->indexBuffer[backend->bufferIndex] contents] + backend->indexBufferOffset; char* indexBase = (char*)[backend->indexBuffer[backend->bufferIndex] contents] + backend->indexBufferOffset;
//TODO: add maxShapeCount
backend->interface.vertexLayout = (mg_vertex_layout){ backend->interface.vertexLayout = (mg_vertex_layout){
.maxVertexCount = MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH, .maxVertexCount = MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH - backend->vertexBufferOffset/sizeof(mg_vertex),
.maxIndexCount = MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH, .maxIndexCount = MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH - backend->indexBufferOffset/sizeof(int),
.cubicBuffer = vertexBase + offsetof(mg_vertex, cubic), .cubicBuffer = vertexBase + offsetof(mg_vertex, cubic),
.cubicStride = sizeof(mg_vertex), .cubicStride = sizeof(mg_vertex),
.posBuffer = vertexBase + offsetof(mg_vertex, pos), .posBuffer = vertexBase + offsetof(mg_vertex, pos),
@ -117,11 +120,10 @@ void mg_mtl_canvas_begin(mg_canvas_backend* interface, mg_color clearColor)
backend->indexBufferOffset = 0; backend->indexBufferOffset = 0;
backend->shapeBufferOffset = 0; backend->shapeBufferOffset = 0;
dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER);
backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_MAX_BUFFERS_IN_FLIGHT;
mg_mtl_canvas_update_vertex_layout(backend); mg_mtl_canvas_update_vertex_layout(backend);
mg_mtl_surface_acquire_command_buffer(surface);
@autoreleasepool @autoreleasepool
{ {
MTLClearColor mtlClearColor = MTLClearColorMake(clearColor.r, MTLClearColor mtlClearColor = MTLClearColorMake(clearColor.r,
@ -150,7 +152,7 @@ void mg_mtl_canvas_end(mg_canvas_backend* interface)
{ {
@autoreleasepool @autoreleasepool
{ {
mg_mtl_surface_acquire_drawable_and_command_buffer(surface); mg_mtl_surface_acquire_drawable(surface);
if(surface->drawable != nil) if(surface->drawable != nil)
{ {
f32 scale = surface->mtlLayer.contentsScale; f32 scale = surface->mtlLayer.contentsScale;
@ -175,13 +177,15 @@ void mg_mtl_canvas_end(mg_canvas_backend* interface)
vertexStart: 0 vertexStart: 0
vertexCount: 3 ]; vertexCount: 3 ];
[renderEncoder endEncoding]; [renderEncoder endEncoding];
[surface->commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer)
{
dispatch_semaphore_signal(backend->bufferSemaphore);
}
];
} }
[surface->commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer)
{
dispatch_semaphore_signal(backend->bufferSemaphore);
}
];
dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER);
backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_MAX_BUFFER_AVAILABLE;
} }
} }
} }
@ -358,7 +362,7 @@ void mg_mtl_canvas_destroy(mg_canvas_backend* interface)
{ {
[backend->outTexture release]; [backend->outTexture release];
for(int i=0; i < MG_MTL_MAX_BUFFERS_IN_FLIGHT; i++) for(int i=0; i < MG_MTL_MAX_BUFFER_AVAILABLE; i++)
{ {
[backend->vertexBuffer[i] release]; [backend->vertexBuffer[i] release];
[backend->indexBuffer[i] release]; [backend->indexBuffer[i] release];
@ -481,13 +485,13 @@ mg_canvas_backend* mg_mtl_canvas_create(mg_surface surface)
//NOTE(martin): create buffers //NOTE(martin): create buffers
//----------------------------------------------------------- //-----------------------------------------------------------
backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_MAX_BUFFERS_IN_FLIGHT); backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_MAX_BUFFER_AVAILABLE);
backend->bufferIndex = 0; backend->bufferIndex = 0;
MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined
| MTLResourceStorageModeShared; | MTLResourceStorageModeShared;
for(int i=0; i<MG_MTL_MAX_BUFFERS_IN_FLIGHT; i++) for(int i=0; i<MG_MTL_MAX_BUFFER_AVAILABLE; i++)
{ {
backend->indexBuffer[i] = [metalSurface->device newBufferWithLength: MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH*sizeof(int) backend->indexBuffer[i] = [metalSurface->device newBufferWithLength: MG_MTL_CANVAS_DEFAULT_BUFFER_LENGTH*sizeof(int)
options: bufferOptions]; options: bufferOptions];

View File

@ -53,7 +53,16 @@ void mg_mtl_surface_destroy(mg_surface_data* interface)
//NOTE: we don't use mp_layer_cleanup here, because the CAMetalLayer is taken care off by the surface itself //NOTE: we don't use mp_layer_cleanup here, because the CAMetalLayer is taken care off by the surface itself
} }
void mg_mtl_surface_acquire_drawable_and_command_buffer(mg_mtl_surface* surface) void mg_mtl_surface_acquire_command_buffer(mg_mtl_surface* surface)
{
if(surface->commandBuffer == nil)
{
surface->commandBuffer = [surface->commandQueue commandBuffer];
[surface->commandBuffer retain];
}
}
void mg_mtl_surface_acquire_drawable(mg_mtl_surface* surface)
{@autoreleasepool{ {@autoreleasepool{
/*WARN(martin): /*WARN(martin):
//TODO: we should stop trying to render if we detect that the app is in the background //TODO: we should stop trying to render if we detect that the app is in the background
@ -72,17 +81,12 @@ void mg_mtl_surface_acquire_drawable_and_command_buffer(mg_mtl_surface* surface)
[surface->drawable retain]; [surface->drawable retain];
} }
} }
if(surface->commandBuffer == nil)
{
surface->commandBuffer = [surface->commandQueue commandBuffer];
[surface->commandBuffer retain];
}
}} }}
void mg_mtl_surface_prepare(mg_surface_data* interface) void mg_mtl_surface_prepare(mg_surface_data* interface)
{ {
mg_mtl_surface* surface = (mg_mtl_surface*)interface; mg_mtl_surface* surface = (mg_mtl_surface*)interface;
mg_mtl_surface_acquire_drawable_and_command_buffer(surface); mg_mtl_surface_acquire_command_buffer(surface);
} }
void mg_mtl_surface_present(mg_surface_data* interface) void mg_mtl_surface_present(mg_surface_data* interface)
@ -90,18 +94,18 @@ void mg_mtl_surface_present(mg_surface_data* interface)
mg_mtl_surface* surface = (mg_mtl_surface*)interface; mg_mtl_surface* surface = (mg_mtl_surface*)interface;
@autoreleasepool @autoreleasepool
{ {
if(surface->drawable != nil) if(surface->commandBuffer != nil)
{ {
[surface->commandBuffer presentDrawable: surface->drawable]; if(surface->drawable != nil)
[surface->drawable release]; {
surface->drawable = nil; [surface->commandBuffer presentDrawable: surface->drawable];
[surface->drawable release];
surface->drawable = nil;
}
[surface->commandBuffer commit];
[surface->commandBuffer release];
surface->commandBuffer = nil;
} }
[surface->commandBuffer commit];
// [surface->commandBuffer waitUntilCompleted];
//TODO: do we really need this?
[surface->commandBuffer release];
surface->commandBuffer = nil;
} }
} }
@ -128,6 +132,10 @@ void mg_mtl_surface_set_frame(mg_surface_data* interface, mp_rect frame)
//TODO fix that according to real scaling, depending on the monitor settings //TODO fix that according to real scaling, depending on the monitor settings
static const f32 MG_MTL_SURFACE_CONTENTS_SCALING = 2; static const f32 MG_MTL_SURFACE_CONTENTS_SCALING = 2;
//NOTE: max frames in flight (n frames being queued on the GPU while we're working on the n+1 frame).
// for triple buffering, there's 2 frames being queued on the GPU while we're working on the 3rd frame
static const int MG_MTL_MAX_FRAMES_IN_FLIGHT = 2;
mg_surface_data* mg_mtl_surface_create_for_window(mp_window window) mg_surface_data* mg_mtl_surface_create_for_window(mp_window window)
{ {
mg_mtl_surface* surface = 0; mg_mtl_surface* surface = 0;
@ -214,6 +222,7 @@ void* mg_mtl_surface_drawable(mg_surface surface)
if(surfaceData && surfaceData->backend == MG_BACKEND_METAL) if(surfaceData && surfaceData->backend == MG_BACKEND_METAL)
{ {
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData; mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
mg_mtl_surface_acquire_drawable(mtlSurface);
return(mtlSurface->drawable); return(mtlSurface->drawable);
} }
else else
@ -228,6 +237,7 @@ void* mg_mtl_surface_command_buffer(mg_surface surface)
if(surfaceData && surfaceData->backend == MG_BACKEND_METAL) if(surfaceData && surfaceData->backend == MG_BACKEND_METAL)
{ {
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData; mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
mg_mtl_surface_acquire_command_buffer(mtlSurface);
return(mtlSurface->commandBuffer); return(mtlSurface->commandBuffer);
} }
else else