From 989df5d2bf79eaee2ee4fb21d9be4341b60569c7 Mon Sep 17 00:00:00 2001 From: Franklin Wei Date: Sun, 11 Aug 2024 20:52:52 -0400 Subject: [PATCH] Add draw_polygon_fallback() for platforms without a native polygon fill. This adds a portable, scanline-based polygon filling algorithm, which fills a polygon by drawing a collection of adjacent horizontal lines. This change is motivated by the Rockbox port's current lack of a true polygon fill capability. Until now, it attempted to approximate a polygon fill by performing a series of triangle fills, but this worked reliably only for convex polygons. I originally considered making this new rasterizer part of the Rockbox front end itself, but I ultimately decided that it made more sense to include it here, in the Puzzles distribution, where other platforms may benefit from it in the future. No in-tree front ends use this new function quite yet, but I plan to follow this commit with a compile-time option to force front ends to use it for testing. This new polygon drawing code also comes with its own standalone driver code to test it out in isolation. This code currently relies on SDL 2.0 to present a GUI window to the user, which unfortunately adds a build-time dependency. To lessen the impact of this change, this program is gated behind a CMake build option. To use it, run: $ cmake -DBUILD_SDL_PROGRAMS=true --- CMakeLists.txt | 10 +- cmake/setup.cmake | 10 +- devel.but | 38 ++++++ draw-poly.c | 302 ++++++++++++++++++++++++++++++++++++++++++++++ nullfe.c | 2 + puzzles.h | 2 + 6 files changed, 359 insertions(+), 5 deletions(-) create mode 100644 draw-poly.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4153efc..ce3ce3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,13 +6,17 @@ project(puzzles include(cmake/setup.cmake) add_library(core_obj OBJECT - combi.c divvy.c drawing.c dsf.c findloop.c grid.c latin.c - laydomino.c loopgen.c malloc.c matching.c midend.c misc.c penrose.c - penrose-legacy.c ps.c random.c sort.c tdq.c tree234.c version.c + combi.c divvy.c draw-poly.c drawing.c dsf.c findloop.c grid.c + latin.c laydomino.c loopgen.c malloc.c matching.c midend.c misc.c + penrose.c penrose-legacy.c ps.c random.c sort.c tdq.c tree234.c + version.c ${platform_common_sources}) add_library(core $) add_library(common $ hat.c spectre.c) +cliprogram(polygon-test draw-poly.c + SDL2_LIB COMPILE_DEFINITIONS STANDALONE_POLYGON) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) puzzle(blackbox diff --git a/cmake/setup.cmake b/cmake/setup.cmake index ffb8019..bfacd73 100644 --- a/cmake/setup.cmake +++ b/cmake/setup.cmake @@ -7,6 +7,7 @@ set(build_cli_programs TRUE) set(build_gui_programs TRUE) set(build_icons FALSE) set(need_c_icons FALSE) +option(BUILD_SDL_PROGRAMS "build test programs requiring SDL" FALSE) # Don't disable assertions, even in release mode. Our assertions # generally aren't expensive and protect against more annoying crashes @@ -148,7 +149,7 @@ endfunction() # a command-line helper tool. function(cliprogram NAME) cmake_parse_arguments(OPT - "CORE_LIB" "" "COMPILE_DEFINITIONS" ${ARGN}) + "CORE_LIB;SDL2_LIB" "" "COMPILE_DEFINITIONS" ${ARGN}) if(OPT_CORE_LIB) set(lib core) @@ -156,13 +157,18 @@ function(cliprogram NAME) set(lib common) endif() - if(build_cli_programs) + if(build_cli_programs AND ((NOT OPT_SDL2_LIB) OR BUILD_SDL_PROGRAMS)) add_executable(${NAME} ${CMAKE_SOURCE_DIR}/nullfe.c ${OPT_UNPARSED_ARGUMENTS}) target_link_libraries(${NAME} ${lib} ${platform_libs}) if(OPT_COMPILE_DEFINITIONS) target_compile_definitions(${NAME} PRIVATE ${OPT_COMPILE_DEFINITIONS}) endif() + if(OPT_SDL2_LIB) + find_package(SDL2 REQUIRED) + include_directories(${NAME} ${SDL2_INCLUDE_DIRS}) + target_link_libraries(${NAME} ${SDL2_LIBRARIES}) + endif() endif() endfunction() diff --git a/devel.but b/devel.but index 3a7c9cd..b5d462a 100644 --- a/devel.but +++ b/devel.but @@ -2752,6 +2752,14 @@ function; see \k{drawing-draw-line}. This function behaves exactly like the back end \cw{draw_polygon()} function; see \k{drawing-draw-polygon}. +An implementation of this API which doesn't have a native polygon fill +primitive is permitted to define this function pointer to point to the +middleware's \c{draw_polygon_fallback()} (see +\k{drawing-draw-polygon-fallback}), which is a fallback polygon +rasterizer that produces a series of \c{draw_line()} calls to fill and +outline the polygon. However, it is explicitly \e{not} permitted for +this function pointer to be \cw{NULL}. + \S{drawingapi-draw-circle} \cw{draw_circle()} \c void (*draw_circle)(drawing *dr, int cx, int cy, int radius, @@ -3084,6 +3092,36 @@ with the RGB values of the desired colour (if printing in colour), or all filled with the grey-scale value (if printing in black and white). +\S{drawing-draw-polygon-fallback} \cw{draw_polygon_fallback()} + +\c void draw_polygon_fallback(drawing *dr, +\c const int *coords, int npoints, +\c int fillcolour, int outlinecolour); + +This function is intended for use by front ends which do not have a +native polygon fill primitive. Its signature and semantics are exactly +the same as \cw{draw_polygon()} (\k{drawing-draw-polygon}); however, +instead of being implemented by a front end, it is implemented as a +piece of middleware that uses a scanline algorithm to produce a series +of calls to \cw{draw_line()} that have the effect of filling and +outlining the desired polygon. + +Although a front end may choose to call this function directly (such +as from a stub implementation of \c{draw_polygon()}), this function is +intended to be \e{pointed to} by the \c{draw_polygon} field of the +front end's \c{drawing_api}; that is, a frontend without a polygon +fill primitive should set \c{drawing_api}'s \c{draw_polygon} field to +\c{draw_polygon_fallback}. + +The motivation for this rather unwieldy method of employing this +fallback function (instead of simply setting \c{draw_polygon} to +\c{NULL} in \c{drawing_api}) is that it allows a link-time optimizing +compiler to prune this function's implementation on platforms that +provide their own \c{draw_polygon()}, since this function would never +be referenced on those platforms. (But this function is still +unconditionally compiled on all platforms, thus protecting it from +bit-rot.) + \C{midend} The API provided by the mid-end This chapter documents the API provided by the mid-end to be called diff --git a/draw-poly.c b/draw-poly.c new file mode 100644 index 0000000..f838996 --- /dev/null +++ b/draw-poly.c @@ -0,0 +1,302 @@ +/* + * draw-poly.c: Fallback polygon drawing function. + */ + +#include + +#include "puzzles.h" + +struct edge { + int x1, y1; + int x2, y2; + bool active; + + /* whether y1 is a closed endpoint (i.e. this edge should be + * active when y == y1) */ + bool closed_y1; + + /* (x2 - x1) / (y2 - y1) as 16.16 signed fixed point; precomputed + * for speed */ + long inverse_slope; +}; + +#define FRACBITS 16 +#define ONEHALF (1 << (FRACBITS-1)) + +void draw_polygon_fallback(drawing *dr, const int *coords, int npoints, + int fillcolour, int outlinecolour) +{ + struct edge *edges; + int min_y = INT_MAX, max_y = INT_MIN, i, y; + int n_edges = 0; + int *intersections; + + if(npoints < 3) + return; + + if(fillcolour < 0) + goto draw_outline; + + /* This uses a basic scanline rasterization algorithm for polygon + * filling. First, an "edge table" is constructed for each pair of + * neighboring points. Horizontal edges are excluded. Then, the + * algorithm iterates a horizontal "scan line" over the vertical + * (Y) extent of the polygon. At each Y level, it maintains a set + * of "active" edges, which are those which intersect the scan + * line at the current Y level. The X coordinates where the scan + * line intersects each active edge are then computed via + * fixed-point arithmetic and stored. Finally, horizontal lines + * are drawn between each successive pair of intersection points, + * in the order of ascending X coordinate. This has the effect of + * "even-odd" filling when the polygon is self-intersecting. + * + * I (vaguely) based this implementation off the slides below: + * + * https://www.khoury.northeastern.edu/home/fell/CS4300/Lectures/CS4300F2012-9-ScanLineFill.pdf + * + * I'm fairly confident that this current implementation is + * correct (i.e. draws the right polygon, free from artifacts), + * but it isn't quite as efficient as it could be. Namely, it + * currently maintains the active edge set by setting the `active` + * flag in the `edge` array, which is quite inefficient. Perhaps + * future optimization could see this replaced with a tree + * set. Additionally, one could perhaps replace the current linear + * search for edge endpoints (i.e. the inner loop over `edges`) by + * sorting the edge table by upper and lower Y coordinate. + * + * A final caveat comes from the use of fixed point arithmetic, + * which is motivated by performance considerations on FPU-less + * platforms (e.g. most Rockbox devices, and maybe others?). I'm + * currently using 16 fractional bits to compute the edge + * intersections, which (in the case of a 32-bit int) limits the + * acceptable range of coordinates to roughly (-2^14, +2^14). This + * ought to be acceptable for the forseeable future, but + * ultra-high DPI screens might one day exceed this. In that case, + * options include switching to int64_t (but that comes with its + * own portability headaches), reducing the number of fractional + * bits, or just giving up and using floating point. + */ + + /* Build edge table from coords. Horizontal edges are filtered + * out, so n_edges <= n_points in general. */ + edges = smalloc(npoints * sizeof(struct edge)); + + for(i = 0; i < npoints; i++) { + int x1, y1, x2, y2; + + x1 = coords[(2*i+0)]; + y1 = coords[(2*i+1)]; + x2 = coords[(2*i+2) % (npoints * 2)]; + y2 = coords[(2*i+3) % (npoints * 2)]; + + if(y1 < min_y) + min_y = y1; + if(y1 > max_y) + max_y = y1; + +#define COORD_LIMIT (1< y2; + int lower_endpoint = swap ? (i + 1) : i; + + /* Compute index of the point adjacent to lower end of + * this edge (which is not the upper end of this edge). */ + int lower_neighbor = swap ? (lower_endpoint + 1) % npoints : (lower_endpoint + npoints - 1) % npoints; + + struct edge *edge = edges + (n_edges++); + + edge->active = false; + edge->x1 = swap ? x2 : x1; + edge->y1 = swap ? y2 : y1; + edge->x2 = swap ? x1 : x2; + edge->y2 = swap ? y1 : y2; + edge->inverse_slope = ((edge->x2 - edge->x1) << FRACBITS) / (edge->y2 - edge->y1); + edge->closed_y1 = edge->y1 < coords[2*lower_neighbor+1]; + } + } + + /* a generous upper bound on number of intersections is n_edges */ + intersections = smalloc(n_edges * sizeof(int)); + + for(y = min_y; y <= max_y; y++) { + int n_intersections = 0; + for(i = 0; i < n_edges; i++) { + struct edge *edge = edges + i; + /* Update active edge set. These conditions are mutually + * exclusive because of the invariant that y1 < y2. */ + if(edge->y1 + (edge->closed_y1 ? 0 : 1) == y) + edge->active = true; + else if(edge->y2 + 1 == y) + edge->active = false; + + if(edge->active) { + int x = edges[i].x1; + x += (edges[i].inverse_slope * (y - edges[i].y1) + ONEHALF) >> FRACBITS; + intersections[n_intersections++] = x; + } + } + + qsort(intersections, n_intersections, sizeof(int), compare_integers); + + assert(n_intersections % 2 == 0); + assert(n_intersections <= n_edges); + + /* Draw horizontal lines between successive pairs of + * intersections of the scanline with active edges. */ + for(i = 0; i + 1 < n_intersections; i += 2) { + draw_line(dr, + intersections[i], y, + intersections[i+1], y, + fillcolour); + } + } + + sfree(intersections); + sfree(edges); + +draw_outline: + assert(outlinecolour >= 0); + for (i = 0; i < 2 * npoints; i += 2) + draw_line(dr, + coords[i], coords[i+1], + coords[(i+2)%(2*npoints)], coords[(i+3)%(2*npoints)], + outlinecolour); +} + +#ifdef STANDALONE_POLYGON + +/* + * Standalone program to test draw_polygon_fallback(). By default, + * creates a window and allows clicking points to build a + * polygon. Optionally, can draw a randomly growing polygon in + * "screensaver" mode. + */ + +#include + +void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour) +{ + SDL_Renderer *renderer = GET_HANDLE_AS_TYPE(dr, SDL_Renderer); + SDL_RenderDrawLine(renderer, x1, y1, x2, y2); +} + +#define WINDOW_WIDTH 800 +#define WINDOW_HEIGHT 600 +#define MAX_SCREENSAVER_POINTS 1000 + +int main(int argc, char *argv[]) { + SDL_Window* window = NULL; + SDL_Event event; + SDL_Renderer *renderer = NULL; + int running = 1; + int i; + drawing dr; + bool screensaver = false; + + if(argc >= 2) { + if(!strcmp(argv[1], "--screensaver")) + screensaver = true; + else + printf("usage: %s [--screensaver]\n", argv[0]); + } + + int *poly = NULL; + int n_points = 0; + + /* Initialize SDL */ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); + return 1; + } + + /* Create window */ + window = SDL_CreateWindow("Polygon Drawing Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN); + if (!window) { + printf("Window could not be created! SDL_Error: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + /* Create renderer */ + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (!renderer) { + printf("Renderer could not be created! SDL_Error: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + dr.handle = renderer; + + if(!screensaver) + printf("Click points in the window to create vertices. Pressing C resets.\n"); + + while (running) { + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_QUIT) { + running = 0; + } + else if (event.type == SDL_MOUSEBUTTONDOWN) { + if (event.button.button == SDL_BUTTON_LEFT) { + int mouseX = event.button.x; + int mouseY = event.button.y; + + poly = realloc(poly, ++n_points * 2 * sizeof(int)); + poly[2 * (n_points - 1)] = mouseX; + poly[2 * (n_points - 1) + 1] = mouseY; + } + } + else if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_c) { + free(poly); + poly = NULL; + n_points = 0; + } + } + } + + if(screensaver) { + poly = realloc(poly, ++n_points * 2 * sizeof(int)); + poly[2 * (n_points - 1)] = rand() % WINDOW_WIDTH; + poly[2 * (n_points - 1) + 1] = rand() % WINDOW_HEIGHT; + + if(n_points >= MAX_SCREENSAVER_POINTS) { + free(poly); + poly = NULL; + n_points = 0; + } + } + + /* Clear the screen with a black color */ + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + /* Set draw color to white */ + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + + draw_polygon_fallback(&dr, poly, n_points, 1, 1); + + SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); + for(i = 0; i < 2*n_points; i+=2) + SDL_RenderDrawPoint(renderer, poly[i], poly[i+1]); + + /* Present the back buffer */ + SDL_RenderPresent(renderer); + } + + /* Clean up and quit SDL */ + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} +#endif diff --git a/nullfe.c b/nullfe.c index b6022d4..42cf532 100644 --- a/nullfe.c +++ b/nullfe.c @@ -19,7 +19,9 @@ void drawing_free(drawing *dr) { sfree(dr); } void draw_text(drawing *dr, int x, int y, int fonttype, int fontsize, int align, int colour, const char *text) {} void draw_rect(drawing *dr, int x, int y, int w, int h, int colour) {} +#ifndef STANDALONE_POLYGON void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour) {} +#endif void draw_thick_line(drawing *dr, float thickness, float x1, float y1, float x2, float y2, int colour) {} void draw_polygon(drawing *dr, const int *coords, int npoints, diff --git a/puzzles.h b/puzzles.h index f04bdbf..cf944eb 100644 --- a/puzzles.h +++ b/puzzles.h @@ -266,6 +266,8 @@ void draw_rect(drawing *dr, int x, int y, int w, int h, int colour); void draw_line(drawing *dr, int x1, int y1, int x2, int y2, int colour); void draw_polygon(drawing *dr, const int *coords, int npoints, int fillcolour, int outlinecolour); +void draw_polygon_fallback(drawing *dr, const int *coords, int npoints, + int fillcolour, int outlinecolour); void draw_circle(drawing *dr, int cx, int cy, int radius, int fillcolour, int outlinecolour); void draw_thick_line(drawing *dr, float thickness,