-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
extmod/modframebuf: Add ellipse & poly methods. #9045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
d467473
to
6dfb462
Compare
You re-wrote my PR so that I can no longer pass the polygon coordinates a list of lists/list of tuples 👎 I'm trying to be very responsive feedback on my original PR, why not discuss changes there? |
I'm just trying to do what I can do as a maintainer to get your code merged as quickly and easily as possible without making you duplicate the effort that I just spent my Friday evening on. Not trying to diminish your work or anything and I'm sorry that it seems that way! I'm glad to see these features added to framebuf. These two features represent an extremely significant code size increase (+1132 bytes total) and merging this is not an easy decision. I spent a good chunk of time discussing this with Damien today and then more time trying to optimise the two PRs. Sure I can turn all that work into a bunch of comments on your PR and then go backwards-and-forwards on it for the next days/weeks but I'd still need to make a PR for the first two commits, and now we'd have three PRs to wrangle. I definitely agree that list-of-lists or lists-of-tuples is syntactically convenient, but I don't feel that it's worth either the code size or the runtime RAM cost. |
Ellipse goes like a bat out of hell now. Using the same script and test conditions, times are
|
Just a quick note on the efficiency of the So for example, the "M" polygon from the test cases could be written as:
Obviously this isn't great for readability, but if your program has a number of polygons then these can be generated programatically.
Thanks for testing @peterhinch ! |
Thanks -- this is all important context that I was missing! As an outside contributor I am asked to sign off on submissions, so it seemed against the spirit of that policy to merge something different to what I signed off on without at least some explanation of what was going on. :-) |
Yes this is a very good point. I just left the commits with the original author (of course) but didn't think about the sign off. Our main intention here is that the sign off is conveying that the contribution meets the licence requirements and the code doesn't have any encumbrances. This is actually something we do all the time (make changes to commits during merge while preserving the author/signoff), although usually the changes are minor or squashes/rebases. @dpgeorge we should probably clarify this particular scenario. |
@jimmo I now have prototype rounded rectangle (filled and outline) C code. Naturally this is dependent on The idea of shapes in ROM is excellent. On a typical display all shapes are likely to be small enough to fit in a [EDIT] Deleted the rest. My enthusiasm had got ahead of my brain :) |
Hi @jimmo this morning I tested this series of patches on my ESP32 device and have a couple of comments. Because I have to take the extra steps of flattening my point lists and converting it to an Much more concerning to me are the visual defects that have been introduced: framebuf_defects_cut.mp4Compare with the behaviour of my original PR: tidal3d_demo.mp4I will try to come up with a small reproducing test case. |
@mbooth101 Your videos aren't visible here. |
Works fine for me. It's just an MP4 file in a HTML5 video tag, so maybe your browser doesn't know how to decode MP4? In any case you can download the videos for offline viewing by right-clicking -> save link as on these links:
|
@jimmo Here's a small reproducer for the issue visible in the above video:
Something is wacky with the intersection node detection at the corners: |
Backporting an enhancement from upstream micropython, see: micropython/micropython#9045
568822f
to
bcd465d
Compare
@mbooth101 thanks for the repro! The problem is this line:
which I changed from your PR, where it was
The reason I changed it was that in the existing test cases I noticed the fill wasn't quite reaching the exact bounds of the outline. Notably the last row of the M's and the bottom edge of the hourglass and the star. This is why I added the tests that draws the fill on top of the outline to make this obvious. All the ff in the following snippets are incorrect:
You can also see this with your repro screenshot above, which includes a pixel at (20, 68). If I revert that line back to the original version, the glitch goes away, but now that pixel isn't drawn. The new version glitches because the end of one edge and the start of the next edge now both "intersect" with the row and so it duplicates the node. Whereas the original will never include the "bottom" pixel of any edge, which is usually fine because same pixel will be the top of the next edge, except when that next edge is horizontal or going upwards. In the meantime I've updated this PR to avoid the glitch, but the test cases show the incorrect fill. I suspect we can add some logic to fix this by detecting if the next edge is horizontal or upwards, so I've added a comment to that effect for now. |
Aha, that's a good find @jimmo
Thanks for fixing the glitch and adding the extra test case -- in the meantime I will have a think about the bottom edge problem |
bcd465d
to
23fd6be
Compare
@mbooth101 I don't know if you came across this page while writing this code, but it describes the exact same algorithm and the implementation is identical. Note that on the linked page about polygon-in-point, it says
This is exactly our problem. I spent some time thinking about ways to solve this but then had an embarrassingly simple (and almost zero code size cost) idea -- just always unconditionally draw the outline after completing the fill. Unfortunately this will obviously take a bit of a performance hit, would be keen to see how this affects your demo. I have updated the PR to do that.
Yes, hopefully it is possible to just use array.array through your entire renderer, and I imagine this will considerably improve performance especially if you re-use arrays which will allow you to eliminate almost all heap allocations. |
I don't know if this is of interest. The following script runs a "fuzz test" on polygon fill. I ran it against the original PR and against this one as it was before the bug was fixed. The buggy version typically fails almost immediately. The original PR ran for >260K shapes before I cancelled it. I run on an RP2. import framebuf
from array import array
from random import randint
def print_buffer(fbuf, w, h):
for x in range(h):
for y in range(w):
p = "x" if fbuf.pixel(x, y) else "."
print(p, end="")
print()
w = 32
h = 32
c = 16 # centre
buf = bytearray(w * h)
fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
nvertices = 10
npoints = nvertices * 2
vmax = 12 # Max X or Y excursion
vmin = -12
np = 0 # Pass and fail count
nf = 0
arr = array('H', (0 for _ in range(npoints)))
while True:
fbuf.fill(0)
for k in range(npoints):
arr[k] = randint(0, vmax-vmin)
#fbuf.poly(c + vmin, c + vmin, arr, 0xff, True) # New API
fbuf.fill_poly(c + vmin, c + vmin, arr, 0xff) # Old API
try:
for x in range(w):
for y in range(h):
if fbuf.pixel(x, y):
if not (c + vmin <= x <= c + vmax) or not (c + vmin <= y <= c + vmax):
print(x, y)
raise OSError
except OSError:
print("Fail")
nf += 1
else:
np += 1
print('Count', np, nf)
if nf:
fbuf.rect(c + vmin, c + vmin, vmax-vmin + 1, vmax-vmin + 1, 0xff)
print_buffer(fbuf, w, h)
break |
@mbooth101 Based on the idea that drawing the outline can solve this, I have implemented a more optimised fix for this. The idea is to detect the "lost" pixels during the node calculations, but rather than getting the node list to account for these, just explicitly draw the pixels there (either via setpixel or line). This adds +72 bytes on PYBV11, but makes filled poly() about 10-15% faster. Baseline is 32 ms/frame (empty scene no polygons)
I've put it in a separate commit... we can squash during merge if desired. It's still not exactly perfect -- there are some pixels along diagonal edges that it misses, but visually it's pretty good. |
FWIW, this PR is now at +1204 bytes.
Thanks @peterhinch ! I ran this on the current version and no problems. |
Codecov Report
@@ Coverage Diff @@
## master #9045 +/- ##
========================================
Coverage 98.37% 98.37%
========================================
Files 156 156
Lines 20310 20424 +114
========================================
+ Hits 19979 20092 +113
- Misses 331 332 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
Oh great, thanks for putting in the extra work! In a 104 triangle scene I lose about 3 or 4 ms per frame in my 3D demo with this fix (compared to no fix), which I can live with.
I can't see any missing pixels, but triangles that share vertices will overlap by one pixel anyway and hide that -- and where they overlap I don't think I can see any new Z-fighting, so I'd be totally happy to see these 6 commits merged as is 👍 |
724e677
to
4c077f2
Compare
Backporting an enhancement from upstream micropython, see: micropython/micropython#9045
@jimmo Re rounded rect please could you advise how you want to proceed. I have working code, but there is a dependency on ellipse which isn't yet merged. I'd planned to wait until this PR is merged before submitting a PR, but I guess merging may take a while. It occurred to me that to save time you might want to include the change in this PR. If so, please let me know what I should do bearing in mind my limited git-fu. One point re the code. There is a case for checking whether |
Thanks @peterhinch I think it's valuable to be able to support round rect, although we need to consider the trade off against doing it in Python. Here's a Python version for comparison. It's 242 bytes of bytecode, and it takes 11 ms to outline 60 rectangles (10ms with def round_rect(fb, x, y, w, h, r, c, f):
xpw = x + w
xpr = x + r
yph = y + h
ypr = y + r
r2 = r * 2
wmr2 = w - r2
hmr2 = h - r2
xpwmr = xpw - r
yphmr = yph - r
fb.ellipse(xpwmr, ypr, r, r, c, f, 1)
fb.ellipse(xpr, ypr, r, r, c, f, 2)
fb.ellipse(xpr, yphmr, r, r, c, f, 4)
fb.ellipse(xpwmr, yphmr, r, r, c, f, 8)
if f:
fb.rect(xpr, y, wmr2, r + 1, c, True) # top
fb.rect(x, ypr, w + 1, hmr2, c, True) # middle
fb.rect(xpr, yphmr, wmr2, r + 1, c, True) # bottom
else:
fb.hline(xpr, y, wmr2, c) # top
fb.vline(x, ypr, hmr2, c) # middle
fb.vline(xpw, ypr, hmr2, c) # middle
fb.hline(xpr, yph, wmr2, c) # bottom I cherry-picked your commit, it's +440 bytes and 7 ms to outline and 11 ms to fill. I had an idea for a compromise which is a slightly awkward API but smaller and marginally faster. That is to add an optional "expand x/y" parameter to ellipse, which shifts out the four quadrants by this amount and then fills/outlines the gaps as necessary. Implementation is here -- c055a42 So for example, This is +248 bytes and 4ms to outline and 11 ms to fill. What do you think @peterhinch ? The API is awkward, and it's a bit weird when you pass a mask that isn't 0xf, although it does let you draw just the top or bottom half of a rounded rect and could probably fix it without much cost to do the quadrants properly. |
Ingenious, but odd, not least specifying a rectangle by its centre. As an example of the implications, in micro-gui a user can specify an on-screen Pushbutton object. This can be round, rectangular or a rounded rectangle. All rectangles are positioned relative to their top left corner. With this API normal and rounded rectangles would need to be treated differently with the coordinates of the latter being tweaked before being passed to the C code. The API is a bit of a value judgement - perhaps worth canvassing the view of @dpgeorge? One factor in the tradeoff of C vs Python is that a C implementation saves RAM. With the other changes it provides a more complete feature set for FrameBuffer. However I'm surprised that the speed difference is so small - these are all times for 60 shapes? |
Yes I agree... FWIW I implemented a different version that makes Another reason that the ellipse-based approach and also this rect->ellipse approach is awkward is because it ultimately involves drawing at a center coordinate, it doesn't work when the width is even...
That's right. And I quoted "242 bytes of bytecode" above, which is just the difference in size of a .mpy file containing this function, but the reality is that slightly more actual RAM will be used to load that code.
Yes, unless I've missed something. The code is N = 20
R = 60
F = True
t = time.ticks_ms()
for i in range(N):
for j in range(R):
#pass
roundrect(fb, 10, 10, 80, 30, 10, 0xff, F)
#fb.ellipse(50, 25, 10, 10, 0xff, F, 0xf, 30, 5)
#fb.round_rect(10, 10, 80, 30, 10, 0xff, F)
lcd.show_framebuf(buf)
print("outline", time.ticks_diff(time.ticks_ms(), t) // N, "ms per frame") The baseline (with pass) is 31ms per frame, then the times quoted in the earlier comment are relative to that. |
OK, one final alternative to add to the mix...! This is basically identical to @peterhinch's round rect implementation, except it's implemented by adding an additional It comes in at +336 bytes. @dpgeorge over to you :p |
FWIW I like that. 👍 |
Just wanted to say a quick thanks to all of the contributors here; it's great to see framebuf get some attention! |
Several methods extract mp_int_t from adjacent arguments. This reduces code size for the repeated calls to mp_obj_get_int. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
We plan to add `ellipse` and `poly` methods, but rather than having to implement a `fill_xyz` version of each, we can make them take an optional fill argument. This commit add this to `rect` as a starting point. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Add method for drawing polygons. For non-filled polygons, uses the existing line-drawing code to render arbitrary polygons using the given coords list, at the given x,y position, in the given colour. For filled polygons, arbitrary closed polygons are rendered using a fast point-in-polygon algorithm to determine where the edges of the polygon lie on each pixel row. Tests and documentation updates are also included. Signed-off-by: Mat Booth <mat.booth@gmail.com>
Rather than drawing the entire boundary to catch missing pixels, just detect the cases where boundary pixels are skipped during node calculation and pre-emptively draw them then. This adds 72 bytes on PYBV11, but makes filled poly() 20% faster. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
4c077f2
to
321e3de
Compare
Rebased and added tests for drawing ellipse/poly out of bounds. |
Thanks everyone here for the work on these changes and additions. I think what's done here now is a good tradeoff between code size and features. At this stage I don't think it's worth adding a There are still other improvements to be made to |
@dpgeorge Many thanks for merging this :-) |
This snapshot includes the modframebuf improvements from ticket micropython/micropython#9045
This snapshot includes the modframebuf improvements from ticket micropython/micropython#9045
This snapshot includes the modframebuf improvements from ticket micropython/micropython#9045
I want to use the poly method to draw a triangle, what should I do? Any suggestions are welcome! |
The poly method has been merged, you can find them on a nightly build. Create a framebuf and then use poly - something like: import framebuf
import array
# Allocate a bytearray sufficiently large to store the pixel data
# This example is mono so 1 bit = 1 pixel, resolution of 24x24
fb = framebuf.FrameBuffer(bytearray(24*24//8), 24, 24, framebuf.MONO_VLSB)
# For efficiency, poly takes an array; define a triangular polygon
tri = array.array('h', [0, 0, 10, 0, 0, 10])
fb.poly(0, 0, tri)
# fb is ready to blit to a monochrome display Untested, but hopefully that'll get you started! |
Thanks for your code! ! ! I already understand how to use poly method. |
This allows subfolders to be treated similar to / for multiple apps within different folders. Also, fix up the internal current working directory so it doesn't depend on volumes. Fixes micropython#9045 and fixes micropython#8409.
This allows subfolders to be treated similar to / for multiple apps within different folders. Also, fix up the internal current working directory so it doesn't depend on volumes. Fixes micropython#9045 and fixes micropython#8409.
This combines #9038 by @peterhinch and #8987 by @mbooth101 with the following changes:
fill
argument torect
(with the aim of deprecating fill_rect). Withellipse
andpoly
now it's wasteful to have separatefill_ellipse
andfill_poly
methods. This adds 32 bytes.mp_obj_is_true
to dynruntime.h to support the fill argument. This doesn't require a mpy version jump as the table doesn't need to be updated.ellipse
from the original PR to match the argument order ofrect
(fill before mask).ellipse
fill use fill_rect rather than line. This will be more efficient as they're horizontal lines, so hopefully a bit of a performance boost there (ref extmod/framebuf: Add ellipse drawing method. #9038 (comment))ellipse
is now +564 bytes.poly
only supportarray.array
. This saves about 150 bytes.float
calculation with integer, removed heap allocation, minor size optimisations. All uppoly
is now +616 bytes (the original PR was just over 1k).(All size diffs calculated on PYBV11)
Note that only
ellipse
is added to the natmod version of framebuf as dynruntime.h doesn't supportmp_binary_get_size
&mp_binary_get_val_array
required bypoly
. We can do that in the next mpy version jump.Thank you @peterhinch and @mbooth101 for the work on this, it's a great improvement to framebuf!