Commit graph

181 commits

Author SHA1 Message Date
Daniel Eklöf
49fb0cf359
sixel: re-scale images when the cell dimensions change
Before this patch, when the cell dimensions changed (i.e. when the
font size changes), sixel images were either removed (the new cell
dimensions are smaller than the old), or simply kept at their original
size (new cell dimensions are larger).

With this patch, sixels are instead resized. This means a
sixel *always* occupies the same number of rows and columns,
regardless of how much the font size is changed.

This is done by maintaining two sets of image data and pixman images,
as well as their dimensions. These two sets are the new ‘original’ and
‘scaled’ members of the sixel struct.

The "top-level" pixman image pointer, and the ‘width’ and ‘height’
members either point to the "original", or the "scaled" version.

They are invalidated as soon as the cell dimensions change. They, and
the ‘scaled’ image is updated on-demand (when we need to render a
sixel).

Note that the ‘scaled’ image is always NULL when the current cell
dimensions matches the ones used when emitting the sixel (to save
run-time memory).

Closes #1383
2023-06-30 08:29:35 +02:00
Daniel Eklöf
5e9d68695c
sixel: add_ar_11(): manually unroll loop
This generates both smaller, and faster code
2023-06-29 15:40:00 +02:00
Daniel Eklöf
75f9bed6b6
sixel: refactor: shorten very verbose switch case statements 2023-06-29 15:40:00 +02:00
Daniel Eklöf
3555e81fee
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).

It makes a lot of sense, performance wise, to special case
them.

Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.

sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.

Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.

Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-29 15:40:00 +02:00
Daniel Eklöf
2388015b10
sixel: assert upper pixel of last sixel maps to last image row, *or lower* 2023-06-24 07:31:08 +02:00
Daniel Eklöf
c15e75357a
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:

a) scroll terminal content to ensure the new image fits
b) position the text cursor

Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.

Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.

In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.

This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.

The fix is this:

* Always scroll by the number of rows occupied by the image, minus
  one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-24 07:31:08 +02:00
Daniel Eklöf
425cf894d4
sixel: resize(): handle no size change 2023-06-24 07:31:08 +02:00
Daniel Eklöf
5d576fccba
sixel: regression: linefeed count for chunked up sixel image
All image chunks but the last *should* scroll the screen content.
2023-06-24 07:31:08 +02:00
Daniel Eklöf
1eb90b2405
sixel: minor fixes after implementing support for non-1:1 aspect ratios
* Lazy initialize image height. This is necessary to prevent garbage
  from being rendered for "empty" sixels.
* Fix plotting of non-1:1 pixels
* Fix calculation of height in resize(), for non-1:1 aspect ratios
2023-06-24 07:31:08 +02:00
Daniel Eklöf
774570ec41
sixel: stop cropping images to the last non-transparent row 2023-06-24 07:31:08 +02:00
Daniel Eklöf
d6d143e2a6
sixel: respect sixel aspect ratio
That is, parse P1 when initializing a new sixel, and don’t ignore
pad/pad in the raster attributes command.

The default aspect ratio is 2:1, but most sixels will override it in
the raster attributes command (to 1:1).
2023-06-24 07:31:08 +02:00
Daniel Eklöf
66d9b8da60
sixel: fix cursor positioning logic
This adjusts the logic that positions the text cursor after emitting a
sixel, when sixel scrolling mode is *enabled*.

We’ve always mimicked XTerm’s behavior. However, XTerm recently
changed its behavior, to better match that of an VT382.

Now, the cursor is placed *on* the last row of the sixel, instead of
on a new row after the sixel.

This allows applications to print sixels to the bottom row of the
terminal, without causing the content to scroll.

Finally, there was a bug in the horizontal positioning of the cursor;
it was placed on the *first* column of the row, instead of on the
first column of the sixel.
2023-06-24 07:31:02 +02:00
Daniel Eklöf
1433a81c08
sixel: apply background alpha when P2=0 or P2=2, and current bg color is the default bg color
Closes #1360
2023-05-31 16:27:48 +02:00
Daniel Eklöf
1c16e4a575
Tag a couple variables with UNUSED, to fix warnings with clang-15
Closes #1278
2023-02-12 19:09:48 +01:00
Daniel Eklöf
43a48f53d4
sixel: don’t crash when sixel image exceeds current sixel max height
When we try to resize a sixel past the current max height, we set
col > image-width to signal this.

This means ‘width’ could be smaller than ‘col’. When calculating how
many sixels to emit in sixel_add_many(), we didnt’ account for this.

The resulting value was -1, converted to ‘unsigned’. I.e. a very large
value. This resulted in an assert triggering in sixel_add() in debug
builds, and a crash in release builds.
2022-10-13 17:52:34 +02:00
Daniel Eklöf
f70c34c5a8
sixel: add sixel_reflow_grid()
This function reflows all sixels in the specified grid.

The pre-existing sixel_reflow() function is a shortcut for

  sixel_reflow_grid(term, &term->normal)
  sixel_reflow_grid(term, &term->alt);
2022-10-10 17:19:18 +02:00
Daniel Eklöf
1d4e1b921d
sixel/terminal: use the new grid and selection APIs
Use grid_row_abs_to_sb() instead of manually “rebasing” row numbers.

