summaryrefslogtreecommitdiffstats
path: root/gfx/wr/webrender/doc/text-rendering.md
blob: b965562b99a21ec6d65998a4e1d2928e70cb5861 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# Text Rendering

This document describes the details of how WebRender renders text, particularly the blending stage of text rendering.
We will go into grayscale text blending, subpixel text blending, and "subpixel text with background color" blending.

### Prerequisites

The description below assumes you're familiar with regular rgba compositing, operator over,
and the concept of premultiplied alpha.

### Not covered in this document

We are going to treat the origin of the text mask as a black box.
We're also going to assume we can blend text in the device color space and will not go into the gamma correction and linear pre-blending that happens in some of the backends that produce the text masks.

## Grayscale Text Blending

Grayscale text blending is the simplest form of text blending. Our blending function has three inputs:

 - The text color, as a premultiplied rgba color.
 - The text mask, as a single-channel alpha texture.
 - The existing contents of the framebuffer that we're rendering to, the "destination". This is also a premultiplied rgba buffer.

Note: The word "grayscale" here does *not* mean that we can only draw gray text.
It means that the mask only has a single alpha value per pixel, so we can visualize
the mask in our minds as a grayscale image.

### Deriving the math

We want to mask our text color using the single-channel mask, and composite that to the destination.
This compositing step uses operator "over", just like regular compositing of rgba images.

I'll be using GLSL syntax to describe the blend equations, but please consider most of the code below pseudocode.

We can express the blending described above as the following blend equation:

```glsl
vec4 textblend(vec4 text_color, vec4 mask, vec4 dest) {
  return over(in(text_color, mask), dest);
}
```

with `over` being the blend function for (premultiplied) operator "over":

```glsl
vec4 over(vec4 src, vec4 dest) {
  return src + (1.0 - src.a) * dest;
}
```

and `in` being the blend function for (premultiplied) operator "in", i.e. the masking operator:

```glsl
vec4 in(vec4 src, vec4 mask) {
  return src * mask.a;
}
```

So the complete blending function is:

```glsl
result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r;
result.g = text_color.g * mask.a + (1.0 - text_color.a * mask.a) * dest.g;
result.b = text_color.b * mask.a + (1.0 - text_color.a * mask.a) * dest.b;
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
```

### Rendering this with OpenGL

In general, a fragment shader does not have access to the destination.
So the full blend equation needs to be expressed in a way that the shader only computes values that are independent of the destination,
and the parts of the equation that use the destination values need to be applied by the OpenGL blend pipeline itself.
The OpenGL blend pipeline can be tweaked using the functions `glBlendEquation` and `glBlendFunc`.

In our example, the fragment shader can output just `text_color * mask.a`:

```glsl
  oFragColor = text_color * mask.a;
```

and the OpenGL blend pipeline can be configured like so:

```rust
    pub fn set_blend_mode_premultiplied_alpha(&self) {
        self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
        self.gl.blend_equation(gl::FUNC_ADD);
    }
```

This results in an overall blend equation of

```
result.r = 1 * oFragColor.r + (1 - oFragColor.a) * dest.r;
           ^                ^  ^^^^^^^^^^^^^^^^^
           |                |         |
           +--gl::ONE       |         +-- gl::ONE_MINUS_SRC_ALPHA
                            |
                            +-- gl::FUNC_ADD

         = 1 * (text_color.r * mask.a) + (1 - (text_color.a * mask.a)) * dest.r
         = text_color.r * mask.a + (1 - text_color.a * mask.a) * dest.r
```

which is exactly what we wanted.

### Differences to the actual WebRender code

There are two minor differences between the shader code above and the actual code in the text run shader in WebRender:

```glsl
oFragColor = text_color * mask.a;    // (shown above)
// vs.
oFragColor = vColor * mask * alpha;  // (actual webrender code)
```

`vColor` is set to the text color. The differences are:

 - WebRender multiplies with all components of `mask` instead of just with `mask.a`.
   However, our font rasterization code fills the rgb values of `mask` with the value of `mask.a`,
   so this is completely equivalent.
 - WebRender applies another alpha to the text. This is coming from the clip.
   You can think of this alpha to be a pre-adjustment of the text color for that pixel, or as an
   additional mask that gets applied to the mask.

## Subpixel Text Blending

