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
This commit is contained in:
Franklin Wei
2024-08-11 20:52:52 -04:00
committed by Simon Tatham
parent 4149f2cb9c
commit 989df5d2bf
6 changed files with 359 additions and 5 deletions

View File

@ -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 $<TARGET_OBJECTS:core_obj>)
add_library(common $<TARGET_OBJECTS:core_obj> hat.c spectre.c)
cliprogram(polygon-test draw-poly.c
SDL2_LIB COMPILE_DEFINITIONS STANDALONE_POLYGON)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
puzzle(blackbox

View File

@ -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()

View File

@ -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

302
draw-poly.c Normal file
View File

@ -0,0 +1,302 @@
/*
* draw-poly.c: Fallback polygon drawing function.
*/
#include <assert.h>
#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<<sizeof(int)*CHAR_BIT-2 - FRACBITS)
/* Prevent integer overflow when computing `inverse_slope',
* which shifts the coordinates left by FRACBITS, and for
* which we'd like to avoid relying on `long long'. */
/* If this ever causes issues, see the above comment about
possible solutions. */
assert(x1 < COORD_LIMIT && y1 < COORD_LIMIT);
/* Only create non-horizontal edges, and require y1 < y2. */
if(y1 != y2) {
bool swap = y1 > 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 <SDL.h>
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

View File

@ -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,

View File

@ -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,