Use selection_get_{start,end}() to retrieve the current selection
coordinates.
2022-04-25 20:00:14 +02:00
Daniel Eklöf
58bbbb0a31
sixel: revert to default state when an invalid DECGRI character is received 2022-02-04 18:14:25 +01:00
Daniel Eklöf
9e8d553a21
sixel: DECGRI (repeat) with a count of 0 should emit a single sixel 2022-02-04 18:14:24 +01:00
Daniel Eklöf
9150507209
sixel: resize: truncate instead of failing, when new size exceeds max size
If the size we’re trying to set exceeds the configured max size,
truncate instead of failing.
2022-02-04 18:14:24 +01:00
Daniel Eklöf
b218b8cfb0
sixel: VT state’s bg color may now be an index, rather than an actual color value 2021-12-26 12:37:48 +01:00
Daniel Eklöf
d46af6bd7a
term: track cell color source
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...
2021-11-20 16:46:38 +01:00
Daniel Eklöf
b1a4d43845
sixel: call decsixel() directly, instead of going through sixel_put() 2021-09-05 11:09:45 +02:00
Daniel Eklöf
47c32d5913
sixel: avoid looking up color from palette for each sixel
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.
2021-09-05 11:08:13 +02:00
Daniel Eklöf
f9642e9597
sixel: calculate default bg once, in init
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
2021-09-05 10:27:13 +02:00
Daniel Eklöf
f65e062ce4
sixel: destroy(): don’t unref a NULL pixman image 2021-07-18 16:30:42 +02:00
Daniel Eklöf
a52d867947
sixel: fix crash when splitting up an image across the scrollback
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
2021-06-24 17:08:33 +02:00
Daniel Eklöf
6d336fcadd
sixel: add support for overlapping sixels
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
2021-06-09 10:00:19 +02:00
Daniel Eklöf
e0f1a4ae33
sixel: don’t *ever* shrink image below its SRA size
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
2021-05-16 11:00:32 +02:00
Daniel Eklöf
ba451af5c7
sixel: don’t emit sixels that will end up covering more than the entire scrollback
This limit can be reached by the sixel alone, or the sixel + the final
newline.

Closes #494
2021-05-08 20:28:16 +02:00
Daniel Eklöf
9bd14c0fd2
sixel: fix assertion: image is allowed to cover the entire scrollback 2021-05-08 20:28:16 +02:00
Daniel Eklöf
5f8a8951c7
sixel: unhook: tighten up the condition needed for us to free image data pre-maturely 2021-04-07 19:08:29 +02:00
Daniel Eklöf
f9a730f33b
sixel: sixel_fini(): free image data
Normally, this data is always free:d indirectly, when the sixel image
that took over ownership is destroyed. But let’s play it safe.
2021-04-07 19:07:43 +02:00
Daniel Eklöf
00083125a1
sixel: fix double free caused by bad free() in sixel_colors_set()
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
2021-03-30 11:08:03 +02:00
Daniel Eklöf
19289bad5e
sixel: free backing buffer if final image size is zero 2021-03-28 13:22:10 +02:00
Daniel Eklöf
3566be591a
sixel: initialize max_non_empty_row_no to -1, not 0
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.
2021-03-28 13:22:09 +02:00
Daniel Eklöf
60b3ccc641
term: runtime switch between a ‘fast’ and a ‘generic’ ASCII print function
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
2021-03-16 08:45:18 +01:00
Daniel Eklöf
c5c3447ca8
sixel: sixel_overwrite_at_cursor(): early exit when the image list is empty
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().
2021-03-14 14:19:12 +01:00
Daniel Eklöf
6eb68ffbd9
sixel: max geometry should be max geometry, not current window size
“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.
2021-03-14 10:58:13 +01:00
Daniel Eklöf
7d315d7bf9
sixel: implement P2=1 - transparent pixels
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.
2021-03-11 17:34:19 +01:00
Daniel Eklöf
6f6bcbc1bc
sixel: decgra: set max-non-empty-row-no when resizing the image
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.
2021-03-11 17:33:01 +01:00
Daniel Eklöf
ae86043780
sixel: decgri: handle a repeat count of 0, by ignoring it 2021-03-11 17:32:59 +01:00
Daniel Eklöf
6d208fa5e0
sixel: add: add sixel_add_many(), improving performance of DECGRI
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.
2021-03-11 17:32:59 +01:00
Daniel Eklöf
6e963dbf68
sixel: add: calculate absolute row no inside the loop
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.
2021-03-11 17:32:59 +01:00
Daniel Eklöf
751ccf5316
sixel: add: increase data pointer instead of offset
Same ‘add’ instruction to increase the offset, but simpler ‘mov’
instruction when writing the pixel data.
2021-03-11 17:32:58 +01:00
Daniel Eklöf
777576b66b
sixel: decgri: avoid load inside for-loop 2021-03-11 17:32:58 +01:00
Daniel Eklöf
9f224a13df
sixel: add optimized resize_horizontally() and resize_vertically()
The resize operations performed in the “hot” path either change the
width, or the height, but not both at the same time.
2021-03-11 17:32:58 +01:00
Daniel Eklöf
c181eb2bf6
sixel: empty pixels in the last sixel row doesn’t contribute to the image height
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.
2021-03-11 17:32:58 +01:00
Daniel Eklöf
d35963f584
sixel: resize: width is no longer a multiple of 6 2021-03-11 17:32:58 +01:00
Daniel Eklöf
6ab7052be4
sixel: set ‘col’ outside image boundaries when we’ve reached max height
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().
2021-03-11 17:32:58 +01:00