Now that we have the blend equation for single-channel text blending, we can look at subpixel text blending.

The main difference between subpixel text blending and grayscale text blending is the fact that,
for subpixel text, the text mask contains a separate alpha value for each color component.

### Component alpha

Regular painting uses four values per pixel: three color values, and one alpha value. The alpha value applies to all components of the pixel equally.

Imagine for a second a world in which you have *three alpha values per pixel*, one for each color component.

 - Old world: Each pixel has four values: `color.r`, `color.g`, `color.b`, and `color.a`.
 - New world: Each pixel has *six* values: `color.r`, `color.a_r`, `color.g`, `color.a_g`, `color.b`, and `color.a_b`.

In such a world we can define a component-alpha-aware operator "over":

```glsl
vec6 over_comp(vec6 src, vec6 dest) {
  vec6 result;
  result.r = src.r + (1.0 - src.a_r) * dest.r;
  result.g = src.g + (1.0 - src.a_g) * dest.g;
  result.b = src.b + (1.0 - src.a_b) * dest.b;
  result.a_r = src.a_r + (1.0 - src.a_r) * dest.a_r;
  result.a_g = src.a_g + (1.0 - src.a_g) * dest.a_g;
  result.a_b = src.a_b + (1.0 - src.a_b) * dest.a_b;
  return result;
}
```

and a component-alpha-aware operator "in":

```glsl
vec6 in_comp(vec6 src, vec6 mask) {
  vec6 result;
  result.r = src.r * mask.a_r;
  result.g = src.g * mask.a_g;
  result.b = src.b * mask.a_b;
  result.a_r = src.a_r * mask.a_r;
  result.a_g = src.a_g * mask.a_g;
  result.a_b = src.a_b * mask.a_b;
  return result;
}
```

and even a component-alpha-aware version of `textblend`:

```glsl
vec6 textblend_comp(vec6 text_color, vec6 mask, vec6 dest) {
  return over_comp(in_comp(text_color, mask), dest);
}
```

This results in the following set of equations:

```glsl
result.r = text_color.r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.r;
result.g = text_color.g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.g;
result.b = text_color.b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.b;
result.a_r = text_color.a_r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.a_r;
result.a_g = text_color.a_g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.a_g;
result.a_b = text_color.a_b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.a_b;
```

### Back to the real world

If we want to transfer the component alpha blend equation into the real world, we need to make a few small changes:

 - Our text color only needs one alpha value.
   So we'll replace all instances of `text_color.a_r/g/b` with `text_color.a`.
 - We're currently not making use of the mask's `r`, `g` and `b` values, only of the `a_r`, `a_g` and `a_b` values.
   So in the real world, we can use the rgb channels of `mask` to store those component alphas and
   replace `mask.a_r/g/b` with `mask.r/g/b`.

These two changes give us:

```glsl
result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
result.a_r = text_color.a * mask.r + (1.0 - text_color.a * mask.r) * dest.a_r;
result.a_g = text_color.a * mask.g + (1.0 - text_color.a * mask.g) * dest.a_g;
result.a_b = text_color.a * mask.b + (1.0 - text_color.a * mask.b) * dest.a_b;
```

There's a third change we need to make:

 - We're rendering to a destination surface that only has one alpha channel instead of three.
   So `dest.a_r/g/b` and `result.a_r/g/b` will need to become `dest.a` and `result.a`.

This creates a problem: We're currently assigning different values to `result.a_r`, `result.a_g` and `result.a_b`.
Which of them should we use to compute `result.a`?

This question does not have an answer. One alpha value per pixel is simply not sufficient
to express the same information as three alpha values.

However, see what happens if the destination is already opaque:

We have `dest.a_r == 1`, `dest.a_g == 1`, and `dest.a_b == 1`.

```
result.a_r = text_color.a * mask.r + (1 - text_color.a * mask.r) * dest.a_r
           = text_color.a * mask.r + (1 - text_color.a * mask.r) * 1
           = text_color.a * mask.r + 1 - text_color.a * mask.r
           = 1
same for result.a_g and result.a_b
```

In other words, for opaque destinations, it doesn't matter what which channel of the mask we use when computing `result.a`, the result will always be completely opaque anyways. In WebRender we just pick `mask.g` (or rather,
have font rasterization set `mask.a` to the value of `mask.g`) because it's as good as any.

