Skip to content
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

Transparent blitting doesn't properly stack #1068

Closed
dankamongmen opened this issue Oct 18, 2020 · 21 comments · Fixed by #1317
Closed

Transparent blitting doesn't properly stack #1068

dankamongmen opened this issue Oct 18, 2020 · 21 comments · Fixed by #1317
Assignees
Labels
bug Something isn't working
Milestone

Comments

@dankamongmen
Copy link
Owner

dankamongmen commented Oct 18, 2020

This was first discovered in #1065, and I've made it its own bug. This is independent of quadblitter (it can also happen with halfblitter). See the image below:

96387250-ac510e00-116e-11eb-8f61-7e2023e41d05

So each place where we have the invalid green is a cell from CelesF.png where we have a transparent pixel above a non-transparent pixel, and the corresponding map area has two black pixels above two green pixels. So in such a case, the quadblitter/halfblitter is going to solve for a bottom half with appropriate foreground and transparent background for the top plane. The bottom plane then cheerfully sets the background to its solved background, which is, of course, green (since it always emits lower halves). This problem exists even if we always emit upper as opposed to lower half blocks (which we can't do anyway, as it screws up transparency) -- quadblitter could emit say a bottom+right triblock. Well, shit. This is a deep problem, and solving it correctly will require some extensive plumbing. We'd basically need to recognize that we are serving as the background of any already-selected, independent glyph, figure out what color of ours dominates the uncovered area, and make that our background color. That's the fully general solution, of course; we can probably get by with just the simple geometry of quadblitting. Still kinda delicate and subtle, though.

@dankamongmen
Copy link
Owner Author

dankamongmen commented Oct 19, 2020

So to do this right, you absolutely have to do it in notcurses_render(). When you hit the lower plane below a partially- or wholly- color-transparent glyph, you know:

  • the chosen EGC
  • the fg thus far
  • the bg thus far

The EGC is always solved for by this time, but one or both of the colors are not yet solved. So essentially as you're descending, you need to identify cases where the background vs. foreground were chosen arbitrarily, and both are in use, and your unsolved area is within their foreground area. This can only be the case for half blocks (upper, lower, left, or right), quarter blocks (all four), and three-quarter blocks (again all four) (and since Unicode 13 -- see #137 -- the sixth blocks) -- spaces vs full blocks only use one of bg or fg (and we always generate spaces, so we always get bg, which works by itself), and no other glyphs have symmetry across bg/fg regions.

(I leave aside the case of, e.g. a space being written with both colors transparent. Imagine that there is a half block underneath it. Whatever we solve for the foreground will be ignored; only the background matters here. Which color do we take? Always the background? Perhaps. Now imagine we have a three-quarter underneath it. Ought we take the foreground, since that would dominate in the space's absence? Or the background, despite only being a quarter of the cell? I think always the background in both cases here, and for all other glyphs not closed under inversion within Unicode.)

and there are cases we can't get exactly right. imagine we have a RIGHT HALF BLOCK atop a THREE-QUARTERS BLOCK ocupying the lower left and both right quarters. The RIGHT HALF BLOCK leaves the left hand side in its entirety transparent. What color ought the left hand side get? Here again I say we default to the background, since we must somehow break the symmetry.

So in paint():

  • categorize the specified block glyphs in terms of their background relative to other block glyphs. this can be a big-ass constant table
  • for a given plane, solve egc after solving colors (we currently do it before)
  • when we solve for the EGC, check to see if it's one of these block glyphs.
    • If so, set a bool jigsaw.
  • when we attempt to solve for the background only, check to see if jigsaw is set (we could check only when solving for background if we only cared about solving this for our blitters... which maybe we do? i.e. if a person hand-writes a RIGHT HALF with both colors transparent, and the egc below is a LEFT HALF with both colors set, ought we invert? surely not -- that's not what the user intended. so yes, only for background).
    • if so, check the current plane's EGC. is it one of these block glyphs?
    • if not, continue on as usual
    • otherwise, see if the solved EGC's background is entirely within the current EGC's foreground
    • if not, continue on as usual
    • if so, solve using the current cell's foreground color instead of background color

yessssss, i think that works.

@grendello , @dcantrell , hell @dcantrell , i'd like some additional eyes on this problem and plan. it's very subtle.

@dankamongmen
Copy link
Owner Author

...can't decide whether i want to block 2.1.0 on this...

@dankamongmen dankamongmen modified the milestones: 2.1.0, 2.2.0 Nov 28, 2020
@dcantrell
Copy link
Contributor

I just saw this now...oops. While I've been playing with notcurses on the side, I have not had a lot of time to dig in to the details and really haven't had time to look in to this one. So as of right now I have no opinion. I am taking a lot of time off this December, so I'll probably have time to catch up on !work, but I can't promise anything.

@dankamongmen
Copy link
Owner Author

I just saw this now...oops. While I've been playing with notcurses on the side, I have not had a lot of time to dig in to the details and really haven't had time to look in to this one. So as of right now I have no opinion. I am taking a lot of time off this December, so I'll probably have time to catch up on !work, but I can't promise anything.

no worries at all man, was just inviting your comments as a respected computer scientist =].

@dankamongmen
Copy link
Owner Author

so i think the first thing to do is to make some solid unit tests around this, and make them WARN for now.

dankamongmen added a commit that referenced this issue Jan 8, 2021
@dankamongmen
Copy link
Owner Author

I've added the Stacking unit tests, which (as expected) currently fail.

@dankamongmen
Copy link
Owner Author

I really want to get this done for 2.2.0, where it's the last major remaining feature.

dankamongmen added a commit that referenced this issue Jan 29, 2021
For properly stacking transparent blittings (#1068), we
need tag those cells which both (1) originated in an
ncvisual operation and (2) have some transparency. For
the three affected blitters (halfblock, quadrant, and
sexblitter), call cell_set_blitted().
dankamongmen added a commit that referenced this issue Jan 29, 2021
We need track whether we've entered blitterstacking
mode across the paint() of a given cell. This means
stashing it in the crender rvec #1068.
@dankamongmen
Copy link
Owner Author

I've started moving on this. I've added cell_blitted_p() and cell_set_blitted(). These are called from the halfblock, quad, and sexblitter when there's a transparent background. That's nice -- we only mess with setting this exactly when we need to.

I've added blitterstacked to the struct crender. This will be set iff cell_blitted_p() is set when a glyph is selected.

dankamongmen added a commit that referenced this issue Jan 29, 2021
Iff cell_blitted_p() when we select the glyph in paint(),
we ought enter blitter stacking mode. Make this decision
in paint() #1068.
@dankamongmen
Copy link
Owner Author

So what remains is to amend paint() so that when we're setting the background, and blitterstacked is set, we possibly use the foreground instead. We currently solve background prior to foreground, but we can change that -- it is an artifact of how we used to do CELL_ALPHA_HIGHCONTRAST. Check for blitterstacked. If it is set, our glyph could not have been selected, since we solve for the glyph after the colors. So check our glyph against the selected glyph (we need to build this table) at that point. If our foreground is in their transparent region, use the foreground for the background. That's the local foreground, not the computed foreground.

If we do those maps in O(1), this is all O(1), and only ever touched where necessary.

@dankamongmen
Copy link
Owner Author

I'm now doing the inversion in paint(). We just need to build and check the maps.

@dankamongmen
Copy link
Owner Author

OK, so for the last part, here's what I'm thinking. We can't do a conditionless O(1) map so far as I can tell. But we can get pretty close, with O(1) using four conditions. There's no point in doing this when we set blitterstacked, since we don't know we'll need the mapping, and we don't get any savings. Instead, do it lazily when we hit a lower blitted glyph in a blitterstacking situation. with that said, cache the result just in case we hit a second glyph. actually...i think blitterstacking might only be a one-shot deal. we're certainly not going to generate two levels of Cartesians, and it's doubtful that we could even take advantage of it...yeah. the map is to { NORMAL, INVERT, TRANS }, and in the case of trans you just take a no-op and let the blitterstacked propagate downward. otherwise, you reset it. w00t! that's a good insight. but in that case, do we still want to cache? no, i think not, that's an 8-byte space penalty to every crender struct for a vanishingly rare case. do a repeated lookup if need be -- after all, it's O(1).

so, when you hit all three parts of the blitter stacking conditional, you look up a map set based on the chosen glyph, and a secondary set based on the current glyph. i don't think this can be parallelized. the O(1)+4cond lookup involves getting an index from the glyph. you need the four conditionals to determine whether the glyph is in the half, quad, or sex range of Unicode. then you take a difference against the range base, add it to the skipped count, and get your index. this is done twice to find the correct map entry, which will be precomputed (this will require 5041 bytes assuming one byte per map entry, nothing doing). the map entry is, as mentioned above, {NORMAL, INVERT, TRANS}. if NORMAL, use the background color. if INVERT, use the foreground color. if TRANS....oh, i think we can do without TRANS, actually? that's just NORMAL + local trans. so that's a single bit. we could do 631 bytes if we used a bit per entry.

ok, i think this is a good plan. this ought solve the deep problem of transparent blitter stacking, and do it all in O(N) time and O(1) space. the O(N) time refers to N planes deep, when 99.999% of the time we'll only touch one plane. we add a total of 1 conditional per plane that's not affected, and 9 conditionals per plane that is affected, and this will be a very rare effect. i like it.

@dankamongmen
Copy link
Owner Author

I think we can actually do this without the full map. We can just track the possible regions, associate them with each piece, and do the bitwise calculation. That drops us to a byte or less per glyph, so fewer than 100 total bytes.

@dankamongmen
Copy link
Owner Author

If we keep it down to 8 regions or fewer (done by definition), we can just encode this directly into the nccell, and we don't need any additional space. yes!

@dankamongmen
Copy link
Owner Author

So we currently have 6 free bits in the 64-bit channels. We could add a quadrant bitmask and have 2 left. Can we reclaim any existing ones?

  • we have a bit for "drawn by ncvisual", which is used to turn on blitterstacking mode. is this bit properly implied by any of the four quadrants being set? yes. what about if ncvisual draws a transparent cell? then we don't want to go into blitter stacking mode, because there has been no effect. so yes, we can reclaim this bit.
  • we have a bit for "entirely foreground", which is used to elide emission of a background color. is this properly implied by all of the four quadrants being set? it depends. let's say we have the sextant 🬰. under one design, we would set all four quadrant bits. under another design, we would set...none? yeah, i actually think this glyph ought set none, since we want any underlying half/quadrant to fill the empty space...but it would anyway; the only question is whether we want foreground color in the background. i don't think we do, but i'm also not convinced that it matters (the choice seems arbitrary). let's look at it another way: to properly imply full foreground, only true full foreground glyphs could set all four quadrants. that basically implies that a sextant glyph must fully occupy the quadrant -- missing the center row misses the fill. so yeah, in the most pathological case, 🬰. sets zero quadrant bits despite occupying 2/3 of the glyph area =[. but the result is only that a blitted cell below us uses its foreground as its background, and we reclaim a precious bit. do it.

so we have 6 free bits, and we're using 4, but we're reclaiming two, with a net result of four free bits. i think it's worth it to get a no-spacecost solution to this vexing bug.

@dankamongmen
Copy link
Owner Author

it's safe to change the meaning of those bits, as they were never exposed to the user. it's unsafe however, from an ABI perspective, to go shuffling bits around arbitrarily like i was starting to do in this branch, argh. reverting all that noise.

@dankamongmen
Copy link
Owner Author

i believe we have a good fix!

dankamongmen added a commit that referenced this issue Jan 31, 2021
This completes the work for #1068. This addressed a subtle issue.
When we're using pixel->semigraphic art, we want slightly different
rendering. Essentially, imagine that we have two images, each two
pixels tall and one pixel wide. The top image is a transparent pixel
above a white pixel. The bottom image is a white pixel above a black
pixel. We'd expect the result to be two white pixels, but we can
instead get a black pixel above a white pixel. This is because the
*background* color is being merged from the bottom plane, but really
we want the *top* color. Ncvisuals are now blitted along with
information regarding which quadrants they draw over, and when
appropriate, we invert the foreground and background. Closes #1068.
@dankamongmen
Copy link
Owner Author

all unit tests pass, and there is no measurable performance impact on notcurses-demo. we are using an extra 4 bits per struct crender, which we can reclaim, for zero net space impact. we have reduced the number of free bits per nccell from six to four. the rendering patch has been complicated; paint() is noticeably nastier. i'm willing to accept that. w00t!!!!

w0000t!

...in fact...

i don't want nobody beating up the dance floor but us

dankamongmen added a commit that referenced this issue Jan 31, 2021
For properly stacking transparent blittings (#1068), we
need tag those cells which both (1) originated in an
ncvisual operation and (2) have some transparency. For
the three affected blitters (halfblock, quadrant, and
sexblitter), call cell_set_blitted().
dankamongmen added a commit that referenced this issue Jan 31, 2021
We need track whether we've entered blitterstacking
mode across the paint() of a given cell. This means
stashing it in the crender rvec #1068.
dankamongmen added a commit that referenced this issue Jan 31, 2021
Iff cell_blitted_p() when we select the glyph in paint(),
we ought enter blitter stacking mode. Make this decision
in paint() #1068.
dankamongmen added a commit that referenced this issue Jan 31, 2021
Reclaim the "blitted" and "full foreground" bits in the
channels. Instead, we now write four bits, encoding the
four quadrants we might occupy as the result of a blit.
These four imply the previous two, leaving us with four
free bits remaining in the channels. This opens a clear
path to O(1)-time, zero-space blitter stacking #1068.
w00t!
dankamongmen added a commit that referenced this issue Jan 31, 2021
This completes the work for #1068. This addressed a subtle issue.
When we're using pixel->semigraphic art, we want slightly different
rendering. Essentially, imagine that we have two images, each two
pixels tall and one pixel wide. The top image is a transparent pixel
above a white pixel. The bottom image is a white pixel above a black
pixel. We'd expect the result to be two white pixels, but we can
instead get a black pixel above a white pixel. This is because the
*background* color is being merged from the bottom plane, but really
we want the *top* color. Ncvisuals are now blitted along with
information regarding which quadrants they draw over, and when
appropriate, we invert the foreground and background. Closes #1068.
dankamongmen added a commit that referenced this issue Feb 1, 2021
@dankamongmen
Copy link
Owner Author

OK, i've found weird behavior in StackedQuadCrossed, and added the new test LowerAtopUpperWhite, which is broken. So more work needs to be done here. See #1318.

@dankamongmen
Copy link
Owner Author

Figured out what was wrong -- we wanted to invert the bits, not invert the logical result -- ~, not !. Doh! Ready to merge.

dankamongmen added a commit that referenced this issue Feb 2, 2021
For properly stacking transparent blittings (#1068), we
need tag those cells which both (1) originated in an
ncvisual operation and (2) have some transparency. For
the three affected blitters (halfblock, quadrant, and
sexblitter), call cell_set_blitted().
dankamongmen added a commit that referenced this issue Feb 2, 2021
We need track whether we've entered blitterstacking
mode across the paint() of a given cell. This means
stashing it in the crender rvec #1068.
dankamongmen added a commit that referenced this issue Feb 2, 2021
Iff cell_blitted_p() when we select the glyph in paint(),
we ought enter blitter stacking mode. Make this decision
in paint() #1068.
dankamongmen added a commit that referenced this issue Feb 2, 2021
Reclaim the "blitted" and "full foreground" bits in the
channels. Instead, we now write four bits, encoding the
four quadrants we might occupy as the result of a blit.
These four imply the previous two, leaving us with four
free bits remaining in the channels. This opens a clear
path to O(1)-time, zero-space blitter stacking #1068.
w00t!
dankamongmen added a commit that referenced this issue Feb 2, 2021
This completes the work for #1068. This addressed a subtle issue.
When we're using pixel->semigraphic art, we want slightly different
rendering. Essentially, imagine that we have two images, each two
pixels tall and one pixel wide. The top image is a transparent pixel
above a white pixel. The bottom image is a white pixel above a black
pixel. We'd expect the result to be two white pixels, but we can
instead get a black pixel above a white pixel. This is because the
*background* color is being merged from the bottom plane, but really
we want the *top* color. Ncvisuals are now blitted along with
information regarding which quadrants they draw over, and when
appropriate, we invert the foreground and background. Closes #1068.
dankamongmen added a commit that referenced this issue Feb 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants