diff --git a/.agent/plans/retained_text_small_batch_optimization_plan.md b/.agent/plans/retained_text_small_batch_optimization_plan.md new file mode 100644 index 000000000..50e57b315 --- /dev/null +++ b/.agent/plans/retained_text_small_batch_optimization_plan.md @@ -0,0 +1,337 @@ +# Retained Text Small-Batch Optimization Plan + +Status: proposal; no implementation started. + +Date: 2026-07-25 + +## Goal + +Determine whether small retained `Text` objects should submit their already-cached geometry to +`GlobalBatchRenderer` instead of issuing independent direct draw calls, and implement that hybrid +path only if controlled benchmarks show a meaningful improvement without regressing large text, +ordinary grayscale rendering, LCD subpixel rendering, color emoji, or memory use. + +The initial threshold to investigate is 512 rendered glyph quads, but 512 must not become a fixed +policy until measurements establish the crossover point. The decision should use rendered geometry +size rather than `String::size()` because shaping, whitespace, fallback glyphs, decorations, and +outlines break the one-code-point/one-quad assumption. + +In this document, “retained text” means a `Text` object with cached geometry that currently draws +its arrays directly. It is not OpenGL instanced rendering (`glDraw*Instanced`). “Static text” means +the static `Text::draw()` convenience path that emits glyphs into `GlobalBatchRenderer`. + +## Current tradeoff + +### Static `Text::draw()` + +Advantages: + +- Consecutive compatible strings can remain in one global batch. +- Many small labels can become one draw submission instead of one submission per label. +- The renderer already flushes when texture, blend state, or LCD coverage mode changes. + +Costs: + +- Glyph traversal and geometry emission happen on every call. +- Shaped text may repeat layout work unless another layer caches it. +- Vertices, texture coordinates, and colors are copied into the batch every frame. + +### Retained `Text::draw()` + +Advantages: + +- Layout and vertex construction are cached until invalidated. +- Large stable text avoids rebuilding and copying all of its geometry each frame. +- Per-character colors and mixed render-mode ranges are already represented. + +Costs: + +- It flushes the global batch before drawing. +- Every retained `Text` normally creates at least one independent draw submission. +- LCD ranges require multiple channel passes; mixed mask/LCD content can create additional ranges. +- A UI containing hundreds of short labels can become draw-call bound even though each label has + very little geometry. + +The proposed hybrid keeps retained layout/geometry caching but batches small cached geometry at draw +time. Its cost is a bounded CPU-side copy into the batch in exchange for fewer flushes and draw +calls. + +## Questions the benchmark must answer + +1. At what rendered-quad count does copying cached geometry into the global batch become slower than + drawing the retained arrays directly? +2. Does that crossover differ materially between OpenGL 2 (the primary renderer), GL3/core, and + GLES2? +3. How much does LCD rendering move the crossover because each LCD batch flush performs RGB and + alpha passes? +4. Do many small labels benefit more than a single string of the same total glyph count? +5. How much time is actually spent in layout/geometry generation by static `Text::draw()` compared + with submission and driver overhead? +6. Does batching retained text reduce draw calls in realistic ecode UI scenes, or do intervening + textures, clipping changes, and render modes force immediate flushes and erase the benefit? +7. Is a fixed threshold sufficient, or should the decision also consider whether the cached text can + join the batch currently being built? + +## Candidate solutions + +### Option A: Geometry-count threshold + +For retained text below a configurable/internal threshold, append cached quads and colors to +`GlobalBatchRenderer`. Draw larger text through the existing direct path. + +Conceptually: + +```cpp +const size_t quadCount = mVertices.size() / GLi->quadVertex(); +if ( quadCount <= retainedTextBatchThreshold ) + submitCachedGeometryToBatch(); +else + drawCachedGeometryDirectly(); +``` + +This is the simplest useful experiment. Candidate thresholds should include 0 (always direct), 32, +64, 128, 256, 512, 1024, and an always-batched mode. The production value should be an internal +constant unless runtime tuning proves necessary. + +### Option B: Threshold plus batch compatibility + +Batch only when the retained geometry can join a compatible pending batch. Compatibility includes: + +- font atlas texture and coordinate type; +- blend mode; +- clipping/scissor state; +- primitive representation; +- LCD versus ordinary mask rendering; +- active shader and any other renderer state that currently forces a flush. + +If submitting a `Text` would create an otherwise empty batch that must be flushed immediately, +direct drawing is likely cheaper. This option should follow Option A only if instrumentation shows +that incompatible state transitions frequently defeat the size-only heuristic. + +### Option C: Store render-mode runs instead of one mode per quad + +Retained LCD text currently needs enough metadata to map every quad to its render mode. A compact +run list can represent the common cases more efficiently: + +```text +first quad | quad count | mode +0 | 34 | Subpixel +34 | 1 | Color/Mask +35 | 37 | Subpixel +``` + +Uniform LCD text needs one run; all-mask text can keep the existing empty-vector fast path. The run +list maps directly to drawing and batch submission. It avoids retaining a mode value per glyph but +adds construction and invalidation complexity. Implement it only if memory measurements show the +current sparse mode vector is material or if a run API substantially simplifies hybrid submission. + +### Option D: Batch command references without copying geometry + +Teach the renderer to queue references to immutable retained buffers and merge or multi-draw +compatible commands later. This could avoid CPU copies, but it introduces lifetime, invalidation, +ordering, clipping, and backend complexity. It is not the first implementation candidate. Consider +it only if Option A demonstrates that draw-call reduction is valuable but vertex copying prevents a +useful crossover. + +## Recommended implementation sequence + +### Phase 1: Instrument the existing paths + +Add benchmark-only or opt-in counters for: + +- `Text` draw calls split by static and retained paths; +- rendered quads and vertices; +- global batch flush count and reason; +- underlying `drawArrays()` calls; +- LCD channel-pass draw calls; +- texture, shader, clipping, blend, and render-mode transitions; +- bytes copied into batch arrays; +- retained geometry rebuild count and time. + +Counters must be disabled or compile away in normal production builds unless their measured overhead +is negligible. Do not add atomics or logging to the render loop. + +### Phase 2: Build a dedicated text-rendering benchmark + +Add a focused benchmark to `src/benchmarks/` and the existing `eepp-benchmarks` target. It should +create one window/context, preload every glyph and atlas page, warm up shader creation, and run all +strategies against identical geometry. + +The benchmark must expose three forced policies so results are comparable: + +- `direct`: existing retained draw path; +- `batch`: force cached retained geometry through the proposed batch submission path; +- `auto-N`: hybrid path with threshold `N`. + +The forcing mechanism should be benchmark-only or a narrow internal testing hook, not a permanent +public `Text` API. + +### Phase 3: Implement cached-geometry batch submission + +Add a bulk submission API to `BatchRenderer` rather than calling its per-quad API in a loop if the +bulk API can preserve existing array growth and state invariants. It should accept non-owning spans +for the duration of the call and copy once into already-reserved batch storage. It must not allocate +per glyph. + +Submit retained render-mode runs in original order: + +- mask/decorations through the normal batch mode; +- LCD glyph ranges through `setSubpixelText( true )`; +- color glyphs through their established normal/color behavior; +- outline, shadow, and fill in their existing paint order. + +The path must preserve transforms, per-vertex colors, atlas coordinates, blend mode, clipping, and +the current pixel alignment rules. Avoid converting cached geometry into temporary vectors. + +### Phase 4: Tune and select the policy + +Run the complete benchmark matrix on at least the primary OpenGL 2 renderer. Test another +programmable backend where available. Select a threshold only when it improves the small-label +workloads and does not materially regress large-text workloads. + +Start with a simple fixed threshold. Add compatibility-aware logic only if counters demonstrate a +real problem. Prefer the smallest policy that captures most of the measured benefit. + +### Phase 5: Validate in ecode + +Run ecode with its benchmark mode and representative layouts: + +- empty editor with menus, tabs, status bar, and side panels; +- a code document with typical visible line lengths; +- multiple splits and minimap enabled; +- terminal output with frequent updates; +- menus and nested context menus containing many short labels; +- grayscale and subpixel antialiasing; +- color emoji embedded in UI/editor/terminal text. + +Compare frame time and counters against the direct-retained baseline, not only maximum FPS. + +## Benchmark matrix + +### Geometry sizes + +Use rendered glyph-quad counts near likely crossover points: + +- 4, 8, 16, 32, 64, 128, 256, 512, 1024, and 4096 quads. + +Include both one object of size `N` and many objects with the same aggregate glyph count. Examples: + +- 1 × 512 glyphs; +- 8 × 64 glyphs; +- 32 × 16 glyphs; +- 128 × 4 glyphs. + +This distinguishes vertex-volume cost from per-object draw-call cost. + +### Rendering content + +Test each relevant coverage/state pattern: + +- grayscale/mask-only text; +- LCD subpixel-only text; +- LCD text with occasional color emoji or grayscale fallback glyphs; +- alternating render modes as an intentional worst case; +- outline and shadow; +- underline and strike-through; +- per-character colors; +- one shared atlas page versus strings spanning multiple atlas pages/fonts. + +### Update patterns + +- Stable retained geometry: draw the same objects for every measured frame. +- Position-only movement: cached geometry remains valid but transforms change. +- Color-only updates. +- One object mutated per frame. +- Every object mutated per frame. +- Static `Text::draw()` baseline to quantify the full geometry-generation cost. + +### Scene/state patterns + +- All compatible text drawn consecutively. +- Text interleaved with rectangles/icons using the same clipping region. +- Text interleaved across different font textures. +- Frequent clipping/scissor changes, representative of widgets and editor lines. +- Empty batch before every label as a worst case for hybrid submission. +- Existing compatible pending batch before every label as the best case. + +## Measurement methodology + +- Build and measure an optimized release configuration. Debug/ASan builds remain mandatory for + correctness but are not performance evidence. +- Disable VSync and frame-rate limits. +- Preload fonts, glyphs, atlas pages, and the lazy LCD shader before timing. +- Run warm-up frames until allocations and shader/driver initialization stabilize. +- Use a fixed number of frames or submissions large enough to exceed timer noise. +- Repeat each case several times and report median plus a tail measure such as p95; retain raw + samples where practical. +- Randomize or alternate policy order to reduce thermal and clock-frequency bias. +- Keep window size, pixel density, font, font size, atlas contents, and visible output identical. +- Prevent dead-code elimination and ensure submitted work is consumed. +- Separate CPU submission time from completed GPU time. Use an explicit synchronization boundary + outside the timed inner loop when comparing total frame completion, or GPU timer queries where + supported. Do not put `glFinish()` after every individual text draw. +- Record hardware, driver, renderer backend, build flags, and commit identifier with results. +- Report milliseconds/frame and draw calls in addition to FPS. Very high FPS compresses meaningful + differences and can obscure CPU/GPU synchronization effects. + +## Metrics and decision criteria + +Primary metrics: + +- median and p95 CPU frame/submission time; +- median completed frame time or GPU time; +- GL draw calls per frame; +- global batch flushes per frame; +- bytes copied into batch storage per frame; +- retained geometry rebuilds and allocations. + +Suggested acceptance criteria: + +- At least a repeatable 10% CPU-frame improvement in the many-small-label workload, or a substantial + draw-call reduction that produces a measurable ecode improvement. +- No more than a 2% regression in representative large stable editor/terminal workloads. +- No measurable regression in the ordinary grayscale path when the hybrid path is disabled or the + object exceeds the threshold. +- No additional per-frame heap allocations after warm-up. +- Exact visual equivalence in the existing subpixel golden test and new hybrid-specific golden + cases. + +If no threshold meets those criteria, keep the existing direct retained path and remove the +experiment rather than retaining unused complexity. + +## Correctness tests + +Extend rendering tests before enabling the automatic policy: + +- Render identical content through forced direct and forced batched-retained paths and compare the + resulting images exactly where the backend is deterministic. +- Cover text immediately below, at, and above the selected threshold. +- Cover direct/static and retained text on both light and dark backgrounds. +- Cover first-glyph LCD text to preserve the render-mode bookkeeping regression test. +- Cover mixed LCD plus color emoji/fallback glyphs. +- Cover mask-only text and verify it never enters the LCD compositor. +- Cover outlines, shadows, underline, strike-through, per-character colors, fractional positions, + clipping, transforms, and transparent framebuffers. +- Verify invalidation after antialiasing changes rebuilds both geometry and texture coordinates. + +The existing `FontRendering.subpixelCoverageCompositesPerChannel` golden is the baseline guard and +must remain byte/pixel identical throughout threshold experiments. + +## Performance and allocation constraints + +- The normal mask-only path must not allocate render-mode metadata merely to support LCD text. +- Bulk batch submission must reserve geometrically and avoid temporary per-draw vectors. +- Do not add `std::function`, callbacks, dynamic dispatch, logging, or per-glyph heap activity to the + render loop. +- A run-based mode representation should use compact trivially copyable records and merge adjacent + equal modes during geometry construction. +- Threshold checks must be constant-time and based on already available geometry counts. +- Any compatibility inspection must use cached renderer/batch state, not expensive GL state queries. + +## Expected outcome + +The likely useful design is a hybrid: small, compatible retained text copies cached geometry into +the global batch, while large retained text continues to draw directly. The exact threshold may be +well below 512 quads and may differ for LCD and mask text. Measurements, not the initial estimate, +will decide whether one threshold, separate mode-specific thresholds, or no hybrid path is the best +production choice. diff --git a/.agent/plans/subpixel_text_rendering_plan.md b/.agent/plans/subpixel_text_rendering_plan.md new file mode 100644 index 000000000..f9801b3c0 --- /dev/null +++ b/.agent/plans/subpixel_text_rendering_plan.md @@ -0,0 +1,388 @@ +# Subpixel Text Rendering Plan + +Status: implemented after maintainer approval; review fixes completed on 2026-07-25. + +Date: 2026-07-25 + +## Goal + +Make `FontAntialiasing::Subpixel` render useful horizontal RGB LCD text in eepp and ecode, +using the coverage filter and per-channel compositing model used by lite-xl while preserving +eepp's renderer portability, mixed-font behavior, transparent framebuffers, and hot text paths. + +The public option is historically named “subpixel hinting”, but the work here is principally +**LCD subpixel antialiasing and compositing**. `FontHinting` continues to control FreeType outline +hinting independently. + +## Evidence and current-state analysis + +### The FreeType half is already present + +`FontTrueType::fontSetRenderOptions()` already performs the important rasterization setup: + +- `FontAntialiasing::Subpixel` selects `FT_RENDER_MODE_LCD`. +- The LCD filter weights are `{ 0x10, 0x40, 0x70, 0x40, 0x10 }`, exactly the weights used by + the supplied lite-xl renderer. +- LCD bitmaps are interpreted at one logical pixel per three FreeType bitmap bytes. +- Each glyph atlas pixel stores the three LCD coverages in RGB. + +The atlas alpha is currently set to the arithmetic mean of those coverages. Normal eepp texture +blending then treats that one alpha value as the coverage for all destination channels. That loses +the information LCD rendering needs and is the direct reason the option does not work correctly. + +Repository history confirms this is deliberately unfinished rather than dead code. Commit +`7425b77f9` introduced the settings with the note that subpixel support still needed a fragment +shader. The ecode menu later exposed that state as “SubPixel (not working)”. + +### What lite-xl does + +The supplied `ren_draw_text()` path has two separable behaviors: + +1. It rasterizes horizontal LCD masks using the same five FreeType filter weights already used by + eepp. +2. Its software compositor blends each destination color channel with its corresponding LCD + coverage: + + ```text + out[c] = text[c] * text_alpha * coverage[c] + + dst[c] * (1 - text_alpha * coverage[c]) + ``` + +lite-xl also caches three glyph masks translated by 0, 1/3, and 2/3 pixel and selects one from the +fractional pen position. That is an additional positioning-quality refinement, not the missing +compositor itself. eepp currently quantizes some shaped positions and its retained `Text` path does +not know the final fractional screen position while building the glyph cache/vertices, so adopting +that part requires a larger cache and layout change. It is explicitly separated into a follow-up +scope below. + +### A whole-scene framebuffer is not the required mechanism + +`SceneNode` already has an optional framebuffer path (`enableFrameBuffer()`), using a texture-backed +RGBA framebuffer. Enabling it does not restore the three coverages after ordinary alpha blending: +once RGB coverage has been reduced to the atlas's average alpha, a post-process shader cannot infer +the original channel masks. + +A destination-sampling implementation could use ping-pong framebuffers, but reading and writing the +same color attachment is not a valid general solution, and ping-pong rendering would add full-screen +copies/bandwidth and nested-FBO complexity. It is unnecessary here. The correct ownership boundary +is the glyph draw operation while both the LCD mask and destination blend are still available. + +Therefore this plan makes **no change to `SceneNode` and does not force the application framebuffer +on**. The result must work equally when a scene framebuffer is enabled for an independent reason. + +### All render paths that must be covered + +`Text` has two materially different pipelines: + +- The static/high-throughput `Text::draw()` path emits glyphs through `GlobalBatchRenderer`. This is + ecode's main editor path and batches across calls. +- Retained `Text` objects build vertex/color arrays and later draw them directly. They support + per-character colors and their own transforms. + +Both paths can mix LCD glyphs, grayscale fallback glyphs, color emoji, effects, and solid atlas +quads for underline/strike-through. A global `FontAntialiasing` check is consequently not a safe +draw discriminator. The actual rasterized glyph format must travel with the cached glyph/drawable. + +### Scope-level policy issue in ecode startup + +`FontService` is correctly a `ResourceScope`-level policy owner, and changing its hinting or +antialiasing clears associated `FontTrueType` caches. ecode's menu updates that service at runtime. + +At startup, however, ecode applies the loaded policy to individually loaded primary fonts without +first updating `defaultResourceScope().getFontService()`. System fallback fonts created later can +therefore inherit the service's default grayscale policy until the user changes the menu. The +implementation needs to establish both loaded policies on the owning service before asynchronous +font/fallback loading starts. + +## Proposed architecture + +### 1. Describe what each cached glyph contains + +Add a compact internal render-kind enum, conceptually: + +- `Mask`: monochrome/grayscale coverage and solid decoration texels. +- `LCDMask`: three horizontal RGB coverages. +- `Color`: BGRA/color emoji or another intrinsically colored glyph. + +Determine it from the actual FreeType bitmap/pixel mode after rendering, not merely from the font's +requested policy. Store it on the cached `Glyph`, propagate it through `GlyphDrawable`, and preserve +it in text draw ranges. This correctly handles fallback fonts and formats that cannot produce LCD +bitmaps. + +This should remain internal rendering metadata; no public API is needed unless implementation shows +that an external renderer consumer genuinely needs it. + +### 2. Add a renderer-owned LCD compositor + +The built-in renderer, rather than `Text`, `FontService`, or `ResourceScope`, should own the shader +program and cached uniform/attribute locations. GL programs are context/render-pipeline resources, +and renderer lifetime already governs the other built-in programs. + +The shader samples the existing atlas RGB mask. To reproduce lite-xl's per-channel equation with +arbitrary vertex colors and ordinary OpenGL blending, render each contiguous LCD text range once per +destination color channel: + +1. Select the red mask component in the fragment shader and enable only red in `glColorMask()`. +2. Repeat for green and blue. +3. Use eepp's normal source-over RGB blend factors for every channel pass. +4. Perform an alpha-only pass using the existing mean coverage and source-over alpha factors. + +The alpha-only pass is needed because eepp frequently renders into transparent RGBA framebuffers; +lite-xl's software surface simply preserves destination alpha. Omitting a meaningful alpha update +would make correctly colored text disappear or composite incorrectly when that framebuffer is drawn +later. The alpha pass must use eepp's `BlendMode::Alpha()` alpha factors (`One`, +`OneMinusSrcAlpha`), not square the source alpha. + +This four-pass strategy is local to LCD ranges, uses no destination texture reads, supports +per-character/vertex text colors, and preserves ordering. It costs additional glyph fragments and +draw calls, so ranges and state transitions must be coarse, cached, and benchmarked. Normal +grayscale and color-glyph paths remain single-pass and unchanged. + +The compositor must: + +- have shader sources for the supported programmable renderer variants (GL2, GL3/core, and GLES2, + following the existing renderer conventions); +- compile once per renderer/context, cache all locations, and never do string lookup or shader + compilation per text draw; +- preserve clipping and the existing model-view/projection conventions; +- save/restore the prior program, color mask, blend mode/equation, texture state, and batch state; +- coexist with externally selected shaders rather than silently replacing unrelated application + drawing state; +- fail softly and log once if the LCD program is unavailable. + +For shaderless/unsupported contexts, an LCD request must fall back to a neutral grayscale mask +(white RGB plus the mean coverage alpha, or grayscale rasterization before upload). It must never +fall through to today's colored-mask-with-average-alpha output. + +### 3. Integrate the static batch path without per-glyph overhead + +Extend `BatchRenderer` with an internal text coverage mode (`normal` or `LCD`) and flush only when +that mode actually changes. `Text::drawGlyph()` selects the mode from `GlyphDrawable` metadata. + +The batch flush delegates an LCD range to the renderer compositor; the batcher must not own or +compile GL programs. Consecutive editor glyphs then stay in one large LCD batch, while transitions +to color emoji, grayscale fallback, or decoration quads produce the minimum necessary flushes. + +Shadows and outline glyphs use the glyph's LCD mode. Underline and strike-through atlas quads always +use normal scalar-alpha rendering. + +No callback, heap allocation, dynamic cast, or program lookup is permitted per glyph in this hot +path. + +### 4. Integrate retained `Text` with compact ordered ranges + +While rebuilding retained geometry, record contiguous `(first vertex, vertex count, render kind)` +ranges alongside the existing fill and outline arrays. Draw those ranges in original order: + +- normal masks and colored glyphs use their current single draw; +- LCD ranges use the renderer compositor; +- decoration ranges remain normal. + +Prefer an inline/small-vector representation because the common case has one range. Do not add a +render-mode field to every vertex: that would increase persistent text memory and GPU bandwidth for +all text to solve a range-level state problem. + +Preserve existing character-color behavior, emoji whitening rules, clipping, shadows, outlines, +underline/strike-through, and fallback texture/page changes. + +### 5. Apply LCD rendering only where the pixel geometry is valid + +This first implementation defines the existing `Subpixel` option as horizontal **RGB** stripes, +matching the supplied lite-xl code and FreeType LCD mode. The current API has no RGB/BGR or vertical +panel-order selection. + +LCD masks are only valid when their horizontal samples reach physical framebuffer pixels at a 1:1, +axis-aligned transform. At each LCD range/batch—not per glyph—check the effective 2D transform. Use +the LCD compositor only for unit-scale, non-rotated output; use the neutral average-coverage +grayscale path for rotation, non-unit scaling, shear, or otherwise unsuitable transforms. + +This protects retained text transformations and scene/world text. An independently enabled scene +FBO remains eligible only when it is finally presented 1:1 without scaling/filtering that would mix +the RGB samples. + +BGR/vertical stripe layouts and transformed-output reconstruction are deferred rather than guessed. + +### 6. Correct policy initialization and ecode presentation + +After ecode loads font settings and before it starts asynchronous main/fallback font loading: + +- set the loaded hinting and antialiasing values on the default scope's `FontService`; +- keep per-font setup only where it is still needed for ownership/thread timing; +- verify local scene-scope fonts and imported/default fonts retain the intended owning-service + semantics; +- leave runtime policy changes cache-invalidating and immediately visible; +- rename the menu item from “SubPixel (not working)” to “SubPixel” only after the renderer path is + complete and tested. + +## Expected file areas + +Exact signatures should follow local conventions discovered during implementation, but the expected +touch points are: + +- `include/eepp/graphics/font.hpp` and/or internal font/glyph headers: glyph render-kind metadata. +- `src/eepp/graphics/fonttruetype.cpp`: derive the actual kind and provide neutral fallback data. +- `include/eepp/graphics/glyphdrawable.hpp`, `src/eepp/graphics/glyphdrawable.cpp`: propagate kind. +- `include/eepp/graphics/batchrenderer.hpp`, `src/eepp/graphics/batchrenderer.cpp`: range mode and + transition flushes. +- renderer headers/implementations and built-in shader source area: context-owned LCD compositor, + capability detection, and full state restoration. +- `include/eepp/graphics/text.hpp`, `src/eepp/graphics/text.cpp`: both static and retained paths, + ordered mode ranges, effects, and transform eligibility. +- ecode application/font setup and `src/tools/ecode/settingsmenu.cpp`: service initialization and + final menu label. +- `src/tests/unit_tests/fontrendering_tests.cpp`: focused coverage and regression tests. + +`src/eepp/scene/scenenode.cpp` is deliberately not an expected implementation file. + +## Implementation sequence + +### Phase A — establish a measurable baseline + +1. Add a small diagnostic/test scene that renders the same colored edge onto opaque and transparent + contrasting backgrounds using grayscale and current subpixel policies. +2. Record baseline pixels and batch/draw counts for the static editor-like path and retained path. +3. Confirm active test renderers and framebuffer formats so shader variants and alpha behavior are + exercised deliberately. + +### Phase B — metadata and safe fallback + +1. Introduce glyph render-kind metadata derived from the actual rasterized bitmap. +2. Propagate it through `GlyphDrawable` without changing layout metrics or cache keys unnecessarily. +3. Make unsupported LCD compositing neutral and grayscale instead of colored. +4. Add unit coverage for bitmap-mode classification and policy-driven cache invalidation. + +### Phase C — renderer compositor + +1. Add and validate built-in shader variants. +2. Add the RGB channel passes and alpha-only pass with scoped state restoration. +3. Add transform/capability gating and the grayscale fallback path. +4. Exercise opaque and transparent targets before wiring the main editor path. + +### Phase D — both `Text` paths + +1. Add mode-aware static batching and transition flushes. +2. Add compact ordered ranges to retained fill/outline geometry. +3. Audit shadows, outlines, decorations, fallback texture pages, color emoji, clipping, and + per-character colors. +4. Compare both paths pixel-for-pixel where their geometry is otherwise identical. + +### Phase E — service and ecode integration + +1. Initialize the default `FontService` policy before font work starts. +2. Verify live option changes and cache rebuilds. +3. Remove the “not working” suffix. +4. Manually inspect editor text on dark/light themes and layered/transparent UI surfaces. + +### Phase F — performance and correctness validation + +1. Ensure LCD text is range-batched and no new allocation occurs per glyph or per frame. +2. Compare frame time, draw calls, flush count, atlas memory, and glyph rebuild behavior in a large + syntax-highlighted ecode document. +3. If four-pass LCD ranges regress the editor materially, optimize range coalescing/state caching + before considering a destination-sampling/FBO design. +4. Run formatting, the focused unit tests, the complete suite, and an ASan/debug build according to + the project rules. + +## Test plan and acceptance criteria + +### Automated rendering tests + +- Render colored LCD text over a non-neutral opaque background and inspect edge pixels. Each output + channel must follow its own atlas coverage rather than the mean coverage. +- Render the same case through static `Text::draw()` and retained `Text`; tolerate only documented + geometry differences. +- Render into a transparent RGBA framebuffer, composite that texture onto another background, and + verify text remains visible with correct alpha and RGB behavior. +- Exercise LCD text adjacent to grayscale/system fallback glyphs, color emoji, underline, + strike-through, shadow, and outline without mode leakage or tinting. +- Exercise per-character colors and syntax-style runs in a single batch. +- Change `FontService` between grayscale and subpixel at runtime and verify cache invalidation, + render-kind changes, and stable metrics. +- Verify rotated/scaled text and unsupported shader contexts take the neutral grayscale fallback. +- Compare scene-FBO off/on at a 1:1 presentation within a small pixel tolerance; enabling the FBO + must not be a prerequisite. +- Compile/link every applicable built-in shader variant covered by the test environment. + +### Manual ecode checks + +- Toggle None, Grayscale, and SubPixel live and restart with each persisted selection. +- Inspect small editor fonts, syntax colors, selection/search overlays, terminal text, popups, light + and dark themes, fallback scripts, and emoji. +- Check secondary windows and UI opacity/layering paths. +- Check normal and HiDPI configurations, and verify non-1:1 transforms fall back cleanly rather than + showing colored fringes in the wrong geometry. + +### Performance acceptance + +- No shader construction, uniform lookup, heap allocation, or glyph-cache lookup added per glyph + beyond the existing lookup. +- Ordinary grayscale/color text retains its current one-pass path and batching behavior. +- LCD draw calls scale with contiguous batches/ranges, not with glyph count. +- Editor frame-time and batch counters are documented before/after; a material regression blocks + completion until understood and reduced. + +### Project validation commands for the implementation phase + +Follow `.agent/rules/build-project.md` and `.agent/rules/unit-tests.md` exactly: + +1. Regenerate the Linux project with debug symbols and ASan, retaining the current graphics backend + and using mold when available. +2. Format only changed C/C++ files with the repository format. +3. Build with `make -C make/linux -j$(nproc)`. +4. Run focused `FontRendering.Subpixel*` tests through `projects/scripts/xvfb-run-eepp`. +5. Run the full test binary through the same wrapper. +6. Run `git diff --check` and inspect the complete diff for accidental public API/ABI or unrelated + changes. + +## Deliberately deferred work + +### Three fractional glyph phases + +lite-xl caches three horizontally translated LCD bitmaps for every glyph and selects a phase from the +fractional pen position. Adding this in the initial patch would: + +- multiply LCD bitmap/cache variants by three; +- require phase-aware glyph keys and drawables; +- require retaining fractional shaped positions now truncated in part of `Text::draw()`; +- require the retained path to select or rebuild phases using the eventual screen-space origin; +- complicate transformed and fallback text behavior. + +Recommendation: land and measure the correct LCD compositor first. Then add the three-phase cache as +a separately reviewed quality improvement using the same pixel tests and memory/performance +benchmarks. This still follows lite-xl's essential visual algorithm in the first patch: identical +filter weights and independent per-channel blending. + +### Other deferred extensions + +- Configurable RGB versus BGR panel order and vertical subpixel layouts. +- Gamma-linearized coverage/compositing; lite-xl's cited implementation blends byte-space values, so + changing color space would no longer be a direct match. +- Enabling the scene framebuffer for unrelated post-processing. + +## Risks and mitigations + +- **Four-pass LCD cost:** batch contiguous ranges, cache all state/program data, benchmark ecode's + actual editor workload, and keep all non-LCD text on the original path. +- **GL state leakage:** use a single renderer-owned entry point with explicit scoped restoration and + regression tests that draw other primitives immediately before/after LCD text. +- **Mixed atlas content:** classify actual glyph bitmap formats and ordered ranges; never infer the + whole draw mode solely from the font setting. +- **Transparent FBO alpha:** retain the explicit alpha-only coverage pass and test the final + framebuffer composition, not just its RGB attachment. +- **Transforms/panel assumptions:** gate LCD at range granularity and fall back neutrally when the + physical pixel mapping is unsuitable. +- **Startup fallback mismatch:** initialize the owning `FontService` before asynchronous font loads. +- **Legacy renderer support:** compile per-renderer variants and provide a neutral non-LCD fallback; + do not make subpixel support a requirement for eepp to render text. + +## Approval decisions + +The recommended initial implementation assumes: + +1. Direct per-glyph-range compositing; no forced `SceneNode` framebuffer. +2. Correct RGB and alpha output via three color-channel passes plus one alpha-only pass. +3. Horizontal RGB order only, with neutral grayscale fallback for unsuitable transforms/contexts. +4. Matching lite-xl's existing filter weights and per-channel blend equation now. +5. Deferring lite-xl's three fractional-position glyph phases to a follow-up patch. + +Implementation should begin only after these scope decisions are approved or revised. diff --git a/.github/workflows/ecode-nightly.yml b/.github/workflows/ecode-nightly.yml index cdc3bb5fa..b76b6743e 100644 --- a/.github/workflows/ecode-nightly.yml +++ b/.github/workflows/ecode-nightly.yml @@ -443,40 +443,40 @@ jobs: files: | projects/freebsd/ecode/ecode-freebsd-${{ env.INSTALL_REF }}-x86_64.tar.gz - build_haiku_x86_64: - name: Haiku x86_64 Nightly - needs: release - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v6 - with: { fetch-depth: 0, submodules: 'recursive' } - - name: Set Environment Variables - run: | - echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - echo "RARCH=$(uname -m)" >> "$GITHUB_ENV" - - uses: vmactions/haiku-vm@v1 - env: - INSTALL_REF: ${{ needs.release.outputs.version }} - with: - envs: 'INSTALL_REF' - usesh: true - mem: 8192 - disable-cache: true - prepare: | - pkgman install -y libsdl2 libsdl2_devel git gcc premake5 - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - bash projects/scripts/patch_commit_number.sh - sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }} - - name: Upload Files - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ needs.release.outputs.version }} - draft: false - prerelease: true - files: | - projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz + # build_haiku_x86_64: + # name: Haiku x86_64 Nightly + # needs: release + # runs-on: ubuntu-latest + # steps: + # - name: Checkout Code + # uses: actions/checkout@v6 + # with: { fetch-depth: 0, submodules: 'recursive' } + # - name: Set Environment Variables + # run: | + # echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" + # echo "RARCH=$(uname -m)" >> "$GITHUB_ENV" + # - uses: vmactions/haiku-vm@v1 + # env: + # INSTALL_REF: ${{ needs.release.outputs.version }} + # with: + # envs: 'INSTALL_REF' + # usesh: true + # mem: 8192 + # disable-cache: true + # prepare: | + # pkgman install -y libsdl2 libsdl2_devel git gcc premake5 + # run: | + # git config --global --add safe.directory "$GITHUB_WORKSPACE" + # bash projects/scripts/patch_commit_number.sh + # sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }} + # - name: Upload Files + # uses: softprops/action-gh-release@v3 + # with: + # tag_name: ${{ needs.release.outputs.version }} + # draft: false + # prerelease: true + # files: | + # projects/haiku/ecode/ecode-haiku-${{ env.INSTALL_REF }}-x86_64.tar.gz build_windows_msvc: name: Windows x86_64 MSVC Nightly diff --git a/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp b/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp new file mode 100644 index 000000000..15ddbc5fa Binary files /dev/null and b/bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp differ diff --git a/bin/assets/fontrendering/eepp-subpixel-text.webp b/bin/assets/fontrendering/eepp-subpixel-text.webp new file mode 100644 index 000000000..85bbb729a Binary files /dev/null and b/bin/assets/fontrendering/eepp-subpixel-text.webp differ diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index 93cad5519..f8fe0654e 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -103,7 +103,6 @@ MarkdownView h2 { } MarkdownView img { - scale-type: fit-inside; max-width: 100%; max-height: 100vh; } diff --git a/include/eepp/graphics/batchrenderer.hpp b/include/eepp/graphics/batchrenderer.hpp index 469b62a55..fd96eb90c 100644 --- a/include/eepp/graphics/batchrenderer.hpp +++ b/include/eepp/graphics/batchrenderer.hpp @@ -50,6 +50,9 @@ class EE_API BatchRenderer { /** Set the predefined blending function to use on the batch */ void setBlendMode( const BlendMode& blend ); + /** Selects RGB subpixel coverage compositing for subsequently queued textured quads. */ + void setSubpixelText( bool enabled ); + /** Set if every batch call have to be immediately rendered */ void setBatchForceRendering( const bool& force ) { mForceRendering = force; } @@ -339,6 +342,7 @@ class EE_API BatchRenderer { bool mForceRendering{ false }; bool mForceBlendMode{ true }; + bool mSubpixelText{ false }; void flush(); diff --git a/include/eepp/graphics/font.hpp b/include/eepp/graphics/font.hpp index 4283e1a21..14456a8b0 100644 --- a/include/eepp/graphics/font.hpp +++ b/include/eepp/graphics/font.hpp @@ -21,7 +21,8 @@ struct EE_API Glyph { Sizef size; ///< The glyph bitmap size on screen int lsbDelta{ 0 }; //!< Left offset after forced autohint. Internally used by getKerning() int rsbDelta{ 0 }; //!< Right offset after forced autohint. Internally used by getKerning() - Font* font{ nullptr }; ///< The glyph font + GlyphRenderMode renderMode{ GlyphRenderMode::Mask }; ///< Atlas texel compositing mode + Font* font{ nullptr }; ///< The glyph font }; enum class FontType { TTF, BMF, Sprite }; @@ -47,7 +48,7 @@ enum class FontAntialiasing { None, Grayscale, Subpixel }; /** @brief Font interface class. */ class EE_API Font { public: - enum Event { Load, Unload }; + enum Event { Load, Unload, CacheClear }; typedef std::function FontEventCallback; diff --git a/include/eepp/graphics/glyphdrawable.hpp b/include/eepp/graphics/glyphdrawable.hpp index 94f8efc4d..ff872ab55 100644 --- a/include/eepp/graphics/glyphdrawable.hpp +++ b/include/eepp/graphics/glyphdrawable.hpp @@ -9,6 +9,8 @@ namespace EE { namespace Graphics { class VertexBuffer; +enum class GlyphRenderMode : Uint8 { Mask, Subpixel, Color }; + class EE_API GlyphDrawable : public DrawableResource { public: static GlyphDrawable* New( TexturePtr texture, const Rect& srcRect, const Sizef& destSize = {}, @@ -70,6 +72,10 @@ class EE_API GlyphDrawable : public DrawableResource { void setAdvance( Float advance ); + GlyphRenderMode getGlyphRenderMode() const; + + void setGlyphRenderMode( GlyphRenderMode renderMode ); + protected: TexturePtr mTexture; Rectf mSrcRect; @@ -78,6 +84,7 @@ class EE_API GlyphDrawable : public DrawableResource { Vector2f mGlyphOffset; DrawMode mDrawMode{ DrawMode::Image }; Float mAdvance{ 0 }; + GlyphRenderMode mGlyphRenderMode{ GlyphRenderMode::Mask }; bool mIsItalic{ false }; }; diff --git a/include/eepp/graphics/renderer/opengl.hpp b/include/eepp/graphics/renderer/opengl.hpp index 6eaaba95f..30d2477cc 100644 --- a/include/eepp/graphics/renderer/opengl.hpp +++ b/include/eepp/graphics/renderer/opengl.hpp @@ -148,7 +148,6 @@ typedef char GLchar; #ifndef GL_NUM_EXTENSIONS #define GL_NUM_EXTENSIONS 0x821D #endif - #else //! Mobile platform ( Android / iPhone / Maemo ) #ifndef GL_GLEXT_PROTOTYPES @@ -398,6 +397,19 @@ typedef char GLchar; #endif +#ifndef GL_SRC1_COLOR +#define GL_SRC1_COLOR 0x88F9 +#endif +#ifndef GL_ONE_MINUS_SRC1_COLOR +#define GL_ONE_MINUS_SRC1_COLOR 0x88FA +#endif +#ifndef GL_ONE +#define GL_ONE 1 +#endif +#ifndef GL_ONE_MINUS_SRC_ALPHA +#define GL_ONE_MINUS_SRC_ALPHA 0x0303 +#endif + #ifndef GL_MULTISAMPLE #define GL_MULTISAMPLE 0x809D #endif diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index 830f768ee..5e0fcc834 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -90,6 +90,12 @@ class EE_API Renderer { void drawArrays( unsigned int mode, int first, int count ); + /** Draws an LCD coverage range into RGB independently and updates destination alpha. */ + bool drawSubpixelArrays( unsigned int mode, int first, int count ); + + /** Draws an LCD coverage range as neutral grayscale using its mean coverage. */ + bool drawSubpixelFallbackArrays( unsigned int mode, int first, int count ); + void drawElements( unsigned int mode, int count, unsigned int type, const void* indices ); void bindTexture( unsigned int target, unsigned int texture ); @@ -105,6 +111,9 @@ class EE_API Renderer { void blendEquationSeparate( unsigned int modeRGB, unsigned int modeAlpha ); + bool bindFragDataLocationIndexed( unsigned int program, unsigned int colorNumber, + unsigned int index, const char* name ); + void blitFrameBuffer( int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, unsigned int mask, unsigned int filter ); @@ -197,6 +206,9 @@ class EE_API Renderer { virtual void setShader( ShaderProgram* Shader ); + /** Selects built-in texture sampling: normal RGBA (0), LCD R/G/B (1-3), or LCD mean (4). */ + virtual bool setTextureColorMode( Int32 mode ); + virtual void clip2DPlaneEnable( const Int32& x, const Int32& y, const Int32& Width, const Int32& Height ) = 0; @@ -237,6 +249,8 @@ class EE_API Renderer { void colorMask( Uint8 red, Uint8 green, Uint8 blue, Uint8 alpha ); + void getColorMask( Uint8 mask[4] ) const; + void bindVertexArray( unsigned int array ); void deleteVertexArrays( int n, const unsigned int* arrays ); @@ -351,6 +365,10 @@ class EE_API Renderer { void waitForIdle(); protected: + virtual bool drawSubpixelDualSourceArrays( unsigned int mode, int first, int count ); + + static const Vector3ff& textureColorChannel( Int32 mode ); + static Renderer* sSingleton; enum RendererStateFlags { @@ -366,6 +384,7 @@ class EE_API Renderer { int mQuadVertex; float mLineWidth; unsigned int mCurVAO; + Uint8 mColorMask[4]{ 1, 1, 1, 1 }; ClippingMask* mClippingMask; diff --git a/include/eepp/graphics/renderer/renderergl.hpp b/include/eepp/graphics/renderer/renderergl.hpp index 34e548718..c168c5d97 100644 --- a/include/eepp/graphics/renderer/renderergl.hpp +++ b/include/eepp/graphics/renderer/renderergl.hpp @@ -83,7 +83,22 @@ class EE_API RendererGL : public Renderer { const float projMatrix[16], const int viewport[4], float* objx, float* objy, float* objz ); + void setShader( ShaderProgram* shader ); + + bool setTextureColorMode( Int32 mode ); + protected: + bool ensureSubpixelShader(); + bool ensureSubpixelDualSourceShader(); + bool drawSubpixelDualSourceArrays( unsigned int mode, int first, int count ); + + ShaderProgramPtr mSubpixelShader; + ShaderProgramPtr mSubpixelDualSourceShader; + ShaderProgram* mPreviousShader{ nullptr }; + Int32 mSubpixelChannelLoc{ -1 }; + bool mUsingSubpixelShader{ false }; + bool mSubpixelShaderInitializationAttempted{ false }; + bool mSubpixelDualSourceShaderInitializationAttempted{ false }; }; #endif diff --git a/include/eepp/graphics/renderer/renderergl3.hpp b/include/eepp/graphics/renderer/renderergl3.hpp index ab04da4af..772cf35fd 100644 --- a/include/eepp/graphics/renderer/renderergl3.hpp +++ b/include/eepp/graphics/renderer/renderergl3.hpp @@ -87,6 +87,8 @@ class EE_API RendererGL3 : public RendererGLShader { void planeStateCheck( bool tryEnable ); void reloadShader( ShaderProgram* Shader ); + ShaderProgramPtr createSubpixelDualSourceShader(); + bool canUseSubpixelDualSourceShader() const; }; }} // namespace EE::Graphics diff --git a/include/eepp/graphics/renderer/renderergl3cp.hpp b/include/eepp/graphics/renderer/renderergl3cp.hpp index 86be0c1ff..54a460490 100644 --- a/include/eepp/graphics/renderer/renderergl3cp.hpp +++ b/include/eepp/graphics/renderer/renderergl3cp.hpp @@ -96,6 +96,8 @@ class EE_API RendererGL3CP : public RendererGLShader { void reloadShader( ShaderProgram* Shader ); void allocateBuffers( const Uint32& size ); + ShaderProgramPtr createSubpixelDualSourceShader(); + bool canUseSubpixelDualSourceShader() const; }; }} // namespace EE::Graphics diff --git a/include/eepp/graphics/renderer/renderergles2.hpp b/include/eepp/graphics/renderer/renderergles2.hpp index 3760a27e1..7345ed3d3 100644 --- a/include/eepp/graphics/renderer/renderergles2.hpp +++ b/include/eepp/graphics/renderer/renderergles2.hpp @@ -98,6 +98,8 @@ class EE_API RendererGLES2 : public RendererGLShader { void reloadShader( ShaderProgram* Shader ); void checkLocalShader(); + ShaderProgramPtr createSubpixelDualSourceShader(); + bool canUseSubpixelDualSourceShader() const; }; }} // namespace EE::Graphics diff --git a/include/eepp/graphics/renderer/rendererglshader.hpp b/include/eepp/graphics/renderer/rendererglshader.hpp index b07aadb94..67df1abb9 100644 --- a/include/eepp/graphics/renderer/rendererglshader.hpp +++ b/include/eepp/graphics/renderer/rendererglshader.hpp @@ -54,14 +54,29 @@ class EE_API RendererGLShader : public Renderer { const float projMatrix[16], const int viewport[4], float* objx, float* objy, float* objz ); + bool setTextureColorMode( Int32 mode ); + protected: + bool drawSubpixelDualSourceArrays( unsigned int mode, int first, int count ); + ShaderProgramPtr createSubpixelDualSourceShader( const std::string& vertexShader, + const std::string& fragmentShader, + bool bindFragmentOutputs ); + static const char* subpixelDualSourceFragmentShaderGLSL130(); + virtual ShaderProgramPtr createSubpixelDualSourceShader() = 0; + virtual bool canUseSubpixelDualSourceShader() const = 0; + Private::MatrixStack* mStack; int mProjectionMatrix_id; // cpu-side hook to shader uniform int mModelViewMatrix_id; // cpu-side hook to shader uniform int mTextureMatrix_id; // cpu-side hook to shader uniform + int mTextureColorMode_id; + int mTextureColorChannel_id; + Int32 mTextureColorMode; unsigned int mCurrentMode; ShaderProgram* mCurShader; ShaderProgram* mShaderPrev; + ShaderProgramPtr mSubpixelDualSourceShader; + bool mSubpixelDualSourceShaderInitializationAttempted{ false }; void updateMatrix(); }; diff --git a/include/eepp/graphics/renderer/rendererhelper.hpp b/include/eepp/graphics/renderer/rendererhelper.hpp index 2218f179a..22234a6ef 100644 --- a/include/eepp/graphics/renderer/rendererhelper.hpp +++ b/include/eepp/graphics/renderer/rendererhelper.hpp @@ -33,6 +33,8 @@ enum GraphicsLibraryExtension { EEGL_EXT_blend_minmax, EEGL_EXT_blend_subtract, EEGL_ARB_ES3_compatibility, + EEGL_ARB_blend_func_extended, + EEGL_EXT_blend_func_extended, }; /// Graphics Library Renderer version available. diff --git a/include/eepp/graphics/shaderprogram.hpp b/include/eepp/graphics/shaderprogram.hpp index cdd0ee409..901c48408 100644 --- a/include/eepp/graphics/shaderprogram.hpp +++ b/include/eepp/graphics/shaderprogram.hpp @@ -56,6 +56,9 @@ class EE_API ShaderProgram { /** Add a vector of shaders */ void addShaders( const std::vector& shaders ); + /** Bind a fragment output to a color number and source index before linking. */ + bool bindFragDataLocationIndexed( Uint32 colorNumber, Uint32 index, const char* name ); + virtual bool link(); /** @return If the shader program is valid */ @@ -160,6 +163,12 @@ class EE_API ShaderProgram { std::vector mShaders; std::map mUniformLocations; std::map mAttributeLocations; + struct FragmentOutputBinding { + Uint32 colorNumber; + Uint32 index; + std::string name; + }; + std::vector mFragmentOutputBindings; ShaderProgramReloadCb mReloadCb; diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index 9b392cb8a..a99cbd13a 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -410,7 +410,7 @@ class EE_API Text { Float mCachedWidth{ 0 }; Uint32 mAlign{ TEXT_ALIGN_LEFT }; Uint32 mTabWidth{ 4 }; - Uint32 mInvalidationId{ 0 }; + Uint32 mInvalidationId{ GlobalInvalidationId }; Uint32 mTextHints{ 0 }; Float mMaxWrapWidth{ 0 }; LineWrapMode mLineWrapMode{ LineWrapMode::NoWrap }; @@ -422,8 +422,10 @@ class EE_API Text { std::vector mVertices; std::vector mColors; + std::vector mRenderModes; std::vector mOutlineVertices; std::vector mOutlineColors; + std::vector mOutlineRenderModes; void ensureGeometryUpdate(); @@ -432,12 +434,12 @@ class EE_API Text { /** Force to cache the width of the current text */ void cacheWidth(); - static void addLine( std::vector& vertices, Float lineLength, Float lineTop, - Float offset, Float thickness, Float outlineThickness, Int32 centerDiffX ); + void addLine( std::vector& vertices, Float lineLength, Float lineTop, + Float offset, Float thickness, Float outlineThickness, Int32 centerDiffX ); - static void addGlyphQuad( std::vector& vertices, Vector2f position, - const EE::Graphics::Glyph& glyph, Float italic, - Float outlineThickness, Int32 centerDiffX ); + void addGlyphQuad( std::vector& vertices, Vector2f position, + const EE::Graphics::Glyph& glyph, Float italic, Float outlineThickness, + Int32 centerDiffX ); Uint32 getTotalVertices(); diff --git a/include/eepp/ui/uiapplication.hpp b/include/eepp/ui/uiapplication.hpp index b9fcdbcc9..fd5e5b26c 100644 --- a/include/eepp/ui/uiapplication.hpp +++ b/include/eepp/ui/uiapplication.hpp @@ -1,6 +1,7 @@ #ifndef EE_UI_UIAPPLICATION #define EE_UI_UIAPPLICATION +#include #include #include @@ -45,6 +46,10 @@ class EE_API UIApplication { //! The default fallback font for the UI. If not provided it will load Droid Sans Fallback //! Full ( it will look at "assets/fonts/DroidSansFallbackFull.ttf" ) Font* fallbackFont{ nullptr }; + //! The hinting policy applied to fonts owned by the default and UI resource scopes. + FontHinting fontHinting{ FontHinting::Full }; + //! The antialiasing policy applied to fonts owned by the default and UI resource scopes. + FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale }; }; UIApplication( const WindowSettings& windowSettings, const Settings& appSettings = Settings(), @@ -70,6 +75,7 @@ class EE_API UIApplication { bool showMemoryManagerResult() const; String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; } + protected: UISceneNode* mUISceneNode{ nullptr }; EE::Window::Window* mWindow{ nullptr }; diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 3252ef752..0d7454952 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -161,9 +161,9 @@ class EE_API UISceneNode : public SceneNode { * @brief Binds an embedded scene to host-scene services without copying document state. * * Copies only shared platform/configuration services: dispatcher, DPI/window pointer, - * thread pool, color/contrast preferences, and default font/theme pointers. Stylesheets, - * URI, referer, cookies, navigation callbacks, actions, roots, resource scope, and dirty queues - * remain owned by this scene. + * thread pool, color/contrast and font-rendering preferences, and default font/theme pointers. + * Stylesheets, URI, referer, cookies, navigation callbacks, actions, roots, resource scope, and + * dirty queues remain owned by this scene. */ void initializeEmbeddedFromHost( UISceneNode* hostScene ); diff --git a/src/eepp/graphics/batchrenderer.cpp b/src/eepp/graphics/batchrenderer.cpp index 0928d87eb..b1e909a51 100644 --- a/src/eepp/graphics/batchrenderer.cpp +++ b/src/eepp/graphics/batchrenderer.cpp @@ -60,6 +60,7 @@ void BatchRenderer::setTexture( const Texture* texture, Texture::CoordinateType if ( mTexture != texture || mCoordinateType != coordinateType ) { flush(); mTextureOwner.reset(); + mSubpixelText = false; } mTexture = texture; @@ -81,6 +82,12 @@ void BatchRenderer::setBlendMode( const BlendMode& blend ) { mBlend = blend; } +void BatchRenderer::setSubpixelText( bool enabled ) { + if ( enabled != mSubpixelText ) + flush(); + mSubpixelText = enabled; +} + void BatchRenderer::addVertices( const unsigned int& num ) { mNumVertex += num; @@ -151,17 +158,21 @@ void BatchRenderer::flush() { 4, GL_UNSIGNED_BYTE, sizeof( VertexData ), reinterpret_cast( &mVertex[0] ) + sizeof( Vector2f ) + sizeof( Vector2f ), alloc ); - if ( !GLi->quadsSupported() ) { - if ( PRIMITIVE_QUADS == mCurrentMode ) { - GLi->drawArrays( PRIMITIVE_TRIANGLES, 0, NumVertex ); - } else if ( PRIMITIVE_POLYGON == mCurrentMode ) { - GLi->drawArrays( PRIMITIVE_TRIANGLE_FAN, 0, NumVertex ); - } else { - GLi->drawArrays( mCurrentMode, 0, NumVertex ); - } - } else { - GLi->drawArrays( mCurrentMode, 0, NumVertex ); + unsigned int drawMode = mCurrentMode; + if ( !GLi->quadsSupported() && PRIMITIVE_QUADS == mCurrentMode ) + drawMode = PRIMITIVE_TRIANGLES; + else if ( !GLi->quadsSupported() && PRIMITIVE_POLYGON == mCurrentMode ) + drawMode = PRIMITIVE_TRIANGLE_FAN; + + bool drawn = false; + if ( mSubpixelText && NULL != mTexture ) { + if ( mRotation == 0.f && mScale == 1.f ) + drawn = GLi->drawSubpixelArrays( drawMode, 0, NumVertex ); + if ( !drawn ) + drawn = GLi->drawSubpixelFallbackArrays( drawMode, 0, NumVertex ); } + if ( !drawn ) + GLi->drawArrays( drawMode, 0, NumVertex ); if ( createMatrix ) { GLi->popMatrix(); diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index d27d8dead..2a5f12308 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -931,6 +932,7 @@ GlyphDrawable* FontTrueType::getGlyphDrawable( Uint32 codePoint, unsigned int ch getGlyphTopOffset( characterSize ) + glyph.bounds.Top - outlineThickness } ); region->setAdvance( glyph.advance ); region->setIsItalic( isItalic ); + region->setGlyphRenderMode( glyph.renderMode ); drawables[key] = region; return region; @@ -960,6 +962,7 @@ GlyphDrawable* FontTrueType::getGlyphDrawableFromGlyphIndex( Uint32 glyphIndex, getGlyphTopOffset( characterSize ) + glyph.bounds.Top - outlineThickness } ); region->setAdvance( glyph.advance ); region->setIsItalic( italic ); + region->setGlyphRenderMode( glyph.renderMode ); drawables[key] = region; return region; @@ -1453,6 +1456,14 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, } FT_Bitmap& bitmap = reinterpret_cast( glyphDesc )->bitmap; + if ( bitmap.pixel_mode == FT_PIXEL_MODE_LCD ) { + const bool programmableRenderer = + GLi && + ( ( GLi->version() == GLv_2 && GLi->shadersSupported() ) || GLi->version() == GLv_3 || + GLi->version() == GLv_3CP || GLi->version() == GLv_ES2 ); + glyph.renderMode = programmableRenderer ? GlyphRenderMode::Subpixel : GlyphRenderMode::Mask; + } else if ( bitmap.pixel_mode == FT_PIXEL_MODE_BGRA ) + glyph.renderMode = GlyphRenderMode::Color; // Apply bold if necessary -- fallback technique using bitmap (lower quality) if ( !outline ) { @@ -1549,6 +1560,14 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, glyph.bounds.Bottom = static_cast( slot->metrics.height ) / static_cast( 1 << 6 ) + outlineThickness * 2; + if ( bitmap.pixel_mode == FT_PIXEL_MODE_LCD ) { + const FT_BitmapGlyph bitmapGlyph = reinterpret_cast( glyphDesc ); + // LCD filtering may shift the bitmap beyond the outline bearing. Keep the logical + // dimensions expected by retained Text, but position the filtered ink from its actual + // bitmap origin. + glyph.bounds.Left = bitmapGlyph->left + outlineThickness; + glyph.bounds.Top = -bitmapGlyph->top + outlineThickness; + } // Resize the pixel buffer to the new size and fill it with transparent white pixels const Uint32 bufferSize = width * height * 4; @@ -1602,17 +1621,50 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, destHeight = dest.getHeight() + 2 * padding; } } else if ( bitmap.pixel_mode == FT_PIXEL_MODE_LCD ) { - for ( int y = padding; y < height - padding; ++y ) { - for ( int x = padding; x < width - padding; ++x ) { - const std::size_t index = ( x + y * width ) * 4; - const Uint8* px = &pixels[( x - padding ) * 3]; - mPixelBuffer[index + 0] = px[0]; - mPixelBuffer[index + 1] = px[1]; - mPixelBuffer[index + 2] = px[2]; - mPixelBuffer[index + 3] = - (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + if ( scale < 1.f ) { + for ( int y = 0; y < height; ++y ) { + for ( int x = 0; x < width; ++x ) { + const std::size_t index = ( x + y * width ) * 4; + const Uint8* px = &pixels[x * 3]; + mPixelBuffer[index + 0] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[0] : 255; + mPixelBuffer[index + 1] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[1] : 255; + mPixelBuffer[index + 2] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[2] : 255; + mPixelBuffer[index + 3] = + (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + } + pixels += bitmap.pitch; + } + + Image dest( &mPixelBuffer[0], width, height, 4 ); + dest.avoidFreeImage( true ); + dest.scale( scale ); + dest.avoidFreeImage( true ); + pixelPtr = dest.getPixels(); + glyph.bounds.Left *= scale; + glyph.bounds.Right *= scale; + glyph.bounds.Top *= scale; + glyph.bounds.Bottom *= scale; + destWidth = dest.getWidth() + 2 * padding; + destHeight = dest.getHeight() + 2 * padding; + } else { + for ( int y = padding; y < height - padding; ++y ) { + for ( int x = padding; x < width - padding; ++x ) { + const std::size_t index = ( x + y * width ) * 4; + const Uint8* px = &pixels[( x - padding ) * 3]; + mPixelBuffer[index + 0] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[0] : 255; + mPixelBuffer[index + 1] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[1] : 255; + mPixelBuffer[index + 2] = + glyph.renderMode == GlyphRenderMode::Subpixel ? px[2] : 255; + mPixelBuffer[index + 3] = + (Uint8)( ( (int)px[0] + (int)px[1] + (int)px[2] ) / 3.f ); + } + pixels += bitmap.pitch; } - pixels += bitmap.pitch; } } else { if ( scale < 1.f ) { @@ -2042,6 +2094,7 @@ FontTrueType::Page::~Page() { } void FontTrueType::clearCache() { + sendEvent( Event::CacheClear ); mPages.clear(); mClosestCharacterSize.clear(); mCodePointIndexCache.clear(); diff --git a/src/eepp/graphics/glyphdrawable.cpp b/src/eepp/graphics/glyphdrawable.cpp index 1ddea05de..12eddc289 100644 --- a/src/eepp/graphics/glyphdrawable.cpp +++ b/src/eepp/graphics/glyphdrawable.cpp @@ -36,6 +36,7 @@ void GlyphDrawable::draw( const Vector2f& position, const Sizef& size ) { BatchRenderer* BR = GlobalBatchRenderer::instance(); BR->setTexture( mTexture, mTexture->getCoordinateType() ); BR->setBlendMode( BlendMode::Alpha() ); + BR->setSubpixelText( mGlyphRenderMode == GlyphRenderMode::Subpixel ); BR->quadsBegin(); BR->quadsSetColor( mColor ); BR->quadsSetTexCoord( mSrcRect.Left, mSrcRect.Top, mSrcRect.Left + mSrcRect.Right, @@ -87,6 +88,7 @@ DrawablePtr GlyphDrawable::clone() const { instance->setDrawMode( mDrawMode ); instance->setIsItalic( mIsItalic ); instance->setAdvance( mAdvance ); + instance->setGlyphRenderMode( mGlyphRenderMode ); instance->setColor( mColor ); instance->setPosition( mPosition ); return instance; @@ -152,4 +154,12 @@ void GlyphDrawable::setAdvance( Float advance ) { mAdvance = advance; } +GlyphRenderMode GlyphDrawable::getGlyphRenderMode() const { + return mGlyphRenderMode; +} + +void GlyphDrawable::setGlyphRenderMode( GlyphRenderMode renderMode ) { + mGlyphRenderMode = renderMode; +} + }} // namespace EE::Graphics diff --git a/src/eepp/graphics/renderer/openglext.hpp b/src/eepp/graphics/renderer/openglext.hpp index 340005955..58752516e 100644 --- a/src/eepp/graphics/renderer/openglext.hpp +++ b/src/eepp/graphics/renderer/openglext.hpp @@ -44,6 +44,8 @@ typedef void( APIENTRY* pglBlendFuncSeparate )( GLenum sfactorRGB, GLenum dfacto typedef void( APIENTRY* pglDiscardFramebufferEXT )( GLenum target, GLsizei numAttachments, const GLenum* attachments ); typedef void( APIENTRY* pglBlendEquationSeparate )( GLenum modeRGB, GLenum modeAlpha ); +typedef void( APIENTRY* pglBindFragDataLocationIndexed )( GLuint program, GLuint colorNumber, + GLuint index, const GLchar* name ); typedef void( APIENTRY* pglBlitFramebufferEXT )( GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter ); diff --git a/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index 68b3fec29..e1ef66947 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -260,6 +260,9 @@ void Renderer::init() { writeExtension( EEGL_EXT_blend_func_separate, GLEW_EXT_blend_func_separate ); writeExtension( EEGL_EXT_blend_minmax, GLEW_EXT_blend_minmax ); writeExtension( EEGL_EXT_blend_subtract, GLEW_EXT_blend_subtract ); + writeExtension( EEGL_ARB_blend_func_extended, + GLEW_ARB_blend_func_extended || GLEW_VERSION_3_3 ); + writeExtension( EEGL_EXT_blend_func_extended, GLEW_EXT_blend_func_extended ); } else #endif { @@ -296,6 +299,13 @@ void Renderer::init() { glVersion >= 140 || isExtension( "GL_EXT_blend_minmax" ) ); writeExtension( EEGL_EXT_blend_subtract, glVersion >= 140 || isExtension( "GL_EXT_blend_subtract" ) ); + writeExtension( EEGL_ARB_blend_func_extended, + !is_es && + ( glVersion >= 330 || isExtension( "GL_ARB_blend_func_extended" ) ) ); + writeExtension( EEGL_EXT_blend_func_extended, + is_es && ( isExtension( "GL_EXT_blend_func_extended" ) || + isExtension( "GL_WEBGL_blend_func_extended" ) || + isExtension( "WEBGL_blend_func_extended" ) ) ); } // NVIDIA added support for GL_OES_compressed_ETC1_RGB8_texture in desktop GPUs @@ -476,6 +486,42 @@ void Renderer::drawArrays( unsigned int mode, int first, int count ) { glDrawArrays( mode, first, count ); } +bool Renderer::drawSubpixelArrays( unsigned int mode, int first, int count ) { + if ( drawSubpixelDualSourceArrays( mode, first, count ) ) + return true; + + if ( !setTextureColorMode( 1 ) ) + return false; + + Uint8 previousColorMask[4]; + getColorMask( previousColorMask ); + for ( Int32 channel = 0; channel < 3; ++channel ) { + setTextureColorMode( channel + 1 ); + colorMask( channel == 0 && previousColorMask[0], channel == 1 && previousColorMask[1], + channel == 2 && previousColorMask[2], 0 ); + drawArrays( mode, first, count ); + } + setTextureColorMode( 4 ); + colorMask( 0, 0, 0, previousColorMask[3] ); + drawArrays( mode, first, count ); + setTextureColorMode( 0 ); + colorMask( previousColorMask[0], previousColorMask[1], previousColorMask[2], + previousColorMask[3] ); + return true; +} + +bool Renderer::drawSubpixelDualSourceArrays( unsigned int, int, int ) { + return false; +} + +bool Renderer::drawSubpixelFallbackArrays( unsigned int mode, int first, int count ) { + if ( !setTextureColorMode( 4 ) ) + return false; + drawArrays( mode, first, count ); + setTextureColorMode( 0 ); + return true; +} + void Renderer::drawElements( unsigned int mode, int count, unsigned int type, const void* indices ) { glDrawElements( mode, count, type, indices ); @@ -534,6 +580,29 @@ void Renderer::blendEquationSeparate( unsigned int modeRGB, unsigned int modeAlp eeglBlendEquationSeparate( modeRGB, modeAlpha ); } +bool Renderer::bindFragDataLocationIndexed( unsigned int program, unsigned int colorNumber, + unsigned int index, const char* name ) { +#ifndef EE_GLES + static pglBindFragDataLocationIndexed bindFragDataLocationIndexed = NULL; + if ( NULL == bindFragDataLocationIndexed ) + bindFragDataLocationIndexed = + (pglBindFragDataLocationIndexed)getProcAddress( "glBindFragDataLocationIndexed" ); + if ( NULL != bindFragDataLocationIndexed ) { + bindFragDataLocationIndexed( program, colorNumber, index, name ); + return true; + } +#endif + static pglBindFragDataLocationIndexed bindFragDataLocationIndexedEXT = NULL; + if ( NULL == bindFragDataLocationIndexedEXT ) + bindFragDataLocationIndexedEXT = + (pglBindFragDataLocationIndexed)getProcAddress( "glBindFragDataLocationIndexedEXT" ); + if ( NULL != bindFragDataLocationIndexedEXT ) { + bindFragDataLocationIndexedEXT( program, colorNumber, index, name ); + return true; + } + return false; +} + void Renderer::blitFrameBuffer( int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, unsigned int mask, unsigned int filter ) { static pglBlitFramebufferEXT eeglBlitFramebufferEXT = NULL; @@ -556,6 +625,19 @@ void Renderer::setShader( ShaderProgram* Shader ) { #endif } +bool Renderer::setTextureColorMode( Int32 ) { + return false; +} + +const Vector3ff& Renderer::textureColorChannel( Int32 mode ) { + static const Vector3ff channels[] = { { 0.f, 0.f, 0.f }, + { 1.f, 0.f, 0.f }, + { 0.f, 1.f, 0.f }, + { 0.f, 0.f, 1.f }, + { 1.f / 3.f, 1.f / 3.f, 1.f / 3.f } }; + return channels[mode]; +} + bool Renderer::isLineSmooth() { return BitOp::readBitKey( &mStateFlags, RSF_LINE_SMOOTH ); } @@ -743,9 +825,17 @@ void Renderer::stencilMask( unsigned int mask ) { } void Renderer::colorMask( Uint8 red, Uint8 green, Uint8 blue, Uint8 alpha ) { + mColorMask[0] = red; + mColorMask[1] = green; + mColorMask[2] = blue; + mColorMask[3] = alpha; glColorMask( red, green, blue, alpha ); } +void Renderer::getColorMask( Uint8 mask[4] ) const { + std::copy( std::begin( mColorMask ), std::end( mColorMask ), mask ); +} + const int& Renderer::quadVertex() const { return mQuadVertex; } diff --git a/src/eepp/graphics/renderer/renderergl.cpp b/src/eepp/graphics/renderer/renderergl.cpp index 7df0a006f..1d51a1309 100644 --- a/src/eepp/graphics/renderer/renderergl.cpp +++ b/src/eepp/graphics/renderer/renderergl.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -61,6 +62,151 @@ RendererGL::RendererGL() { RendererGL::~RendererGL() {} +bool RendererGL::ensureSubpixelShader() { +#ifndef EE_GLES1 + if ( mSubpixelShader ) + return true; + if ( mSubpixelShaderInitializationAttempted || !shadersSupported() ) + return false; + mSubpixelShaderInitializationAttempted = true; + static const char vertexShader[] = R"( +#version 120 +varying vec4 dgl_Color; +varying vec2 dgl_TexCoord; +void main() { + gl_Position = ftransform(); + gl_ClipVertex = gl_ModelViewMatrix * gl_Vertex; + dgl_Color = gl_Color; + dgl_TexCoord = ( gl_TextureMatrix[0] * gl_MultiTexCoord0 ).xy; +} +)"; + static const char fragmentShader[] = R"( +#version 120 +uniform sampler2D textureUnit0; +uniform vec3 dgl_TextureColorChannel; +varying vec4 dgl_Color; +varying vec2 dgl_TexCoord; +void main() { + vec4 texel = texture2D( textureUnit0, dgl_TexCoord ); + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); +} +)"; + mSubpixelShader = ShaderProgram::New( vertexShader, sizeof( vertexShader ) - 1, fragmentShader, + sizeof( fragmentShader ) - 1, "eepp-subpixel-text" ); + if ( mSubpixelShader && mSubpixelShader->isValid() ) { + Renderer::setShader( mSubpixelShader.get() ); + mSubpixelChannelLoc = mSubpixelShader->getUniformLocation( "dgl_TextureColorChannel" ); + mSubpixelShader->setUniform( "textureUnit0", 0 ); + Renderer::setShader( mPreviousShader ); + } else { + mSubpixelShader.reset(); + } + return mSubpixelShader && mSubpixelChannelLoc != -1; +#else + return false; +#endif +} + +bool RendererGL::ensureSubpixelDualSourceShader() { +#ifndef EE_GLES1 + if ( mSubpixelDualSourceShader ) + return true; + if ( mSubpixelDualSourceShaderInitializationAttempted || + !isExtension( EEGL_ARB_blend_func_extended ) || !shadersSupported() ) + return false; + mSubpixelDualSourceShaderInitializationAttempted = true; + static const char vertexShader[] = R"( +#version 130 +varying vec4 dgl_Color; +varying vec2 dgl_TexCoord; +void main() { + gl_Position = ftransform(); + gl_ClipVertex = gl_ModelViewMatrix * gl_Vertex; + dgl_Color = gl_Color; + dgl_TexCoord = ( gl_TextureMatrix[0] * gl_MultiTexCoord0 ).xy; +} +)"; + static const char fragmentShader[] = R"( +#version 130 +uniform sampler2D textureUnit0; +varying vec4 dgl_Color; +varying vec2 dgl_TexCoord; +out vec4 dgl_FragColor; +out vec4 dgl_FragCoverage; +void main() { + vec3 coverage = texture2D( textureUnit0, dgl_TexCoord ).rgb; + float meanCoverage = dot( coverage, vec3( 1.0 / 3.0 ) ); + dgl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * meanCoverage ); + dgl_FragCoverage = vec4( dgl_Color.a * coverage, 0.0 ); +} +)"; + ShaderProgramPtr shader = ShaderProgram::New( "eepp-subpixel-dual-source-text" ); + ShaderPtr vs( eeNew( VertexShader, ( vertexShader, sizeof( vertexShader ) - 1 ) ), + ResourceDeleter() ); + ShaderPtr fs( eeNew( FragmentShader, ( fragmentShader, sizeof( fragmentShader ) - 1 ) ), + ResourceDeleter() ); + if ( shader && vs->isValid() && fs->isValid() ) { + shader->addShader( std::move( vs ) ); + shader->addShader( std::move( fs ) ); + if ( shader->bindFragDataLocationIndexed( 0, 0, "dgl_FragColor" ) && + shader->bindFragDataLocationIndexed( 0, 1, "dgl_FragCoverage" ) && shader->link() ) { + mSubpixelDualSourceShader = std::move( shader ); + Renderer::setShader( mSubpixelDualSourceShader.get() ); + mSubpixelDualSourceShader->setUniform( "textureUnit0", 0 ); + Renderer::setShader( mPreviousShader ); + } + } + return !!mSubpixelDualSourceShader; +#else + return false; +#endif +} + +bool RendererGL::drawSubpixelDualSourceArrays( unsigned int mode, int first, int count ) { + if ( !ensureSubpixelDualSourceShader() ) + return false; + const BlendMode previousBlendMode = BlendMode::getPreBlendFunc(); + Renderer::setShader( mSubpixelDualSourceShader.get() ); + blendFuncSeparate( GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR, GL_ONE, GL_ONE_MINUS_SRC_ALPHA ); + drawArrays( mode, first, count ); + BlendMode::setMode( previousBlendMode, true ); + Renderer::setShader( mPreviousShader ); + return true; +} + +void RendererGL::setShader( ShaderProgram* shader ) { + if ( !mUsingSubpixelShader ) + mPreviousShader = shader; + Renderer::setShader( shader ); +} + +bool RendererGL::setTextureColorMode( Int32 mode ) { +#ifdef EE_GLES1 + return false; +#else + if ( mode < 0 || mode > 4 ) + return false; + if ( mode != 0 && !ensureSubpixelShader() ) + return false; + if ( !mSubpixelShader || !mSubpixelShader->isValid() || mSubpixelChannelLoc == -1 ) + return false; + if ( mode == 0 ) { + if ( mUsingSubpixelShader ) { + Renderer::setShader( mPreviousShader ); + mUsingSubpixelShader = false; + } + return true; + } + if ( !mUsingSubpixelShader ) { + Renderer::setShader( mSubpixelShader.get() ); + mUsingSubpixelShader = true; + } + mSubpixelShader->setUniform( mSubpixelChannelLoc, textureColorChannel( mode ) ); + return true; +#endif +} + GraphicsLibraryVersion RendererGL::version() { #ifndef EE_GLES1 return GLv_2; diff --git a/src/eepp/graphics/renderer/renderergl3.cpp b/src/eepp/graphics/renderer/renderergl3.cpp index 9c082a64d..06913833a 100644 --- a/src/eepp/graphics/renderer/renderergl3.cpp +++ b/src/eepp/graphics/renderer/renderergl3.cpp @@ -115,6 +115,21 @@ void RendererGL3::reloadCurrentShader() { reloadShader( mCurShader ); } +ShaderProgramPtr RendererGL3::createSubpixelDualSourceShader() { + std::string vertexShader = mBaseVertexShader; + String::replaceAll( vertexShader, "#version 120", "#version 130" ); + return RendererGLShader::createSubpixelDualSourceShader( + vertexShader, subpixelDualSourceFragmentShaderGLSL130(), true ); +} + +bool RendererGL3::canUseSubpixelDualSourceShader() const { + for ( Int32 state : mPlanesStates ) { + if ( state != 0 ) + return false; + } + return true; +} + void RendererGL3::reloadShader( ShaderProgram* Shader ) { mCurShader = NULL; @@ -148,6 +163,8 @@ void RendererGL3::setShader( ShaderProgram* Shader ) { mProjectionMatrix_id = mCurShader->getUniformLocation( "dgl_ProjectionMatrix" ); mModelViewMatrix_id = mCurShader->getUniformLocation( "dgl_ModelViewMatrix" ); mTextureMatrix_id = mCurShader->getUniformLocation( "dgl_TextureMatrix" ); + mTextureColorMode_id = mCurShader->getUniformLocation( "dgl_TextureColorMode" ); + mTextureColorChannel_id = mCurShader->getUniformLocation( "dgl_TextureColorChannel" ); mTexActiveLoc = mCurShader->getUniformLocation( "dgl_TexActive" ); mPointSpriteLoc = mCurShader->getUniformLocation( "dgl_PointSpriteActive" ); mClippingEnabledLoc = mCurShader->getUniformLocation( "dgl_ClippingEnabled" ); @@ -168,6 +185,11 @@ void RendererGL3::setShader( ShaderProgram* Shader ) { } useProgram( mCurShader->getHandler() ); + if ( mTextureColorMode_id != -1 ) + mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); + if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) enableClientState( GL_VERTEX_ARRAY ); diff --git a/src/eepp/graphics/renderer/renderergl3cp.cpp b/src/eepp/graphics/renderer/renderergl3cp.cpp index af5bf5cf1..384892991 100644 --- a/src/eepp/graphics/renderer/renderergl3cp.cpp +++ b/src/eepp/graphics/renderer/renderergl3cp.cpp @@ -147,6 +147,32 @@ void RendererGL3CP::reloadCurrentShader() { reloadShader( mCurShader ); } +ShaderProgramPtr RendererGL3CP::createSubpixelDualSourceShader() { + static const char fragmentShader[] = R"(#version 330 +uniform sampler2D textureUnit0; +in vec4 dgl_Color; +in vec4 dgl_TexCoord[1]; +out vec4 dgl_FragColor; +out vec4 dgl_FragCoverage; +void main() { + vec3 coverage = texture( textureUnit0, dgl_TexCoord[0].xy ).rgb; + float meanCoverage = dot( coverage, vec3( 1.0 / 3.0 ) ); + dgl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * meanCoverage ); + dgl_FragCoverage = vec4( dgl_Color.a * coverage, 0.0 ); +} +)"; + return RendererGLShader::createSubpixelDualSourceShader( mBaseVertexShader, fragmentShader, + true ); +} + +bool RendererGL3CP::canUseSubpixelDualSourceShader() const { + for ( Int32 state : mPlanesStates ) { + if ( state != 0 ) + return false; + } + return true; +} + void RendererGL3CP::reloadShader( ShaderProgram* Shader ) { mCurShader = NULL; @@ -175,6 +201,8 @@ void RendererGL3CP::setShader( ShaderProgram* Shader ) { mProjectionMatrix_id = mCurShader->getUniformLocation( "dgl_ProjectionMatrix" ); mModelViewMatrix_id = mCurShader->getUniformLocation( "dgl_ModelViewMatrix" ); mTextureMatrix_id = mCurShader->getUniformLocation( "dgl_TextureMatrix" ); + mTextureColorMode_id = mCurShader->getUniformLocation( "dgl_TextureColorMode" ); + mTextureColorChannel_id = mCurShader->getUniformLocation( "dgl_TextureColorChannel" ); mTexActiveLoc = mCurShader->getUniformLocation( "dgl_TexActive" ); mPointSpriteLoc = mCurShader->getUniformLocation( "dgl_PointSpriteActive" ); mClippingEnabledLoc = mCurShader->getUniformLocation( "dgl_ClippingEnabled" ); @@ -195,6 +223,11 @@ void RendererGL3CP::setShader( ShaderProgram* Shader ) { } useProgram( mCurShader->getHandler() ); + if ( mTextureColorMode_id != -1 ) + mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); + if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) enableClientState( GL_VERTEX_ARRAY ); diff --git a/src/eepp/graphics/renderer/renderergles2.cpp b/src/eepp/graphics/renderer/renderergles2.cpp index 83de668b1..e485ed359 100644 --- a/src/eepp/graphics/renderer/renderergles2.cpp +++ b/src/eepp/graphics/renderer/renderergles2.cpp @@ -148,6 +148,35 @@ void RendererGLES2::reloadCurrentShader() { reloadShader( mCurShader ); } +ShaderProgramPtr RendererGLES2::createSubpixelDualSourceShader() { +#ifdef EE_GLES2 + static const char fragmentShader[] = R"(#extension GL_EXT_blend_func_extended : require +precision mediump float; +precision lowp int; +uniform sampler2D textureUnit0; +varying vec4 dgl_Color; +varying mediump vec4 dgl_TexCoord[1]; +void main() { + vec3 coverage = texture2D( textureUnit0, dgl_TexCoord[0].xy ).rgb; + float meanCoverage = dot( coverage, vec3( 1.0 / 3.0 ) ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * meanCoverage ); + gl_SecondaryFragColorEXT = vec4( dgl_Color.a * coverage, 0.0 ); +} +)"; + return RendererGLShader::createSubpixelDualSourceShader( mBaseVertexShader, fragmentShader, + false ); +#else + std::string vertexShader = mBaseVertexShader; + String::replaceAll( vertexShader, "#version 120", "#version 130" ); + return RendererGLShader::createSubpixelDualSourceShader( + vertexShader, subpixelDualSourceFragmentShaderGLSL130(), true ); +#endif +} + +bool RendererGLES2::canUseSubpixelDualSourceShader() const { + return !mClippingEnabled && !mPointSpriteEnabled; +} + void RendererGLES2::reloadShader( ShaderProgram* Shader ) { mCurShader = NULL; @@ -198,6 +227,8 @@ void RendererGLES2::setShader( ShaderProgram* Shader ) { mProjectionMatrix_id = mCurShader->getUniformLocation( "dgl_ProjectionMatrix" ); mModelViewMatrix_id = mCurShader->getUniformLocation( "dgl_ModelViewMatrix" ); mTextureMatrix_id = mCurShader->getUniformLocation( "dgl_TextureMatrix" ); + mTextureColorMode_id = mCurShader->getUniformLocation( "dgl_TextureColorMode" ); + mTextureColorChannel_id = mCurShader->getUniformLocation( "dgl_TextureColorChannel" ); mTexActiveLoc = mCurShader->getUniformLocation( "dgl_TexActive" ); mClippingEnabledLoc = mCurShader->getUniformLocation( "dgl_ClippingEnabled" ); mPointSizeLoc = mCurShader->getUniformLocation( "dgl_PointSize" ); @@ -222,6 +253,11 @@ void RendererGLES2::setShader( ShaderProgram* Shader ) { } useProgram( mCurShader->getHandler() ); + if ( mTextureColorMode_id != -1 ) + mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); + if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) enableClientState( GL_VERTEX_ARRAY ); diff --git a/src/eepp/graphics/renderer/rendererglshader.cpp b/src/eepp/graphics/renderer/rendererglshader.cpp index a0818d6bd..809a7e42c 100644 --- a/src/eepp/graphics/renderer/rendererglshader.cpp +++ b/src/eepp/graphics/renderer/rendererglshader.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,6 +9,9 @@ RendererGLShader::RendererGLShader() : mProjectionMatrix_id( 0 ), mModelViewMatrix_id( 0 ), mTextureMatrix_id( 0 ), + mTextureColorMode_id( -1 ), + mTextureColorChannel_id( -1 ), + mTextureColorMode( 0 ), mCurrentMode( 0 ), mCurShader( NULL ), mShaderPrev( NULL ) { @@ -21,6 +25,78 @@ RendererGLShader::~RendererGLShader() { eeSAFE_DELETE( mStack ); } +const char* RendererGLShader::subpixelDualSourceFragmentShaderGLSL130() { + return R"(#version 130 +uniform sampler2D textureUnit0; +varying vec4 dgl_Color; +varying vec4 dgl_TexCoord[1]; +out vec4 dgl_FragColor; +out vec4 dgl_FragCoverage; +void main() { + vec3 coverage = texture2D( textureUnit0, dgl_TexCoord[0].xy ).rgb; + float meanCoverage = dot( coverage, vec3( 1.0 / 3.0 ) ); + dgl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * meanCoverage ); + dgl_FragCoverage = vec4( dgl_Color.a * coverage, 0.0 ); +} +)"; +} + +ShaderProgramPtr RendererGLShader::createSubpixelDualSourceShader( + const std::string& vertexShader, const std::string& fragmentShader, bool bindFragmentOutputs ) { + ShaderProgramPtr shader = ShaderProgram::New( "eepp-subpixel-dual-source-text" ); + const bool shaderEnsure = Shader::ensure(); + Shader::ensure( false ); + ShaderPtr vs( eeNew( VertexShader, ( vertexShader.c_str(), vertexShader.size() ) ), + ResourceDeleter() ); + ShaderPtr fs( eeNew( FragmentShader, ( fragmentShader.c_str(), fragmentShader.size() ) ), + ResourceDeleter() ); + Shader::ensure( shaderEnsure ); + if ( !shader || !vs->isValid() || !fs->isValid() ) + return {}; + shader->addShader( std::move( vs ) ); + shader->addShader( std::move( fs ) ); + if ( bindFragmentOutputs && + ( !shader->bindFragDataLocationIndexed( 0, 0, "dgl_FragColor" ) || + !shader->bindFragDataLocationIndexed( 0, 1, "dgl_FragCoverage" ) ) ) + return {}; + return shader->link() ? shader : ShaderProgramPtr{}; +} + +bool RendererGLShader::drawSubpixelDualSourceArrays( unsigned int mode, int first, int count ) { + if ( !isExtension( EEGL_ARB_blend_func_extended ) && + !isExtension( EEGL_EXT_blend_func_extended ) ) + return false; + if ( !canUseSubpixelDualSourceShader() ) + return false; + if ( !mSubpixelDualSourceShader && !mSubpixelDualSourceShaderInitializationAttempted ) { + mSubpixelDualSourceShaderInitializationAttempted = true; + mSubpixelDualSourceShader = createSubpixelDualSourceShader(); + } + if ( !mSubpixelDualSourceShader ) + return false; + ShaderProgram* previousShader = mCurShader; + const BlendMode previousBlendMode = BlendMode::getPreBlendFunc(); + setShader( mSubpixelDualSourceShader.get() ); + blendFuncSeparate( GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR, GL_ONE, GL_ONE_MINUS_SRC_ALPHA ); + drawArrays( mode, first, count ); + BlendMode::setMode( previousBlendMode, true ); + setShader( previousShader ); + return true; +} + +bool RendererGLShader::setTextureColorMode( Int32 mode ) { + if ( mTextureColorMode_id == -1 ) + return false; + if ( mTextureColorMode != mode ) { + mTextureColorMode = mode; + mCurShader->setUniform( mTextureColorMode_id, mode ); + if ( mTextureColorChannel_id != -1 ) { + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mode ) ); + } + } + return true; +} + void RendererGLShader::updateMatrix() { switch ( mCurrentMode ) { case GL_PROJECTION: { diff --git a/src/eepp/graphics/renderer/shaders/base.frag.h b/src/eepp/graphics/renderer/shaders/base.frag.h index 6fc76c4a8..f17ec98b3 100644 --- a/src/eepp/graphics/renderer/shaders/base.frag.h +++ b/src/eepp/graphics/renderer/shaders/base.frag.h @@ -1,6 +1,8 @@ const GLchar * EEGLES2_SHADER_BASE_FS = R"( uniform sampler2D textureUnit0; +uniform int dgl_TextureColorMode; +uniform vec3 dgl_TextureColorChannel; varying vec4 dgl_Color; #ifndef GL_ES varying vec4 dgl_TexCoord[ 1 ]; @@ -9,6 +11,12 @@ varying mediump vec4 dgl_TexCoord[ 1 ]; #endif void main(void) { - gl_FragColor = dgl_Color * texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + vec4 texel = texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 0 == dgl_TextureColorMode ) + gl_FragColor = dgl_Color * texel; + else { + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); + } } )"; diff --git a/src/eepp/graphics/renderer/shaders/basegl3.frag.h b/src/eepp/graphics/renderer/shaders/basegl3.frag.h index b169b9b64..f3631e4a1 100644 --- a/src/eepp/graphics/renderer/shaders/basegl3.frag.h +++ b/src/eepp/graphics/renderer/shaders/basegl3.frag.h @@ -3,6 +3,8 @@ const GLchar * EEGL3_SHADER_BASE_FS = R"( #define MAX_CLIP_PLANES 6 uniform sampler2D textureUnit0; uniform int dgl_TexActive; +uniform int dgl_TextureColorMode; +uniform vec3 dgl_TextureColorChannel; uniform int dgl_PointSpriteActive; uniform int dgl_ClippingEnabled; uniform int dgl_ClipEnabled[ MAX_CLIP_PLANES ]; @@ -20,8 +22,15 @@ void main(void) } } if ( 0 == dgl_PointSpriteActive ) { - if ( 1 == dgl_TexActive ) - gl_FragColor = dgl_Color * texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 1 == dgl_TexActive ) { + vec4 texel = texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 0 == dgl_TextureColorMode ) + gl_FragColor = dgl_Color * texel; + else { + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); + } + } else gl_FragColor = dgl_Color; } else diff --git a/src/eepp/graphics/renderer/shaders/basegl3cp.frag.h b/src/eepp/graphics/renderer/shaders/basegl3cp.frag.h index e47d4c71e..ad6eb55a3 100644 --- a/src/eepp/graphics/renderer/shaders/basegl3cp.frag.h +++ b/src/eepp/graphics/renderer/shaders/basegl3cp.frag.h @@ -3,6 +3,8 @@ const GLchar * EEGL3CP_SHADER_BASE_FS = R"( #define MAX_CLIP_PLANES 6 uniform sampler2D textureUnit0; uniform int dgl_TexActive; +uniform int dgl_TextureColorMode; +uniform vec3 dgl_TextureColorChannel; uniform int dgl_PointSpriteActive; uniform int dgl_ClippingEnabled; uniform int dgl_ClipEnabled[ MAX_CLIP_PLANES ]; @@ -21,8 +23,15 @@ void main(void) } } if ( 0 == dgl_PointSpriteActive ) { - if ( 1 == dgl_TexActive ) - dgl_FragColor = dgl_Color * texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 1 == dgl_TexActive ) { + vec4 texel = texture( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 0 == dgl_TextureColorMode ) + dgl_FragColor = dgl_Color * texel; + else { + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + dgl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); + } + } else dgl_FragColor = dgl_Color; } else diff --git a/src/eepp/graphics/renderer/shaders/basegl3cp.gles2.frag.h b/src/eepp/graphics/renderer/shaders/basegl3cp.gles2.frag.h index f1b6c5b10..76d9d6322 100644 --- a/src/eepp/graphics/renderer/shaders/basegl3cp.gles2.frag.h +++ b/src/eepp/graphics/renderer/shaders/basegl3cp.gles2.frag.h @@ -3,6 +3,8 @@ const GLchar * EEGL3CP_SHADER_BASE_FS = R"( #define MAX_CLIP_PLANES 6 uniform sampler2D textureUnit0; uniform int dgl_TexActive; +uniform int dgl_TextureColorMode; +uniform vec3 dgl_TextureColorChannel; uniform int dgl_PointSpriteActive; uniform int dgl_ClippingEnabled; uniform int dgl_ClipEnabled[ MAX_CLIP_PLANES ]; @@ -20,8 +22,15 @@ void main(void) } } if ( 0 == dgl_PointSpriteActive ) { - if ( 1 == dgl_TexActive ) - gl_FragColor = dgl_Color * texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 1 == dgl_TexActive ) { + vec4 texel = texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 0 == dgl_TextureColorMode ) + gl_FragColor = dgl_Color * texel; + else { + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); + } + } else gl_FragColor = dgl_Color; } else diff --git a/src/eepp/graphics/renderer/shaders/clipped.frag.h b/src/eepp/graphics/renderer/shaders/clipped.frag.h index a55f6bc9a..0df2dc74b 100644 --- a/src/eepp/graphics/renderer/shaders/clipped.frag.h +++ b/src/eepp/graphics/renderer/shaders/clipped.frag.h @@ -2,6 +2,8 @@ const GLchar * EEGLES2_SHADER_CLIPPED_FS = R"(#define MAX_CLIP_PLANES 6 uniform sampler2D textureUnit0; uniform int dgl_TexActive; +uniform int dgl_TextureColorMode; +uniform vec3 dgl_TextureColorChannel; #ifndef GL_ES uniform int dgl_ClippingEnabled; uniform int dgl_ClipEnabled[ MAX_CLIP_PLANES ]; @@ -26,10 +28,16 @@ void main(void) discard; } } - if ( 1 == dgl_TexActive ) - gl_FragColor = dgl_Color * texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 1 == dgl_TexActive ) { + vec4 texel = texture2D( textureUnit0, dgl_TexCoord[ 0 ].xy ); + if ( 0 == dgl_TextureColorMode ) + gl_FragColor = dgl_Color * texel; + else { + float coverage = dot( texel.rgb, dgl_TextureColorChannel ); + gl_FragColor = vec4( dgl_Color.rgb, dgl_Color.a * coverage ); + } + } else gl_FragColor = dgl_Color; } )"; - diff --git a/src/eepp/graphics/shaderprogram.cpp b/src/eepp/graphics/shaderprogram.cpp index 3396ccea3..5e3810834 100644 --- a/src/eepp/graphics/shaderprogram.cpp +++ b/src/eepp/graphics/shaderprogram.cpp @@ -231,8 +231,19 @@ void ShaderProgram::addShaders( const std::vector& shaders ) { addShader( shader ); } +bool ShaderProgram::bindFragDataLocationIndexed( Uint32 colorNumber, Uint32 index, + const char* name ) { + if ( !GLi->bindFragDataLocationIndexed( getHandler(), colorNumber, index, name ) ) + return false; + mFragmentOutputBindings.push_back( { colorNumber, index, name } ); + return true; +} + bool ShaderProgram::link() { #ifdef EE_SHADERS_SUPPORTED + for ( const auto& binding : mFragmentOutputBindings ) + GLi->bindFragDataLocationIndexed( getHandler(), binding.colorNumber, binding.index, + binding.name.c_str() ); GLi->linkProgram( getHandler() ); Int32 linked; diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index c0bfaab91..a50b5c98a 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -231,6 +231,7 @@ Text* Text::New( Font* font, unsigned int characterSize ) { static inline void drawGlyph( BatchRenderer* BR, GlyphDrawable* gd, const Vector2f& position, const Color& color, bool isItalic ) { + BR->setSubpixelText( gd->getGlyphRenderMode() == GlyphRenderMode::Subpixel ); BR->quadsSetColor( color ); BR->quadsSetTexCoord( gd->getSrcRect().Left, gd->getSrcRect().Top, gd->getSrcRect().Left + gd->getSrcRect().Right, @@ -253,6 +254,7 @@ static inline void _drawUnderline( Font* font, Float fontSize, const Color& font Float outlineThickness, const Vector2f& pos, Float width, const Color& shadowColor, const Vector2f& shadowOffset, const Color& outlineColor ) { + BR->setSubpixelText( false ); Float underlineOffset = font->getUnderlinePosition( fontSize ); Float underlineThickness = font->getUnderlineThickness( fontSize ); Float top = @@ -294,6 +296,7 @@ static inline void _drawStrikeThrough( Font* font, Float fontSize, const Color& Float outlineThickness, const Vector2f& pos, Float width, const Color& shadowColor, const Vector2f& shadowOffset, const Color& outlineColor ) { + BR->setSubpixelText( false ); Rectf xBounds = font->getGlyph( L'x', fontSize, style & Text::Bold, style & Text::Italic ).bounds; Float strikeThroughOffset = xBounds.Top + xBounds.Bottom * 0.5f; @@ -1683,6 +1686,36 @@ Float Text::getLineSpacing() const { : 0; } +static void drawTextVertexRanges( const std::vector& renderModes, + unsigned int numVertices, bool allowSubpixel ) { + const unsigned int verticesPerQuad = GLi->quadVertex(); + const unsigned int primitive = GLi->quadsSupported() ? GL_QUADS : GL_TRIANGLES; + if ( renderModes.empty() || renderModes.size() * verticesPerQuad != numVertices ) { + GLi->drawArrays( primitive, 0, numVertices ); + return; + } + + size_t rangeStart = 0; + while ( rangeStart < renderModes.size() ) { + size_t rangeEnd = rangeStart + 1; + while ( rangeEnd < renderModes.size() && renderModes[rangeEnd] == renderModes[rangeStart] ) + ++rangeEnd; + + const int first = rangeStart * verticesPerQuad; + const int count = ( rangeEnd - rangeStart ) * verticesPerQuad; + bool drawn = false; + if ( renderModes[rangeStart] == GlyphRenderMode::Subpixel ) { + if ( allowSubpixel ) + drawn = GLi->drawSubpixelArrays( primitive, first, count ); + if ( !drawn ) + drawn = GLi->drawSubpixelFallbackArrays( primitive, first, count ); + } + if ( !drawn ) + GLi->drawArrays( primitive, first, count ); + rangeStart = rangeEnd; + } +} + Uint32 Text::getTextHints() const { return mTextHints; } @@ -1699,6 +1732,9 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl return; unsigned int numvert = mVertices.size(); + const bool containsSubpixel = !mRenderModes.empty(); + const Float drawX = containsSubpixel && rotation == 0.f && scale == 1.f ? std::trunc( X ) : X; + const Float drawY = containsSubpixel && rotation == 0.f && scale == 1.f ? std::trunc( Y ) : Y; GlobalBatchRenderer::instance()->draw(); @@ -1729,7 +1765,7 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl GLi->rotatef( rotation, 0.0f, 0.0f, 1.0f ); GLi->translatef( -center.x + cX, -center.y + cY, 0.f ); } else { - GLi->translatef( X, Y, 0 ); + GLi->translatef( drawX, drawY, 0 ); } if ( backgroundColor != Color::Transparent ) { @@ -1759,7 +1795,7 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl if ( rotation != 0.0f || scale != 1.0f ) { GLi->popMatrix(); } else { - GLi->translatef( -X, -Y, 0 ); + GLi->translatef( -drawX, -drawY, 0 ); } return; } @@ -1772,43 +1808,36 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl return; texture->bind(); BlendMode::setMode( effect ); + const bool allowSubpixel = effect == BlendMode::Alpha() && rotation == 0.f && scale == 1.f; Uint32 alloc = numvert * sizeof( VertexCoords ); - Uint32 allocC = numvert * GLi->quadVertex(); if ( 0 != mFontStyleConfig.OutlineThickness ) { GLi->colorPointer( 4, GL_UNSIGNED_BYTE, 0, - reinterpret_cast( outlineColors.data() ), allocC ); + reinterpret_cast( outlineColors.data() ), + outlineColors.size() * sizeof( Color ) ); GLi->texCoordPointer( 2, GL_FP, sizeof( VertexCoords ), reinterpret_cast( &mOutlineVertices[0] ), alloc ); GLi->vertexPointer( 2, GL_FP, sizeof( VertexCoords ), reinterpret_cast( &mOutlineVertices[0] ) + sizeof( Float ) * 2, alloc ); - if ( GLi->quadsSupported() ) { - GLi->drawArrays( GL_QUADS, 0, numvert ); - } else { - GLi->drawArrays( GL_TRIANGLES, 0, numvert ); - } + drawTextVertexRanges( mOutlineRenderModes, numvert, allowSubpixel ); } GLi->colorPointer( 4, GL_UNSIGNED_BYTE, 0, reinterpret_cast( colors.data() ), - allocC ); + colors.size() * sizeof( Color ) ); GLi->texCoordPointer( 2, GL_FP, sizeof( VertexCoords ), reinterpret_cast( &mVertices[0] ), alloc ); GLi->vertexPointer( 2, GL_FP, sizeof( VertexCoords ), reinterpret_cast( &mVertices[0] ) + sizeof( Float ) * 2, alloc ); - if ( GLi->quadsSupported() ) { - GLi->drawArrays( GL_QUADS, 0, numvert ); - } else { - GLi->drawArrays( GL_TRIANGLES, 0, numvert ); - } + drawTextVertexRanges( mRenderModes, numvert, allowSubpixel ); if ( rotation != 0.0f || scale != 1.0f ) { GLi->popMatrix(); } else { - GLi->translatef( -X, -Y, 0 ); + GLi->translatef( -drawX, -drawY, 0 ); } } @@ -1860,7 +1889,9 @@ void Text::ensureGeometryUpdate() { // Clear the previous geometry mVertices.clear(); + mRenderModes.clear(); mOutlineVertices.clear(); + mOutlineRenderModes.clear(); if ( mCachedWidthNeedUpdate ) mLinesWidth.clear(); @@ -2650,6 +2681,9 @@ void Text::setFillColor( const std::vector& colors ) { // Add an underline or strikethrough line to the vertex array void Text::addLine( std::vector& vertices, Float lineLength, Float lineTop, Float offset, Float thickness, Float outlineThickness, Int32 centerDiffX ) { + auto& renderModes = &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes; + if ( !renderModes.empty() ) + renderModes.push_back( GlyphRenderMode::Mask ); Float top = std::floor( lineTop + offset - ( thickness / 2 ) + 0.5f ); Float bottom = top + std::floor( thickness + 0.5f ); Float u1 = 0; @@ -2725,10 +2759,23 @@ void Text::addLine( std::vector& vertices, Float lineLength, Float void Text::addGlyphQuad( std::vector& vertices, Vector2f position, const EE::Graphics::Glyph& glyph, Float italic, Float outlineThickness, Int32 centerDiffX ) { + auto& renderModes = &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes; + if ( glyph.renderMode == GlyphRenderMode::Subpixel ) { + if ( renderModes.empty() ) + renderModes.resize( vertices.size() / GLi->quadVertex(), GlyphRenderMode::Mask ); + renderModes.push_back( GlyphRenderMode::Subpixel ); + } else if ( !renderModes.empty() ) { + renderModes.push_back( GlyphRenderMode::Mask ); + } + if ( glyph.renderMode == GlyphRenderMode::Subpixel ) + position = position.trunc(); Float padding = 1.0; Float left = glyph.bounds.Left - padding; Float top = glyph.bounds.Top - padding; - Float right = glyph.bounds.Left + glyph.bounds.Right + padding; + Float right = glyph.bounds.Left + + ( glyph.renderMode == GlyphRenderMode::Subpixel ? glyph.size.getWidth() + : glyph.bounds.Right ) + + padding; Float bottom = glyph.bounds.Top + glyph.bounds.Bottom + padding; Float u1 = static_cast( glyph.textureRect.Left - padding ); diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp index b15b34e97..b1d224a2b 100644 --- a/src/eepp/ui/uiapplication.cpp +++ b/src/eepp/ui/uiapplication.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -54,7 +55,14 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin FileSystem::changeWorkingDirectory( path ); } + FontService& defaultFontService = defaultResourceScope().getFontService(); + defaultFontService.setHinting( appSettings.fontHinting ); + defaultFontService.setAntialiasing( appSettings.fontAntialiasing ); + mUISceneNode = UISceneNode::New(); + FontService& uiFontService = mUISceneNode->getResourceScope()->getFontService(); + uiFontService.setHinting( appSettings.fontHinting ); + uiFontService.setAntialiasing( appSettings.fontAntialiasing ); SceneManager::instance()->add( mUISceneNode ); if ( !appSettings.loadBaseResources ) diff --git a/src/eepp/ui/uiicon.cpp b/src/eepp/ui/uiicon.cpp index f11cd410a..40b863435 100644 --- a/src/eepp/ui/uiicon.cpp +++ b/src/eepp/ui/uiicon.cpp @@ -65,7 +65,9 @@ UIGlyphIcon::UIGlyphIcon( const std::string& name, FontTrueType* font, const Uin UIIcon( name ), mFont( font ), mCodePoint( codePoint ) { eeASSERT( mFont ); mCloseCb = mFont->pushFontEventCallback( [this]( Uint32, Font::Event event, Font* ) { - if ( event == Font::Event::Unload ) + if ( event == Font::Event::CacheClear ) + mSizes.clear(); + else if ( event == Font::Event::Unload ) mFont = nullptr; } ); } diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index b39dc158d..9dea0d7dc 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -317,6 +317,10 @@ void UISceneNode::initializeEmbeddedFromHost( UISceneNode* hostScene ) { mThreadPool = hostScene->getThreadPool(); mColorSchemePreference = hostScene->getColorSchemePreference(); mContrastPreference = hostScene->getContrastPreference(); + const FontService& hostFontService = hostScene->getResourceScope()->getFontService(); + FontService& fontService = mResourceScope->getFontService(); + fontService.setHinting( hostFontService.getHinting() ); + fontService.setAntialiasing( hostFontService.getAntialiasing() ); UIThemeManager* hostThemeManager = hostScene->getUIThemeManager(); if ( hostThemeManager ) { diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index a33d8319d..507f213d9 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -2,6 +2,7 @@ #include #include +#include EE_MAIN_FUNC int main( int argc, char** argv ) { std::shared_ptr threadPool( @@ -25,6 +26,23 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { args::ValueFlag pixelDensityConf( parser, "pixel-density", "Set default application pixel density", { 'd', "pixel-density" } ); + const std::unordered_map fontHintingMap{ + { "none", FontHinting::None }, + { "slight", FontHinting::Slight }, + { "full", FontHinting::Full }, + }; + args::MapFlag fontHinting( + parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)", + { "font-hinting" }, fontHintingMap, FontHinting::Full ); + const std::unordered_map fontAntialiasingMap{ + { "none", FontAntialiasing::None }, + { "grayscale", FontAntialiasing::Grayscale }, + { "subpixel", FontAntialiasing::Subpixel }, + }; + args::MapFlag fontAntialiasing( + parser, "font-antialiasing", + "Font antialiasing mode (accepted values: none, grayscale, subpixel)", + { "font-antialiasing" }, fontAntialiasingMap, FontAntialiasing::Grayscale ); try { parser.ParseCLI( Sys::parseArguments( argc, argv ) ); @@ -41,10 +59,14 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { return EXIT_FAILURE; } + UIApplication::Settings appSettings( {}, pixelDensityConf ? pixelDensityConf.Get() : 0.f ); + appSettings.fontHinting = fontHinting.Get(); + appSettings.fontAntialiasing = fontAntialiasing.Get(); + UIApplication app( WindowSettings{ 1280, 720, "eepp - UI HTML Example", WindowStyle::Default, WindowBackend::Default, 32, Sys::getProcessPath() + "assets/icon/ee.png" }, - UIApplication::Settings( {}, pixelDensityConf ? pixelDensityConf.Get() : 0.f ), + appSettings, ContextSettings( false, benchmarkMode.Get() ? 0 : ContextSettings::FrameRateLimitScreenRefreshRate, 4 ) ); diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index 8a5f66332..266df855a 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -209,6 +209,16 @@ class TerminalDisplay : public ITerminalDisplay { void setFont( Font* font ); + FontHinting getFontHinting() const; + + /** Updates the externally-owned font hinting policy and invalidates cached terminal glyphs. */ + void setFontHinting( FontHinting fontHinting ); + + FontAntialiasing getFontAntialiasing() const; + + /** Updates the externally-owned antialiasing policy and selects the matching draw path. */ + void setFontAntialiasing( FontAntialiasing fontAntialiasing ); + const Float& getFontSize() const; void setFontSize( const Float& FontSize ); @@ -317,6 +327,8 @@ class TerminalDisplay : public ITerminalDisplay { Uint32 mColumns{ 0 }; Uint32 mRows{ 0 }; Uint32 mClickStep{ 5 }; + FontHinting mFontHinting{ FontHinting::Full }; + FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale }; FrameBufferUniquePtr mFrameBuffer; VertexBufferUniquePtr mVBBackground; VertexBufferUniquePtr mVBForeground; diff --git a/src/modules/eterm/include/eterm/ui/uiterminal.hpp b/src/modules/eterm/include/eterm/ui/uiterminal.hpp index 8125bcbc7..18751cc0c 100644 --- a/src/modules/eterm/include/eterm/ui/uiterminal.hpp +++ b/src/modules/eterm/include/eterm/ui/uiterminal.hpp @@ -52,6 +52,9 @@ class UITerminal : public UIWidget { void setFont( Font* font ); + /** Synchronizes terminal rendering policy from the service that owns its font. */ + void syncFontRenderingConfig(); + void setKeyBindings( const KeyBindings& keyBindings ); void addKeyBindingString( const std::string& shortcut, const std::string& command ); diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 67831a668..8699188a9 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -1214,6 +1214,9 @@ void TerminalDisplay::drawbox( float x, float y, float w, float h, Color fg, Col } void TerminalDisplay::drawGrid( const Vector2f& pos ) { + const bool subpixelFont = mFontAntialiasing == FontAntialiasing::Subpixel; + VertexBuffer* foregroundVBO = subpixelFont ? nullptr : mVBForeground.get(); + if ( mFrameBuffer ) { mFrameBuffer->setPosition( mPosition.floor() + Vector2f( mPadding.Left, mPadding.Top ) ); mFrameBuffer->bind(); @@ -1313,7 +1316,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { if ( pos.y + lineHeight * j > pos.y + mSize.getHeight() ) break; - if ( ( mFrameBuffer || mVBForeground ) && !mDirtyLines[j] ) { + if ( ( mFrameBuffer || foregroundVBO ) && !mDirtyLines[j] ) { y += lineHeight; continue; } @@ -1358,8 +1361,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { auto advanceX = spaceCharAdvanceX * ( isWide ? 2.0f : 1.0f ); if ( glyph.mode & ATTR_WDUMMY ) { - if ( mVBForeground ) { - mVBForeground->setQuadColor( mCurGridPos, Color::Transparent ); + if ( foregroundVBO ) { + foregroundVBO->setQuadColor( mCurGridPos, Color::Transparent ); dirtyFG = true; } continue; @@ -1367,8 +1370,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { if ( glyph.u == 32 && !( glyph.mode & ( ATTR_UNDERLINE | ATTR_STRUCK ) ) ) { x += advanceX; - if ( mVBForeground ) { - mVBForeground->setQuadColor( mCurGridPos, Color::Transparent ); + if ( foregroundVBO ) { + foregroundVBO->setQuadColor( mCurGridPos, Color::Transparent ); dirtyFG = true; } continue; @@ -1377,8 +1380,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { if ( glyph.mode & ATTR_BOXDRAW ) { auto bd = TerminalEmulator::boxdrawindex( &glyph ); drawbox( x, y, advanceX, lineHeight, fg, bg, bd ); - if ( mVBForeground ) { - mVBForeground->setQuadColor( mCurGridPos, Color::Transparent ); + if ( foregroundVBO ) { + foregroundVBO->setQuadColor( mCurGridPos, Color::Transparent ); dirtyFG = true; } } else { @@ -1399,8 +1402,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { gd->setDrawMode( glyph.mode & ATTR_ITALIC ? GlyphDrawable::DrawMode::TextItalic : GlyphDrawable::DrawMode::Text ); - if ( mVBForeground ) { - gd->drawIntoVertexBuffer( mVBForeground.get(), mCurGridPos, { x, y } ); + if ( foregroundVBO ) { + gd->drawIntoVertexBuffer( foregroundVBO, mCurGridPos, { x, y } ); } else { gd->draw( { x, y } ); } @@ -1515,13 +1518,13 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) { } } - if ( mVBForeground ) { + if ( foregroundVBO ) { mFont->getTexture( mFontSize )->bind(); if ( dirtyFG ) - mVBForeground->update( VERTEX_FLAGS_DEFAULT, false ); - mVBForeground->bind(); - mVBForeground->draw(); - mVBForeground->unbind(); + foregroundVBO->update( VERTEX_FLAGS_DEFAULT, false ); + foregroundVBO->bind(); + foregroundVBO->draw(); + foregroundVBO->unbind(); } if ( !mVBStyles.empty() ) { @@ -1841,6 +1844,28 @@ void TerminalDisplay::setFont( Font* font ) { } } +FontHinting TerminalDisplay::getFontHinting() const { + return mFontHinting; +} + +void TerminalDisplay::setFontHinting( FontHinting fontHinting ) { + if ( mFontHinting != fontHinting ) { + mFontHinting = fontHinting; + invalidateLines(); + } +} + +FontAntialiasing TerminalDisplay::getFontAntialiasing() const { + return mFontAntialiasing; +} + +void TerminalDisplay::setFontAntialiasing( FontAntialiasing fontAntialiasing ) { + if ( mFontAntialiasing != fontAntialiasing ) { + mFontAntialiasing = fontAntialiasing; + invalidateLines(); + } +} + const Float& TerminalDisplay::getFontSize() const { return mFontSize; } diff --git a/src/modules/eterm/src/eterm/ui/uiterminal.cpp b/src/modules/eterm/src/eterm/ui/uiterminal.cpp index 1346001c0..dfd17e85f 100644 --- a/src/modules/eterm/src/eterm/ui/uiterminal.cpp +++ b/src/modules/eterm/src/eterm/ui/uiterminal.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -82,6 +83,7 @@ UITerminal::UITerminal( const std::shared_ptr& terminalDisplay mFlags |= UI_TAB_STOP | UI_SCROLLABLE; if ( !terminalDisplay ) return; + syncFontRenderingConfig(); registerNewTerminal(); mVScroll->setParent( this ); mVScroll->on( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } ); @@ -183,7 +185,9 @@ int UITerminal::getVisibleArea() const { void UITerminal::updateScrollPosition() { if ( mTerm && mTerm->getTerminal() ) { int historySize = mTerm->getTerminal()->getHistorySize(); - Float val = historySize > 0 ? ( 1.f - mTerm->getTerminal()->scrollPos() / (Float)historySize ) : 1.f; + Float val = historySize > 0 + ? ( 1.f - mTerm->getTerminal()->scrollPos() / (Float)historySize ) + : 1.f; mVScroll->setValue( val, false ); } } @@ -366,6 +370,19 @@ Font* UITerminal::getFont() const { void UITerminal::setFont( Font* font ) { mTerm->setFont( font ); + syncFontRenderingConfig(); +} + +void UITerminal::syncFontRenderingConfig() { + if ( !mTerm || !getUISceneNode() || !getUISceneNode()->getResourceScope() ) + return; + const FontService* fontService = nullptr; + if ( mTerm->getFont() && mTerm->getFont()->getType() == FontType::TTF ) + fontService = static_cast( mTerm->getFont() )->getFontService(); + if ( !fontService ) + fontService = &getUISceneNode()->getResourceScope()->getFontService(); + mTerm->setFontHinting( fontService->getHinting() ); + mTerm->setFontAntialiasing( fontService->getAntialiasing() ); } void UITerminal::setKeyBindings( const KeyBindings& keyBindings ) { @@ -615,6 +632,7 @@ void UITerminal::restart() { mTerm->getProgram(), mTerm->getArgs(), mTerm->getWorkingDir(), mTerm->getHistorySize(), nullptr, mTerm->useFrameBuffer(), mTerm->getKeepAlive(), mTerm->getEnv() ); + syncFontRenderingConfig(); } }} // namespace eterm::UI diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index b5a4053c8..506ac2505 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +76,180 @@ UTEST( FontRendering, glyphAdvanceDoesNotCreateTexturePages ) { EXPECT_EQ( textureCount, textureFactory->getTextureCount() ); } +UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { + UIApplication app( + WindowSettings( 360, 220, "eepp - Subpixel Text Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( "SubpixelText-Regular", scope ); + ASSERT_TRUE( + font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/NotoSans-Regular.ttf" ) ); + const Float grayscaleLAdvance = font->getGlyphAdvance( 'l', 28 ); + const Float grayscaleDAdvance = font->getGlyphAdvance( 'd', 28 ); + const Float grayscaleKerning = font->getKerning( 'i', 'd', 28, false, false ); + UIIconPtr icon = UIGlyphIcon::New( "glyph-cache-test", font.get(), 'S' ); + const DrawablePtr grayscaleIcon = icon->getSource( 28 ); + ASSERT_TRUE( grayscaleIcon ); + ASSERT_EQ( GlyphRenderMode::Mask, + static_cast( grayscaleIcon.get() )->getGlyphRenderMode() ); + font->setAntialiasing( FontAntialiasing::Subpixel ); + EXPECT_EQ( grayscaleLAdvance, font->getGlyphAdvance( 'l', 28 ) ); + EXPECT_EQ( grayscaleDAdvance, font->getGlyphAdvance( 'd', 28 ) ); + EXPECT_EQ( grayscaleKerning, font->getKerning( 'i', 'd', 28, false, false ) ); + const DrawablePtr subpixelIcon = icon->getSource( 28 ); + ASSERT_TRUE( subpixelIcon ); + EXPECT_NE( grayscaleIcon.get(), subpixelIcon.get() ); + EXPECT_EQ( GlyphRenderMode::Subpixel, + static_cast( subpixelIcon.get() )->getGlyphRenderMode() ); + + GlyphDrawable* drawable = font->getGlyphDrawable( 'S', 28 ); + ASSERT_TRUE( drawable ); + ASSERT_EQ( GlyphRenderMode::Subpixel, drawable->getGlyphRenderMode() ); + + EE::Window::Window* window = app.getWindow(); + window->setClearColor( Color::White ); + window->clear(); + Text::draw( String( "Subpixel static" ), { 8.f, 4.f }, font.get(), 28, Color::Black ); + + Text retained( "Subpixel retained", font.get(), 28 ); + retained.setFillColor( Color::Black ); + retained.draw( 8.f, 52.f ); + + Primitives primitives; + primitives.setColor( Color( 40, 42, 54 ) ); + primitives.drawRectangle( Rectf( Vector2f( 0.f, 110.f ), Sizef( 360.f, 110.f ) ) ); + const Color lightText( 248, 248, 242 ); + Text::draw( String( "Subpixel static light" ), { 8.f, 114.f }, font.get(), 28, lightText ); + retained.setString( "Subpixel retained light" ); + retained.setFillColor( lightText ); + retained.draw( 8.f, 162.f ); + + Image image = window->getFrontBufferImage(); + auto hasColoredCoverage = [&image]( Uint32 top, Uint32 bottom ) { + for ( Uint32 y = top; y < bottom; ++y ) { + for ( Uint32 x = 0; x < image.getWidth(); ++x ) { + Color pixel = image.getPixel( x, y ); + const Int32 redGreenDelta = static_cast( pixel.r ) - pixel.g; + const Int32 greenBlueDelta = static_cast( pixel.g ) - pixel.b; + if ( ( redGreenDelta < 0 ? -redGreenDelta : redGreenDelta ) > 3 || + ( greenBlueDelta < 0 ? -greenBlueDelta : greenBlueDelta ) > 3 ) + return true; + } + } + return false; + }; + + EXPECT_TRUE_MSG( hasColoredCoverage( 0, image.getHeight() / 2 ), + "Static text lost independent LCD channel coverage" ); + EXPECT_TRUE_MSG( hasColoredCoverage( image.getHeight() / 2, image.getHeight() ), + "Retained text lost independent LCD channel coverage" ); + compareImages( utest_state, utest_result, window, "eepp-subpixel-text" ); + + FrameBufferUniquePtr frameBuffer = FrameBuffer::New( 240, 48, false, false, false, 4, window ); + ASSERT_TRUE( frameBuffer && frameBuffer->created() ); + frameBuffer->setClearColor( ColorAf( 0.f, 0.f, 0.f, 0.f ) ); + frameBuffer->bind(); + frameBuffer->clear(); + Text::draw( String( "Transparent subpixel" ), { 4.f, 4.f }, font.get(), 28, Color::White ); + GlobalBatchRenderer::instance()->draw(); + std::vector pixels( frameBuffer->getWidth() * frameBuffer->getHeight() * 4 ); + GLi->readPixels( 0, 0, frameBuffer->getWidth(), frameBuffer->getHeight(), pixels.data() ); + frameBuffer->unbind(); + bool hasCoverageAlpha = false; + for ( size_t i = 3; i < pixels.size(); i += 4 ) { + if ( pixels[i] != 0 ) { + hasCoverageAlpha = true; + break; + } + } + EXPECT_TRUE_MSG( hasCoverageAlpha, + "Subpixel text did not update a transparent target's alpha" ); +} + +#if EE_PLATFORM == EE_PLATFORM_LINUX +UTEST( FontRendering, subpixelCoverageAllRenderers ) { + struct RendererCase { + GraphicsLibraryVersion version; + const char* name; + }; + static constexpr RendererCase renderers[] = { { GLv_2, "OpenGL 2" }, + { GLv_3, "OpenGL 3" }, + { GLv_3CP, "OpenGL 3 Core" }, + { GLv_ES2, "OpenGL ES 2" } }; + + for ( const RendererCase& renderer : renderers ) { + UIApplication app( + WindowSettings( 320, 96, renderer.name, WindowStyle::Default, WindowBackend::Default, + 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ), + ContextSettings( false, 0, 0, renderer.version ) ); + ASSERT_TRUE_MSG( app.getWindow() && app.getWindow()->isOpen(), renderer.name ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( renderer.name, scope ); + ASSERT_TRUE_MSG( + font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/NotoSans-Regular.ttf" ), + renderer.name ); + font->setAntialiasing( FontAntialiasing::Subpixel ); + + EE::Window::Window* window = app.getWindow(); + window->setClearColor( Color::White ); + window->clear(); + Text::draw( String( "Direct LCD" ), { 8.f, 4.f }, font.get(), 24, Color::Black ); + Text retained( "Retained LCD", font.get(), 24 ); + retained.setFillColor( Color::Black ); + retained.draw( 8.f, 44.f ); + + Image image = window->getFrontBufferImage(); + auto hasColoredCoverage = [&image]( Uint32 top, Uint32 bottom ) { + for ( Uint32 y = top; y < bottom; ++y ) { + for ( Uint32 x = 0; x < image.getWidth(); ++x ) { + const Color pixel = image.getPixel( x, y ); + const Int32 redGreenDelta = static_cast( pixel.r ) - pixel.g; + const Int32 greenBlueDelta = static_cast( pixel.g ) - pixel.b; + if ( ( redGreenDelta < 0 ? -redGreenDelta : redGreenDelta ) > 3 || + ( greenBlueDelta < 0 ? -greenBlueDelta : greenBlueDelta ) > 3 ) + return true; + } + } + return false; + }; + EXPECT_TRUE_MSG( hasColoredCoverage( 0, 40 ), renderer.name ); + EXPECT_TRUE_MSG( hasColoredCoverage( 40, image.getHeight() ), renderer.name ); + } +} +#endif + +UTEST( FontRendering, scaledSubpixelGlyphAtlas ) { + UIApplication app( + WindowSettings( 256, 64, "eepp - Scaled Subpixel Glyph Atlas", VisualTestWindowStyle, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + ResourceScope& scope = *app.getUI()->getResourceScope(); + FontTrueTypePtr font = FontTrueType::New( "ScaledSubpixelNonicons", scope ); + ASSERT_TRUE( font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/nonicons.ttf" ) ); + font->setAntialiasing( FontAntialiasing::Subpixel ); + font->setIsEmojiFont( true ); + + EE::Window::Window* window = app.getWindow(); + window->setClearColor( Color( 40, 44, 52 ) ); + window->clear(); + const std::array codePoints = { 61718, 61719, 61720, 61743, + 61752, 61775, 61789, 61799 }; + Float x = 8.f; + for ( Uint32 codePoint : codePoints ) { + GlyphDrawable* glyph = font->getGlyphDrawable( codePoint, 18 ); + ASSERT_TRUE( glyph ); + ASSERT_EQ( GlyphRenderMode::Subpixel, glyph->getGlyphRenderMode() ); + const Sizef size = glyph->getPixelsSize(); + glyph->setColor( Color::White ); + glyph->draw( { std::trunc( x + ( 24.f - size.getWidth() ) * 0.5f ), + std::trunc( ( 64.f - size.getHeight() ) * 0.5f ) } ); + x += 30.f; + } + compareImages( utest_state, utest_result, window, "eepp-scaled-subpixel-glyph-atlas" ); +} + UTEST( FontRendering, loadingFontFamilyDoesNotCreateTexturePages ) { UIApplication app( WindowSettings( 320, 240, "eepp - Font Family Metrics Test", WindowStyle::Default, diff --git a/src/tests/unit_tests/uiwebview_tests.cpp b/src/tests/unit_tests/uiwebview_tests.cpp index 77cf21aed..1f972fa8d 100644 --- a/src/tests/unit_tests/uiwebview_tests.cpp +++ b/src/tests/unit_tests/uiwebview_tests.cpp @@ -58,6 +58,30 @@ static bool readHttpRequestHeaders( TcpSocket& client, std::string* headers = nu return true; } +UTEST( UIWebView, DocumentSceneInheritsHostFontRenderingPolicy ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 320, 240, "UIWebView Font Policy Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + ASSERT_TRUE( win != nullptr ); + + UISceneNode* sceneNode = UISceneNode::New(); + FontService& hostFontService = sceneNode->getResourceScope()->getFontService(); + hostFontService.setHinting( FontHinting::Slight ); + hostFontService.setAntialiasing( FontAntialiasing::Subpixel ); + SceneManager::instance()->add( sceneNode ); + + UIWebView* webView = UIWebView::New(); + webView->setParent( sceneNode->getRoot() ); + UISceneNode* documentScene = webView->getDocumentSceneNode(); + ASSERT_TRUE( documentScene != nullptr ); + const FontService& documentFontService = documentScene->getResourceScope()->getFontService(); + EXPECT_EQ( FontHinting::Slight, documentFontService.getHinting() ); + EXPECT_EQ( FontAntialiasing::Subpixel, documentFontService.getAntialiasing() ); + + Engine::destroySingleton(); +} + UTEST( UIWebView, OwnedDocumentSceneScrollTarget ) { auto win = Engine::instance()->createWindow( WindowSettings( 640, 480, "UIWebView Document Scene Test", WindowStyle::Default, diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index c28160671..67c95f606 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -843,6 +843,8 @@ bool App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool s mConfig.load( mConfigPath, mKeybindingsPath, mInitColorScheme, mRecentFiles, mRecentFolders, mResPath, mPluginManager.get(), displaySize.asInt(), sync ); + defaultResourceScope().getFontService().setHinting( mConfig.ui.fontHinting ); + defaultResourceScope().getFontService().setAntialiasing( mConfig.ui.fontAntialiasing ); return firstRun; } diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 5c92f9121..3af9789cc 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -3164,6 +3164,7 @@ UIMenu* SettingsMenu::createFontHintMenu() { auto hint = id.substr( 5 ).toUtf8(); mApp->getConfig().ui.fontHinting = FontTrueType::fontHintingFromString( hint ); defaultResourceScope().getFontService().setHinting( mApp->getConfig().ui.fontHinting ); + forEachTerminal( []( UITerminal* term ) { term->syncFontRenderingConfig(); } ); } } ); @@ -3189,7 +3190,7 @@ UIMenu* SettingsMenu::createFontAntiAliasingMenu() { mFontAntiAliasingMenu->addRadioButton( i18n( "none", "None" ) )->setId( "aa_none" ); mFontAntiAliasingMenu->addRadioButton( i18n( "grayscale", "Grayscale" ) ) ->setId( "aa_grayscale" ); - mFontAntiAliasingMenu->addRadioButton( i18n( "subpixel", "SubPixel (not working)" ) ) + mFontAntiAliasingMenu->addRadioButton( i18n( "subpixel", "SubPixel" ) ) ->setId( "aa_subpixel" ); } @@ -3206,6 +3207,7 @@ UIMenu* SettingsMenu::createFontAntiAliasingMenu() { FontTrueType::fontAntialiasingFromString( hint ); defaultResourceScope().getFontService().setAntialiasing( mApp->getConfig().ui.fontAntialiasing ); + forEachTerminal( []( UITerminal* term ) { term->syncFontRenderingConfig(); } ); } } ); diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 6b4f6fb11..e1450be0a 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -2,6 +2,7 @@ #include #include #include +#include EE::Window::Window* win = NULL; std::shared_ptr terminal = nullptr; @@ -176,6 +177,23 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { args::ValueFlag fallbackFontPathF( parser, "fallback-fontpath", "Fallback Font path", { "fallback-font" } ); args::ValueFlag fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 ); + const std::unordered_map fontHintingMap{ + { "none", FontHinting::None }, + { "slight", FontHinting::Slight }, + { "full", FontHinting::Full }, + }; + args::MapFlag fontHinting( + parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)", + { "font-hinting" }, fontHintingMap, FontHinting::Full ); + const std::unordered_map fontAntialiasingMap{ + { "none", FontAntialiasing::None }, + { "grayscale", FontAntialiasing::Grayscale }, + { "subpixel", FontAntialiasing::Subpixel }, + }; + args::MapFlag fontAntialiasing( + parser, "font-antialiasing", + "Font antialiasing mode (accepted values: none, grayscale, subpixel)", + { "font-antialiasing" }, fontAntialiasingMap, FontAntialiasing::Grayscale ); args::ValueFlag width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 ); args::ValueFlag height( parser, "winheight", "Window height (in dp)", { "height" }, 720 ); @@ -258,6 +276,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { displayManager->enableScreenSaver(); displayManager->enableMouseFocusClickThrough(); displayManager->disableBypassCompositor(); + defaultResourceScope().getFontService().setHinting( fontHinting.Get() ); + defaultResourceScope().getFontService().setAntialiasing( fontAntialiasing.Get() ); Sizei winSize( width.Get(), height.Get() ); win = Engine::instance()->createWindow( @@ -326,6 +346,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { MemoryManager::showResults(); return EXIT_FAILURE; } + terminal->setFontHinting( fontHinting.Get() ); + terminal->setFontAntialiasing( fontAntialiasing.Get() ); terminal->getTerminal()->setAllowMemoryTrimnming( true ); terminal->setCursorMode( cursorStyle.Get() );