The takeaway here is: **Subpixel text blending is only supported for opaque destinations.** Attempting to render subpixel
text into partially transparent destinations will result in bad alpha values. Or rather, it will result in alpha values which
are not anticipated by the r, g, and b values in the same pixel, so that subsequent blend operations, which will mix r and a values
from the same pixel, will produce incorrect colors.

Here's the final subpixel blend function:

```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
  vec4 result;
  result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
  result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
  result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
  result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
  return result;
}
```

or for short:

```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
  return text_color * mask + (1.0 - text_color.a * mask) * dest;
}
```

To recap, here's what we gained and lost by making the transition from the full-component-alpha world to the
regular rgba world: All colors and textures now only need four values to be represented, we still use a
component alpha mask, and the results are equivalent to the full-component-alpha result assuming that the
destination is opaque. We lost the ability to draw to partially transparent destinations.

### Making this work in OpenGL

We have the complete subpixel blend function.
Now we need to cut it into pieces and mix it with the OpenGL blend pipeline in such a way that
the fragment shader does not need to know about the destination.

Compare the equation for the red channel and the alpha channel between the two ways of text blending:

```
  single-channel alpha:
    result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r
    result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r

  component alpha:
    result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r
    result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
```

Notably, in the single-channel alpha case, all three destination color channels are multiplied with the same thing:
`(1.0 - text_color.a * mask.a)`. This factor also happens to be "one minus `oFragColor.a`".
So we were able to take advantage of OpenGL's `ONE_MINUS_SRC_ALPHA` blend func.

In the component alpha case, we're not so lucky: Each destination color channel
is multiplied with a different factor. We can use `ONE_MINUS_SRC_COLOR` instead,
and output `text_color.a * mask` from our fragment shader.
But then there's still the problem that the first summand of the computation for `result.r` uses
`text_color.r * mask.r` and the second summand uses `text_color.a * mask.r`.

There are multiple ways to deal with this. They are:

 1. Making use of `glBlendColor` and the `GL_CONSTANT_COLOR` blend func.
 2. Using a two-pass method.
 3. Using "dual source blending".

Let's look at them in order.

#### 1. Subpixel text blending in OpenGL using `glBlendColor`

In this approach we return `text_color.a * mask` from the shader.
Then we set the blend color to `text_color / text_color.a` and use `GL_CONSTANT_COLOR` as the source blendfunc.
This results in the following blend equation:

```
result.r = (text_color.r / text_color.a) * oFragColor.r + (1 - oFragColor.r) * dest.r;
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                ^  ^^^^^^^^^^^^^^^^^
                         |                              |      |
                         +--gl::CONSTANT_COLOR          |      +-- gl::ONE_MINUS_SRC_COLOR
                                                        |
                                                        +-- gl::FUNC_ADD

         = (text_color.r / text_color.a) * (text_color.a * mask.r) + (1 - (text_color.a * mask.r)) * dest.r
         = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```

At the very beginning of this document, we defined `text_color` as the *premultiplied* text color.
So instead of actually doing the calculation `text_color.r / text_color.a` when specifying the blend color,
we really just want to use the *unpremultiplied* text color in that place.
That's usually the representation we start with anyway.

#### 2. Two-pass subpixel blending in OpenGL

The `glBlendColor` method has the disadvantage that the text color is part of the OpenGL state.
So if we want to draw text with different colors, we have two use separate batches / draw calls
to draw the differently-colored parts of text.

Alternatively, we can use a two-pass method which avoids the need to use the `GL_CONSTANT_COLOR` blend func:

 - The first pass outputs `text_color.a * mask` from the fragment shader and
   uses `gl::ZERO, gl::ONE_MINUS_SRC_COLOR` as the glBlendFuncs. This achieves:

```
oFragColor = text_color.a * mask;

result_after_pass0.r = 0 * oFragColor.r + (1 - oFragColor.r) * dest.r
                     = (1 - text_color.a * mask.r) * dest.r

result_after_pass0.g = 0 * oFragColor.g + (1 - oFragColor.g) * dest.r
                     = (1 - text_color.a * mask.r) * dest.r

...
```

 - The second pass outputs `text_color * mask` from the fragment shader and uses
   `gl::ONE, gl::ONE` as the glBlendFuncs. This results in the correct overall blend equation.

