From 27a57960f4106176425a9e8c344aa143a16b1263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Jul 2026 13:32:53 -0300 Subject: [PATCH 01/10] SubPixel hinting support. --- .agent/plans/subpixel_text_rendering_plan.md | 388 ++++++++++++++++++ bin/assets/ui/breeze.css | 1 - include/eepp/graphics/batchrenderer.hpp | 4 + include/eepp/graphics/font.hpp | 3 +- include/eepp/graphics/glyphdrawable.hpp | 7 + include/eepp/graphics/renderer/renderer.hpp | 14 + include/eepp/graphics/renderer/renderergl.hpp | 11 + .../graphics/renderer/rendererglshader.hpp | 7 + include/eepp/graphics/text.hpp | 12 +- src/eepp/graphics/batchrenderer.cpp | 31 +- src/eepp/graphics/fonttruetype.cpp | 28 +- src/eepp/graphics/glyphdrawable.cpp | 10 + src/eepp/graphics/renderer/renderer.cpp | 51 +++ src/eepp/graphics/renderer/renderergl.cpp | 78 ++++ src/eepp/graphics/renderer/renderergl3.cpp | 7 + src/eepp/graphics/renderer/renderergl3cp.cpp | 7 + src/eepp/graphics/renderer/renderergles2.cpp | 7 + .../graphics/renderer/rendererglshader.cpp | 34 ++ .../graphics/renderer/shaders/base.frag.h | 10 +- .../graphics/renderer/shaders/basegl3.frag.h | 13 +- .../renderer/shaders/basegl3cp.frag.h | 13 +- .../renderer/shaders/basegl3cp.gles2.frag.h | 13 +- .../graphics/renderer/shaders/clipped.frag.h | 14 +- src/eepp/graphics/text.cpp | 74 +++- .../eterm/terminal/terminaldisplay.hpp | 12 + .../eterm/include/eterm/ui/uiterminal.hpp | 3 + .../src/eterm/terminal/terminaldisplay.cpp | 53 ++- src/modules/eterm/src/eterm/ui/uiterminal.cpp | 20 +- src/tests/unit_tests/fontrendering_tests.cpp | 70 ++++ src/tools/ecode/ecode.cpp | 2 + src/tools/ecode/settingsmenu.cpp | 4 +- 31 files changed, 941 insertions(+), 60 deletions(-) create mode 100644 .agent/plans/subpixel_text_rendering_plan.md 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/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..772f2d909 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 + Font* font{ nullptr }; ///< The glyph font + GlyphRenderMode renderMode{ GlyphRenderMode::Mask }; ///< Atlas texel compositing mode }; enum class FontType { TTF, BMF, Sprite }; 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/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index 830f768ee..fdda462b9 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 ); @@ -197,6 +203,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 +246,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 +362,8 @@ class EE_API Renderer { void waitForIdle(); protected: + static const Vector3ff& textureColorChannel( Int32 mode ); + static Renderer* sSingleton; enum RendererStateFlags { @@ -366,6 +379,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..f3d2a9da0 100644 --- a/include/eepp/graphics/renderer/renderergl.hpp +++ b/include/eepp/graphics/renderer/renderergl.hpp @@ -83,7 +83,18 @@ 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(); + + ShaderProgramPtr mSubpixelShader; + ShaderProgram* mPreviousShader{ nullptr }; + Int32 mSubpixelChannelLoc{ -1 }; + bool mUsingSubpixelShader{ false }; + bool mSubpixelShaderInitializationAttempted{ false }; }; #endif diff --git a/include/eepp/graphics/renderer/rendererglshader.hpp b/include/eepp/graphics/renderer/rendererglshader.hpp index b07aadb94..9b6f0622b 100644 --- a/include/eepp/graphics/renderer/rendererglshader.hpp +++ b/include/eepp/graphics/renderer/rendererglshader.hpp @@ -54,11 +54,18 @@ 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: 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; + ShaderProgram* mTextureColorPreviousShader; + bool mUsingTextureColorFallbackShader; unsigned int mCurrentMode; ShaderProgram* mCurShader; ShaderProgram* mShaderPrev; diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index 9b392cb8a..85723c0c9 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -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/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..f813a1235 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; @@ -1606,9 +1625,12 @@ Glyph FontTrueType::loadGlyphByIndex( Uint32 index, unsigned int characterSize, 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 + 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 ); } 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/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index 68b3fec29..fc66f95e2 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -476,6 +476,36 @@ 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 ( !setTextureColorMode( 1 ) ) + return false; + + Uint8 previousColorMask[4]; + getColorMask( previousColorMask ); + for ( Int32 channel = 0; channel < 3; ++channel ) { + if ( channel != 0 ) + 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::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 ); @@ -556,6 +586,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 +786,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..76b0b544f 100644 --- a/src/eepp/graphics/renderer/renderergl.cpp +++ b/src/eepp/graphics/renderer/renderergl.cpp @@ -61,6 +61,84 @@ 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 +} + +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..2d3235a5a 100644 --- a/src/eepp/graphics/renderer/renderergl3.cpp +++ b/src/eepp/graphics/renderer/renderergl3.cpp @@ -148,6 +148,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 +170,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..e22ef6687 100644 --- a/src/eepp/graphics/renderer/renderergl3cp.cpp +++ b/src/eepp/graphics/renderer/renderergl3cp.cpp @@ -175,6 +175,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 +197,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..6a96f9621 100644 --- a/src/eepp/graphics/renderer/renderergles2.cpp +++ b/src/eepp/graphics/renderer/renderergles2.cpp @@ -198,6 +198,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 +224,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..2ffe8ee9c 100644 --- a/src/eepp/graphics/renderer/rendererglshader.cpp +++ b/src/eepp/graphics/renderer/rendererglshader.cpp @@ -8,6 +8,11 @@ RendererGLShader::RendererGLShader() : mProjectionMatrix_id( 0 ), mModelViewMatrix_id( 0 ), mTextureMatrix_id( 0 ), + mTextureColorMode_id( -1 ), + mTextureColorChannel_id( -1 ), + mTextureColorMode( 0 ), + mTextureColorPreviousShader( nullptr ), + mUsingTextureColorFallbackShader( false ), mCurrentMode( 0 ), mCurShader( NULL ), mShaderPrev( NULL ) { @@ -21,6 +26,35 @@ RendererGLShader::~RendererGLShader() { eeSAFE_DELETE( mStack ); } +bool RendererGLShader::setTextureColorMode( Int32 mode ) { + if ( mode < 0 || mode > 4 ) + return false; + if ( mode != 0 && ( mTextureColorMode_id == -1 || mTextureColorChannel_id == -1 ) ) { + mTextureColorPreviousShader = mCurShader; + setShader( static_cast( nullptr ) ); + if ( mTextureColorMode_id == -1 || mTextureColorChannel_id == -1 ) { + setShader( mTextureColorPreviousShader ); + mTextureColorPreviousShader = nullptr; + return false; + } + mUsingTextureColorFallbackShader = true; + } + if ( mTextureColorMode != mode ) { + mTextureColorMode = mode; + mCurShader->setUniform( mTextureColorMode_id, mode ); + if ( mTextureColorChannel_id != -1 ) { + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mode ) ); + } + } + if ( mode == 0 && mUsingTextureColorFallbackShader ) { + ShaderProgram* previousShader = mTextureColorPreviousShader; + mTextureColorPreviousShader = nullptr; + mUsingTextureColorFallbackShader = false; + setShader( previousShader ); + } + 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/text.cpp b/src/eepp/graphics/text.cpp index c0bfaab91..8c2e2cb82 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,6 +1808,7 @@ 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(); @@ -1785,11 +1822,7 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl 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() ), @@ -1799,16 +1832,12 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl 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,22 @@ 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 && renderModes.empty() ) + renderModes.resize( vertices.size() / GLi->quadVertex(), GlyphRenderMode::Mask ); + if ( !renderModes.empty() ) + renderModes.push_back( glyph.renderMode == GlyphRenderMode::Subpixel + ? GlyphRenderMode::Subpixel + : 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/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..e5fd66d04 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 @@ -74,6 +75,75 @@ UTEST( FontRendering, glyphAdvanceDoesNotCreateTexturePages ) { EXPECT_EQ( textureCount, textureFactory->getTextureCount() ); } +UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { + UIApplication app( + WindowSettings( 360, 120, "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 ); + 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 ) ); + + 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, 60.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 ); + if ( eeabs( static_cast( pixel.r ) - static_cast( pixel.g ) ) > 3 || + eeabs( static_cast( pixel.g ) - static_cast( pixel.b ) ) > 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" ); + + 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" ); +} + UTEST( FontRendering, loadingFontFamilyDoesNotCreateTexturePages ) { UIApplication app( WindowSettings( 320, 240, "eepp - Font Family Metrics 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(); } ); } } ); From c0debbee9ff231fff1836a3a19087b0cd02c8edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Jul 2026 20:26:24 -0300 Subject: [PATCH 02/10] Rollback to fix rendering. --- .../fontrendering/eepp-subpixel-text.webp | Bin 0 -> 8384 bytes include/eepp/graphics/renderer/renderer.hpp | 2 -- include/eepp/graphics/renderer/renderergl.hpp | 2 ++ .../graphics/renderer/rendererglshader.hpp | 2 -- src/eepp/graphics/renderer/renderer.cpp | 12 +------- src/eepp/graphics/renderer/renderergl.cpp | 13 +++++++-- src/eepp/graphics/renderer/renderergl3.cpp | 7 ++++- src/eepp/graphics/renderer/renderergl3cp.cpp | 7 ++++- src/eepp/graphics/renderer/renderergles2.cpp | 7 ++++- .../graphics/renderer/rendererglshader.cpp | 27 +++++------------- src/eepp/graphics/text.cpp | 17 ++++------- src/tests/unit_tests/fontrendering_tests.cpp | 14 +++++++-- 12 files changed, 56 insertions(+), 54 deletions(-) create mode 100644 bin/assets/fontrendering/eepp-subpixel-text.webp diff --git a/bin/assets/fontrendering/eepp-subpixel-text.webp b/bin/assets/fontrendering/eepp-subpixel-text.webp new file mode 100644 index 0000000000000000000000000000000000000000..85bbb729ad99ab7801ba86701fd4eb924e32419b GIT binary patch literal 8384 zcmYj#WmFtIu=e6E3luNz?(SOLb&=xk#fuhM+}(=1yZhp$ErsIlP!@N&?R)Qc&X*ra z&Y4LjC-Y=x@@Oi^$#qZw0D7`g>bmLz%GH18>^6gZ06OL0uhm{8b~%4IxIND1-xV)y zjk{S|cQZw3GHD37WoBg&y>qqP=jb9GqiL>9wa|178C@P~cAphHI4x$*JF&Okr<|*? zxtIK_>1*=#t>L1;B&4%^W{o3FG;S7(r?9`rJy=t?|}N@4H~VZ1#e7mktHTxbpzE>01>Iz%`XHYp;-14mseAnYN z<$D-wyY8C&ss0SH#FsY1fyMpBhjiF!w7|xV2tw&|drqX_fo}<1s1?S1jI(TABrRSm z=)=E)2N;yZ^WICX(^_I&DDdb(Un$4=ZS5-80UQq9qf_qj{*FGlmD-Bd*{V(}flX|~ zNFNblYCF7t@u0~OCAW7UnCf;lDR1-#2S0|cwbkO{@ohZ?=h;)Wt}b0yPxX^XR!>Y* zN5D+4n;lKI<(EV2`(OCCS||+WX2-Mf&~ZNR%2l3m?JGcd}|#%-Wjx!>%H1Q z6pO^WUzd>^0Qv<1Re$XBf(NEmdS!>ryMA87r6|voGtaW{5CsqX;^XcP%0bb@3m%A7 zp;3#F^DaGlvWUf?A0^M<5f6S6@N^$@sfJHIJV>up+_vf22oIM10qBk<*`y+Km`MWv@zglgi`tO1w zSdN{a0}*@Virq4vdR$7gLwp>C>96LkZ4wd!xj(iPl&Ra5&K1K96nDS;=uS?#(&-Yp zFeAg5W;C{(%gHK-VSyj4oJf`Yc)ev3r<|B{V^%5(zz3K`M_0+JlyX5W>^YbPRDJj@ zF*)*RNe76fahe<|?6To5@U~zaAybnJ8go2o-{w#0q+xEGYcq%|%X}U+hX}r^W*oBb zidtLd=#+_QR@x@L2V4!l-tBv6tg&%nQ&{Rt>0s3kSc?{plFV^Z0eRzN(vSd3VlyzRpo_jW3jh#em(v}jq^ITl4%9PU zSK>IJq$0%yk36vDC@ z93zG_mL}W*v>|S^O1n+!JQ}B;Y9%>5ea5^i%UaOxzolKf|)^L-dv)ARiO z{n0Im!Ga;JOEquQc;Y42RjfIW-&nhXu8c!RPp$D!&;fuR90u!b1;9-gWTV*bP@2l> zU?@mQ7#3UK=cr7~Gv)A0H0WBTU?{quW!4*wyx$~V76cX|vBwQ;nK2Vp?(pW3ZLJl~rjn*sQk#JVG{T!lMN$N9MKjH@S%UI%T6nmdQR>I&Gjn$u$E?@fXyJt07(-e4oEID&p0Hs_XiLX>g&wjRj zGeU&Nzgb(hnJhzy$xL8^9Mq=pU?H;*H7MCt`3!6QS|}05CTx5QTdwi*b-g!YKwUv3 zJ4tc*y5Bfg2ydIvD_&8MrE>W-!Qv}CAtb1%j7WZ)iVVQ|Si}X`p_9V1Y-xd(*yDIn+6pDdx+lthfEsQp!R~Zo&ZtK|72{qR z_CFX0CeHr+j8vuEmHTSCa}u$Im7vKN`xOG17D7_1exv@4T5erf?L*R^PreRa&K!+} z#r_b{*-~mrj1WeWE^$(vr8Eo#W(4tb2qvOi=Af63PE5k*8s2hD6R41BLcAO5J>+u{;<8js~g)>tK1RSvOF`6c_6 z6kaY|zrWO1TNTGf)qAZD0-@lrQ!Btyf+B>g9b)6)HhANagb&>3jS4Oq+H>*k$on`E z7LK=-mvDg8R7%Acc|C;fP~hppR2?(;Cmo04wlm;D1lRBDGOK^kDTfEX7 zK1wr0=&fwpjwae=BZzuZhG9i>=M0velp2S`X|)#|=CTC+#HuUF|3K^Q=wUbtx3e^I zs|926vg@w(^Va)IdS09787Hm+2*7}dTl0lyAivw@IYCqQB{hJ%Ex$ka`%R{C*3sTCo;|Q= z^)RfS6AL%sE1q(l$hK=!ay9}rIyv#5gBc!sLXk!#@!>DJWmekXZLe0gO)pg`HlFfca=8kvuuxC(i=3p})inefc@gho;! zgrrGhz9fBE>OFT4Poynj4t~d0{#Kfe(g~7iNe>-p3h{p{(HN8b;r~)HRsIKyL%JHF zdht{Ys`y)expG!x>lR54dXdh?u>S5NL^tL{AmX}@J;voM_)tW`I+u+N; z`VMGtUztsgu&3K~f%zfvaU@ENa}O(CqlfE&qrXd>+jrkq)iL^NStvA4^5Z01-&XSs zqOi^K(;$L<`~t<=M~w#O00#EH4_U!$N#XtL#>|EJ_}Fo!3^3=jIsW1#7skoXgkZh} zQqm)xR3vrc`&&IucN(?g>`Q`(VI@9df7WNEF=_}Q$#60on;-}Sa9J)ezmCPW`Qy^Z z==m=?Nl`pof^B``0^Zy2{W*AdJG+DlAJKZ7EcRahGE|kiuCIV=s*+To>S5}X zD^c*@a`**wp%NaI6#Bw=d~A%lFEccPRCiLk5P%KMlHub|>&7&PU_M+JFB%oWn z``LurqpFwOsAto}8irS;F3f0A@5+Gzt*C};5?ty%^6q2YtHIk~(sNfk5{c2&z5LQh zugcTJ$KYp1)z-T&N^|)Wx93JEz90!6&VzwTisgwtu5iOVbK3V-aSnm!Szf9=chXs* z>}IMh!42Gc!$~6XBeJ3r4@E)T>FcQ(=4swg^l9(lJ>lfM3x-j_$|z z3wzd|#HYDt|HL2uSL`08?ZiBzKp*TGL2u(w!7`ECJ>muU=gPtuFR68W>;Ru+t*Udu zI>fi{$=&?BAnyn>j=sKDrDfxdJqyWw<-gedp*X$)TZE?O&fRp;JY~qs2OqK&sg}_8 z9$~y-`~OqWb5F?z-J)Q9y35fj!5xTY&G_XZv;|R8#K;2^6KoWqn%zMd-p}=O;flMBPc%KOVJol$}gP^cEWz9Nw6I z0GqIS=W8p4Hcz3cEn{!bru@{jQ9L)Fan71PBKf>{wu?McjB1SP5t*Br#};jg2$|$* zOOyLyE;C9PLp5tRqTwRs=m_S==kF`sIp@!x4&10M9F&h^#g|c|N?SVeFa7{Yu*mj} z59h2F4k+0Je?f;j`;IJaEF!qun{Bc87LcUScHn7w^Mib)i>G&!`5tKH~11`+J* zQ>`ftCs=U##>W|4i-QTHTww7zn4YpkriOgv)tqFU`JXBG<^FY&eU3KIdLs>rrZKz| zPtW%z#m?0Irq8iPM+D;3YKq9xr`ja8jkL=#h3Ub25TZW}{+tX|S@uBsL*^Ao2NI$RVH(Fr=`S4hdP3x4V2K0MX7?(`$*J?p_m7c4a zFYn4+>)ov6o>*RSGUCS3qbmcaqOcLB+{OG|0@syH$jV>i@4QbYdy1vh(sCx+v{Xvc zS0;T}X%4#AXRFi}EIF4nPZKPf<--m0Q%hpP9<}+mO?U0_oVnL&X!KHC0xj zm6K?^EXhWTNLTu5UNiMn7rb|$s=0CakVn567a1ElMn#7rwqYHbj%X={8j*tYgq#0( zD2L!)XpPRtVh_b-<#trX5+|wfBZ?zAA6y{gM&H=ZiCN^Um zDXAxeYgHmD&`?MB6M;Ien9aG; zOF!PwWD912L6o?w;^Tu30qUxwfR$i1>w&1!2iYEU@~3a+cFmk~O1BK>{@zbL?a|sR zbe2z1@EFgSq<;g`>>>e7jJ{!(NT$|(>7$o8)xzqI#yV(DXB@ONDDG&AVP3KPbn%cr z7=AIF-n#h`{?cXSk=5A!enrpr*T`D|9M*beQz)y?QszWfj{o_q>XaA>VUf#@z+Mf@ z?*d<&R8nBtHqgcU!*pSq^@VY*08H+lu7_#4B{jrCD*Q%yU3Bo;@|J)sox;3d@d85d zS!B(itLJT>(|USI4`FaHiX5AL7&##-Oj4uCw}lbc5@AZ~f)iYP-gjVGgg^gf=|-?_ z0EL0AJr^ihM8bn{(E%%7Eee#<#$7L40wY;>8#sZni@r9l;btdvtYKY!R_Sxp(-bg| zJRbPHv*rQvWue5v+9z~FapMttmK3|Gm1#k~SQHT_@EtU{X}yZ{p@k3r(g>zkn6^(adzp={&+3~0+k{LHSPmF5)&{%lulyCnljb)Y=LhV(7?bj_3Q6Nbg ze-}{J9EQzLDqIUJ=KL81j8U+$u!h5=xE*cg^Ddgmd64auB@O;9@(k{SJ49+r^E9J^ zJg`w2u_$vZFN7#$8kxaSUpI2pdJP(9#vY}$b4cC32`i%N$P_NJhjA#R6BvCZ0we7S z^y#hBqNX2&#*1Q;-;n^5%uiq%g^kB>oe8o+YvaQ(^p!pp;8Qr)aXl(62}Zo&`wt;j zPDPQC@V}Ym)EcJ{e@B`4^r{^!#cG&gGPnF(Gnc>H8+yN{x+`m$XgH}A8) zdM;_Bace)70mADRR1+ka2Ipx4IrBG+9^Od<7igUb>E)5H)Nj+Y+E6kFqihf+uzE44 z=2hG>;GMo`Virvf>Cc?5&$*9SD%bbK0lCE6_yvh|0&Yw_SgOJs9R%Uqg$v%Qz(I>U zri+u+KEG{UZSc) zu9aKeDZkOy+4G^~0j4eMK8W|3H~YR55%_&UWaY&Cf+xvzmC%p6KI30`d4Lof2nL@D zcAuMt+(8@zA8%g*VNYraL$;vHOa zd_3~##kNT`D2XZlbv2XALv}x>>!#mg@LCXSy^PF^l2Sn-`6j`(AZE zG`2x>>Jn9kTws*+V@cw(4HNLHzp%N6qk61enA*~6raF;V8YNlM^?y5;D&Cz=5(8K!8h0#=?up+43vq;^Tw{*etEjg|V44Ma*9vLZ@kC zCS**YzyZV+2bCioHhl=4$TW1L9zG*l@K!DI9FNRX&ez&x+}vl@&$~x2%e+ljgmac|J!K@W10;<`ktPlCUV}p+v4rxJOBsAU|EvkSjoq`i zFaL{$T0lr4#3%o@q?#TWD6*k(hmEu&TF=NrDU*13)^B!jDeX)aR*=@{B!xc}j>5LO z7^89j!=DYfy;rnBWfDq%3PQr&%v!B!hHOp z*7qTk&$whc&^s0`m;9CQQ{c~dDH9%%YhOyNh9|W!{R4dCSo@}wFnUkDJCGM)auv9b z8qYEufYNB8XA~o7XC&(JK5i;Le_A1Re6j7}UkwJb`VvYiBFvx68s*^*h&v291Eo?D zC)(LEs5Koi0&CwgSfWM8nAPfqOgF8E@AV1mg=s%Y@lcJH?azLC(+nqR?y#*#r#CZ7 zn{lK>C0-UQ5(LY?UIlVKG44>#O<_O%>NC7*02#1jM%F9qJbm_TE`h#%Sd zH*iKrrxc%sonBVrd_Axp?Lce!Vg#qUs3YMCHrtcDjj23gUoMe|&hwW+c=U7cVZG72 zX=xP1CV+@L#Njcvo!;^?3`CwVeXlq^^*&GUFpa87o6^Q*^jOK9SX!aEk1V&&uWlO{ zfN+|m`|+;A-K{b#pXSrv-SjaxA9k9?2l*&MmZ~T}>oI?$uNQk53O?Z4EfLBR+52e6 z$~5*wox_EoKtm;teGij6Cay1t3h5xIeo~ByKZN-GFP1k??fJsp< z1;=w)D2CU@N}q56PQqtx`jm1p1G(MqJNzHMc9&M-2HuKrN zndT+$`KLm++B-03S=YC!r<=A5R@j+uSN?c79A~^|Hb_fXk{Ybqk%UfFPYsVN4-cU? zxXx6?fA*t>hDjl+b8t~MQBsO8s^1Jn zlMJ0n<_t{z4tg?Z-Dtstm5dG~1o$l)C+wXl-quYP=W{Syah0uLPW|5+L?#x+@#H}p zT2~r*u@|D0pN*nGg&bXl?s;xTjiACo@eAiv-G&YsZs6UUg9|Wu!fd~6JC+e|k5a?$ zw=%@tj5v+ncCUF7?N+u=?g2PFp?@}Qc2BD?J9M{Vic;4-KqpaShuiofYki{9Ei8l% zO!#*7*fe1X%)m}7w%AfoBd4OXF(53Dn0uAR>^XX;D@1APPHkRNR^+jrl8(e$Lkjc` z{Mq60;4e%SyNw3CVn@@-bS!r$a=bL>ol{{LeCWio;&| zI*zFZGZ0+MD7ES_1BxYDJU&T+2X6yi2LmC(fpdhE@bXgcSt*FmT>R5UwPlmW41f6j zcBNtO2oKm*Sv2fDklY=xdg!JyEu8*=K#92WG#GAv#8loXg`6KIWGKNivT4&FE@QCU z_$K~zxBcN2u!gUcXnxk@zj&(oXWy9V|Cu}bVsj>i{C}jzVR)obZd6gl;A=MbpWVxt z+2M;K6{*FMR#~h9$hZHqQ|j57AS$Bwj}}mw7Hr@Gd%1TiZ#A;;h3P~EU3Z_78e^Lu z_L5KHeMa#pLI}(2B>uDOeluo)-}Jl$)A>03f7`?{n_;#DyE>jYy2m+)rIj*`p_QT7 z9vp^ZOP{`sqlo+?Os=;+oDJs6Z%C!NCrZSeT+=TL{>2ftzqmWB+JCf!Kr$%HIbl!J ziYTjd>;4y`S3DasCuBV#cOWUT9YKaXeiy+(K&}^$EbU}C^^4DiQG~JFe>dHD8+cfh z_n%Jw?%DMIPYcxN=kG1tA%oBLSFJqGz)x*=U&wBSc>dwV1{x`5i1GlE__U?K#)RyJ z)l;jk9pv~b#_uk)A4VAPzk?_>D>A2_oFTUPHF^uh$T^X%BzUWvIx;j-_--SQ ITm=C59~GTp^Z)<= literal 0 HcmV?d00001 diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index fdda462b9..55aeb3f0b 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -362,8 +362,6 @@ class EE_API Renderer { void waitForIdle(); protected: - static const Vector3ff& textureColorChannel( Int32 mode ); - static Renderer* sSingleton; enum RendererStateFlags { diff --git a/include/eepp/graphics/renderer/renderergl.hpp b/include/eepp/graphics/renderer/renderergl.hpp index f3d2a9da0..231e6aa9b 100644 --- a/include/eepp/graphics/renderer/renderergl.hpp +++ b/include/eepp/graphics/renderer/renderergl.hpp @@ -14,6 +14,8 @@ class EE_API RendererGL : public Renderer { ~RendererGL(); + void init(); + GraphicsLibraryVersion version(); std::string versionStr(); diff --git a/include/eepp/graphics/renderer/rendererglshader.hpp b/include/eepp/graphics/renderer/rendererglshader.hpp index 9b6f0622b..0d19f8baa 100644 --- a/include/eepp/graphics/renderer/rendererglshader.hpp +++ b/include/eepp/graphics/renderer/rendererglshader.hpp @@ -64,8 +64,6 @@ class EE_API RendererGLShader : public Renderer { int mTextureColorMode_id; int mTextureColorChannel_id; Int32 mTextureColorMode; - ShaderProgram* mTextureColorPreviousShader; - bool mUsingTextureColorFallbackShader; unsigned int mCurrentMode; ShaderProgram* mCurShader; ShaderProgram* mShaderPrev; diff --git a/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index fc66f95e2..31f77763e 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -483,8 +483,7 @@ bool Renderer::drawSubpixelArrays( unsigned int mode, int first, int count ) { Uint8 previousColorMask[4]; getColorMask( previousColorMask ); for ( Int32 channel = 0; channel < 3; ++channel ) { - if ( channel != 0 ) - setTextureColorMode( channel + 1 ); + setTextureColorMode( channel + 1 ); colorMask( channel == 0 && previousColorMask[0], channel == 1 && previousColorMask[1], channel == 2 && previousColorMask[2], 0 ); drawArrays( mode, first, count ); @@ -590,15 +589,6 @@ 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 ); } diff --git a/src/eepp/graphics/renderer/renderergl.cpp b/src/eepp/graphics/renderer/renderergl.cpp index 76b0b544f..45cae505b 100644 --- a/src/eepp/graphics/renderer/renderergl.cpp +++ b/src/eepp/graphics/renderer/renderergl.cpp @@ -61,6 +61,10 @@ RendererGL::RendererGL() { RendererGL::~RendererGL() {} +void RendererGL::init() { + Renderer::init(); +} + bool RendererGL::ensureSubpixelShader() { #ifndef EE_GLES1 if ( mSubpixelShader ) @@ -117,8 +121,6 @@ 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 ) @@ -134,7 +136,12 @@ bool RendererGL::setTextureColorMode( Int32 mode ) { Renderer::setShader( mSubpixelShader.get() ); mUsingSubpixelShader = true; } - mSubpixelShader->setUniform( mSubpixelChannelLoc, textureColorChannel( 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 } }; + mSubpixelShader->setUniform( mSubpixelChannelLoc, channels[mode] ); return true; #endif } diff --git a/src/eepp/graphics/renderer/renderergl3.cpp b/src/eepp/graphics/renderer/renderergl3.cpp index 2d3235a5a..9d40dee25 100644 --- a/src/eepp/graphics/renderer/renderergl3.cpp +++ b/src/eepp/graphics/renderer/renderergl3.cpp @@ -173,7 +173,12 @@ void RendererGL3::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + 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 } }; + mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/renderergl3cp.cpp b/src/eepp/graphics/renderer/renderergl3cp.cpp index e22ef6687..8bbe6e405 100644 --- a/src/eepp/graphics/renderer/renderergl3cp.cpp +++ b/src/eepp/graphics/renderer/renderergl3cp.cpp @@ -200,7 +200,12 @@ void RendererGL3CP::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + 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 } }; + mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/renderergles2.cpp b/src/eepp/graphics/renderer/renderergles2.cpp index 6a96f9621..3f0c38bb1 100644 --- a/src/eepp/graphics/renderer/renderergles2.cpp +++ b/src/eepp/graphics/renderer/renderergles2.cpp @@ -227,7 +227,12 @@ void RendererGLES2::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); + 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 } }; + mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/rendererglshader.cpp b/src/eepp/graphics/renderer/rendererglshader.cpp index 2ffe8ee9c..5fca99084 100644 --- a/src/eepp/graphics/renderer/rendererglshader.cpp +++ b/src/eepp/graphics/renderer/rendererglshader.cpp @@ -11,8 +11,6 @@ RendererGLShader::RendererGLShader() : mTextureColorMode_id( -1 ), mTextureColorChannel_id( -1 ), mTextureColorMode( 0 ), - mTextureColorPreviousShader( nullptr ), - mUsingTextureColorFallbackShader( false ), mCurrentMode( 0 ), mCurShader( NULL ), mShaderPrev( NULL ) { @@ -27,31 +25,20 @@ RendererGLShader::~RendererGLShader() { } bool RendererGLShader::setTextureColorMode( Int32 mode ) { - if ( mode < 0 || mode > 4 ) + if ( mTextureColorMode_id == -1 ) return false; - if ( mode != 0 && ( mTextureColorMode_id == -1 || mTextureColorChannel_id == -1 ) ) { - mTextureColorPreviousShader = mCurShader; - setShader( static_cast( nullptr ) ); - if ( mTextureColorMode_id == -1 || mTextureColorChannel_id == -1 ) { - setShader( mTextureColorPreviousShader ); - mTextureColorPreviousShader = nullptr; - return false; - } - mUsingTextureColorFallbackShader = true; - } if ( mTextureColorMode != mode ) { mTextureColorMode = mode; mCurShader->setUniform( mTextureColorMode_id, mode ); if ( mTextureColorChannel_id != -1 ) { - mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( 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 } }; + mCurShader->setUniform( mTextureColorChannel_id, channels[mode] ); } } - if ( mode == 0 && mUsingTextureColorFallbackShader ) { - ShaderProgram* previousShader = mTextureColorPreviousShader; - mTextureColorPreviousShader = nullptr; - mUsingTextureColorFallbackShader = false; - setShader( previousShader ); - } return true; } diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 8c2e2cb82..de2a820e3 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -1732,7 +1732,8 @@ 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 bool containsSubpixel = std::find( mRenderModes.begin(), mRenderModes.end(), + GlyphRenderMode::Subpixel ) != mRenderModes.end(); 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; @@ -2681,9 +2682,8 @@ 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 ); + ( &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes ) + .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; @@ -2759,13 +2759,8 @@ 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 && renderModes.empty() ) - renderModes.resize( vertices.size() / GLi->quadVertex(), GlyphRenderMode::Mask ); - if ( !renderModes.empty() ) - renderModes.push_back( glyph.renderMode == GlyphRenderMode::Subpixel - ? GlyphRenderMode::Subpixel - : GlyphRenderMode::Mask ); + ( &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes ) + .push_back( glyph.renderMode ); if ( glyph.renderMode == GlyphRenderMode::Subpixel ) position = position.trunc(); Float padding = 1.0; diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index e5fd66d04..c69cecfb5 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -77,7 +77,7 @@ UTEST( FontRendering, glyphAdvanceDoesNotCreateTexturePages ) { UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { UIApplication app( - WindowSettings( 360, 120, "eepp - Subpixel Text Test", WindowStyle::Default, + WindowSettings( 360, 220, "eepp - Subpixel Text Test", WindowStyle::Default, WindowBackend::Default, 32 ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); ResourceScope& scope = *app.getUI()->getResourceScope(); @@ -103,7 +103,16 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { Text retained( "Subpixel retained", font.get(), 28 ); retained.setFillColor( Color::Black ); - retained.draw( 8.f, 60.f ); + 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 ) { @@ -122,6 +131,7 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { "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() ); From b71ef9522dd925b8cfd8626bd1528732dff3a3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Jul 2026 20:31:52 -0300 Subject: [PATCH 03/10] Clean up. --- include/eepp/graphics/renderer/renderer.hpp | 2 ++ include/eepp/graphics/renderer/renderergl.hpp | 2 -- src/eepp/graphics/renderer/renderer.cpp | 9 +++++++++ src/eepp/graphics/renderer/renderergl.cpp | 13 +++---------- src/eepp/graphics/renderer/renderergl3.cpp | 7 +------ src/eepp/graphics/renderer/renderergl3cp.cpp | 7 +------ src/eepp/graphics/renderer/renderergles2.cpp | 7 +------ src/eepp/graphics/renderer/rendererglshader.cpp | 7 +------ 8 files changed, 18 insertions(+), 36 deletions(-) diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index 55aeb3f0b..fdda462b9 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -362,6 +362,8 @@ class EE_API Renderer { void waitForIdle(); protected: + static const Vector3ff& textureColorChannel( Int32 mode ); + static Renderer* sSingleton; enum RendererStateFlags { diff --git a/include/eepp/graphics/renderer/renderergl.hpp b/include/eepp/graphics/renderer/renderergl.hpp index 231e6aa9b..f3d2a9da0 100644 --- a/include/eepp/graphics/renderer/renderergl.hpp +++ b/include/eepp/graphics/renderer/renderergl.hpp @@ -14,8 +14,6 @@ class EE_API RendererGL : public Renderer { ~RendererGL(); - void init(); - GraphicsLibraryVersion version(); std::string versionStr(); diff --git a/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index 31f77763e..0c21d2d1f 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -589,6 +589,15 @@ 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 ); } diff --git a/src/eepp/graphics/renderer/renderergl.cpp b/src/eepp/graphics/renderer/renderergl.cpp index 45cae505b..76b0b544f 100644 --- a/src/eepp/graphics/renderer/renderergl.cpp +++ b/src/eepp/graphics/renderer/renderergl.cpp @@ -61,10 +61,6 @@ RendererGL::RendererGL() { RendererGL::~RendererGL() {} -void RendererGL::init() { - Renderer::init(); -} - bool RendererGL::ensureSubpixelShader() { #ifndef EE_GLES1 if ( mSubpixelShader ) @@ -121,6 +117,8 @@ 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 ) @@ -136,12 +134,7 @@ bool RendererGL::setTextureColorMode( Int32 mode ) { Renderer::setShader( mSubpixelShader.get() ); mUsingSubpixelShader = true; } - 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 } }; - mSubpixelShader->setUniform( mSubpixelChannelLoc, channels[mode] ); + mSubpixelShader->setUniform( mSubpixelChannelLoc, textureColorChannel( mode ) ); return true; #endif } diff --git a/src/eepp/graphics/renderer/renderergl3.cpp b/src/eepp/graphics/renderer/renderergl3.cpp index 9d40dee25..2d3235a5a 100644 --- a/src/eepp/graphics/renderer/renderergl3.cpp +++ b/src/eepp/graphics/renderer/renderergl3.cpp @@ -173,12 +173,7 @@ void RendererGL3::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - 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 } }; - mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/renderergl3cp.cpp b/src/eepp/graphics/renderer/renderergl3cp.cpp index 8bbe6e405..e22ef6687 100644 --- a/src/eepp/graphics/renderer/renderergl3cp.cpp +++ b/src/eepp/graphics/renderer/renderergl3cp.cpp @@ -200,12 +200,7 @@ void RendererGL3CP::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - 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 } }; - mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/renderergles2.cpp b/src/eepp/graphics/renderer/renderergles2.cpp index 3f0c38bb1..6a96f9621 100644 --- a/src/eepp/graphics/renderer/renderergles2.cpp +++ b/src/eepp/graphics/renderer/renderergles2.cpp @@ -227,12 +227,7 @@ void RendererGLES2::setShader( ShaderProgram* Shader ) { if ( mTextureColorMode_id != -1 ) mCurShader->setUniform( mTextureColorMode_id, mTextureColorMode ); if ( mTextureColorChannel_id != -1 && mTextureColorMode != 0 ) { - 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 } }; - mCurShader->setUniform( mTextureColorChannel_id, channels[mTextureColorMode] ); + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mTextureColorMode ) ); } if ( -1 != mAttribsLoc[EEGL_VERTEX_ARRAY] ) diff --git a/src/eepp/graphics/renderer/rendererglshader.cpp b/src/eepp/graphics/renderer/rendererglshader.cpp index 5fca99084..7894db4e7 100644 --- a/src/eepp/graphics/renderer/rendererglshader.cpp +++ b/src/eepp/graphics/renderer/rendererglshader.cpp @@ -31,12 +31,7 @@ bool RendererGLShader::setTextureColorMode( Int32 mode ) { mTextureColorMode = mode; mCurShader->setUniform( mTextureColorMode_id, mode ); if ( mTextureColorChannel_id != -1 ) { - 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 } }; - mCurShader->setUniform( mTextureColorChannel_id, channels[mode] ); + mCurShader->setUniform( mTextureColorChannel_id, textureColorChannel( mode ) ); } } return true; From 5aeb0b1acfa750df074b77f909200723d618bbe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Jul 2026 20:41:35 -0300 Subject: [PATCH 04/10] The new sparse invariant is: - Empty vector: every quad is Mask; use the existing single draw call. - Non-empty vector: the text contains subpixel glyphs; entries correspond to every quad. addGlyphQuad() therefore delays allocation until the first subpixel glyph. At that point it backfills earlier quads as Mask, records the current subpixel glyph, and tracks every subsequent quad. addLine() only appends Mask after tracking has started. A line before the first subpixel glyph will be included by the later backfill; a line after it must be appended to preserve the one-entry-per-quad mapping. This protects the common grayscale path from subpixel-related overhead while correctly handling mixed mask/subpixel text. It introduces no allocations until subpixel rendering is actually encountered. The critical fix versus the earlier broken version is that the first subpixel glyph is appended unconditionally. The old code accidentally skipped it when it was the first quad, causing the first character to be classified as Mask. --- src/eepp/graphics/text.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index de2a820e3..277f927f5 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -1732,8 +1732,7 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl return; unsigned int numvert = mVertices.size(); - const bool containsSubpixel = std::find( mRenderModes.begin(), mRenderModes.end(), - GlyphRenderMode::Subpixel ) != mRenderModes.end(); + 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; @@ -2682,8 +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 ) { - ( &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes ) - .push_back( GlyphRenderMode::Mask ); + 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; @@ -2759,8 +2759,14 @@ 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 ) { - ( &vertices == &mOutlineVertices ? mOutlineRenderModes : mRenderModes ) - .push_back( glyph.renderMode ); + 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; From 9f4ae9e83cf8a6b3c078017d40b11fa9ccf0cd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 25 Jul 2026 21:24:45 -0300 Subject: [PATCH 05/10] Font SubPixel antialiasing support for eterm and ui_html. --- ...ined_text_small_batch_optimization_plan.md | 337 ++++++++++++++++++ include/eepp/ui/uiapplication.hpp | 6 + include/eepp/ui/uiscenenode.hpp | 6 +- src/eepp/ui/uiapplication.cpp | 8 + src/eepp/ui/uiscenenode.cpp | 4 + src/examples/ui_html/ui_html.cpp | 24 +- src/tests/unit_tests/uiwebview_tests.cpp | 24 ++ src/tools/eterm/eterm.cpp | 22 ++ 8 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 .agent/plans/retained_text_small_batch_optimization_plan.md 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/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/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/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/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/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() ); From 521e2af2e216be8b446b7e26666b1a44ddbd9921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 00:04:03 -0300 Subject: [PATCH 06/10] Should fix unit-tests. --- include/eepp/graphics/font.hpp | 2 +- include/eepp/graphics/text.hpp | 2 +- src/eepp/graphics/fonttruetype.cpp | 1 + src/eepp/ui/uiicon.cpp | 4 +++- src/tests/unit_tests/fontrendering_tests.cpp | 11 +++++++++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/include/eepp/graphics/font.hpp b/include/eepp/graphics/font.hpp index 772f2d909..e4f248d4d 100644 --- a/include/eepp/graphics/font.hpp +++ b/include/eepp/graphics/font.hpp @@ -48,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/text.hpp b/include/eepp/graphics/text.hpp index 85723c0c9..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 }; diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index f813a1235..a402433b0 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -2064,6 +2064,7 @@ FontTrueType::Page::~Page() { } void FontTrueType::clearCache() { + sendEvent( Event::CacheClear ); mPages.clear(); mClosestCharacterSize.clear(); mCodePointIndexCache.clear(); 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/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index c69cecfb5..523af25c8 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -87,10 +88,20 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { 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 ); From 0bf2b89970f9cad0b2630f0e6ce473e7fc24eaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 00:23:24 -0300 Subject: [PATCH 07/10] Handle downscaled LCD glyphs with a dedicated resize path before atlas upload. This preserves row stride and RGB coverage, fixing corrupted icon-font glyphs at small sizes. Add a golden-image regression test covering scaled subpixel glyphs from the Nonicons font. --- .../eepp-scaled-subpixel-glyph-atlas.webp | Bin 0 -> 4208 bytes src/eepp/graphics/fonttruetype.cpp | 56 ++++++++++++++---- src/tests/unit_tests/fontrendering_tests.cpp | 30 ++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 bin/assets/fontrendering/eepp-scaled-subpixel-glyph-atlas.webp 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 0000000000000000000000000000000000000000..15ddbc5fae97d60eb73ce6527bf0e2e498af6b6b GIT binary patch literal 4208 zcmV-$5RdOtNk&F!5C8yIMM6+kP&iCn5C8x#|G*CbHQCYse|;n8LO=Z_`7cZV0mEo! zHDEt$9`MY=%xDku7>1c=d3P8zJj=X0%uHsG$Cl)ghv7T?`%Alh2a8VkDvfd^h{x3(>9tmh^0@*;7q zq~69GX~(L^-GAx%IPNaBbTr1qL);bUzXP|C6v=&BhB}^^Com+}^y@nQyH*1XIROb% z2r|@Hx>`F*8u&X~hzf}lWFjk74wDHAqN_#KUU#J`XfPhiamvf=M1^E;Qg$}7R|N*k zOU2OG@FEi57A%0Q1X(F$1$02YDuwArBvN;AEJNFK-r3xGnKCPo0h)*J*Zprf3 zGQk=uCD!R+4Edny_PmnKB2yNitN^dtS6}+Lg;M9)vq90O%U*)S2{J~8f}y{B^1cTU zwo^1f>!{6yhe>szQX+!P%1Omg4BKz%wLAWP%{(*7T}WD!Jj_=U5)r_6xJBZ&6V z555POq|?Z(pe&gI0$XX-N(0QmV)gEU&z=1u(vonIJ1cnE-n$6Erix zr_(_P3s@Pw<-VfXKk=B_yPt)2a5{j=d5-D;VqD41k=a^k5-Uksgs4E4NVq$M-R zNM&g41E-D;zrur7pPM_-9hdpFAd57-y6r8KV=lj88 z)5a|QeaWxbNt~~o`p!!>Lw$@x%I6-tPh2GgwxrC4fWoxIx8ggaWZ&MSZ+G0zsnWo3 zXlCxIX)l>ga*tp}VPp$+ZK247BCThq3=NToA@isb;Yqth0rT8pYRXGiQ?V&n69(Pk^ZdOsN+gDp0XebENsTUStcR^h*yl2kzph* zW7?>@Z|_9k-G>4sS&P+@IjnIWSX5ge0#Hu&WqjNXhL+}MQA^vniM^N4KjrXgU4t4d zcjo=$6^72}3y*~}5k0VVD9beDA5TF&p&3Cm-Qyk<1f@E8#fxeyhS=tA^D5ESfC?^}3%JHZ}TY_X3i*<3%0A2y}w z2to0;kJ%FOY#%2UE!)%&@`h$S;%x2wr(vQE41fT1^0)spMre@z4w^s!Bb&kz&DeUb zaRN`O@$I)Nod+NfiPrvkOY}^lohx%Q^qJzWj*DS5K5hJry%VqQ4LDV21@C0nIp2i> zY*&@1T1TUhnlhV%37q^*JgOg+?(D$@%!oeJsg`Cuy0%T7X%|m?6Cc*<%3jA&SLtYmSCX=ZP3%41 zr}VkN!I+(A`d&08<4SN}6c3aUi&i#T(-rJ|=xS!~M>Wmi>t9h2d2T9wFScK*psV%1 zJQc>elgg${W7Vi}{D1y(*=MuM0i0qCz58;3#;WloN&wB)pQfU6N3R3OHw|<;m9G(v z{`kbrNO4s;{5G+RO$6OHP2mV&X40Vh-gm7o>+^X-jy)@ah!Ly2IfMc9`TYyC5r#W4 zIn#F4!#oj%L!1Ro4;=s>4Bxa75&seO_&? z*DI}!xU)n>pwO9V6tqm4LKueQ(cm=@Kt zD#8}#;lljlQcKHLre?Ck>C>Y)Z{obpD!|hMk)C5W@0}ANypPhzKBI_`Z#Z`1!O4_8m)aq}>@uit&U6 zNZC`*&X*Js5kRN$M&T(6DH@+K+V85p&W(M(o_5TuDE0tSK#?Qrd+DaSDK7$re%#iu zo?-P+T%^}l^Xw91Z-}+vVOF?GH)DHK>86~TFvyVeZ-?jB^($FyYkPSJ zvedzwSF^k*>l6~Z#}?+E6jHbofJ?*IZ8g7g`m0rwg4Qb>5x}tFf0r&qELQme&LDN| zz8F9UztG0}e4^>}deYEVtv%m%No~uMrc2`FsvCr1qfPmy45RiR@{Y@9#2#wZ%YWah zNwr8OoXF78>dN*aHLGKIB`Ir^u`+i1sn;NuoSuvGp8E%7#PY?JHU zXtfj8ypsFPcNvg6XIG#ITbQ!ZJ{?BrDU-R}lfRDd zIV3(MW||&w4}r?`&e$`*0}{kIIRm9DBoMtqy92{JkX04{&e>~2V;TosjQ1SEpG0N-xfQpHpO?dL2(7>Y%Q3tii;^C$EKaG1i`V6^eu6}Rl zL*01!sFQVbN2D}kYp7q6)>KqT=6VIs+^S>i90SZ^EOM0pE3e%ljXk!T;(Bv2bhpIN z)Zj8?Dp`nj(v|*^thaf9Ws5ten%QQ!DS1^psTYocdwyuZR!z;go-N?%YNpN-JPAb{ zOesu5{L8*GubeOdo1xThBPnPwhi0}oIpd`abG2t6!%RfqwTB{eg%>-0bLYdLc_K%6 zGufI2!}Gt%X)nE^zdxGwc>J%^Mgxi@aN}(4x`+X+wL@3PFR>!IEr31I^I617y3x`-5e^U z;Odm^26se*S`$D{*11Q}wPd*l3Qc7#uAk zmbwTmEY1Ug4BL4Le-#D9$Scjg#5JDi22Duz-{`E+bE&jQ(r@K&vKJj+P zVFW;~vROi}PB`w!Y%@HN0xCMTG;>F@(pE;vLP@4Z_h?EmV^s62!wyzbh6YM@NMSx% z)a%e5`?m_kxv6?35MP?7xvuso!mVp8(n43Fgtzx7JjCmF{Rq)x>^kZw1Pj1JAah z2efppGmVh=wvDzh#fW&$g9WA#JZ$$vi7cXS>gawH+0@}E&01-kD5x1458u)yt5+V( zZ7x4tCOR*YEavXZ?sxygzqzsCaD!-W=!>7Hn=QBBodIAb)CK~X6qrFXD`W(7J;|H&ubmDBFpujIA6&~xC9?tyzut8^TK1F#v!nS$Zp^%4o~_==G;kG7Ny z&XLo>1c^Kr1yMY*vwp$;8qrOTO(wm~N;V22_gKf|M4zUsj-+~6hx-D^9a#}XNAt3! z)=FKWVdamzxR9Yetn6%HtH~XAX>=Y*X!8~d{oD%ZRj>N_-Z79beaS*;X2Qf;KN})_ zbZq?^eJktpvg>AcWailg?vH_<8opIPd=ZQ!_Z{| literal 0 HcmV?d00001 diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index a402433b0..2a5f12308 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -1621,20 +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] = - 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 ); + 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 ) { diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index 523af25c8..c13dd3ba8 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -165,6 +165,36 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { "Subpixel text did not update a transparent target's alpha" ); } +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, From fac6c4ab05a416a8c39583abd5afb178c5815e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 01:02:06 -0300 Subject: [PATCH 08/10] Single pass subpixel rendering in OpenGL 2 context. Fix unit-tests build issue. --- include/eepp/graphics/renderer/opengl.hpp | 12 ++++ include/eepp/graphics/renderer/renderer.hpp | 5 ++ include/eepp/graphics/renderer/renderergl.hpp | 4 ++ .../eepp/graphics/renderer/rendererhelper.hpp | 1 + include/eepp/graphics/shaderprogram.hpp | 9 +++ src/eepp/graphics/renderer/openglext.hpp | 2 + src/eepp/graphics/renderer/renderer.cpp | 27 ++++++++ src/eepp/graphics/renderer/renderergl.cpp | 68 +++++++++++++++++++ src/eepp/graphics/shaderprogram.cpp | 11 +++ src/tests/unit_tests/fontrendering_tests.cpp | 6 +- 10 files changed, 143 insertions(+), 2 deletions(-) diff --git a/include/eepp/graphics/renderer/opengl.hpp b/include/eepp/graphics/renderer/opengl.hpp index 6eaaba95f..e7dab3b47 100644 --- a/include/eepp/graphics/renderer/opengl.hpp +++ b/include/eepp/graphics/renderer/opengl.hpp @@ -148,6 +148,18 @@ typedef char GLchar; #ifndef GL_NUM_EXTENSIONS #define GL_NUM_EXTENSIONS 0x821D #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 #else //! Mobile platform ( Android / iPhone / Maemo ) diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index fdda462b9..5e0fcc834 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -111,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 ); @@ -362,6 +365,8 @@ 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; diff --git a/include/eepp/graphics/renderer/renderergl.hpp b/include/eepp/graphics/renderer/renderergl.hpp index f3d2a9da0..c168c5d97 100644 --- a/include/eepp/graphics/renderer/renderergl.hpp +++ b/include/eepp/graphics/renderer/renderergl.hpp @@ -89,12 +89,16 @@ class EE_API RendererGL : public Renderer { 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/rendererhelper.hpp b/include/eepp/graphics/renderer/rendererhelper.hpp index 2218f179a..f0058c549 100644 --- a/include/eepp/graphics/renderer/rendererhelper.hpp +++ b/include/eepp/graphics/renderer/rendererhelper.hpp @@ -33,6 +33,7 @@ enum GraphicsLibraryExtension { EEGL_EXT_blend_minmax, EEGL_EXT_blend_subtract, EEGL_ARB_ES3_compatibility, + EEGL_ARB_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/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 0c21d2d1f..d760493e3 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -260,6 +260,8 @@ 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 ); } else #endif { @@ -296,6 +298,9 @@ 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" ) ) ); } // NVIDIA added support for GL_OES_compressed_ETC1_RGB8_texture in desktop GPUs @@ -477,6 +482,9 @@ void Renderer::drawArrays( unsigned int mode, int first, int count ) { } bool Renderer::drawSubpixelArrays( unsigned int mode, int first, int count ) { + if ( drawSubpixelDualSourceArrays( mode, first, count ) ) + return true; + if ( !setTextureColorMode( 1 ) ) return false; @@ -497,6 +505,10 @@ bool Renderer::drawSubpixelArrays( unsigned int mode, int first, int count ) { 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; @@ -563,6 +575,21 @@ 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 + 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; diff --git a/src/eepp/graphics/renderer/renderergl.cpp b/src/eepp/graphics/renderer/renderergl.cpp index 76b0b544f..1d51a1309 100644 --- a/src/eepp/graphics/renderer/renderergl.cpp +++ b/src/eepp/graphics/renderer/renderergl.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -107,6 +108,73 @@ void main() { #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; 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/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index c13dd3ba8..24ea763e2 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -130,8 +130,10 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { for ( Uint32 y = top; y < bottom; ++y ) { for ( Uint32 x = 0; x < image.getWidth(); ++x ) { Color pixel = image.getPixel( x, y ); - if ( eeabs( static_cast( pixel.r ) - static_cast( pixel.g ) ) > 3 || - eeabs( static_cast( pixel.g ) - static_cast( pixel.b ) ) > 3 ) + 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; } } From ce3a10a3bd68ac78d49c248a87ac123b4dbb49a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 01:44:31 -0300 Subject: [PATCH 09/10] Single pass subpixel rendering in all backends. Build fixes. --- include/eepp/graphics/renderer/opengl.hpp | 26 ++++---- .../eepp/graphics/renderer/renderergl3.hpp | 2 + .../eepp/graphics/renderer/renderergl3cp.hpp | 2 + .../eepp/graphics/renderer/renderergles2.hpp | 2 + .../graphics/renderer/rendererglshader.hpp | 10 ++++ .../eepp/graphics/renderer/rendererhelper.hpp | 1 + src/eepp/graphics/renderer/renderer.cpp | 13 ++++ src/eepp/graphics/renderer/renderergl3.cpp | 15 +++++ src/eepp/graphics/renderer/renderergl3cp.cpp | 26 ++++++++ src/eepp/graphics/renderer/renderergles2.cpp | 29 +++++++++ .../graphics/renderer/rendererglshader.cpp | 60 +++++++++++++++++++ src/eepp/graphics/text.cpp | 6 +- src/tests/unit_tests/fontrendering_tests.cpp | 53 ++++++++++++++++ 13 files changed, 229 insertions(+), 16 deletions(-) diff --git a/include/eepp/graphics/renderer/opengl.hpp b/include/eepp/graphics/renderer/opengl.hpp index e7dab3b47..30d2477cc 100644 --- a/include/eepp/graphics/renderer/opengl.hpp +++ b/include/eepp/graphics/renderer/opengl.hpp @@ -148,19 +148,6 @@ typedef char GLchar; #ifndef GL_NUM_EXTENSIONS #define GL_NUM_EXTENSIONS 0x821D #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 - #else //! Mobile platform ( Android / iPhone / Maemo ) #ifndef GL_GLEXT_PROTOTYPES @@ -410,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/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 0d19f8baa..67df1abb9 100644 --- a/include/eepp/graphics/renderer/rendererglshader.hpp +++ b/include/eepp/graphics/renderer/rendererglshader.hpp @@ -57,6 +57,14 @@ class EE_API RendererGLShader : public Renderer { 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 @@ -67,6 +75,8 @@ class EE_API RendererGLShader : public Renderer { 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 f0058c549..22234a6ef 100644 --- a/include/eepp/graphics/renderer/rendererhelper.hpp +++ b/include/eepp/graphics/renderer/rendererhelper.hpp @@ -34,6 +34,7 @@ enum GraphicsLibraryExtension { 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/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index d760493e3..e1ef66947 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -262,6 +262,7 @@ void Renderer::init() { 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 { @@ -301,6 +302,10 @@ void Renderer::init() { 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 @@ -587,6 +592,14 @@ bool Renderer::bindFragDataLocationIndexed( unsigned int program, unsigned int c 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; } diff --git a/src/eepp/graphics/renderer/renderergl3.cpp b/src/eepp/graphics/renderer/renderergl3.cpp index 2d3235a5a..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; diff --git a/src/eepp/graphics/renderer/renderergl3cp.cpp b/src/eepp/graphics/renderer/renderergl3cp.cpp index e22ef6687..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; diff --git a/src/eepp/graphics/renderer/renderergles2.cpp b/src/eepp/graphics/renderer/renderergles2.cpp index 6a96f9621..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; diff --git a/src/eepp/graphics/renderer/rendererglshader.cpp b/src/eepp/graphics/renderer/rendererglshader.cpp index 7894db4e7..809a7e42c 100644 --- a/src/eepp/graphics/renderer/rendererglshader.cpp +++ b/src/eepp/graphics/renderer/rendererglshader.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -24,6 +25,65 @@ 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; diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 277f927f5..a50b5c98a 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -1811,11 +1811,11 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl 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 ), @@ -1826,7 +1826,7 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl } 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 ), diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index 24ea763e2..506ac2505 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -167,6 +167,59 @@ UTEST( FontRendering, subpixelCoverageCompositesPerChannel ) { "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, From 33bba67dde95b87a020038aba92afb5319080703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 26 Jul 2026 13:22:21 -0300 Subject: [PATCH 10/10] Correct paddings in Glyph struct. Disable Haiku build until beta6 vm. --- .github/workflows/ecode-nightly.yml | 68 ++++++++++++++--------------- include/eepp/graphics/font.hpp | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) 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/include/eepp/graphics/font.hpp b/include/eepp/graphics/font.hpp index e4f248d4d..14456a8b0 100644 --- a/include/eepp/graphics/font.hpp +++ b/include/eepp/graphics/font.hpp @@ -21,8 +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 };