Each cell now tracks it’s current color source:
* default fg/bg
* base16 fg/bg (maps to *both* the regular and bright colors)
* base256 fg/bg
* RGB
Note that we don’t have enough bits to separate the regular from the
bright colors. These _shouldn’t_ be the same, so we ought to be
fine...
Instead, do the palette lookup when we receive the DECGCI (i.e. when
the palette entry is selected), and store the actual color value in
our sixel struct.
We have all information we need to calculate the default background
color in sixel_init():
* Whether the image have transparency or not
* The current ANSI background color
If an image was split up across the scrollback, and the first image
chunk was resized due to it being blended with an already existing
sixel, we crashed.
The reason being the second chunk, emitted *after* the scrollback,
tried to memcpy() image data from a now-free:d image buffer.
The fix is pretty simple: create copies for *all* chunks when an image
has to be split up across the scrollback. Previously, the first chunk
re-used the source image buffer.
Closes#608
Writing a sixel on top of an already existing sixel currently has the
following limitations in foot:
* The parts of the first sixel that is covered by the new sixel are
removed, completely. Even if the new sixel has transparent
areas. I.e. writing a transparent sixel on top of another
sixel *replaces* the first sixel with the new sixel, instead of
layering them on top of each other.
* The second sixel erases the first sixel cell-wise. That is, a sixel
whose size isn’t a multiple of the cell dimensions will leave
unsightly holes in the first sixel.
This patch takes care of both issues.
The first one is actually the easiest one: all we need to do is
calculate the intersection, and blend the two images. To keep things
relatively simple, we use the pixman image from the *new* image, and
use the ‘OVER_REVERSE’ operation to blend the new image over the old
one.
That is, the old image is still split into four tiles (top, left,
right, bottom), just like before. But instead of throwing away the
fifth middle tile, we blend it with the new image. As an optimization,
this is only done if the new image has transparency (P1=1).
The second problem is solved by detecting when we’re erasing an area
from the second image that is larger than the new image. In this case,
we enlarge the new image, and copy the old image into the new one.
Finally, when we enlarge the new image, there may be areas in the new
image that is *not* covered by the old image. These areas are made
transparent.
The end result is:
* Each cell is covered by at *most* 1 sixel image. I.e. the total
numbers of sixels are finite. This is important for the ‘mpv
--vo=sixel’ use case - we don’t want to end up with thousands of
sixels layered on top of each other.
* Writing an opaque sixel on top of another sixel has _almost_ zero
performance impact. Especially if the two sixels have the same size,
so that we don’t have to resize the new image. Again, important for
the ‘mpv --vo=sixel’ use case.
Closes#562
If the image was accompanied with a “Set Raster Attributes” (SRA)
command, make sure we *never* shrink the image below the size
specified in the SRA.
Images are normally shrunk when their bottom rows are fully
transparent. This enables sixels that aren’t a multiple of 6 to be
emitted, without also emitting an SRA command.
But if there *is* an SRA command, obey it.
Verified against XTerm-367
sixel_color_set() is called when the number of (sixel) color registers
is changed.
It frees the current palette, and changes the “palette size” variable.
Originally, we only had a single palette. This is the one free:d by
sixel_color_set().
Later, we added support for private vs. shared palettes. With this
change, we now have one palette that is “never” free:d (the shared
one), and a private palette that is always free:d after a sixel has
been emitted.
‘sixel.palette’ is a pointer to the palette currently in use, and
should only be accessed **while emitting a sixel**.
This is the pointer sixel_color_set() free:d. So for example, if
‘sixel.palette’ pointed to the shared palette, we’d free the shared
palette. But, we didn’t reset ‘sixel.shared_palette’, causing a double
free later on.
Closes#427
0 is a perfectly valid row number, and if max_non_empty_row_no==0,
that means we have *1* sixel row, and after trimming the image, the
image will have a height of 6 pixels.
If the sixel sequence is empty (or at least doesn’t emit any non-empty
pixels), then trimming the image should result in an image height of
0.
When max_non_empty_row_no is initialized to -1, it will still have
that value in unhook(), which makes the final image height 0.
term_print() is called whenever the client application “prints”
something to the grid. It is called for both ASCII and UTF-8
characters, and needs to handle sixels, insert mode and ASCII
vs. graphical charsets.
Since it’s on the hot path, this becomes unnecessarily slow.
This patch adds a “fast” version of term_print(), tailored for the
common case: ASCII characters in non-insert mode, without any sixels
and non-graphical charsets.
A new function, term_update_ascii_printer(), has been added, and must
be called whenever:
* The currently selected charset *index* changes
* The currently selected charset changes (from ASCII to graphical, or
vice verse)
* Sixels are added to the grid
* Sixels are removed from the grid
* Insert mode is enabled/disabled
This avoids a call to sixel_overwrite_by_row() (where we also exit
early if the image list is empty).
This saves a couple of instructions to set up the arguments for
sixel_overwrite_by_row().
“current geometry” will report whatever value is the smallest; the max
geometry or the current window size.
But “max geometry” always returns the configured max geometry.
This aligns foot’s behavior with XTerm.
When P2=1, empty pixels are transparent.
This patch also changes the behavior of P2=0|2, from setting empty
pixels to the default background color, to instead use the *current*
background color.
To implement this, a couple of changes are needed:
* Sixel pixels always use alpha=1.0, except for *empty* cells when
P2=1 (i.e. transparent pixels).
* The renderer draws sixels with the OVER operator, instead of the SRC
operator.
* The renderer *must* now render the cells beneath the sixel. As an
optimization, this is only done for sixels where P2=1. I.e. for
fully opaque sixels, there’s no need to render the cells beneath.
The sixel renderer isn’t yet hooked into the multi-threaded
renderer. This means *rows* (not just the cells) beneath
maybe-transparent sixels are rendered single-threaded.
Closes#391.
This ensures we don’t trim off bottom rows in unhook().
This could happen either because the application used “Set Raster
Attributes” to configure an image size larger than the sixels later
emitted.
Or, the last sixel row contains empty pixel rows.
In either case, the size set with “Set Raster Attributes” defines
the *minimum* image size; the image may still end up being larger.
DECGRI, i.e. repeat sixel character, only need to do image resizing,
and updating the current ‘column’ value *once*.
By adding sixel_add_many(), and doing the size/resize checking there,
the performance of sixel_add() is made much simpler.
This boosts performance quite noticeably when the application is
emitting many and/or long repeat sequences.
This results in the same number of instructions inside the loop, with
a ‘lea’ instead of a ‘mov’, but simplifies the post-loop logic to
update the global state.
All-empty pixels rows in the last sixel row should not be included in
the final sixel image.
This allows applications to emit sixels whose height is not a multiple
of 6, and is how XTerm works.
This is done by tracking the largest row number that contains
non-empty pixels.
In unhook, when emitting the image, the image height is adjusted based
on this value.
This is to optimize sixel_add(). It already checks ‘pos’, to see if it
needs to resize the image, and if the resize fails (either due to
allocation failures, or because we’ve reached the maximum width), we
bail out.
We need to do the same thing for height. But, we don’t want to add
another check to sixel_add() since its’s performance critical.
By setting ‘col’ outside the image boundaries when ‘row’ reaches the
maximum image height, we can “re-use” the existing code in
sixel_add().
This reverts commit 5a9d70da837c20a9641d6cbadccc962a8e5a4283.
This broke jexer.
Comparing with what XTerm does, we can see that it updates its image
height for *each* pixel in *each* sixel. I.e. empty pixels at the
bottom of a sixel will not increase the image size.
Foot currently bumps the image height 6 pixels at a time, i.e. always
a whole pixel.
Given this, always rounding up the height to the nearest multiple of
6 (say, for example, when responding to a DECGRA command), is wrong.
Now, we use the image size specified in DECGRA as is, while still
allowing the image to grow beyond that if necessary.
What’s left to do, if we want to behave *exactly* like XTerm, is stop
growing the image with 6 pixels at a time when dynamically adjusting
the image size.
By storing the current row’s byte offset into the backing image in the
terminal struct.
This replaces the ‘imul’ with a load, which can potentially be
slow. But, this data should already be in the cache.
When we allocate the backing buffer, the number of allocated rows is a
multiple of 6.
When persisting the image, make sure its height does not exceed the
current maximum height.
Foot has, up until now, used a fixed image size when the application
used DECGRA (Raster Attributes) to “configure” the image size.
This was based on a misunderstanding, that this was how you emitted
sixels where the height was *not* a multiple of 6.
This isn’t the case. The VT340 documentation is actually pretty clear
about this:
Ph and Pv do not limit the size of the image defined by the sixel
data. However, Ph and Pv let you omit background sixel data from the
image definition and still have a color background. They also
provide a concise way for the application or terminal to encode the
size of an image.
This is also how XTerm behaves. Test image:
\EPq
"1;1;1;1
#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0
#1~~@@vv@@~~@@~~$
#2??}}GG}}??}}??-
#1!14@
\E\
This uses DECGRA to set the image size to 1x1. The sixel however
is *not* clipped to 1x1, but is resized to 14x12
When sixel scrolling is disabled (private mode 80 is off), and scroll
margins have been set, XTerm seems to ignore the top margin (sixel
still begins at (0,0)), but does not go past the bottom margin.
This patch implements the same behavior in foot.
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.