```
oFragColor = text_color * mask;

result_after_pass1.r
 = 1 * oFragColor.r + 1 * result_after_pass0.r
 = text_color.r * mask.r + result_after_pass0.r
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```

#### 3. Dual source subpixel blending in OpenGL

The third approach is similar to the second approach, but makes use of the [`ARB_blend_func_extended`](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_blend_func_extended.txt) extension
in order to fold the two passes into one:
Instead of outputting the two different colors in two separate passes, we output them from the same pass,
as two separate fragment shader outputs.
Those outputs can then be treated as two different sources in the blend equation.

## Subpixel Text Rendering to Transparent Destinations with a Background Color Hint

### Motivation

As we've seen in the previous section, subpixel text drawing has the limitation that it only works on opaque destinations.

In other words, if you use the `subpixeltextblend` function to draw something to a transparent surface,
and then composite that surface onto on opaque background,
the result will generally be different from drawing the text directly onto the opaque background.

Let's express that inequality in code.

```
 - vec4 text_color
 - vec4 mask
 - vec4 transparency = vec4(0.0, 0.0, 0.0, 0.0)
 - vec4 background with background.a == 1.0

over(subpixeltextblend(text_color, mask, transparency), background).rgb
 is, in general, not equal to
subpixeltextblend(text_color, mask, background).rgb
```

However, one interesting observation is that if the background is black, the two *are* equal:

```
vec4 black = vec4(0.0, 0.0, 0.0, 1.0);

over(subpixeltextblend(text_color, mask, transparency), black).r
 = subpixeltextblend(text_color, mask, transparency).r +
     (1 - subpixeltextblend(text_color, mask, transparency).a) * black.r
 = subpixeltextblend(text_color, mask, transparency).r +
     (1 - subpixeltextblend(text_color, mask, transparency).a) * 0
 = subpixeltextblend(text_color, mask, transparency).r
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * transparency.r
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * 0
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * black.r
 = subpixeltextblend(text_color, mask, black).r
```

So it works out for black backgrounds. The further your *actual* background color gets away from black,
the more incorrect your result will be.

If it works for black, is there a way to make it work for other colors?
This is the motivating question for this third way of text blending:

We want to be able to specify an *estimated background color*, and have a blending function
`vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest)`,
in such a way that the error we get by using an intermediate surface is somehow in relation
to the error we made when estimating the background color. In particular, if we estimated
the background color perfectly, we want the intermediate surface to go unnoticed.

Expressed as code:

```
over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency), bg_color)
 should always be equal to
subpixeltextblend(text_color, mask, bg_color)
```

This is one of three constraints we'd like `subpixeltextblend_withbgcolor` to satisfy.

The next constraint is the following: If `dest` is already opaque, `subpixeltextblend_withbgcolor`
should have the same results as `subpixeltextblend`, and the background color hint should be ignored.

```
 If dest.a == 1.0,
subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest)
 should always be equal to
subpixeltextblend(text_color, mask, dest)
```

And there's a third condition we'd like it to fulfill:
In places where the mask is zero, the destination should be unaffected.

```
subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest)
 should always be equal to
dest
```

### Use cases

The primary use case for such a blend method is text on top of vibrant areas of a window on macOS.

Vibrant backgrounds with behind-window blending are computed by the window server, and they are tinted
in a color that's based on the chosen vibrancy type.

The window's rgba buffer is transparent in the vibrant areas. Window contents, even text, are drawn onto
that transparent rgba buffer. Then the window server composites the window onto an opaque backdrop.
So the results on the screen are computed as follows:

```glsl
window_buffer_pixel = subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency);
screen_pixel = over(window_buffer_pixel, window_backdrop);
```

### Prior art

Apple has implemented such a method of text blending in CoreGraphics, specifically for rendering text onto vibrant backgrounds.
It's hidden behind the private API `CGContextSetFontSmoothingBackgroundColor` and is called by AppKit internally before
calling the `-[NSView drawRect:]` method of your `NSVisualEffectView`, with the appropriate font smoothing background color
for the vibrancy type of that view.

I'm not aware of any public documentation of this way of text blending.
It seems to be considered an implementation detail by Apple, and is probably hidden by default because it can be a footgun:
If the font smoothing background color you specify is very different from the actual background that our surface is placed
on top of, the text will look glitchy.

### Deriving the blending function from first principles

