EXOTIC SILICON
“Blink and you'll miss it! 4096 colours and flashing text on the console!”
Implementing 4-bit RGB colour, 8-bit greyscale, and the blink attribute on wscons
Since posting the latest version of our console enhancement patchset for OpenBSD 7.7, we've received a fair amount of feedback about it, (and most of it positive, too!).
A couple of feature requests came up that seemed worthy of a detailed response, specifically:
  • Supporting more than 256 colors
  • Implementing the blink attribute
Let's see how it's done!
If you just want to try the code out on local machines without seeing how it works, patches are available for download.
Note that these are designed to be applied on top of our existing console enhancement patchset, so download and apply that first if you haven't already.
More than 256 colors
Introduction and background information
Before:
xxxxFFFFxxxxBBBB654321iIwsdcufhr
After:
FFFFFFFFBBBBBBBB654321iIwsdcufhr
Additional unused bits were conveniently already available.
Expanding the existing code in wscons and rasops from 16 colors to 256 was fairly straightforward.
There was already space in the 32-bit attribute value to expand the two four-bit fields, (for foreground and background color), to eight bits each.
Beyond this, it was just necessary to calculate the additional color values, and implement some logic to enable the required CSI 38;5, and CSI 48;5 control sequences to select them as foreground and background colors.
Key to bits / flags:
F - Foreground
B - Background
x - unused
r - reverse
h - highlight (bold)
f - flash (blink)
u - underline, (previously in bit 0 in much older wscons code)
c - color active
The following are Exotic Silicon extensions to the OpenBSD code:
d - dim
s - strike
w - double underline
I - invisible
i - italic, (rendered as oblique or slanted)
The following bits are unused:
1 - unused
2 - unused
3 - unused
4 - unused
5 - unused
6 - unused
More than 256 colors
24-bit color
The next obvious step would be to expand the color range to 24-bit direct RGB color, allowing sixteen million colors to be used.
Several control sequences to do this are already in common use, but the two sequences CSI 38;2;R;G;Bm and CSI 48;2;R;G;Bm for setting foreground and background color respectively are fairly well standardised in practice. The code shown here will implement these, although adding support for other common sequences as necessary would also be trivial.
Unfortunately, the existing foreground and background fields in the attribute value can't be conveniently expanded in the same way as we did for the move from 16 colors to 256, as there are no spare unused bits above them.
Changing the definition of attr from uint32_t to uint64_t is one possibility. In fact, some time ago attr was actually defined as a long. It was changed to the current uint32_t in 2020 during the development cycle for OpenBSD 6.8. Looking at the CVS logs for that change allows us to see quite easily the amount of code that was touched, and which would need to be changed again to make attr a uint64_t.
The additional 32 bits gained by making this change would provide exactly the extra storage necessary to store a 24-bit RGB value for both foreground and background, as the existing 8-bit fields could be re-purposed to hold one of the color channels. We would also need to use an additional two flag bits to indicate whether each of the foreground and background colors was to be interpreted an an 8-bit indexed color, or as a 24-bit direct RGB value.
rrrrrrrrggggggggRRRRRRRRGGGGGGGGbbbbbbbbBBBBBBBB6543YZiidsdcufhr
The 64-bit attr value would therefore have a layout along these lines.
Key:
r - foreground red value, (8-bit)
g - foreground green value, (8-bit)
b - foreground blue value, (8-bit)
R - background red value, (8-bit)
G - background green value, (8-bit)
B - background blue value, (8-bit)
Y - flag foreground high bits are valid color data
Z - flag background high bits are valid color data
This seems like a good idea, except that since the low-level DRI drivers call ri_ops functions that use attr values, expanding attr to 64 bits would greatly increase the complexity of the console enhancement patchset as well as the work involved porting it to each new OpenBSD release.
More than 256 colors
Other less complex possibilities
FFFFFFFFBBBBBBBB654321iIdsdcufhr
Unused flag bits
Nevertheless, we can still improve the current 256 color selection slightly whilst avoiding this level of complexity.
Notice that we have a total of six unused flag bits in the 32-bit attr when running with 256 colors:
If we use one flag bit each for foreground and background to actually function as a flag and indicate that the color data is direct RGB and not an index in to the color map, then we can use the other four spare bits to increase the eight bit color value fields to ten bits each. This isn't very much at all, and would only allow a small increase to 1024 directly selected RGB foreground and background colors.
However, if we implement RGB color representation for just one of either foreground or background, we would have an extra five bits available, making 13 bits in total when combined with the existing field. This would allow for up to 8192 RGB colors.
Alternatively, if we limit ourselves to 12 bits then each RGB channel can have the same bit depth of 4 bits. This produces 4096 distinct colors.
Since 4 bits per channel RGB is indeed something of an improvement over the 256 color palette, (which effectively has around 2.6 bits of entropy per channel), I took the time to implement it.
More than 256 colors
Implementing 4096 color direct RGB
rrrrbbbbBBBBBBBBgggg2TiIdsdcufhr
Flag bit layout for 4096 colors
With this plan, the attr bit values will be allocated like this, where T is the new flag to enable 4096 color operation, and r, b, and g, are the direct 4-bit red, green, and blue color values.
Don't confuse lower-case b, which is the blue component of the foreground, with upper case B, which remains as the 8-bit indexed color for the background.
The first step is to define two new flags in wsdisplayvar.h:
#define WSATTR_TRUECOLOUR_FG 1024 /* Attribute is to be interpreted as RGB value instead of an index */
#define WSSCREEN_TRUECOLOUR 64 /* Supports direct 24-bit RGB values */
The code excerpts shown here that deal with expanded color spaces are intended to be used on systems running OpenBSD 7.7 with our existing console enhancement patchset already applied, as some of the code relies on the 256 color functionality being present.
More than 256 colors
Parsing the control sequence
The foreground color selection control sequence is CSI 38;2;R;G;Bm. One, two, or all three of the parameters can be omited, so CSI 38;2m is a valid control sequence that sets R=0, G=0, and B=0.
Parsing this sequence in wsemul_vt100_handle_csi() is simple enough.
First, we define three macros that each return either the corresponding red, green, or blue value in the range 0 - 255 if it was supplied, or otherwise return zero.
#define RGB_RED (edp->nargs - n >= 3 ? ARG(n + 2) : 0)
#define RGB_GREEN (edp->nargs - n >= 4 ? ARG(n + 3) : 0)
#define RGB_BLUE (edp->nargs - n >= 5 ? ARG(n + 4) : 0)
We'll also define a macro that indicates whether the supplied arguments are within the valid range of 0 - 255:
#define RGB_RANGE_VALID (RGB_RED < 256 && RGB_GREEN < 256 && RGB_BLUE < 256)
Then, assuming that we are actually running on a display which has WSSCREEN_TRUECOLOUR enabled, we just need to set both WSATTR_WSCOLORS and WSATTR_TRUECOLOUR_FG in flags, and finally shift each of the color values so that they end up in the correct bits of attr.
This code is basically a no-op on displays which do not support WSSCREEN_TRUECOLOUR, instead just skipping over any supplied arguments to the CSI 38;2 control sequence.
/*
* 2 introduces a sequence of up to three
* arguments, specifying RGB.
*
* Missing values are treated as zero by xterm,
* so we do the same.
*
* This means that even CSI 38;2m is valid and
* sets RGB = 0, 0, 0, I.E. black.
*/
if (ARG(n+1)==2) {
if (edp->scrcapabilities & WSSCREEN_TRUECOLOUR && RGB_RANGE_VALID) {
flags |= WSATTR_WSCOLORS;
flags |= WSATTR_TRUECOLOUR_FG;
flags &= 0x0fff; /* Clear upper bits as we are or'ing the new values in */
flags |= ((RGB_GREEN & 0xF0) << 8);
fgcol = (RGB_RED & 0xF0) | ((RGB_BLUE & 0xF0) >> 4);
}
n=(edp->nargs-n > 5 ? n+4 : edp->nargs);
break;
}
Of course, we also need to unset the WSATTR_TRUECOLOUR_FG flag when setting a regular foreground color, (SGR 30 - SGR 37), or when setting a foreground color using the palette of 256 indexed colors, (SGR 38;5), or resetting the foreground color, (SGR 39), or setting an extended bright foreground color, (SGR 90 - SGR 97).
flags &= ~WSATTR_TRUECOLOUR_FG;
These are the only changes we need to make to the wscons code.
More than 256 colors
Rasops code changes
Turning our attention to rasops, the change to the actual text rendering code is trivial. The only framebuffer bit-depth that we'll support is 32-bit, so we just add the following code to rasops32_putchar(), (in rasops32.c), after the existing b and f setting code:
if (attr & WSATTR_TRUECOLOUR_FG) {
/*
* Red channel comes from the re-purposed upper bits of the indexed attribute bits
* Blue channel comes from the re-purposed lower bits of the indexed attribute bits
* Green channel comes from bits 12-15 of attr
*/
f = (((attr >> 24) & 0xf0) << ri->ri_rpos);
f |= (((attr >> 28) & 0x0f) << ri->ri_rpos);
f |= (((attr >> 24) & 0x0f) << (ri->ri_bpos + 4));
f |= (((attr >> 24) & 0x0f) << (ri->ri_bpos));
f |= (((attr >> 12) & 0x0f) << (ri->ri_gpos + 4));
f |= (((attr >> 12) & 0x0f) << (ri->ri_gpos));
}
Note that we store the 4-bit values in both the upper and lower nibbles of the corresponding byte.
This ensures that peak white is 0xFFFFFF and not 0xF0F0F0.
Finally, we need to set the WSSCREEN_TRUECOLOUR flag in rasops_reconfig(), (found in rasops.c), if we're running on a 32 bpp display:
if (ri->ri_depth == 32) {
ri->ri_caps |= WSSCREEN_TRUECOLOUR;
}
Patch link
This version of the code corresponds to patch_4096_v1 in the patchset linked at the end of this page.
More than 256 colors
Results and improvements
The results are impressive, as can be seen by using the following shell script:
#!/bin/sh
echo -n "\033[38;2;255;255;255m"
echo "Using 24-bit direct RGB selection:"
echo
echo Grey ramp
for i in `jot 86 0 255 3` ; do echo -n "\033[38;2;$i;$i;$i"m# ; done ; echo
echo "\033[38;2;255;0;0m"
echo Red ramp
for i in `jot 86 0 255 3` ; do echo -n "\033[38;2;$i;0;0"m# ; done ; echo
echo "\033[38;2;0;255;0m"
echo Green ramp
for i in `jot 86 0 255 3` ; do echo -n "\033[38;2;0;$i;0"m# ; done ; echo
echo "\033[38;2;0;0;255m"
echo Blue ramp
for i in `jot 86 0 255 3` ; do echo -n "\033[38;2;0;0;$i"m# ; done ; echo
echo "\033[38;2;255;255;0m"
echo Yellow ramp
for i in `jot 86 0 255 3` ; do echo -n "\033[38;2;$i;$i;0"m# ; done ; echo
echo "\033[38;2;255;255;255m"
echo
echo "Using 8-bit palette for selection:"
echo
for i in `jot 24 232 255 1` ; do echo -n "\033[38;5;$i"m#### ; done ; echo
for i in `jot 6 16 255 36` ; do echo -n "\033[38;5;$i"m################ ; done ; echo
for i in `jot 6 16 46 6` ; do echo -n "\033[38;5;$i"m################ ; done ; echo
for i in `jot 6 16 21 1` ; do echo -n "\033[38;5;$i"m################ ; done ; echo
for i in `jot 6 16 226 42` ; do echo -n "\033[38;5;$i"m################ ; done ; echo
echo "\033[m"
Code link
This shell script is available as 'ramps' in the patchset linked at the end of this page.
The granularity of the red, green, and blue color ramps is clearly smoother with 16 levels per channel instead of the 6 levels of the 256 color indexed palette.
However the 256 color palette includes 24 distinct grey values, which is more than we can achieve with direct 4-bit RGB.
More than 256 colors
Selecting grey shades from the 256 color palette
We can use the above mentioned grey levels within the 256 color palette to our advantage by detecting a SGR sequence that sets all of red, green, and blue to equal values, and in this situation simply selecting the closest of the existing 256 colors and not using direct RGB at all.
#define CLOSEST_GREY_256(x) (232+((x+4)/11))
if (ARG(n+1)==2) {
if (edp->scrcapabilities & WSSCREEN_TRUECOLOUR && RGB_RANGE_VALID) {
flags |= WSATTR_WSCOLORS;
if (RGB_RED == RGB_BLUE && RGB_RED == RGB_GREEN) {
fgcol = CLOSEST_GREY_256(RGB_GREEN);
flags &= ~WSATTR_TRUECOLOUR_FG;
} else {
flags |= WSATTR_TRUECOLOUR_FG;
flags &= 0x0fff; /* Clear upper bits as we are or'ing the new values in */
flags |= ((RGB_GREEN & 0xF0) << 8);
fgcol = (RGB_RED & 0xF0) | ((RGB_BLUE & 0xF0) >> 4);
}
}
n=(edp->nargs-n > 5 ? n+4 : edp->nargs);
break;
}
Patch link
This version of the code corresponds to patch_4096_v2 in the patchset linked at the end of this page.
More than 256 colors
Further improvements to greyscales
But in fact we can do even better, because the preset palette values mostly don't match the rounded 4-bit RGB values:
Greyscale values in the 256-color palette are mostly interleaved with the values that can be achieved via 4-bit RGB
256-color palette:
0x01, 0x0c, 0x17, 0x22, 0x2d, 0x38, 0x43, 0x4e, 0x59, 0x64, 0x6f, 0x7a, 0x85, 0x90, 0x9b, 0xa6, 0xb1, 0xbc, 0xc7, 0xd2, 0xdd, 0xe8, 0xf3, 0xfe
01, 12, 23, 34, 45, 56, 67, 78, 89, 100, 111, 122, 133, 144, 155, 166, 177, 188, 199, 210, 221, 232, 243, 254
4-bit RGB:
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
00, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255
So if we're prepared to accept a degree of non-linearity in the grey ramp as well as complicating the wscons SGR parsing code a bit more, we can get even more granularity.
The following version of the CSI 38;2 parsing code selects the closest value from either the 256 color palette or direct 4-bit RGB tables above:
#define ABS(i) ((i) > 0 ? (i) : (-(i)))
#define CLOSEST_PALETTE_GREY(i) ((i + 4) / 11)
#define PALETTE_RGB(i) (1 + (i * 11))
#define CLOSEST_DIRECT_GREY(i) ((((i + 8) * 16 / 17) & 0xf0) | ((((i + 8) * 16 / 17) & 0xf0) >> 4))
#define PALETTE_GREY_BEST (ABS((int)RGB_GREEN - PALETTE_RGB(CLOSEST_PALETTE_GREY((int)RGB_GREEN))) < ABS((int)RGB_GREEN - CLOSEST_DIRECT_GREY((int)RGB_GREEN)))
if (ARG(n+1)==2) {
if (edp->scrcapabilities & WSSCREEN_TRUECOLOUR && RGB_RANGE_VALID) {
flags |= WSATTR_WSCOLORS;
if (RGB_RED == RGB_BLUE && RGB_RED == RGB_GREEN) {
if (PALETTE_GREY_BEST) {
fgcol = 232 + CLOSEST_PALETTE_GREY(RGB_GREEN);
flags &= ~WSATTR_TRUECOLOUR_FG;
} else {
flags |= WSATTR_TRUECOLOUR_FG;
flags &= 0x0fff; /* Clear upper bits as we are or'ing the new values in */
flags |= (((CLOSEST_DIRECT_GREY(RGB_GREEN)) & 0xF0) << 8);
fgcol = (CLOSEST_DIRECT_GREY(RGB_GREEN));
}
} else {
flags |= WSATTR_TRUECOLOUR_FG;
flags &= 0x0fff; /* Clear upper bits as we are or'ing the new values in */
flags |= ((RGB_GREEN & 0xF0) << 8);
fgcol = (RGB_RED & 0xF0) | ((RGB_BLUE & 0xF0) >> 4);
}
}
n=(edp->nargs-n > 5 ? n+4 : edp->nargs);
break;
}
Patch link
This version of the code corresponds to patch_4096_v3 in the patchset linked at the end of this page.
Overall, the difference is subtle but noticeable compared with the pure 4-bit RGB greyscale code. It definitely seems worth the added complexity in the CSI 38;2 parsing code.
More than 256 colors
True 8-bit greyscale
Of course, if we were prepared to use the last available flag bit to indicate greyscale mode, then we could have 4-bit RGB color at the same time as a full 8-bit greyscale. For completeness, let's look at the code to implement that:
In wsdisplayvar.h:
#define WSATTR_DIRECT_GREY_FG 2048 /* Green channel of RGB data is to be used as a greyscale value, ignoring red and blue */
In wsemul_vt100_subr.c to handle CSI 38;2:
if (ARG(n+1)==2) {
if (edp->scrcapabilities & WSSCREEN_TRUECOLOUR && RGB_RANGE_VALID) {
flags |= WSATTR_WSCOLORS;
if (RGB_RED == RGB_BLUE && RGB_RED == RGB_GREEN) {
fgcol = RGB_GREEN;
flags |= WSATTR_TRUECOLOUR_FG;
flags |= WSATTR_DIRECT_GREY_FG;
} else {
flags |= WSATTR_TRUECOLOUR_FG;
flags &= ~WSATTR_DIRECT_GREY_FG;
flags &= 0x0fff; /* Clear upper bits as we are or'ing the new values in */
flags |= ((RGB_GREEN & 0xF0) << 8);
fgcol = (RGB_RED & 0xF0) | ((RGB_BLUE & 0xF0) >> 4);
}
}
n=(edp->nargs-n > 5 ? n+4 : edp->nargs);
break;
}
And once again, we also need to unset WSATTR_DIRECT_GREY_FG wherever we set a regular color, (SGR 30 - SGR 37, SGR 38;5, SGR 39, and SGR 90 - SGR 97).
flags &= ~WSATTR_DIRECT_GREY_FG;
Finally, the color selection code in rasops32_putchar():
if (attr & WSATTR_DIRECT_GREY_FG) {
f = (((attr >> 24) & 0xff) << ri->ri_rpos);
f |= (((attr >> 24) & 0xff) << ri->ri_gpos);
f |= (((attr >> 24) & 0xff) << ri->ri_bpos);
}
Patch link
This version of the code corresponds to patch_4096_v4 in the patchset linked at the end of this page.
With these changes implemented the greyscale looks really nice, with no visible quantisation at all.
In fact, unless there was a reason to avoid using an extra flag bit, there doesn't seem much point in not using this direct 8-bit greyscale approach in favour of the hybrid 4-bit greyscale and palette grey code that we saw immediately before.
Of course, expanding attr to 64-bits and implementing direct 24-bit RGB color selection, (for both foreground and background), would avoid the need for all of this added complexity, (at the expense of adding complexity elsewhere).
More than 256 colors
Summary
In resume, it's clearly possible to go beyond 256 colors on the text console. We've looked at several fairly non-intrusive patches that expand our color palette to around 4096 colors, along with two ways to improve greyscale rendition.
If full 24-bit RGB color is ever required, we've identified a practical way to implement it, albeit at the expense of increased patch complexity.
Blink attribute
Introduction and background information
Completely separately from the colorspace enhancements, we have the one 'traditional' text attribute that has long had a flag bit allocated to it within wscons and yet has never been functionally implemented in the rasops display drivers on OpenBSD.
The blink attribute is enabled with CSI 5m, and disabled with CSI 25m. On systems using rasops it's non-functional, but the VGA text mode driver, for example, does support it.
Obviously opinions on the usefulness of blinking text vary, but from a purely technical point of view this is missing functionality which could easily be implemented. Besides, visually there are several different ways that text can actually flash, and we can only really judge the merits of one other another by writing code to test them out.
Blink attribute
Implementing a non-static attribute
From a technical point of view, however, blink differs considerably from other attributes such as bold, italic, and underlining. This is because it's not just painted once to the screen and then forgotten about. Instead, the pixel data needs to be periodically updated, with foreground pixels being on at one moment, and then off at the next moment, (at least for the most basic implementation, other more visually pleasing approaches are of course also possible).
On legacy VGA hardware, (as well as certain other devices), this is easy, because it's all done in hardware. In that case, just setting a particular bit in video memory is enough and we don't need to think about that character anymore.
Not so on a pixel-based rasops display.
In this case, a new function is needed which will look through the character-based backing store, (which is also used to implement the scrollback buffer), for characters which have the blink attribute set. For each of these characters, the relevant putchar() routine will need to be called and the character re-painted with it's original attributes if this is an 'on' part of the blinking cycle, or otherwise replaced by a space character or in some other way changed if this is an 'off' part of the blinking cycle.
The new function will be called automatically using a timeout, so blinking characters will be handled entirely by code in kernel space.
Blink attribute
A sysctl to enable and disable blinking
First of all we'll add a new sysctl to the kernel so that we can enable and disable the blinking subsystem as a whole.
The new sysctl will be kern.blinking_subsystem, and it's value will be mirrored in a global variable int flag_blinking_subsystem.
A value of 1 will enable the new code, and any other value will disable it.
To do this we define the global int at the beginning of kern/kern_sysctl.c:
int flag_blinking_subsystem = 0;
Also in kern/kern_sysctl.c we add a trivial handler for the new syscall, which just stores the supplied value in the new variable:
case KERN_BLINK_SUBSYSTEM:
return (sysctl_int(oldp, oldlenp, newp, newlen, &flag_blinking_subsystem));
Then in sys/sysctl.h we increment the existing value of KERN_MAXID, and insert an identifier for our new sysctl after the existing entries:
#define KERN_BLINK_SUBSYSTEM 92 /* flag to enable or disable the blinking subsystem */
Lastly, add it to the list of CTL_KERN_NAMES:
{ "blinking_subsystem", CTLTYPE_INT }, \
To modify our new sysctl from userspace, we'll need to re-compile /sbin/sysctl with the updated sysctl.h kernel header file.
Since the compilation of /sbin/sysctl will look for the header file in /usr/include/sys/, we'll copy the updated version there first:
# cp -p /usr/src/sys/sys/sysctl.h /usr/include/sys/
Now we can re-compile and re-install /sbin/sysctl:
# cd /usr/src/sbin/sysctl
# make
# make install
With this new sysctl in place, our new rasops code will be able to check the value of flag_blinking_subsystem, and behave accordingly.
Blink attribute
A control sequence to configure blinking
Since all of this is being handled in software, every aspect of it is fully configurable.
Various ways exist that we can pass configuration information to a new rasops function, such as expanding the scope of wsconsctl, or using a sysctl.
Each approach has it's own advantages and disadvantages, but since the blinking subsystem is very much experimental code in development and a proof of concept rather than a finished product, we'll use the simplest method of a custom control sequence.
Blink attribute
Overview of the required code in rasops
To fully implement blinking, we'll be adding the following code to the rasops subsystem:
A new function rasops_blink_flash_init()
This will initialize or re-initialize the blinking code with two parameters, blink type to select the visual effect, and blink interval to set the speed.
A new function rasops_blink_flash_do()
This will be called via a timeout, and will immediately schedule a new timeout to call itself again after the blink interval has passed. It will also advance a counter storing the current state of the blinking cycle, (for example, from 'off' to 'on' then back to 'off' and so on), search the backing store for characters with the blink attribute set, repaint those characters, and ensure that if the blinking subsystem has been disactivated that those characters are repainted at normal brightness, (rather than being left invisible).
A wrapper function for rasops_vcons_blink_flash_init()
This is just so that rasops_blink_flash_init() can be called when virtual consoles are being used.
Various code in rasops_init()
This is where we set various function pointers and configure the initial timeout to call rasops_blink_flash().
Various code in rasops_reconfig()
To enable WSSCREEN_BLINK for the screen being configured and set the initial blink phase value.
Additional fields in struct rasops_info defined in rasops.h
To store state and settings, as well as to add a function pointer for our new rasops_blink_flash_init() function.
Visual effects code to rasops32.c
To actually paint the characters in various different ways, depending on the blink phase value.
One line change to rasops_wronly_do_cursor()
To avoid the cursor flashing when displayed on top of flashing text.
Blink attribute
Code additions to wscons
The wscons subsystem already has the necessary logic to set and reset the blinking attribute WSATTR_BLINK for individual characters, so technically we don't need to make any changes here at all. It would be possible to implement blinking entirely within the rasops code.
In practice, though, as mentioned above we'll add a special non-standard control sequence to configure parameters such as the blinking time interval and visual effect.
Still, the needed changes here are very minimal, a few lines in wsemul_vt100_handle_csi() to process our configuration sequence and pass the values to rasops_blink_flash_init(), and updating the definition of struct wsdisplay_emulops in wsdisplayvar.h to include a function pointer to the same function.
In wsdisplayvar.h:
int (*blink_flash_init)(void *c, int interval, int blinktype);
In wsemul_vt100_subr.c:
case 128: /* initialize and/or configure blinking */
WSEMULOP(m, edp, &edp->abortstate, blink_flash_init, (edp->emulcookie, ((edp->nargs > n+1) ? ARG(n + 1) : 0), ((edp->nargs > n+2) ? ARG(n + 2) : 0)));
if (edp->nargs > n + 1)
n++;
if (edp->nargs > n + 1)
n++;
break;
The newly introduced control sequence is CSI 128;blink interval;blink type;m where blink interval is a time in milliseconds that each phase of the blink cycle will last, and blink type specifies the visual effect, as a simple index in to a number of discrete alternatives.
Blink attribute
Expanding struct rasops_info
Turning our attention back to the rasops code, the first requirement here is to add some fields to struct rasops_info:
int ri_blinkphase; /* current stage of blinking cycle */
int ri_blinkinterval; /* blinking cycle step time in msec */
int ri_blinktype; /* blink type - on and off, or ramping */
struct timeout ri_blinkto;
And after the last existing function pointer:
int (*ri_blink_flash_init)(void *, int, int);
Since we've added a struct timeout, we need to include two more header files in rasops.h:
#include <sys/types.h>
#include <sys/timeout.h>
Blink attribute
The initialisation function
Now we get to the first real piece of blinking subsystem code, the initialisation function.
int rasops_blink_flash_init(void * cookie, int blinkinterval, int blinktype)
{
struct rasops_info * ri;
ri = (struct rasops_info *) cookie;
/*
* A value of 255 disables the blinking subsystem until reboot.
*/
if (ri->ri_blinktype == 255)
return 0;
timeout_del(&ri->ri_blinkto);
/*
* Intervals less than 500 msec are rounded to zero, disabling blinking.
*/
blinkinterval = ((blinkinterval < 500 || blinktype == 255) ? 0 : blinkinterval);
ri->ri_blinkinterval = blinkinterval;
ri->ri_blinktype = blinktype;
if (blinkinterval > 0)
timeout_add_msec(&ri->ri_blinkto, blinkinterval);
rasops_blink_flash_do(cookie);
return 0;
}
The values of blinkinterval and blinktype are those passed via the initialisation control sequence.
First, we check the previously configured value for ri_blinktype, and if it is set to 255 then no further actions are performed and we exit immediately. This allows for a simple way to fully disable all blinking text until the next reboot, (in other words, it's intentionally not possible to turn it back on).
Assuming that the blinking subsystem hasn't been fully disabled, then the function continues.
Any existing timeout is deleted, so that a new one can be added with the supplied interval. We then check that the supplied interval is sensible, in this case at least 500 msec, and if it's not then we set it to zero which will disable blinking, (but not permanently).
If the supplied blinktype is 255, (as opposed to the stored value that we checked earlier), then we also set blinkinterval to zero to prevent another timeout being scheduled. However we don't immediately exit but instead continue processing so that any characters already on the display with the blink attribute set will have an opportunity to be re-painted in the normal way, (in other words, we avoid stopping half way through the blinking cycle and leaving such characters invisible or otherwise incorrectly rendered).
At this point we store the two new supplied values in struct rasops_info ri, and if blinkinterval is non-zero then we schedule a timeout ri_blinkto to call rasops_blink_flash_do(), which is the function responsible for most of the actual processing.
We also call rasops_blink_flash_do() directly at the end of rasops_blink_flash_init(). This not only ensures that the visual effect starts immediately, (rather than after one interval has passed), but it's also essential to ensure that rasop_blink_flash_do() is called one last time when blinking is disabled, (either temporarily or permanently).
Blink attribute
Function rasops_blink_flash_do()
Next stop, the function which is actually called periodically to scan the character backing store and update the display accordingly.
void rasops_blink_flash_do(void * cookie)
{
struct rasops_info * ri;
struct rasops_screen * scr;
int row, col, pos;
ri = (struct rasops_info *) cookie;
if (flag_blinking_subsystem != 1 && !(flag_blinking_subsystem != 1 && ri->ri_blinkinterval == 0))
return ;
if (ri->ri_blinkinterval > 0)
timeout_add_msec(&ri->ri_blinkto, ri->ri_blinkinterval);
scr = ri->ri_active;
if (ri->ri_blinktype == 0)
ri->ri_blinkphase = (ri->ri_blinkphase + 1) % 6;
if (ri->ri_blinktype == 1)
ri->ri_blinkphase = (ri->ri_blinkphase > 0 ? 0 : 3);
/*
* If we're disabling blinking, then leave blinking text as full brightness,
* I.E. phase three of the blink cycle.
*/
if (ri->ri_blinkinterval == 0)
ri->ri_blinkphase = 3;
/*
* Repaint screen
*/
for (row = 0; row < ri->ri_rows; row++)
for (col = 0; col < ri->ri_cols; col++) {
pos = (row * ri->ri_cols + col) + scr->rs_visibleoffset;
if (scr->rs_bs[pos].attr & WSATTR_BLINK)
/*
* Don't re-paint the character under the cursor.
*/
if ((scr->rs_visibleoffset != scr->rs_dispoffset) || (scr->rs_crow != row || scr->rs_ccol != col))
ri->ri_putchar(ri, row, col, scr->rs_bs[pos].uc, scr->rs_bs[pos].attr);
}
return ;
}
Of course, we also need to reference int flag_blinking_subsystem as an external variable at the start of rasops.c:
extern int flag_blinking_subsystem;
First of all, we check the value of flag_blinking_subsystem, which mirrors the sysctl kern.blinking_subsystem.
If it's not set to 1 then we return without any further processing, unless the blink interval value is zero indicating that we are disabling blinking. In this case, the function is allowed to continue even though the blinking subsystem has been disabled, so that blinking characters which have already been displayed can be re-drawn normally.
Next we immediately re-schedule a new timeout so that this function will be called again based in the stored interval.
Then we move on to processing and advancing the blinkphase counter. The initial version of rasops_blink_flash_do() shown above implements two blink types:
blinktype = 0
blinkphase will cycle from 0 to 5 and then repeat from 0 again.
blinktype = 1
blinkphase will alternate between 0 and 3.
The intention is that the actual pixel painting code will interpret blinkphase as follows:
0 Off or very dull
1 Dull
2 Slightly dull
3 Normal brightness
4 Slightly dull
5 Dull
In this way, some code can be shared between the two visual styles, as the simple 'on-off-on' style just ignores the intermediate graduated phases.
Next, we check for blinkinterval being zero, (blinking disabled), and in this case set blinkphase to 3 so that the following code will re-paint the characters in their normal non-blinking state.
Finally, we loop through the visible rows and columns of the display, (remembering that it might be displaying part of the scrollback buffer), and call putchar() for each character that is found with the WSATTR_BLINK attribute bit set, unless that character is at the same location as the cursor.
If we repainted the character under the cursor here, the cursor would disappear until it was moved.
Although we could resolve this by repainting the cursor afterwards with normal attributes, looking at rasops_wronly_do_cursor() a simpler alternative becomes obvious, we can just disable WSATTR_BLINK for the relevant character cell within the cursor code itself.
This is possible in rasops_wronly_do_cursor() as the cursor is drawn by simply painting the character again with it's foreground and background colors swapped. In contrast, rasops_do_cursor() reads the pixel data from the framebuffer and inverts it, (which is visually different).
The solution here seems to be to always use rasops_wronly_do_cursor() anyway, and include the following line at the end of the conditional:
attr &= ~WSATTR_BLINK;
Note that even with this change to rasops_wronly_do_cursor(), we still need to avoid repainting the character under the cursor in rasops_blink_flash_do().
This is because our modified rasops_wronly_do_cursor() only masks out the WSATTR_BLINK flag from the attribute value that it uses in the subsequent call to putchar(), but leaves it set in the character backing store where it will be read by future invocations of rasops_blink_flash_do().
Blink attribute
Pixel painting code
With the timing and counting logic in place, it's now the moment to look at the required changes to rasops32_putchar() in rasops32.c.
In fact, it's very simple. We just bit-shift the foreground and background values right by anything from zero to three bits, depending on which part of the blink phase we are in.
if ((attr & WSATTR_BLINK) != 0) {
if (ri->ri_blinkphase==0) {
f=(f>>3) & 0x1F1F1F1F;
b=(b>>3) & 0x1F1F1F1F;
}
if (ri->ri_blinkphase==1 || ri->ri_blinkphase==5) {
f=(f>>2) & 0x3F3F3F3F;
b=(b>>2) & 0x3F3F3F3F;
}
if (ri->ri_blinkphase==2 || ri->ri_blinkphase==4) {
f=(f>>1) & 0x7F7F7F7F;
b=(b>>1) & 0x7F7F7F7F;
}
}
This code goes after the assignment of variables b and f.
It's fully compatible with the 256 color palette and even the dim attribute, they can all be used together.
In theory, it's also compatible with the 4-bit direct RGB color code and 8-bit greyscale code, so there is nothing to stop us from setting a direct RGB color using the CSI 2;r;g;bm sequence, then dimming it with CSI 2m; and then lastly making it flash.
Obviously already dark colors will eventually be shifted to an RGB value of 0, 0, 0, by the above algorithm making them invisible against a black background for part of the blinking cycle.
Blink attribute
Visual effects
Of course, this visual effect of dimming the foreground and background colors to create blinking is just one of several approaches.
Other possibilities include replacing the printed character with a space, (which would leave underlining and strikethrough unaffected), or setting the foreground color to the same as the background color.
This basically comes down to personal preference.
Blink attribute
Miscellaneous code to make it compile
We still need to add some more code to rasops.c to make the blinking subsystem functional.
First, some function prototypes for our new functions:
int rasops_blink_flash_init(void *, int, int);
void rasops_blink_flash_do(void *);
int rasops_vcons_blink_flash_init(void *, int, int);
Next, in rasops_init() we assign two function pointers, and initialize the timeout ri_blinkto:
ri->ri_blink_flash_init = ri->ri_ops.blink_flash_init;
ri->ri_ops.blink_flash_init = rasops_vcons_blink_flash_init;
timeout_set(&ri->ri_blinkto, rasops_blink_flash_do, ri);
In rasops_reconfig(), we set the initial blink phase and assign another function pointer:
ri->ri_blinkphase = 3;
ri->ri_ops.blink_flash_init = rasops_blink_flash_init;
And we also set WSSCREEN_BLINK if we're running on a 32bpp display:
if (ri->ri_depth == 32) {
ri->ri_caps |= WSSCREEN_BLINK;
}
Not forgetting our wrapper function for rasops_blink_flash_init():
int rasops_vcons_blink_flash_init(void * cookie, int blinkinterval, int blinktype)
{
struct rasops_screen * scr = cookie;
return scr->rs_ri->ri_blink_flash_init(scr->rs_ri, blinkinterval, blinktype);
}
Finally, from rasops_pack_cattr() we need to remove the conditional check for WSATTR_BLINK which returns EINVAL in that case.
Patch link
This version of the code is available as the patch blink_v1 in the patchset linked at the end of this page.
Blink attribute
Testing
The following shell script demonstrates the functionality of the blinking subsystem:
#!/bin/sh
clear
if [ "`sysctl -n kern.blinking_subsystem`" -ne 1 ]
then echo The blinking subsystem is not enabled, to enable set kern.blinking_subsystem=1
exit
fi
echo -n "\033[128m"
echo "\033[mSteady text"
echo "\033[5mBlinking: white text\011\033[91mbright red foreground\011\033[92mbright green foreground\011\033[94mbright blue foreground\033[m"
echo "\033[5mBlinking: white text\011\033[101mbright red background\011\033[102mbright green background\011\033[104mbright blue background"
echo "\033[m"
echo -n "\033[128;750;1m"
echo "Blink code configured for on/off at 750ms intervals"
read
echo -n "\033[128;500;1m"
echo "Blink code configured for on/off at 500ms intervals"
read
echo -n "\033[128;500;0m"
echo "Blink code configured for bitshifting intensity ramping at 500ms step intervals"
read
echo -n "\033[128;30;2m"
echo "Blink code configured for smooth intensity ramping at 30ms step intervals"
read
echo -n "\033[128m"
echo "Blink code configured for blinking disabled"
Code link
This shell script is available as flash_demo in the patchset linked at the end of this page.
Don't forget to enable the blinking subsystem before testing by setting kern.blinking_subsystem to 1 as root!
# sysctl kern.blinking_subsystem=1
When run, the script displays some text with the blink attribute enabled, configures blink type 1, (simple oscillating between dim and normal brightness), with an interval of 750ms, and waits for the user to press enter to continue.
Pressing enter re-configures the blink functionality with an interval of 500ms, (faster blinking of the same type).
The next example is blink type 0, (ramping up and down four distinct brightness levels), with an interval of 500ms between each level.
Finally, the script invokes a blink type which we haven't implemented yet, but will see shortly. For now, note that an unimplemented blink type simply returns blinking text to normal display brightness.
Blink attribute
Disabling blinking until the next reboot
To test the full disabling of the blinking subsystem, simply set blink type 255:
$ echo "\033[128;0;255m"
After this, re-running the test script above will not result in the displayed text flashing. A reboot will be necessary to restore access to the blinking subsystem.
Blink attribute
Results and improvements
The initial implementation certainly works, and gives usable results without excessive CPU usage.
Whilst bit-shifting works very well to produce the simple two step normal/dim visual effect of blink mode 1, it's limitations become apparent when blink mode 0 is selected. Even with four distinct brightness levels, the transition from dim to normal brightness, (and back again), is not visually smooth at all.
Although it might have a limited appeal, a truly smooth fade in and out would probably look nicer.
Blink attribute
Smooth fading of text
To implement the smooth intensity ramping hinted at above in the demonstration shell script, we'll use more distinct brightness levels and calculate them by simple subtraction of a constant from the initial foreground and background values.
Since ri_blinkphase is an int we can use bits 8-15 to store a value between 1 and 255 without duplicating any of the values already being interpreted by our additional code in rasops32.c.
We'll also reduce the minimum selectable interval from 500ms to 30ms, since there are more distinct blink phases to pass through to complete a full on/off cycle.
Code changes
The changed part of rasops_blink_flash_init() looks like this:
/*
* The minimum permitted interval depends on the blinktype value:
* For blinktype 2, intervals less than 30 msec are rounded to zero.
* For all other blinktype values, intervals less than 500 msec are rounded to zero.
*/
#define BLINK_MIN_INTERVAL (blinktype == 2 ? 30 : 500)
blinkinterval = ((blinkinterval < BLINK_MIN_INTERVAL || blinktype == 255) ? 0 : blinkinterval);
Function rasops_blink_flash_do() just requires this addition:
/*
* For blink type 2, we count in bits 8 - 15, but need to avoid storing
* zero because this would be mis-interpreted by the code in rasops32.c.
* To avoid this, we count from 1 - 255 in bits 8 - 15 instead of 0 - 255.
*/
if (ri->ri_blinktype == 2)
ri->ri_blinkphase = ((1 + (((ri->ri_blinkphase >> 8) + 1) % 255)) << 8);
And finally the new code in rasops32.c:
#define BP_FADE ((ri->ri_blinkphase >> 8) < 128 ? (ri->ri_blinkphase >> 8) : 255 - (ri->ri_blinkphase >> 8))
#define BP_FADE_CLAMP(i) ((i - BP_FADE) > 0 ? ((i - BP_FADE)) : 0)
if (ri->ri_blinkphase > 255) {
j = (((BP_FADE_CLAMP(((f & 0x00FF0000) >> 16))) << 16) & 0x00FF0000);
j |= (((BP_FADE_CLAMP(((f & 0x0000FF00) >> 8))) << 8) & 0x0000FF00);
j |= (((BP_FADE_CLAMP(((f & 0x000000FF) >> 0))) << 0) & 0x000000FF);
f = j;
j = (((BP_FADE_CLAMP(((b & 0x00FF0000) >> 16))) << 16) & 0x00FF0000);
j |= (((BP_FADE_CLAMP(((b & 0x0000FF00) >> 8))) << 8) & 0x0000FF00);
j |= (((BP_FADE_CLAMP(((b & 0x000000FF) >> 0))) << 0) & 0x000000FF);
b = j;
}
The first macro, BP_FADE, converts the blink phase value from a range of 1 - 255 in to 1 - 127, followed by 127 - 0.
The second macro, BP_FADE_CLAMP, subtracts the previously calculated value from the supplied color component, returning zero if the result would otherwise be negative.
Patch link
This version of the code is available as the patch blink_v2 in the patchset linked at the end of this page.
Blink attribute
Evaluation and conclusions
The fading effect of blinktype 2 is certainly much smoother visually than the four step bitshifting version. Even though it requires more updates per blink cycle than the other modes of operation, cpu load is still negligible on the test machine.
Overall, the code presented here to implement blinking on a rasops based console works well enough as a proof of concept.
However there is at least one limitation still present that would need to be addressed in order to make this code suitable for production use. Currently, the configuration control sequence can be sent by any user on any virtual terminal and will affect the operation of all virtual terminals.
At least two possible solutions exist:
Additionally, the CSI 128 control sequence is non-standard and could possibly be used elsewhere for purposes completely unrelated to it's use here.
Since demand for implementing blinking on rasops based wscons displays is low, there are no plans to include it as a standard part of the console enhancement patchset.
Download area
Pre-prepared patches to test the code yourself
A tar file containing several versions of the patches and demonstration programs described above is available for download.
The individual patches are each signed with our signify key and are made relative to the top of the source tree.
Assuming that your source tree is in the default location of /usr/src/, and you've placed our signify key in /etc/signify/, then individual patches can be applied like this:
# signify -Vep /etc/signify/exoticsilicon.pub -x /path/to/patch -m - | patch
Note that although these patches are intended to be applied on top of our existing console enhancement patchset, each patch presented here is independent of the others rather than cumulative, so before trying a subsequent patch be sure to un-apply the previously applied one.
Exceptionally, if you want to use both the 4096 color code and the blinking subsystem code together, patch_4096_v4 and patch_blink_v2 can be applied together.
List of patches included in the archive
patch_4096_v1
Basic implementation of 4-bit RGB color.
patch_4096_v2
4-bit RGB color, with requests for RGB greyscale values detected and changed to use entries from the 256 color palette.
patch_4096_v3
4-bit RGB color, with greyscale values using the nearest match out of either 4-bit RGB, or the palette.
patch_4096_v4
4-bit RGB color, with 8-bit direct greyscale, implemented using a second flag bit to select greyscale mode.
patch_blink_v1
Basic implementation of blink types 0 and 1, (four-step fading, and two-step dim/normal).
patch_blink_v2
Implementation of blink types 0, 1, and 2, (adding smooth fading to the previous version).
List of demonstration programs included in the archive
ramps
Displays grey, red, green, blue, and yellow color ramps using both 4-bit RGB color selection, as well as the 256 color palette for comparison.
blinking_text_demo
Displays blinking text using various parameters.