Fix high surrogate calculation for SMP characters in UTF::ToUTF16 - #121
Open
yxing-goog wants to merge 1 commit into
Open
Fix high surrogate calculation for SMP characters in UTF::ToUTF16#121yxing-goog wants to merge 1 commit into
yxing-goog wants to merge 1 commit into
Conversation
Fix high surrogate calculation for SMP characters in UTF::ToUTF16
### Context
Unicode code points beyond the Basic Multilingual Plane (BMP, U+0000..U+FFFF) reside
in the Supplementary Planes (SMP / SIP / TIP / SSP / PUA, U+10000..U+10FFFF).
Because UTF-16 uses 16-bit code units, characters in the range U+10000..U+10FFFF
must be encoded as a surrogate pair consisting of:
1. A High (Lead) Surrogate in the reserved range [0xD800, 0xDBFF]
2. A Low (Trail) Surrogate in the reserved range [0xDC00, 0xDFFF]
According to Section 3.9 (Unicode Encoding Forms) of the Unicode Standard, the
transformation from a Unicode scalar value `uni` to UTF-16 surrogate code units is:
- U' = uni - 0x10000 (a 20-bit value in range 0x00000..0xFFFFF)
- High Surrogate = 0xD800 + (U' >> 10)
- Low Surrogate = 0xDC00 + (U' & 0x3FF)
### Root Cause Analysis
In `src/text/utf.cpp`, `UTF::ToUTF16` attempted an arithmetic optimization to compute
the high surrogate using the bitwise expression:
utf16[0] = castTo<uint16_t>((0xD800 - 64) | (uni >> 10));
Here, `0xD800 - 64` evaluates to `0xD7C0` (`0b1101_0111_1100_0000`).
The intended shortcut assumed that `0xD7C0 | (uni >> 10)` would simulate subtracting
`0x10000` (which is `0x40` or `64` when shifted right by 10) and adding `0xD800`.
However, bitwise OR `|` is only equivalent to addition when the operands share no
overlapping 1-bits. Because `0xD7C0` has multiple 1-bits set in its lower byte
(e.g., bit 6 and bit 7), applying `| (uni >> 10)` fails to perform a true subtraction
and carry propagation. For many Supplementary Multilingual Plane (SMP) characters,
the resulting value is strictly less than `0xD800` and lands in the unassigned
or non-surrogate Unicode block.
Example (Mathematical Bold Capital 'M', U+1D40C):
- Expected:
U' = 0x1D40C - 0x10000 = 0xD40C
High Surrogate = 0xD800 + (0xD40C >> 10) = 0xD800 + 0x35 = 0xD835
Low Surrogate = 0xDC00 | (0x1D40C & 0x3FF) = 0xDC00 | 0x00C = 0xDC0C
Resulting Pair: [0xD835, 0xDC0C] (Valid UTF-16 for '𝐌')
- Previous Implementation:
(0xD800 - 64) | (0x1D40C >> 10) = 0xD7C0 | 0x75 = 0xD7F5
Resulting Pair: [0xD7F5, 0xDC0C]
0xD7F5 is NOT a valid high surrogate (valid range is 0xD800..0xDBFF).
When passed to downstream text layout engines, CoreText/DirectWrite/HarfBuzz,
or string bridging APIs:
1. The invalid lead code unit `0xD7F5` fails surrogate pair decoding.
2. Shapers fail font fallback resolution, resulting in missing glyphs,
replacement boxes ('B D B D' or [?]), or assertion crashes during text
measurement and run layout.
### Solution
Replace the bitwise approximation with the standard, canonical Unicode arithmetic:
utf16[0] = castTo<uint16_t>(0xD800 + ((uni - 0x10000) >> 10));
This ensures mathematical precision and strict bounds compliance [0xD800, 0xDBFF]
for all 1,048,576 supplementary code points (U+10000..U+10FFFF).
### Verification
- Tested boundary code points (U+10000, U+1D400..U+1D7FF, U+1F600, U+10FFFF).
- Verified valid UTF-16 round-trip decoding with system shaper fallback on iOS/macOS
and data binding with non-BMP character strings (e.g. "𝐌𝐔𝐍𝐃𝐈 𝐎𝐏𝐔𝐒").
rive-engineering
pushed a commit
that referenced
this pull request
Aug 27, 2026
…6 (#13789) 6d8968a21d fix(runtime): high surrogate calculation for SMP chars in UTF::ToUTF16 (#121) ### Context Unicode code points beyond the Basic Multilingual Plane (BMP, U+0000..U+FFFF) reside in the Supplementary Planes (SMP / SIP / TIP / SSP / PUA, U+10000..U+10FFFF). Because UTF-16 uses 16-bit code units, characters in the range U+10000..U+10FFFF must be encoded as a surrogate pair consisting of: 1. A High (Lead) Surrogate in the reserved range [0xD800, 0xDBFF] 2. A Low (Trail) Surrogate in the reserved range [0xDC00, 0xDFFF] According to Section 3.9 (Unicode Encoding Forms) of the Unicode Standard, the transformation from a Unicode scalar value `uni` to UTF-16 surrogate code units is: - U' = uni - 0x10000 (a 20-bit value in range 0x00000..0xFFFFF) - High Surrogate = 0xD800 + (U' >> 10) - Low Surrogate = 0xDC00 + (U' & 0x3FF) ### Root Cause Analysis In `src/text/utf.cpp`, `UTF::ToUTF16` attempted an arithmetic optimization to compute the high surrogate using the bitwise expression: utf16[0] = castTo<uint16_t>((0xD800 - 64) | (uni >> 10)); Here, `0xD800 - 64` evaluates to `0xD7C0` (`0b1101_0111_1100_0000`). The intended shortcut assumed that `0xD7C0 | (uni >> 10)` would simulate subtracting `0x10000` (which is `0x40` or `64` when shifted right by 10) and adding `0xD800`. However, bitwise OR `|` is only equivalent to addition when the operands share no overlapping 1-bits. Because `0xD7C0` has multiple 1-bits set in its lower byte (e.g., bit 6 and bit 7), applying `| (uni >> 10)` fails to perform a true subtraction and carry propagation. For many Supplementary Multilingual Plane (SMP) characters, the resulting value is strictly less than `0xD800` and lands in the unassigned or non-surrogate Unicode block. Example (Mathematical Bold Capital 'M', U+1D40C): - Expected: U' = 0x1D40C - 0x10000 = 0xD40C High Surrogate = 0xD800 + (0xD40C >> 10) = 0xD800 + 0x35 = 0xD835 Low Surrogate = 0xDC00 | (0x1D40C & 0x3FF) = 0xDC00 | 0x00C = 0xDC0C Resulting Pair: [0xD835, 0xDC0C] (Valid UTF-16 for '𝐌') - Previous Implementation: (0xD800 - 64) | (0x1D40C >> 10) = 0xD7C0 | 0x75 = 0xD7F5 Resulting Pair: [0xD7F5, 0xDC0C] 0xD7F5 is NOT a valid high surrogate (valid range is 0xD800..0xDBFF). When passed to downstream text layout engines, CoreText/DirectWrite/HarfBuzz, or string bridging APIs: 1. The invalid lead code unit `0xD7F5` fails surrogate pair decoding. 2. Shapers fail font fallback resolution, resulting in missing glyphs, replacement boxes ('B D B D' or [?]), or assertion crashes during text measurement and run layout. ### Solution Replace the bitwise approximation with the standard, canonical Unicode arithmetic: utf16[0] = castTo<uint16_t>(0xD800 + ((uni - 0x10000) >> 10)); This ensures mathematical precision and strict bounds compliance [0xD800, 0xDBFF] for all 1,048,576 supplementary code points (U+10000..U+10FFFF). ### Verification - Tested boundary code points (U+10000, U+1D400..U+1D7FF, U+1F600, U+10FFFF). - Verified valid UTF-16 round-trip decoding with system shaper fallback on iOS/macOS and data binding with non-BMP character strings (e.g. "𝐌𝐔𝐍𝐃𝐈 𝐎𝐏𝐔𝐒"). Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com> Co-authored-by: yxing-goog <yihanxing@google.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix high surrogate calculation for SMP characters in UTF::ToUTF16
Context
Unicode code points beyond the Basic Multilingual Plane (BMP, U+0000..U+FFFF) reside in the Supplementary Planes (SMP / SIP / TIP / SSP / PUA, U+10000..U+10FFFF). Because UTF-16 uses 16-bit code units, characters in the range U+10000..U+10FFFF must be encoded as a surrogate pair consisting of:
According to Section 3.9 (Unicode Encoding Forms) of the Unicode Standard, the transformation from a Unicode scalar value
unito UTF-16 surrogate code units is:Root Cause Analysis
In
src/text/utf.cpp,UTF::ToUTF16attempted an arithmetic optimization to compute the high surrogate using the bitwise expression:utf16[0] = castTo<uint16_t>((0xD800 - 64) | (uni >> 10));
Here,
0xD800 - 64evaluates to0xD7C0(0b1101_0111_1100_0000). The intended shortcut assumed that0xD7C0 | (uni >> 10)would simulate subtracting0x10000(which is0x40or64when shifted right by 10) and adding0xD800.However, bitwise OR
|is only equivalent to addition when the operands share no overlapping 1-bits. Because0xD7C0has multiple 1-bits set in its lower byte (e.g., bit 6 and bit 7), applying| (uni >> 10)fails to perform a true subtraction and carry propagation. For many Supplementary Multilingual Plane (SMP) characters, the resulting value is strictly less than0xD800and lands in the unassigned or non-surrogate Unicode block.Example (Mathematical Bold Capital 'M', U+1D40C):
When passed to downstream text layout engines, CoreText/DirectWrite/HarfBuzz, or string bridging APIs:
0xD7F5fails surrogate pair decoding.Solution
Replace the bitwise approximation with the standard, canonical Unicode arithmetic:
utf16[0] = castTo<uint16_t>(0xD800 + ((uni - 0x10000) >> 10));
This ensures mathematical precision and strict bounds compliance [0xD800, 0xDBFF] for all 1,048,576 supplementary code points (U+10000..U+10FFFF).
Verification