Before we dive into the math, let's repeat our goal once more.

We want to create a blending function of the form
`vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest)`
(with `bg_color` being an opaque color)
which satisfies the following three constraints:

```
Constraint I:
  over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency), bg_color)
   should always be equal to
  subpixeltextblend(text_color, mask, bg_color)

Constraint II:
   If dest.a == 1.0,
  subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest)
   should always be equal to
  subpixeltextblend(text_color, mask, dest)

Constraint II:
  subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest)
   should always be equal to
  dest
```

Constraint I and constraint II are about what happens depending on the destination's alpha.
In particular: If the destination is completely transparent, we should blend into the
estimated background color, and if it's completely opaque, we should blend into the destination color.
In fact, we really want to blend into `over(dest, bg_color)`: we want `bg_color` to be used
as a backdrop *behind* the current destination. So let's combine constraints I and II into a new
constraint IV:

```
Constraint IV:
  over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color)
   should always be equal to
  subpixeltextblend(text_color, mask, over(dest, bg_color))
```

Let's look at just the left side of that equation and rejiggle it a bit:

```
over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r
 = subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r +
   (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r

<=>

over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r -
(1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
 = subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
```

Now insert the right side of constraint IV:

```
subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
 = over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r -
   (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
 = subpixeltextblend(text_color, mask, over(dest, bg_color)).r -
   (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
```

Our blend function is almost finished. We just need select an alpha for our result.
Constraints I, II and IV don't really care about the alpha value. But constraint III requires that:

```
  subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest).a
   should always be equal to
  dest.a
```

so the computation of the alpha value somehow needs to take into account the mask.

Let's say we have an unknown function `make_alpha(text_color.a, mask)` which returns
a number between 0 and 1 and which is 0 if the mask is entirely zero, and let's defer
the actual implementation of that function until later.

Now we can define the alpha of our overall function using the `over` function:

```
subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a
 := make_alpha(text_color.a, mask) + (1 - make_alpha(text_color.a, mask)) * dest.a
```

We can plug this in to our previous result:

```
subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
 = subpixeltextblend(text_color, mask, over(dest, bg_color)).r
   - (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
 = subpixeltextblend(text_color, mask, over(dest, bg_color)).r
   - (1 - (make_alpha(text_color.a, mask) +
           (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * over(dest, bg_color).r
   - (1 - (make_alpha(text_color.a, mask)
           + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
   - (1 - (make_alpha(text_color.a, mask)
           + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
   - (1 - (make_alpha(text_color.a, mask)
           + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
 = text_color.r * mask.r
   + (dest.r + (1 - dest.a) * bg_color.r)
   - (text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
   - (1 - make_alpha(text_color.a, mask)
      - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
 = text_color.r * mask.r
   + dest.r + (1 - dest.a) * bg_color.r
   - text_color.a * mask.r * dest.r
   - text_color.a * mask.r * (1 - dest.a) * bg_color.r
   - (1 - make_alpha(text_color.a, mask)
      - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
 = text_color.r * mask.r
   + dest.r + (1 - dest.a) * bg_color.r
   - text_color.a * mask.r * dest.r
   - text_color.a * mask.r * (1 - dest.a) * bg_color.r
   - ((1 - make_alpha(text_color.a, mask)) * 1
      - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
 = text_color.r * mask.r
   + dest.r + (1 - dest.a) * bg_color.r
   - text_color.a * mask.r * dest.r
   - text_color.a * mask.r * (1 - dest.a) * bg_color.r
   - ((1 - make_alpha(text_color.a, mask)) * (1 - dest.a)) * bg_color.r
 = text_color.r * mask.r
   + dest.r - text_color.a * mask.r * dest.r
   + (1 - dest.a) * bg_color.r
   - text_color.a * mask.r * (1 - dest.a) * bg_color.r
   - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * dest.r
   + (1 - dest.a) * bg_color.r
   - text_color.a * mask.r * (1 - dest.a) * bg_color.r
   - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * dest.r
   + (1 - text_color.a * mask.r) * (1 - dest.a) * bg_color.r
   - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * dest.r
   + ((1 - text_color.a * mask.r)
      - (1 - make_alpha(text_color.a, mask))) * (1 - dest.a) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * dest.r
   + (1 - text_color.a * mask.r
      - 1 + make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
 = text_color.r * mask.r
   + (1 - text_color.a * mask.r) * dest.r
   + (make_alpha(text_color.a, mask) - text_color.a * mask.r) * (1 - dest.a) * bg_color.r
```

