summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp')
-rw-r--r--Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp345
1 files changed, 345 insertions, 0 deletions
diff --git a/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp
new file mode 100644
index 0000000000..488e469f82
--- /dev/null
+++ b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Array.h>
+#include <AK/IntegralMath.h>
+#include <AK/Types.h>
+#include <LibGfx/AntiAliasingPainter.h>
+#include <LibGfx/EdgeFlagPathRasterizer.h>
+
+#if defined(AK_COMPILER_GCC)
+# pragma GCC optimize("O3")
+#endif
+
+// This a pretty naive implementation of edge-flag scanline AA.
+// The paper lists many possible optimizations, maybe implement one? (FIXME!)
+// https://mlab.taik.fi/~kkallio/antialiasing/EdgeFlagAA.pdf
+// This currently implements:
+// - The scanline buffer optimization (only allocate one scanline)
+// Possible other optimizations according to the paper:
+// - Using fixed point numbers
+// - Edge tracking
+// - Mask tracking
+// - Loop unrolling (compilers might handle this better now, the paper is from 2007)
+// Optimizations I think we could add:
+// - Using fast_u32_fills() for runs of solid colors
+// - Clipping the plotted edges earlier
+
+namespace Gfx {
+
+static Vector<Detail::Edge> prepare_edges(ReadonlySpan<Path::SplitLineSegment> lines, unsigned samples_per_pixel, FloatPoint origin)
+{
+ // FIXME: split_lines() gives similar information, but the form it's in is not that useful (and is const anyway).
+ Vector<Detail::Edge> edges;
+ edges.ensure_capacity(lines.size());
+
+ for (auto& line : lines) {
+ auto p0 = line.from - origin;
+ auto p1 = line.to - origin;
+
+ p0.scale_by(1, samples_per_pixel);
+ p1.scale_by(1, samples_per_pixel);
+
+ i8 winding = -1;
+ if (p0.y() > p1.y()) {
+ swap(p0, p1);
+ } else {
+ winding = 1;
+ }
+
+ if (p0.y() == p1.y())
+ continue;
+
+ auto dx = p1.x() - p0.x();
+ auto dy = p1.y() - p0.y();
+ float dxdy = float(dx) / dy;
+ float x = p0.x();
+ edges.unchecked_append(Detail::Edge {
+ x,
+ static_cast<int>(p0.y()),
+ static_cast<int>(p1.y()),
+ dxdy,
+ winding,
+ nullptr });
+ }
+ return edges;
+}
+
+template<unsigned SamplesPerPixel>
+EdgeFlagPathRasterizer<SamplesPerPixel>::EdgeFlagPathRasterizer(IntSize size)
+ : m_size(size.width() + 1, size.height() + 1)
+{
+ m_scanline.resize(m_size.width());
+ m_edge_table.resize(m_size.height());
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::fill(Painter& painter, Path const& path, Color color, Painter::WindingRule winding_rule, FloatPoint offset)
+{
+ fill_internal(painter, path, color, winding_rule, offset);
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::fill(Painter& painter, Path const& path, PaintStyle const& style, Painter::WindingRule winding_rule, FloatPoint offset)
+{
+ style.paint(enclosing_int_rect(path.bounding_box()), [&](PaintStyle::SamplerFunction sampler) {
+ fill_internal(painter, path, move(sampler), winding_rule, offset);
+ });
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::fill_internal(Painter& painter, Path const& path, auto color_or_function, Painter::WindingRule winding_rule, FloatPoint offset)
+{
+ // FIXME: Figure out how painter scaling works here...
+ VERIFY(painter.scale() == 1);
+
+ auto bounding_box = enclosing_int_rect(path.bounding_box().translated(offset));
+ auto dest_rect = bounding_box.translated(painter.translation());
+ auto origin = bounding_box.top_left().to_type<float>() - offset;
+ m_blit_origin = dest_rect.top_left();
+ m_clip = dest_rect.intersected(painter.clip_rect());
+
+ if (m_clip.is_empty())
+ return;
+
+ auto& lines = path.split_lines();
+ if (lines.is_empty())
+ return;
+
+ auto edges = prepare_edges(lines, SamplesPerPixel, origin);
+
+ int min_scanline = m_size.height();
+ int max_scanline = 0;
+ for (auto& edge : edges) {
+ int start_scanline = edge.min_y / SamplesPerPixel;
+ int end_scanline = edge.max_y / SamplesPerPixel;
+
+ // Create a linked-list of edges starting on this scanline:
+ edge.next_edge = m_edge_table[start_scanline];
+ m_edge_table[start_scanline] = &edge;
+
+ min_scanline = min(min_scanline, start_scanline);
+ max_scanline = max(max_scanline, end_scanline);
+ }
+
+ Detail::Edge* active_edges = nullptr;
+
+ // FIXME: We could probably clip some of the egde plotting if we know it won't be shown.
+ // Though care would have to be taken to ensure the active edges are correct at the first drawn scaline.
+ if (winding_rule == Painter::WindingRule::EvenOdd) {
+ auto plot_edge = [&](Detail::Edge& edge, int start_subpixel_y, int end_subpixel_y) {
+ for (int y = start_subpixel_y; y < end_subpixel_y; y++) {
+ int xi = static_cast<int>(edge.x + SubpixelSample::nrooks_subpixel_offsets[y]);
+ SampleType sample = 1 << y;
+ m_scanline[xi] ^= sample;
+ edge.x += edge.dxdy;
+ }
+ };
+ for (int scanline = min_scanline; scanline <= max_scanline; scanline++) {
+ active_edges = plot_edges_for_scanline(scanline, plot_edge, active_edges);
+ accumulate_even_odd_scanline(painter, scanline, color_or_function);
+ }
+ } else {
+ VERIFY(winding_rule == Painter::WindingRule::Nonzero);
+ // Only allocate the winding buffer if needed.
+ // NOTE: non-zero fills are a fair bit less efficient. So if you can do an even-odd fill do that :^)
+ if (m_windings.is_empty())
+ m_windings.resize(m_size.width());
+
+ auto plot_edge = [&](Detail::Edge& edge, int start_subpixel_y, int end_subpixel_y) {
+ for (int y = start_subpixel_y; y < end_subpixel_y; y++) {
+ int xi = static_cast<int>(edge.x + SubpixelSample::nrooks_subpixel_offsets[y]);
+ SampleType sample = 1 << y;
+ m_scanline[xi] |= sample;
+ m_windings[xi].counts[y] += edge.winding;
+ edge.x += edge.dxdy;
+ }
+ };
+ for (int scanline = min_scanline; scanline <= max_scanline; scanline++) {
+ active_edges = plot_edges_for_scanline(scanline, plot_edge, active_edges);
+ accumulate_non_zero_scanline(painter, scanline, color_or_function);
+ }
+ }
+}
+
+template<unsigned SamplesPerPixel>
+Color EdgeFlagPathRasterizer<SamplesPerPixel>::scanline_color(int scanline, int offset, u8 alpha, auto& color_or_function)
+{
+ using ColorOrFunction = decltype(color_or_function);
+ constexpr bool has_constant_color = IsSame<RemoveCVReference<ColorOrFunction>, Color>;
+ auto color = [&] {
+ if constexpr (has_constant_color) {
+ return color_or_function;
+ } else {
+ return color_or_function({ offset, scanline });
+ }
+ }();
+ return color.with_alpha(color.alpha() * alpha / 255);
+}
+
+template<unsigned SamplesPerPixel>
+Detail::Edge* EdgeFlagPathRasterizer<SamplesPerPixel>::plot_edges_for_scanline(int scanline, auto plot_edge, Detail::Edge* active_edges)
+{
+ auto y_subpixel = [](int y) {
+ return y & (SamplesPerPixel - 1);
+ };
+
+ auto* current_edge = active_edges;
+ Detail::Edge* prev_edge = nullptr;
+
+ // First iterate over the edge in the active edge table, these are edges added on earlier scanlines,
+ // that have not yet reached their end scanline.
+ while (current_edge) {
+ int end_scanline = current_edge->max_y / SamplesPerPixel;
+ if (scanline == end_scanline) {
+ // This edge ends this scanline.
+ plot_edge(*current_edge, 0, y_subpixel(current_edge->max_y));
+ // Remove this edge from the AET
+ current_edge = current_edge->next_edge;
+ if (prev_edge)
+ prev_edge->next_edge = current_edge;
+ else
+ active_edges = current_edge;
+ } else {
+ // This egde sticks around for a few more scanlines.
+ plot_edge(*current_edge, 0, SamplesPerPixel);
+ prev_edge = current_edge;
+ current_edge = current_edge->next_edge;
+ }
+ }
+
+ // Next, iterate over new edges for this line. If active_edges was null this also becomes the new
+ // AET. Edges new will be appended here.
+ current_edge = m_edge_table[scanline];
+ while (current_edge) {
+ int end_scanline = current_edge->max_y / SamplesPerPixel;
+ if (scanline == end_scanline) {
+ // This edge will end this scanlines (no need to add to AET).
+ plot_edge(*current_edge, y_subpixel(current_edge->min_y), y_subpixel(current_edge->max_y));
+ } else {
+ // This edge will live on for a few more scanlines.
+ plot_edge(*current_edge, y_subpixel(current_edge->min_y), SamplesPerPixel);
+ // Add this edge to the AET
+ if (prev_edge)
+ prev_edge->next_edge = current_edge;
+ else
+ active_edges = current_edge;
+ prev_edge = current_edge;
+ }
+ current_edge = current_edge->next_edge;
+ }
+
+ m_edge_table[scanline] = nullptr;
+ return active_edges;
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::write_pixel(Painter& painter, int scanline, int offset, SampleType sample, auto& color_or_function)
+{
+ auto dest = IntPoint { offset, scanline } + m_blit_origin;
+ if (!m_clip.contains_horizontally(dest.x()))
+ return;
+ // FIXME: We could detect runs of full coverage and use fast_u32_fills for those rather than many set_pixel() calls.
+ auto coverage = SubpixelSample::compute_coverage(sample);
+ if (coverage) {
+ auto paint_color = scanline_color(scanline, offset, coverage_to_alpha(coverage), color_or_function);
+ painter.set_physical_pixel(dest, paint_color, true);
+ }
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_even_odd_scanline(Painter& painter, int scanline, auto& color_or_function)
+{
+ auto dest_y = m_blit_origin.y() + scanline;
+ if (!m_clip.contains_vertically(dest_y)) {
+ // FIXME: This memset only really needs to be done on transition from clipped to not clipped,
+ // or not at all if we properly clipped egde plotting.
+ memset(m_scanline.data(), 0, sizeof(SampleType) * m_scanline.size());
+ return;
+ }
+
+ SampleType sample = 0;
+ for (int x = 0; x < m_size.width(); x += 1) {
+ sample ^= m_scanline[x];
+ write_pixel(painter, scanline, x, sample, color_or_function);
+ m_scanline[x] = 0;
+ }
+}
+
+template<unsigned SamplesPerPixel>
+void EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_non_zero_scanline(Painter& painter, int scanline, auto& color_or_function)
+{
+ // NOTE: Same FIXMEs apply from accumulate_even_odd_scanline()
+ auto dest_y = m_blit_origin.y() + scanline;
+ if (!m_clip.contains_vertically(dest_y)) {
+ memset(m_scanline.data(), 0, sizeof(SampleType) * m_scanline.size());
+ memset(m_windings.data(), 0, sizeof(WindingCounts) * m_windings.size());
+ return;
+ }
+
+ SampleType sample = 0;
+ WindingCounts sum_winding = {};
+ for (int x = 0; x < m_size.width(); x += 1) {
+ if (auto edges = m_scanline[x]) {
+ // We only need to process the windings when we hit some edges.
+ for (auto y_sub = 0u; y_sub < SamplesPerPixel; y_sub++) {
+ auto subpixel_bit = 1 << y_sub;
+ if (edges & subpixel_bit) {
+ auto winding = m_windings[x].counts[y_sub];
+ auto previous_winding_count = sum_winding.counts[y_sub];
+ sum_winding.counts[y_sub] += winding;
+ // Toggle fill on change to/from zero
+ if ((previous_winding_count == 0 && sum_winding.counts[y_sub] != 0)
+ || (sum_winding.counts[y_sub] == 0 && previous_winding_count != 0)) {
+ sample ^= subpixel_bit;
+ }
+ }
+ }
+ }
+ write_pixel(painter, scanline, x, sample, color_or_function);
+ m_scanline[x] = 0;
+ m_windings[x] = {};
+ }
+}
+
+static IntSize path_bounds(Gfx::Path const& path)
+{
+ return enclosing_int_rect(path.bounding_box()).size();
+}
+
+// Note: The AntiAliasingPainter and Painter now perform the same antialiasing,
+// since it would be harder to turn it off for the standard painter.
+// The samples are reduced to 8 for Gfx::Painter though as a "speedy" option.
+
+void Painter::fill_path(Path const& path, Color color, WindingRule winding_rule)
+{
+ EdgeFlagPathRasterizer<8> rasterizer(path_bounds(path));
+ rasterizer.fill(*this, path, color, winding_rule);
+}
+
+void Painter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule winding_rule)
+{
+ EdgeFlagPathRasterizer<8> rasterizer(path_bounds(path));
+ rasterizer.fill(*this, path, paint_style, winding_rule);
+}
+
+void AntiAliasingPainter::fill_path(Path const& path, Color color, Painter::WindingRule winding_rule)
+{
+ EdgeFlagPathRasterizer<32> rasterizer(path_bounds(path));
+ rasterizer.fill(m_underlying_painter, path, color, winding_rule, m_transform.translation());
+}
+
+void AntiAliasingPainter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule winding_rule)
+{
+ EdgeFlagPathRasterizer<32> rasterizer(path_bounds(path));
+ rasterizer.fill(m_underlying_painter, path, paint_style, winding_rule, m_transform.translation());
+}
+
+template class EdgeFlagPathRasterizer<8>;
+template class EdgeFlagPathRasterizer<16>;
+template class EdgeFlagPathRasterizer<32>;
+
+}