Files
pebble/src/fw/applib/graphics/text_render.c
Josh Soref a40097521c spelling: boundary
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-28 21:19:00 -05:00

305 lines
13 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "text_render.h"
#include "gcontext.h"
#include "graphics.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "text_resources.h"
#include "util/bitset.h"
#include "util/math.h"
#if !defined(__clang__)
#pragma GCC optimize ("O2")
#endif
static GRect get_glyph_rect(const GlyphData* glyph) {
GRect r = {
.size.w = glyph->header.width_px,
.size.h = glyph->header.height_px,
.origin.x = glyph->header.left_offset_px,
.origin.y = glyph->header.top_offset_px
};
return r;
}
/// This function returns the x coordinate of where to write the contents of a given word (32-bits)
/// of data from the 1-bit frame buffer into the 8-bit framebuffer
/// @param dest_bitmap 8-bit destination frame buffer bitmap
/// @param block_addr source address in 1-bit frame buffer of where the word is being updated
/// within a given row; assumed to be zero-based
/// @param y_offset row offset within the source 1-bit frame buffer
T_STATIC int32_t prv_convert_1bit_addr_to_8bit_x(GBitmap *dest_bitmap, uint32_t *block_addr,
int32_t y_offset) {
// Each byte block_addr corresponds to 8 pixels (i.e. 4-bytes in the 8-bit frame buffer).
// Thus multiply by 8 to get the word offset within the destination 8-bit frame buffer.
// Also need to account for the fact that the 1-bit frame buffer has 16 bits of unused space
// on each row (thus 16 bytes need to be subtracted from the destination address since there is
// no padding on each row of the 8-bit frame buffer.
const int32_t padding = (32 - (dest_bitmap->bounds.size.w % 32)) % 32;
// Calculate the overall offset in the 8-bit bitmap
const int32_t bitmap_offset_8bit = ((uint32_t)block_addr * 8) - (padding * y_offset);
// Calculate just the offset from the start of the target row in the 8-bit bitmap (i.e. "x")
return bitmap_offset_8bit - (dest_bitmap->bounds.size.w * y_offset);
}
// PRO TIP: if you have to modify this function, expect to waste the rest of your day on it
void render_glyph(GContext* const ctx, const uint32_t codepoint, FontInfo* const font,
const GRect cursor) {
if (codepoint_is_special(codepoint)) {
TextRenderState *state = app_state_get_text_render_state();
if (state->special_codepoint_handler_cb) {
state->special_codepoint_handler_cb(ctx, codepoint, cursor,
state->special_codepoint_handler_context);
}
return;
}
const GlyphData* glyph = text_resources_get_glyph(&ctx->font_cache, codepoint, font);
PBL_ASSERTN(glyph);
// Bitfiddle the metrics data:
GRect glyph_metrics = get_glyph_rect(glyph);
// Calculate the box that we intend to draw to the screen, in screen coordinates
GRect glyph_target = {
.origin = { .x = cursor.origin.x + glyph_metrics.origin.x,
.y = cursor.origin.y + glyph_metrics.origin.y },
.size = { .w = glyph_metrics.size.w,
.h = glyph_metrics.size.h }
};
// The destination bitmap's x-coordinate and row advance. Used in the loop below.
GBitmap* dest_bitmap = graphics_context_get_bitmap(ctx);
const int32_t x = (int32_t)((int16_t)cursor.origin.x + (int16_t)glyph_metrics.origin.x);
// Now clip that box against the screen/other UI elements. This rect will be the rect that we
// actually fill with bits on the screen.
GRect clipped_glyph_target = glyph_target;
grect_clip(&clipped_glyph_target, &ctx->draw_state.clip_box);
// The number of bits to be clipped off the edges
const int left_clip = clipped_glyph_target.origin.x - glyph_target.origin.x;
const int right_clip = MIN(glyph_target.size.w,
MAX(0, glyph_target.size.w - clipped_glyph_target.size.w - left_clip));
#if SCREEN_COLOR_DEPTH_BITS == 8
// Set base address to 0 for 8-bit as this will be later translated to the destination bitmap
// address - so do all calculations so everything is offset from 0
uint32_t * base_addr = 0;
#else
uint32_t * base_addr = ((uint32_t*)dest_bitmap->addr);
#endif
const uint32_t * const dest_block_x_begin = base_addr +
(left_clip ?
MAX(0, (((x + left_clip + 31)/ 32) - 1)) : (x / 32));
if (clipped_glyph_target.size.h == 0 || clipped_glyph_target.size.w == 0) {
return;
}
#if SCREEN_COLOR_DEPTH_BITS == 8
// NOTE: Since all calculations are based on 1-bit calculation - use the row size from
// the 1-bit frame buffer
const int row_size_bytes = 4 * ((dest_bitmap->bounds.size.w / 32) +
((dest_bitmap->bounds.size.w % 32) ? 1 : 0));
#else
const int row_size_bytes = dest_bitmap->row_size_bytes;
#endif // SCREEN_COLOR_DEPTH_BITS == 8
// Number of blocks (i.e. 32-bit chunks)
const int dest_row_length = row_size_bytes / 4;
// The number of bits between the beginning of dest_block and glyph_block.
// If x is negative we need to be fancy to get the rounded down remainder. This
// is the number of bits to the right of the next 32-bit boundary to the left.
// For example, if x is -5 we want this shift to be 27, since -32 (the nearest
// boundary) + 27 = -5
const uint8_t dest_shift_at_line_begin = (x >= 0) ?
x % 32 :
(x - ((x / 32) * 32));
uint8_t dest_shift = dest_shift_at_line_begin;
// The glyph bitmap starts the block after the metrics data:
uint32_t const* glyph_block = glyph->data;
// Set up the first piece of source glyph bitmap:
int8_t glyph_block_bits_left = 32;
uint32_t src = *glyph_block;
// Use bit-rotate to align to shift the bitmap to align with the destination.
// The advantage of rotate vs. bitwise shift is that we can use
// the bits that wrapped around for the next dest_block
rotl32(src, dest_shift);
int8_t src_rotated = dest_shift;
// how many 32-bit blocks do we need to bitblt on each row. If we're not word aligned we'll need to
// modify an extra partial word, as we'll have an incomplete word on either side of the line segment
// we're modifying.
// For 1-bit, each pixel goes into one bit in dest bitmap - so 32 pixels per block
const uint8_t num_dest_blocks_per_row = (clipped_glyph_target.size.w / 32) +
(((dest_shift + left_clip) % 32) ? 1 : 0);
// Handle clipping at the top of the character. We need to skip a number of bits in our source data.
const unsigned int bits_to_skip = glyph_metrics.size.w * (clipped_glyph_target.origin.y - glyph_target.origin.y);
if (bits_to_skip) {
glyph_block += bits_to_skip / 32;
src = *glyph_block;
// Simulate the rotate that happens at the bottom of the bitblt loop so our source value is set
// up just as if we actually rendered those first few lines.
rotl32(src, (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32);
src_rotated = (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32;
glyph_block_bits_left -= bits_to_skip % 32;
}
for (int dest_y = clipped_glyph_target.origin.y; dest_y != clipped_glyph_target.origin.y + clipped_glyph_target.size.h; ++dest_y) {
dest_shift = dest_shift_at_line_begin;
// Number of bits to render on this line.
uint8_t glyph_line_bits_left = clipped_glyph_target.size.w;
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length);
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row + 1;
if (left_clip) {
const int left_clip_shift = left_clip % 32;
const int clipped_blocks = left_clip / 32;
dest_shift = (dest_shift + left_clip_shift) % 32;
glyph_block_bits_left -= left_clip_shift;
glyph_block += clipped_blocks;
if (glyph_block_bits_left <= 0) {
src = *(++glyph_block);
glyph_block_bits_left += 32;
// Need to account for the dest_shift when loading up the new glyph block
rotl32(src, glyph_block_bits_left + dest_shift);
src_rotated = glyph_block_bits_left + dest_shift;
}
dest_block += clipped_blocks;
}
while (dest_block != dest_block_end && glyph_line_bits_left) {
PBL_ASSERT(dest_block < dest_block_end, "DB=<%p> DBE=<%p>", dest_block, dest_block_end);
PBL_ASSERTN(dest_block >= (uint32_t*) base_addr);
PBL_ASSERTN(dest_block < (uint32_t*) base_addr + row_size_bytes *
(dest_bitmap->bounds.origin.y + dest_bitmap->bounds.size.h));
// bitblt part of glyph_block:
const uint8_t number_of_bits = MIN(32 - dest_shift, MIN(glyph_line_bits_left, glyph_block_bits_left));
const uint32_t mask = (((1 << number_of_bits) - 1) << dest_shift);
#if SCREEN_COLOR_DEPTH_BITS == 8
// dest_block points to the block if the dest image was a 1-bit buffer
// translate this to an x coordinate in the 8-bit buffer
const int32_t block_start_x = prv_convert_1bit_addr_to_8bit_x(dest_bitmap, dest_block,
dest_y);
const GBitmapDataRowInfo data_row = gbitmap_get_data_row_info(dest_bitmap, dest_y);
// Only enter the loop if the current block is within the valid data row range
if (block_start_x + 31 >= data_row.min_x && block_start_x <= data_row.max_x) {
uint8_t *dest_addr = data_row.data + block_start_x;
// For each bit in block, write that bit to the dest_bitmap
for (unsigned int bitindex = 0; bitindex < 32; bitindex++) {
const int32_t current_x = block_start_x + bitindex;
// Stop iteration early if we have reached the end of the data row
if (current_x > data_row.max_x) {
break;
}
// Skip over pixels outside of the bitmap data's x coordinate range
if (current_x < data_row.min_x) {
continue;
}
// Find position in dest_bitmap that corresponds to the bit index
// Write to that position if mask for that bit is 1
if ((mask & src) & (1 << bitindex)) {
GColor dest_color;
if (ctx->draw_state.compositing_mode == GCompOpSet) {
// Blend (i.e. for transparency) if GCompOpSet
dest_color = gcolor_alpha_blend(ctx->draw_state.text_color,
(GColor) {.argb = dest_addr[bitindex]});
} else {
dest_color = ctx->draw_state.text_color;
dest_color.a = 3;
}
dest_addr[bitindex] = dest_color.argb;
}
}
}
#else
if (gcolor_equal(ctx->draw_state.text_color, GColorBlack)) {
*(dest_block) &= ~(mask & src);
} else {
*(dest_block) |= mask & src;
}
#endif
dest_shift = (dest_shift + number_of_bits) % 32;
glyph_block_bits_left -= number_of_bits;
glyph_line_bits_left -= number_of_bits;
if (glyph_block_bits_left <= 0) {
// We ran out of bits in the current glyph block. Get the next glyph blob:
src = *(++glyph_block);
glyph_block_bits_left += 32;
rotl32(src, dest_shift);
src_rotated = dest_shift;
// Continue with this dest_block if there is still space left:
if (dest_shift) {
continue;
}
}
++dest_block;
}
dest_shift += right_clip % 32;
// emulate having drawn the right clip
if (glyph_block_bits_left <= right_clip) {
int jump_words = (right_clip - glyph_block_bits_left) / 32 + 1;
glyph_block += jump_words;
src = *glyph_block;
rotl32(src, src_rotated);
glyph_block_bits_left += 32 * jump_words;
}
glyph_block_bits_left -= right_clip;
// Rotate the bits into the right position for the next row:
dest_shift = dest_shift_at_line_begin - dest_shift;
rotl32(src, dest_shift % 32);
src_rotated = (src_rotated + dest_shift) % 32;
}
graphics_context_mark_dirty_rect(ctx, clipped_glyph_target);
}
void text_render_set_special_codepoint_cb(SpecialCodepointHandlerCb handler, void *context) {
TextRenderState *state = app_state_get_text_render_state();
state->special_codepoint_handler_cb = handler;
state->special_codepoint_handler_context = context;
}