We now have a term of the form `A + B + C`, with `A` and `B` being guaranteed to
be between zero and one.

We also want `C` to be between zero and one.
We can use this restriction to help us decide on an implementation of `make_alpha`.

If we define `make_alpha` as

```glsl
float make_alpha(text_color_a, mask) {
  float max_rgb = max(max(mask.r, mask.g), mask.b);
  return text_color_a * max_rgb;
}
```

, then `(make_alpha(text_color.a, mask) - text_color.a * mask.r)` becomes
`(text_color.a * max(max(mask.r, mask.g), mask.b) - text_color.a * mask.r)`, which is
`text_color.a * (max(max(mask.r, mask.g), mask.b) - mask.r)`, and the subtraction will
always yield something that's greater or equal to zero for r, g, and b,
because we will subtract each channel from the maximum of the channels.

Putting this all together, we have:

```glsl
vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest) {
  float max_rgb = max(max(mask.r, mask.g), mask.b);
  vec4 result;
  result.r = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r +
             text_color.a * bg_color.r * (max_rgb - mask.r) * (1 - dest.a);
  result.g = text_color.g * mask.g + (1 - text_color.a * mask.g) * dest.g +
             text_color.a * bg_color.g * (max_rgb - mask.g) * (1 - dest.a);
  result.b = text_color.b * mask.b + (1 - text_color.a * mask.b) * dest.b +
             text_color.a * bg_color.b * (max_rgb - mask.b) * (1 - dest.a);
  result.a = text_color.a * max_rgb + (1 - text_color.a * max_rgb) * dest.a;
  return result;
}
```

This is the final form of this blend function. It satisfies all of the four constraints.

### Implementing it with OpenGL

Our color channel equations consist of three pieces:

 - `text_color.r * mask.r`, which simply gets added to the rest.
 - `(1 - text_color.a * mask.r) * dest.r`, a factor which gets multiplied with the destination color.
 - `text_color.a * bg_color.r * (max_rgb - mask.r) * (1 - dest.a)`, a factor which gets multiplied
   with "one minus destination alpha".

We will need three passes. Each pass modifies the color channels in the destination.
This means that the part that uses `dest.r` needs to be applied first.
Then we can apply the part that uses `1 - dest.a`.
(This means that the first pass needs to leave `dest.a` untouched.)
And the final pass can apply the `result.a` equation and modify `dest.a`.

```
pub fn set_blend_mode_subpixel_with_bg_color_pass0(&self) {
    self.gl.blend_func_separate(gl::ZERO, gl::ONE_MINUS_SRC_COLOR, gl::ZERO, gl::ONE);
}
pub fn set_blend_mode_subpixel_with_bg_color_pass1(&self) {
    self.gl.blend_func_separate(gl::ONE_MINUS_DST_ALPHA, gl::ONE, gl::ZERO, gl::ONE);
}
pub fn set_blend_mode_subpixel_with_bg_color_pass2(&self) {
    self.gl.blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
}

Pass0:
    oFragColor = vec4(text.color.a) * mask;
Pass1:
    oFragColor = vec4(text.color.a) * text.bg_color * (vec4(mask.a) - mask);
Pass2:
    oFragColor = text.color * mask;

result_after_pass0.r = 0 * (text_color.a * mask.r) + (1 - text_color.a * mask.r) * dest.r
result_after_pass0.a = 0 * (text_color.a * mask.a) + 1 * dest.a

result_after_pass1.r = (1 - result_after_pass0.a) * (text_color.a * (mask.max_rgb - mask.r) * bg_color.r) + 1 * result_after_pass0.r
result_after_pass1.a = 0 * (text_color.a * (mask.max_rgb - mask.a) * bg_color.a) + 1 * result_after_pass0.a

result_after_pass2.r = 1 * (text_color.r * mask.r) + 1 * result_after_pass1.r
result_after_pass2.a = 1 * (text_color.a * mask.max_rgb) + (1 - text_color.a * mask.max_rgb) * result_after_pass1.a
```

Instead of computing `max_rgb` in the shader, we can just require the font rasterization code to fill
`mask.a` with the `max_rgb` value.