mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-03 03:31:04 +03:00
ui: improve HTML inline layout and old Reddit rendering
- implement per-side CSS border styles and patterned rendering - preserve rectangular rasterization for thin dotted borders - separate CSS and native-widget border defaults - correct inline atomic-box and vertical-align baseline layout - collapse adjacent block margins in rich-text flow - preserve forced line breaks and formatting margins - size HTML text inputs from font ascent and descent - expose replaced-control text baselines to inline layout - enforce single-owner painting for inline backgrounds and borders - enable and update the old Reddit visual regression golden - add generic layout, border, and form-control regression tests - document spec-first HTML work and project build conventions
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
# eepp Linux Build-Time Optimization Plan
|
||||
|
||||
Status: **Planning complete; no implementation or benchmark run has started.**
|
||||
|
||||
Last review: 2026-08-02.
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Reduce clean and incremental Linux compilation time for eepp and ecode without lowering the
|
||||
current optimization level and without degrading runtime performance, memory use, API quality,
|
||||
or supported platforms.
|
||||
|
||||
The work must be measurement-driven. Do not perform broad include removal, container replacement,
|
||||
PImpl conversion, unity builds, or precompiled-header adoption without showing which measured cost
|
||||
the change addresses and whether it improves the relevant end-to-end build.
|
||||
|
||||
The primary machine for the initial investigation is:
|
||||
|
||||
```text
|
||||
AMD Ryzen 9 3900X
|
||||
12 cores / 24 hardware threads
|
||||
Linux
|
||||
Clang 22.1.8
|
||||
Ninja
|
||||
```
|
||||
|
||||
The repository supports Clang time tracing in both generators: `premake4 --time-trace` and
|
||||
`premake5 --time-trace` both add `-ftime-trace` to compile commands. They serve different normal
|
||||
workflows in this repository:
|
||||
|
||||
- **debug and unit-test builds use Premake 4 with the GNU Make generator**, following
|
||||
`.agent/rules/build-project.md`;
|
||||
- **release build-time and runtime-performance investigations use Premake 5 with the Ninja
|
||||
generator**, following the `eepp-linux-ninja` configuration in `.ecode/project_build.json`.
|
||||
|
||||
Do not substitute one workflow for the other merely because both generators expose
|
||||
`--time-trace`. Use Premake 5/Ninja for the primary release compilation-time baseline and Premake
|
||||
4/GNU Make when specifically measuring or validating the normal debug workflow.
|
||||
|
||||
## 2. Scope and non-goals
|
||||
|
||||
### In scope
|
||||
|
||||
- eepp library C++ translation units;
|
||||
- ecode C++ translation units;
|
||||
- eepp modules and tools when they are part of a measured developer workflow;
|
||||
- public and private header fan-out;
|
||||
- template parsing and instantiation cost;
|
||||
- large generated language-syntax translation units;
|
||||
- clean builds and representative incremental rebuilds;
|
||||
- compiler-cache integration for normal developer builds;
|
||||
- selective precompiled headers or unity builds if measurement justifies them;
|
||||
- link-time measurement, while keeping compilation and linking results separate.
|
||||
|
||||
### Out of scope unless evidence changes the decision
|
||||
|
||||
- changing release or debug optimization levels;
|
||||
- optimizing unchanged third-party C libraries;
|
||||
- rewriting HarfBuzz or maintaining a private HarfBuzz fork;
|
||||
- treating already up-to-date object files as a recurring build cost;
|
||||
- replacing containers based only on header size or reputation;
|
||||
- runtime-performance regressions in exchange for faster compilation;
|
||||
- global PImpl conversion or other ABI-wide redesign;
|
||||
- C++20 modules as an initial solution;
|
||||
- using an AddressSanitizer build as the performance baseline.
|
||||
|
||||
Third-party sources are already handled correctly by incremental dependency tracking: unchanged
|
||||
objects are not rebuilt. Most third-party units are C and compile quickly. HarfBuzz is a known
|
||||
heavy dependency, but is considered residual cost rather than an optimization target. Report its
|
||||
time separately so it does not obscure improvements to project-owned code.
|
||||
|
||||
## 3. Current evidence and hypotheses
|
||||
|
||||
At the time this plan was written:
|
||||
|
||||
- `make/linux/release_x86_64/compile_commands.json` contained 1,989 commands: 1,283 C++ and 706 C;
|
||||
- generated commands invoked `clang` / `clang++` directly rather than consistently using ccache or
|
||||
sccache;
|
||||
- ccache, sccache, mold, and Ninja were installed on the machine;
|
||||
- no project PCH or unity-build configuration was found;
|
||||
- historical `.ninja_log` entries showed expensive project-owned objects including `ecode.o`,
|
||||
`chatui.o`, `lspclientserver.o`, several ecode plugin objects,
|
||||
`stylesheetspecification.o`, `uicodeeditor.o`, `uihtml_tests.o`, and generated language syntax
|
||||
objects;
|
||||
- `syntaxdefinitionmanager.hpp` was directly included by at least 169 C++ sources;
|
||||
- large foundational public headers included `scene/node.hpp`, `ui/uinode.hpp`, `ui/uiwidget.hpp`,
|
||||
`core/string.hpp`, `ui/uiscenenode.hpp`, `ui/uicodeeditor.hpp`, `network/http.hpp`, and
|
||||
`ui/doc/textdocument.hpp`.
|
||||
|
||||
These observations identify candidates, not conclusions. Ninja durations from a parallel build
|
||||
include CPU and memory contention and must not be treated as isolated TU compile times.
|
||||
|
||||
The leading hypotheses are:
|
||||
|
||||
1. generated syntax-definition units repeatedly parse more manager/UI infrastructure than needed;
|
||||
2. large ecode and UI source files receive expensive transitive dependency trees;
|
||||
3. implementation-only types are exposed through common headers;
|
||||
4. substantial inline or template code is instantiated repeatedly;
|
||||
5. developer rebuilds are missing avoidable compiler-cache hits;
|
||||
6. a carefully scoped PCH may reduce repeated standard-library and stable framework parsing;
|
||||
7. selective unity grouping may help stable, homogeneous source families, but could hurt
|
||||
incremental builds and peak memory.
|
||||
|
||||
## 4. Required operating rules
|
||||
|
||||
Before any work, read:
|
||||
|
||||
```text
|
||||
.agent/SOUL.md
|
||||
.agent/rules/project-introduction.md
|
||||
.agent/rules/build-project.md
|
||||
.ecode/project_build.json
|
||||
```
|
||||
|
||||
Also follow these rules:
|
||||
|
||||
1. Run all Premake and build commands from the repository root unless the command explicitly uses
|
||||
`-C make/linux`.
|
||||
2. Record `git status --short` before touching files. Preserve all pre-existing user changes.
|
||||
3. Do not commit, push, reset, or discard user work.
|
||||
4. Regenerate project files after source or Premake changes, as required by the build rules.
|
||||
5. Format every modified C/C++ file with `clang-format` before compilation.
|
||||
6. Keep benchmark artifacts outside tracked source directories, preferably under
|
||||
`/tmp/eepp-build-time-<date>/`.
|
||||
7. Do not mix ASan results with build-time baseline results.
|
||||
8. Do not compare runs generated with different compilers, flags, backends, target sets, cache
|
||||
modes, or background load.
|
||||
9. Repeat important measurements at least three times and report the median. If variance exceeds
|
||||
5%, investigate noise before claiming a small improvement.
|
||||
10. Separate clean-build, no-op, incremental, cache-hit, and isolated-TU results.
|
||||
11. Preserve `-O3` for release experiments. Build-time changes must not come from reducing
|
||||
optimization.
|
||||
12. After every structural C++ change, run an allocation and runtime-performance audit as required
|
||||
by `.agent/SOUL.md`.
|
||||
13. Preserve the repository's generator split: Premake 4/GNU Make for normal debug and unit-test
|
||||
workflows, and Premake 5/Ninja for release build-time investigations. Do not compare their
|
||||
timings as though they were the same build configuration.
|
||||
|
||||
## 5. Benchmark matrix
|
||||
|
||||
Do not use a single `ninja release` duration as the only metric. Establish the following named
|
||||
workflows.
|
||||
|
||||
| ID | Workflow | Purpose |
|
||||
|---|---|---|
|
||||
| B1 | clean eepp-owned library build | framework clean-build cost |
|
||||
| B2 | clean ecode build including required dependencies | real application clean-build cost |
|
||||
| B3 | no-op repeat of B2 | generator/dependency overhead sanity check |
|
||||
| B4 | edit/touch one leaf `.cpp`, rebuild ecode | common local edit latency |
|
||||
| B5 | touch a widely used eepp core header, rebuild | public-header blast radius |
|
||||
| B6 | touch a widely used UI header, rebuild | UI dependency blast radius |
|
||||
| B7 | touch syntax-definition interface, rebuild | generated syntax fan-out |
|
||||
| B8 | isolated compilation of each top slow C++ TU | remove parallel contention |
|
||||
| B9 | cache-enabled rebuild after removing only selected outputs | compiler-cache benefit |
|
||||
| B10 | link-only relink | keep link cost separate from compile cost |
|
||||
| B11 | normal Premake 4/GNU Make debug build and representative incremental rebuild | ensure improvements also help or at least do not harm the debug workflow |
|
||||
|
||||
The executing agent must first inspect `make/linux/build.ninja` with `ninja -t targets` and identify
|
||||
the exact target names for eepp, ecode, and their configurations. Do not guess target names. Store
|
||||
the resolved commands in the benchmark report.
|
||||
|
||||
For header invalidation tests, prefer `touch` followed by restoring the original timestamp if that
|
||||
can be done safely, or make a reversible whitespace change in a clean file. Never overwrite user
|
||||
changes. Record the exact header and why it represents the workflow.
|
||||
|
||||
## 6. Phase 0: Prepare the investigation
|
||||
|
||||
### Tasks
|
||||
|
||||
1. Capture repository state and tool versions:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
premake4 --version
|
||||
premake5 --version
|
||||
clang --version
|
||||
clang++ --version
|
||||
ninja --version
|
||||
ccache --version
|
||||
sccache --version
|
||||
mold --version
|
||||
nproc
|
||||
lscpu
|
||||
```
|
||||
|
||||
2. Re-read `.ecode/project_build.json` and use `eepp-linux-ninja` as the source of truth.
|
||||
3. Inspect available Ninja targets:
|
||||
|
||||
```bash
|
||||
ninja -C make/linux -t targets all
|
||||
```
|
||||
|
||||
4. Inspect representative compile commands and confirm:
|
||||
- compiler;
|
||||
- `-O3` in release;
|
||||
- debug-symbol setting;
|
||||
- SDL backend;
|
||||
- architecture;
|
||||
- whether ccache/sccache is actually in the command;
|
||||
- whether the target compiles only required dependencies or the whole workspace.
|
||||
5. Confirm both time-trace options remain defined before relying on them:
|
||||
|
||||
```bash
|
||||
premake4 --help | rg 'time-trace'
|
||||
premake5 --help | rg 'time-trace'
|
||||
```
|
||||
|
||||
6. Create an untracked results directory under `/tmp`, containing:
|
||||
- `environment.txt`;
|
||||
- `commands.txt`;
|
||||
- `baseline.tsv`;
|
||||
- `traces/`;
|
||||
- `reports/`.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- Exact target names are known.
|
||||
- Benchmark commands are reproducible.
|
||||
- Existing user changes are documented and protected.
|
||||
- No source code has changed.
|
||||
|
||||
## 7. Phase 1: Establish baselines
|
||||
|
||||
### Build generation
|
||||
|
||||
Use the current non-ASan Premake 5/Ninja configuration for the primary release baseline:
|
||||
|
||||
```bash
|
||||
premake5 --disable-static-build --with-debug-symbols --with-backend=SDL3 ninja
|
||||
```
|
||||
|
||||
If `.ecode/project_build.json` changes before execution, follow the updated configuration instead
|
||||
and document the difference.
|
||||
|
||||
The normal debug workflow is separate. Generate it with Premake 4 and GNU Make, using the exact
|
||||
current command prescribed by `.agent/rules/build-project.md` and including the conditional mold
|
||||
flag when required. This debug build may use AddressSanitizer because that is the project's normal
|
||||
debug/test configuration, but never use its timings as release-performance measurements or compare
|
||||
them directly with the Premake 5/Ninja release baseline.
|
||||
|
||||
### Timing method
|
||||
|
||||
Use `/usr/bin/time` so wall time, CPU time, and maximum resident set size are captured. A template
|
||||
is:
|
||||
|
||||
```bash
|
||||
/usr/bin/time -f 'wall=%e user=%U sys=%S cpu=%P maxrss_kb=%M exit=%x' \
|
||||
ninja -C make/linux <resolved-target>
|
||||
```
|
||||
|
||||
Run each meaningful baseline three times under the same conditions. For clean builds, use the
|
||||
narrowest safe Ninja clean operation for the measured target/configuration. Inspect the clean
|
||||
command before running it; do not delete the repository or broad directories manually.
|
||||
|
||||
Record:
|
||||
|
||||
- run ID and timestamp;
|
||||
- exact generation and build command;
|
||||
- cold/warm filesystem-cache state, without forcibly dropping kernel caches;
|
||||
- compiler-cache enabled/disabled state;
|
||||
- wall, user, system, CPU%, and peak RSS;
|
||||
- number of commands executed;
|
||||
- target result size and link duration where available;
|
||||
- relevant system load and CPU frequency governor.
|
||||
|
||||
Do not use `ninja -d stats` output as a replacement for wall-clock timing, but capture it where
|
||||
useful.
|
||||
|
||||
### Analyze historical Ninja data
|
||||
|
||||
Use `.ninja_log` only as a candidate generator. Since it can contain repeated historical entries,
|
||||
group by output and report the latest run or distribution rather than blindly taking the maximum.
|
||||
|
||||
Suggested extraction starting point:
|
||||
|
||||
```bash
|
||||
awk 'NR > 1 && $2 >= $1 { print $2-$1, $4 }' make/linux/.ninja_log | sort -nr
|
||||
```
|
||||
|
||||
Classify entries as:
|
||||
|
||||
- project-owned eepp;
|
||||
- project-owned ecode/tools/modules/tests;
|
||||
- generated syntax definitions;
|
||||
- HarfBuzz;
|
||||
- other third-party C/C++;
|
||||
- linking.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- B1 through B7 and B11 have commands and initial measurements, with debug and release results
|
||||
kept separate.
|
||||
- No-op B3 executes zero unexpected compiler commands.
|
||||
- Third-party and link costs are separated from project-owned compilation.
|
||||
- Variance is understood well enough to evaluate later changes.
|
||||
|
||||
## 8. Phase 2: Capture and aggregate Clang time traces
|
||||
|
||||
### Generate traced build files
|
||||
|
||||
Regenerate the primary release trace build with Premake 5 while preserving all baseline options:
|
||||
|
||||
```bash
|
||||
premake5 --disable-static-build --with-debug-symbols --with-backend=SDL3 --time-trace ninja
|
||||
```
|
||||
|
||||
Confirm a representative C++ compile command contains both `-O3` and `-ftime-trace`.
|
||||
|
||||
If tracing the debug workflow as a separate investigation, add `--time-trace` to the current
|
||||
Premake 4 debug generation command from `.agent/rules/build-project.md`, then build with GNU Make.
|
||||
Keep those trace reports in a separate `debug-premake4/` results directory. Debug/ASan traces may
|
||||
identify dependency fan-out, but their total durations and optimization/backend costs are not
|
||||
comparable to the Premake 5 release traces.
|
||||
|
||||
Run a clean, scoped traced build. Trace instrumentation adds overhead, so do not compare traced
|
||||
wall time directly with the untraced baseline. Its purpose is attribution.
|
||||
|
||||
Before compiling, determine where this Clang version writes trace JSON files. Copy them to the
|
||||
temporary results directory after the build while retaining a mapping from trace to source/object.
|
||||
Do not add trace JSON files to Git.
|
||||
|
||||
### Aggregate these categories
|
||||
|
||||
For every C++ trace, extract at least:
|
||||
|
||||
- total compiler duration;
|
||||
- frontend duration;
|
||||
- backend/optimizer duration;
|
||||
- source/header parsing totals;
|
||||
- template instantiation totals;
|
||||
- code generation totals;
|
||||
- expensive individual headers;
|
||||
- expensive template specializations where Clang names them.
|
||||
|
||||
Produce these reports:
|
||||
|
||||
```text
|
||||
reports/tu-total.tsv
|
||||
reports/frontend.tsv
|
||||
reports/backend.tsv
|
||||
reports/header-self.tsv
|
||||
reports/header-cumulative.tsv
|
||||
reports/template-instantiation.tsv
|
||||
reports/project-vs-third-party.tsv
|
||||
```
|
||||
|
||||
If no existing repository tool aggregates traces adequately, create a small standalone analysis
|
||||
script under the temporary results directory first. Only add a reusable script to the repository
|
||||
later if it proves valuable. The parser must stream or process files one at a time rather than load
|
||||
all trace files into memory simultaneously.
|
||||
|
||||
### Required ranking method
|
||||
|
||||
Rank headers by both:
|
||||
|
||||
1. expensive appearance in one TU;
|
||||
2. cumulative cost across all project-owned TUs.
|
||||
|
||||
Also record include fan-out. A 100 ms header parsed in 200 units is usually more valuable than a
|
||||
one-second header parsed once.
|
||||
|
||||
### Isolated TU confirmation
|
||||
|
||||
For the top 10–20 project-owned C++ objects, extract the exact compile command from
|
||||
`compile_commands.json` or Ninja and execute it serially with `/usr/bin/time`. Preserve its output
|
||||
path safely or direct experimental output into `/tmp`; do not corrupt normal build dependencies.
|
||||
|
||||
Run each top candidate at least three times. This distinguishes intrinsic cost from contention in
|
||||
the original parallel build.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- At least 80% of project-owned C++ compilation time is classified by subsystem or candidate.
|
||||
- Top headers are ranked by cumulative cost and fan-out.
|
||||
- Top slow TUs have isolated measurements.
|
||||
- Frontend-heavy and backend-heavy candidates are separated.
|
||||
|
||||
## 9. Phase 3: Header dependency and syntax-definition investigation
|
||||
|
||||
This is the first source-level optimization phase.
|
||||
|
||||
### 9.1 Generated syntax definitions
|
||||
|
||||
Start here if traces confirm the current hypothesis.
|
||||
|
||||
Inspect the generated language source family and answer:
|
||||
|
||||
- Why does each source include `syntaxdefinitionmanager.hpp`?
|
||||
- Does registration require the full manager definition?
|
||||
- Can definition construction use a small declaration/value header?
|
||||
- Can manager registration move to one aggregation `.cpp`?
|
||||
- Are large initializer expressions causing frontend or backend cost?
|
||||
- Are identical template specializations emitted repeatedly?
|
||||
- Can data be expressed in a representation that compiles faster without adding startup work,
|
||||
heap churn, or runtime parsing?
|
||||
|
||||
Preferred low-risk direction:
|
||||
|
||||
```text
|
||||
small syntax-definition declaration/data interface
|
||||
-> individual generated language units
|
||||
full manager implementation
|
||||
-> one or a few aggregation/registration units
|
||||
```
|
||||
|
||||
Do not merge all languages into one huge source unless an isolated experiment shows acceptable
|
||||
incremental behavior and memory use.
|
||||
|
||||
### 9.2 High cumulative-cost headers
|
||||
|
||||
For every top header, use preprocessing/include-tree tools to identify why it is present. Useful
|
||||
commands include the exact compile command augmented with one of:
|
||||
|
||||
```text
|
||||
-H
|
||||
-E
|
||||
-ftime-trace
|
||||
```
|
||||
|
||||
Investigate:
|
||||
|
||||
- includes needed only by `.cpp` implementation;
|
||||
- pointer/reference members that can use forward declarations;
|
||||
- inline functions whose definitions require heavy dependencies;
|
||||
- nested type references that force full includes;
|
||||
- callbacks using `std::function` in ubiquitous public APIs;
|
||||
- private concrete containers exposed in class layout;
|
||||
- umbrella/convenience headers included by lower layers;
|
||||
- templates that can be explicitly instantiated;
|
||||
- duplicated helper templates or traits.
|
||||
|
||||
### Change rules
|
||||
|
||||
Make one logical dependency change per benchmarkable patch. For each change:
|
||||
|
||||
1. record the baseline candidate metrics;
|
||||
2. implement the smallest correction;
|
||||
3. regenerate build files;
|
||||
4. rebuild the affected target;
|
||||
5. run relevant tests;
|
||||
6. repeat isolated TU measurement;
|
||||
7. repeat the relevant clean/incremental benchmark;
|
||||
8. record runtime/allocation/API consequences;
|
||||
9. revert changes that do not produce a repeatable useful gain.
|
||||
|
||||
### PImpl warning
|
||||
|
||||
Do not introduce PImpl solely to hide includes when it adds per-object allocation or indirection to
|
||||
hot eepp types. Prefer, in order:
|
||||
|
||||
1. forward declaration without layout changes;
|
||||
2. moving out-of-line function bodies;
|
||||
3. small non-owning interface types;
|
||||
4. splitting stable data from heavy behavior;
|
||||
5. PImpl only for cold, coarse-grained objects where allocation and ABI tradeoffs are acceptable.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- At least the top five cumulative project header costs have been explained.
|
||||
- The syntax-definition hypothesis has either produced a measured improvement or been rejected
|
||||
with evidence.
|
||||
- Accepted changes improve B1/B2/B5/B6/B7 as applicable, not merely preprocessing byte count.
|
||||
- All affected tests pass.
|
||||
|
||||
## 10. Phase 4: Template and container cost
|
||||
|
||||
Only begin this phase if time traces show meaningful template parsing or instantiation cost.
|
||||
|
||||
### Investigate first
|
||||
|
||||
- which exact templates dominate;
|
||||
- number of unique versus repeated specializations;
|
||||
- whether cost is parsing, instantiation, optimization, or debug information;
|
||||
- whether the specialization is required in public headers;
|
||||
- whether explicit instantiation is legal and useful;
|
||||
- whether a non-template interface boundary would preserve runtime performance.
|
||||
|
||||
### Candidate techniques
|
||||
|
||||
- `extern template` declarations plus explicit instantiation in one `.cpp`;
|
||||
- moving template-heavy operations out of common headers;
|
||||
- reducing accidental type variation that creates near-duplicate specializations;
|
||||
- replacing a container only when both build-time traces and runtime requirements support it;
|
||||
- using spans/views at API boundaries to avoid exporting container implementation choices.
|
||||
|
||||
Do not assume eepp's `UnorderedMap` or `UnorderedSet` is compile-time cheaper merely because it is
|
||||
runtime-preferred. `include/eepp/thirdparty/unordered_dense.h` is itself substantial. Compare small
|
||||
representative compilations and affected end-to-end targets before changing types.
|
||||
|
||||
### Acceptance gate
|
||||
|
||||
A container/template change is accepted only if:
|
||||
|
||||
- it improves a named build metric beyond noise;
|
||||
- runtime benchmarks do not regress materially;
|
||||
- memory/allocation behavior is equal or better, or a tradeoff is explicitly approved;
|
||||
- public API and serialization behavior remain correct;
|
||||
- cross-platform compilers remain supported.
|
||||
|
||||
## 11. Phase 5: Compiler-cache integration
|
||||
|
||||
This phase improves developer workflow but must be reported separately from structural cold-build
|
||||
improvements.
|
||||
|
||||
### Procedure
|
||||
|
||||
1. Inspect how Premake's Ninja generator selects `CC` and `CXX`.
|
||||
2. Prototype ccache first because the machine already has it and it is straightforward locally.
|
||||
3. Prefer a compiler launcher or generated-command prefix over replacing the compiler identity in
|
||||
a way that breaks dependency generation.
|
||||
4. Confirm commands actually invoke ccache.
|
||||
5. Clear only the experimental cache namespace when a cold-cache test is required; do not erase a
|
||||
user's global cache without explicit permission.
|
||||
6. Record `ccache -z`, run the workflow, then record `ccache -s`.
|
||||
7. Test:
|
||||
- empty-cache clean build;
|
||||
- immediate rebuild after removing selected objects;
|
||||
- rebuild after a source edit and revert;
|
||||
- rebuild after switching between debug and release;
|
||||
- cache invalidation after a common header changes.
|
||||
8. Compare cache overhead and hit rate.
|
||||
|
||||
Evaluate sccache only if remote/shared caching or its operational model is desired. Do not enable
|
||||
both simultaneously.
|
||||
|
||||
### Acceptance gate
|
||||
|
||||
- no dependency correctness regressions;
|
||||
- no stale-object behavior;
|
||||
- measurable warm-build benefit;
|
||||
- negligible cold-cache regression;
|
||||
- documented opt-in/default policy;
|
||||
- structural benchmark results remain available with cache disabled.
|
||||
|
||||
## 12. Phase 6: Selective precompiled-header experiment
|
||||
|
||||
Attempt PCH only after trace aggregation identifies a stable common header prefix.
|
||||
|
||||
### Candidate selection
|
||||
|
||||
A PCH candidate should be:
|
||||
|
||||
- parsed by many TUs;
|
||||
- expensive cumulatively;
|
||||
- stable across ordinary edits;
|
||||
- compatible across all commands in the target group;
|
||||
- mostly standard-library or stable project configuration headers;
|
||||
- free of order-dependent macros and per-TU configuration.
|
||||
|
||||
Do not place frequently edited eepp UI headers into the first PCH.
|
||||
|
||||
Prototype separate PCH scopes where appropriate:
|
||||
|
||||
- eepp core/library;
|
||||
- ecode;
|
||||
- generated syntax definitions.
|
||||
|
||||
### Measure
|
||||
|
||||
- clean target build;
|
||||
- leaf `.cpp` incremental build;
|
||||
- common-header invalidation rebuild;
|
||||
- PCH-generation time;
|
||||
- peak memory at `-j24`, `-j12`, and one lower concurrency if memory pressure appears;
|
||||
- binary output and runtime parity.
|
||||
|
||||
### Acceptance gate
|
||||
|
||||
Keep a PCH only when end-to-end benefit remains significant after including PCH generation and its
|
||||
invalidation cost. Avoid a PCH that improves clean builds but makes the dominant edit/rebuild loop
|
||||
worse.
|
||||
|
||||
## 13. Phase 7: Selective unity-build experiment
|
||||
|
||||
Unity builds are optional and lower priority than dependency hygiene and PCH.
|
||||
|
||||
Good initial candidates are homogeneous, stable source families with shared includes, especially
|
||||
generated syntax definitions if Phase 3 shows repeated frontend cost.
|
||||
|
||||
Do not begin with a monolithic eepp or ecode unity file.
|
||||
|
||||
### Required checks
|
||||
|
||||
- static/anonymous namespace symbol collisions;
|
||||
- macro leakage and include-order dependence;
|
||||
- warning changes;
|
||||
- peak compiler memory;
|
||||
- loss of parallelism;
|
||||
- incremental rebuild amplification;
|
||||
- debug experience and source attribution;
|
||||
- generated-code update behavior.
|
||||
|
||||
Test multiple group sizes rather than only on/off. Compare clean time and leaf-edit latency at
|
||||
realistic parallelism.
|
||||
|
||||
### Acceptance gate
|
||||
|
||||
Unity mode should be optional unless it improves both the dominant developer workflow and clean
|
||||
builds without unacceptable memory, diagnostics, or incremental penalties.
|
||||
|
||||
## 14. Phase 8: Parallelism and linker tuning
|
||||
|
||||
This phase does not change optimization levels and should be done after source improvements.
|
||||
|
||||
### Parallelism sweep
|
||||
|
||||
Run the chosen clean target with at least:
|
||||
|
||||
```text
|
||||
-j8
|
||||
-j12
|
||||
-j16
|
||||
-j20
|
||||
-j24
|
||||
```
|
||||
|
||||
Measure wall time and peak RSS. The fastest setting on a 3900X may be below 24 because large Clang
|
||||
jobs compete for memory bandwidth and cache. Recommend the best default separately for clean and
|
||||
incremental builds if they differ.
|
||||
|
||||
### Linker
|
||||
|
||||
Measure mold versus the current linker only for B10 and full target wall time. Linker selection
|
||||
does not explain compilation hotspots. If mold is already active, simply document the residual
|
||||
link fraction.
|
||||
|
||||
## 15. Validation after every accepted code change
|
||||
|
||||
For C/C++ edits:
|
||||
|
||||
```bash
|
||||
git diff --name-only -- '*.c' '*.cpp' '*.h' '*.hpp' | xargs clang-format -i
|
||||
```
|
||||
|
||||
Regenerate the release build using the current approved Premake 5/Ninja command and compile the
|
||||
narrow affected target. Regenerate debug/tests using the current Premake 4/GNU Make command, then
|
||||
run focused unit tests followed by the full relevant unit-test suite when the phase is ready.
|
||||
|
||||
At minimum verify:
|
||||
|
||||
- clean build succeeds;
|
||||
- incremental dependencies rebuild everything required and nothing obviously unrelated;
|
||||
- debug and release configurations compile when the change affects shared build logic;
|
||||
- no warnings were introduced;
|
||||
- unit tests pass;
|
||||
- public headers remain self-contained where expected;
|
||||
- binary behavior and runtime performance remain unchanged;
|
||||
- allocation audit is complete;
|
||||
- `git diff --check` passes.
|
||||
|
||||
For Premake changes, inspect generated commands rather than assuming the intended flag or launcher
|
||||
was applied.
|
||||
|
||||
## 16. Reporting format
|
||||
|
||||
Maintain one result table for accepted and rejected experiments:
|
||||
|
||||
| Experiment | Metric | Before median | After median | Delta | Variance | Decision |
|
||||
|---|---|---:|---:|---:|---:|---|
|
||||
| example | B2 wall | 100.0 s | 91.0 s | -9.0% | 1.2% | accept |
|
||||
|
||||
Each experiment report must include:
|
||||
|
||||
- hypothesis;
|
||||
- exact files changed;
|
||||
- exact commands;
|
||||
- machine/environment differences;
|
||||
- affected trace categories;
|
||||
- clean and incremental results;
|
||||
- peak-memory result;
|
||||
- tests run;
|
||||
- runtime/allocation considerations;
|
||||
- decision and rationale.
|
||||
|
||||
Track improvements cumulatively, but periodically rerun the original baseline command from the
|
||||
same branch state to detect benchmark drift.
|
||||
|
||||
## 17. Final deliverables
|
||||
|
||||
The executing agent should produce:
|
||||
|
||||
1. a reproducible benchmark script or documented command set;
|
||||
2. baseline results for B1 through B11 where applicable, with Premake 4 debug and Premake 5 release
|
||||
results clearly separated;
|
||||
3. aggregated Clang trace reports;
|
||||
4. a ranked list of project-owned TU, header, and template costs;
|
||||
5. accepted source/build-system improvements, each independently measured;
|
||||
6. a list of rejected experiments and why they failed;
|
||||
7. recommended cache configuration for normal development;
|
||||
8. recommended Ninja job count for the 3900X;
|
||||
9. final clean-build and incremental-build comparison;
|
||||
10. remaining known costs, including HarfBuzz, clearly separated from actionable eepp costs.
|
||||
|
||||
## 18. Execution order and stop conditions
|
||||
|
||||
Execute in this order:
|
||||
|
||||
```text
|
||||
Phase 0 environment and targets
|
||||
-> Phase 1 reproducible baselines
|
||||
-> Phase 2 trace aggregation and isolated confirmation
|
||||
-> Phase 3 dependency/syntax refactoring
|
||||
-> Phase 4 template/container work if justified
|
||||
-> Phase 5 compiler cache
|
||||
-> Phase 6 selective PCH if justified
|
||||
-> Phase 7 selective unity if justified
|
||||
-> Phase 8 parallelism/link tuning
|
||||
-> final validation and report
|
||||
```
|
||||
|
||||
Stop or request guidance when:
|
||||
|
||||
- existing user changes overlap a required file and cannot be preserved safely;
|
||||
- the active build configuration differs materially from this plan;
|
||||
- a proposed change adds runtime allocation or indirection to a hot type;
|
||||
- public API/ABI changes appear necessary;
|
||||
- a measurement cannot be reproduced within reasonable variance;
|
||||
- a change improves one workflow but materially harms a more important workflow;
|
||||
- completing an experiment would require destructive cache or build-directory deletion not
|
||||
explicitly authorized.
|
||||
|
||||
Do not declare success based on trace reduction alone. Success means repeatably lower wall time in
|
||||
the named developer workflows, with correct builds and no unacceptable runtime or maintenance
|
||||
cost.
|
||||
@@ -1,398 +0,0 @@
|
||||
# Old Reddit Thread Rendering Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Render `bin/unit_tests/assets/html/reddit_old_thread.html` close to the Chrome reference image for a 1024px old Reddit comments page. The fixture is intentionally large enough to exercise real old Reddit layout patterns while still being local and deterministic.
|
||||
|
||||
The target is not a Reddit-specific hack. Each fix must move the HTML/CSS engine closer to the relevant CSS behavior and be covered by reduced tests before relying on the full-page screenshot.
|
||||
|
||||
## Current Fixture And Harness
|
||||
|
||||
- Fixture: `bin/unit_tests/assets/html/reddit_old_thread.html`
|
||||
- Local assets:
|
||||
- `bin/unit_tests/assets/html/reddit_old_thread_files/reddit.ETA_etA2z5U.css`
|
||||
- `bin/unit_tests/assets/html/reddit_old_thread_files/sprite-reddit.13AvZYXRW_4.png`
|
||||
- `bin/unit_tests/assets/html/reddit_old_thread_files/pixel.png`
|
||||
- Reference image: `bin/unit_tests/assets/html/reddit_old_thread_reference_image.png`
|
||||
- Smoke test: `UIHTML.redditOldThreadWebViewSmoke`
|
||||
|
||||
The smoke test opens the old Reddit fixture through `UIWebView` without loading the app theme. Browser-like HTML defaults are supplied by the HTML base defaults stylesheet when HTML widgets enter the node tree, so the test exercises the same default-style path used by real HTML content. It asserts the important page regions exist and emits `assets/html/eepp-reddit-old-thread-current.webp` through the existing image comparison helper.
|
||||
|
||||
The smoke test is opt-in because the full fixture is slow in ASAN:
|
||||
|
||||
```sh
|
||||
EEPP_REDDIT_OLD_THREAD_VISUAL=1 projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.redditOldThreadWebViewSmoke"
|
||||
```
|
||||
|
||||
It currently writes the visual artifact to `bin/unit_tests/output/eepp-reddit-old-thread-current.webp`. This is not a golden comparison yet. Replace or extend it with a true reference comparison only after the high-priority layout issues are fixed enough for pixel diffs to be useful.
|
||||
|
||||
Current progress:
|
||||
|
||||
- `UIHTMLFloat.leftFloatOverflowHiddenBlockFormattingContextSitsBesideFloat` covers the old Reddit `.midcol` + `.entry { overflow:hidden }` pattern.
|
||||
- `overflow` values other than `visible` are tracked as block-formatting-context semantics on `UIHTMLWidget`.
|
||||
- RichText atomic block metadata now distinguishes normal block boxes from block formatting contexts.
|
||||
- `.entry` now starts to the right of `.midcol` in the fixture (`entry.x=44`, `midcol.x=15`, `midcol.width=19`).
|
||||
- `UIHTMLFloat.rightFloatConstrainsTextInsideFollowingNormalBlock` covers a normal block whose internal text line boxes must avoid an active right float.
|
||||
- `UIHTMLFloat.rightFloatConstrainsNestedBlockFormattingContext` covers inherited right-float exclusions flowing through a normal block into a nested BFC child.
|
||||
- RichText can now receive external float exclusions from a parent block formatting context. Imported exclusions constrain line boxes and BFC placement but do not contribute to the receiver's own content height.
|
||||
- `BlockLayouter` now forwards inherited float exclusions through normal non-floating blocks and preserves the parent-computed used width for BFC match-parent children next to floats.
|
||||
- The fixture now keeps the main `.entry` selftext box to the left of `.side` (`side.x=719`, `entry.x=44`, `entry.width=670` in the current smoke run).
|
||||
- External float exclusions are filtered for fixed-width descendants whose content box is entirely outside the float's horizontal range. This keeps the comment form textarea from being pushed below the sidebar while still letting match-parent content compute a float-constrained used width.
|
||||
- The smoke test no longer loads `breeze.css`; HTML defaults now come from automatic HTML base-default injection.
|
||||
- Legacy `<strike>` is registered as an HTML phrasing element and receives default `line-through` styling, removing a missing-element warning from the fixture.
|
||||
- Horizontal `auto` margins are recomputed at RichText/block layout read points, and the old Reddit vote arrow is centered inside `.midcol` (`arrow.x=17`, `midcol.x=15`, `arrow.width=15`, `midcol.width=19` in the current smoke run).
|
||||
- Vote arrow sprite CSS now resolves relative to the stylesheet file, preserves negative `background-position`, and is asserted in both a reduced sprite test and the old Reddit smoke test.
|
||||
- CSS `white-space` now maps browser values such as `normal`, `nowrap`, `pre`, `pre-wrap`, `pre-line`, and `break-spaces` into RichText whitespace-collapse and soft-wrap behavior. `nowrap` suppresses soft wrapping for text and atomic inline boxes, including old Reddit-style inline flat lists, and overflowing text runs no longer force following inline content onto a new line.
|
||||
- Collapsed whitespace-only text nodes between non-inline boxes no longer create a spurious line box in RichText layout.
|
||||
- HTML `<button>` is created as an HTML rich-text element with browser-like inline-block defaults so it can participate in CSS display/float layout while still rendering child text. This fixes the top-row placement of old Reddit's `#redesign-beta-optin-btn`.
|
||||
- RichText virtual block breaks no longer split a line that contains only floats, allowing a following BFC to remain beside those floats in reduced cases.
|
||||
- Collapsed whitespace-only text between floated inline-display boxes no longer creates in-flow line content. This fixes the old Reddit `#sr-header-area .sr-list` placement: it now sits in the top row beside the floated redesign button and subreddit dropdown, instead of landing at `y=18` and overlapping `#header-bottom-left`.
|
||||
- The old Reddit subreddit bar now remains a single clipped 18px row under `white-space: nowrap`; the current smoke run reports `srList=(252,0 772x18)`.
|
||||
- CSS absolute font-size keywords now use the browser scale (`medium` = 16px, `small` = 13px, `x-small` = 10px) instead of the app theme's 12px UI default. This fixes old Reddit title and metadata sizing rules such as `.link .title { font-size: medium }` and `.tagline { font-size: x-small }`.
|
||||
- Floated inline text elements now follow CSS blockification: once `float` is not `none`, inline spans/anchors stop being rebuilt as inline text and enter the atomic float path. This fixes old Reddit's comment form footer links (`.help-toggle` and `a.reddiquette`) so they align to the right under the textarea.
|
||||
- `vertical-align` on an inline-block `UITextSpan` now applies to the inline-block box in the parent line, not to its own internal self text. This keeps old Reddit's `.domain a { display:inline-block; vertical-align:middle }` from inflating the anchor's own line box while preserving parent-line alignment semantics.
|
||||
- `clear` now ignores external float exclusions inherited from an outer formatting context. This matches the old Reddit `.entry { overflow:hidden }` BFC case: its child `.expando { clear:left }` must not clear the sibling `.midcol` float outside `.entry`. The current smoke run moves `.expando` from `y=126` to `y=119.609` and `.usertext-body .md` from `y=131` to `y=124.609`.
|
||||
- `updateOutOfFlowPosition()` no longer uses the generic `mPacking` early return. That guard kept old Reddit's `#header-bottom-right { position:absolute; right:0; bottom:0 }` at an early `x=297,y=-1` measurement. The current smoke run places it at `x=717,y=41` in the `1024x64` header and asserts its right/bottom edges.
|
||||
- `input checked` / `checked="checked"` now initializes `UIHTMLInput` checkbox and radio children. Checkbox/radio `value` is preserved as the submitted form value instead of being interpreted as checked state by the child control. This fixes old Reddit's checked sidebar flair checkbox and is covered by `UIHTMLInput.sizeAttribute`.
|
||||
- Current smoke geometry on 2026-05-31: `side=(719,64 300x1186.36)`, `entry=(47,71 667x531.609)`, `selftextMd=(47,124.609 667x450)`, `commentArea=(5,610.609 1014x391)`, `commentTextarea=(16,694.609 500x100)`, `srList=(252,0 772x18)`, `headerBottomRight=(717,41 307x22)`.
|
||||
- Remaining visible blockers: the main selftext and comment form still start lower than the Chrome reference; footer/comments vertical spacing still diverges from Chrome; sidebar/footer content below the first viewport still needs tightening; additional header/logo/submit-button sprite details remain.
|
||||
|
||||
## Reference Layout Invariants
|
||||
|
||||
At 1024px wide, the Chrome reference has these visible structural invariants:
|
||||
|
||||
- Header bands consume the top area only; the body content starts below the blue subreddit header.
|
||||
- `.side` is a 300px right float, positioned at the right edge with small horizontal margins.
|
||||
- `.content` is a normal block, not floated. It starts at the left margin and is not pushed below `.side`.
|
||||
- Main post:
|
||||
- `.rank` and `.midcol` sit to the left of `.entry`.
|
||||
- `.midcol` is a left float with a narrow fixed width.
|
||||
- `.arrow.up`, score text, and `.arrow.down` are stacked vertically inside `.midcol`.
|
||||
- `.entry` has `overflow: hidden`, establishing a block formatting context that sits beside the left floats instead of overlapping them.
|
||||
- The post body `.usertext-body .md` has a bordered light-blue box with line-wrapped paragraphs.
|
||||
- `.entry .buttons li` render as a compact horizontal flat list.
|
||||
- Comment controls and comment bodies line up under the main post content, with nested comment vote columns offset but not collapsed into text.
|
||||
- Sprite-backed elements such as arrows, the Reddit logo, and submit button nubs render from `sprite-reddit.13AvZYXRW_4.png` using background position.
|
||||
|
||||
## Pending Issues To Fix
|
||||
|
||||
### 1. Float Placement And Block Formatting Contexts
|
||||
|
||||
Old Reddit depends heavily on CSS2 float rules:
|
||||
|
||||
- `.side { float: right; width: 300px; }`
|
||||
- `.content { margin: 7px 5px 0 5px; }`
|
||||
- `.midcol { float: left; overflow: hidden; }`
|
||||
- `.entry { overflow: hidden; margin-left: 3px; }`
|
||||
|
||||
Important behavior:
|
||||
|
||||
- A normal block following a float keeps its normal-flow block position. The float affects line boxes inside that block, not the block border box itself.
|
||||
- A block formatting context next to a float must not overlap the float. If there is enough horizontal space, it should sit beside the float; otherwise it moves below.
|
||||
- `overflow` values other than `visible` create a block formatting context for block boxes.
|
||||
- Floats participate in the current formatting context and must be visible to later sibling line layout until cleared or until their bottom is passed.
|
||||
- A `clear` inside a nested block formatting context clears floats from that same formatting context, not external float exclusions inherited only for line avoidance and BFC placement.
|
||||
|
||||
Implementation gaps to investigate:
|
||||
|
||||
- `RichText::layoutWithFloats()` currently owns the float exclusion logic for inline atomic boxes, but full block layout still lacks a first-class float context shared across nested block layouters.
|
||||
- `BlockLayouter` needs explicit CSS block formatting context semantics instead of relying on generic layout sizing and RichText atom placement.
|
||||
- Nested line boxes inside `.content` and `.entry` need access to active sibling floats when the CSS formatting context requires it.
|
||||
|
||||
Reduced tests to add before the full-page comparison:
|
||||
|
||||
- Right float plus following normal block: block x/y unchanged, inner text lines avoid the float.
|
||||
- Right float plus following `overflow:hidden` block: BFC block is placed beside the float and width is reduced to available space.
|
||||
- Left float plus following `overflow:hidden` block: old Reddit `.midcol` and `.entry` pattern, with `.entry.x >= midcol.right + margin`.
|
||||
- Left and right floats on the same line: following inline content uses the remaining middle strip.
|
||||
- Clear behavior inside a mixed float context: `.clearleft` lands below left floats but not necessarily right floats.
|
||||
- Clear behavior inside a nested BFC: a child with `clear:left` does not jump below an external sibling float from the parent formatting context.
|
||||
|
||||
### 2. Float Width Resolution
|
||||
|
||||
Recent work fixed floated `li` elements by making auto-width floated match-parent widgets shrink-to-fit. This needs broader coverage:
|
||||
|
||||
- Floats with `width:auto` should use CSS shrink-to-fit width.
|
||||
- Floats with explicit width should honor width plus margins.
|
||||
- Percentage widths on floats should resolve against the containing block.
|
||||
- Min/max width should constrain the shrink-to-fit result.
|
||||
|
||||
Old Reddit examples:
|
||||
|
||||
- `.side` is fixed 300px and should stay fixed.
|
||||
- `.midcol` gets a width from an inline style rule in the page, using `ex`: `width: 3.1ex`.
|
||||
- `.rank` gets `width: 1.1ex`.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- Floated block `width:auto` shrink-to-fit.
|
||||
- Floated block `width:300px` fixed.
|
||||
- Floated block `width:3.1ex` resolves with font metrics.
|
||||
- Float margins are included in placement and exclusion rectangles.
|
||||
|
||||
### 3. `overflow` Semantics
|
||||
|
||||
Old Reddit uses `overflow:hidden` for layout, not only clipping:
|
||||
|
||||
- `.entry { overflow: hidden; }`
|
||||
- `.midcol { overflow: hidden; }`
|
||||
- `.morelink a { overflow: hidden; text-overflow: ellipsis; }`
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Block boxes with `overflow` other than `visible` establish a BFC.
|
||||
- BFC boxes should avoid overlapping active floats in the same block formatting context.
|
||||
- Actual clipping should be handled separately from BFC creation.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- `overflow:hidden` block beside a float.
|
||||
- `overflow:visible` block can overlap float border box while its line boxes avoid the float.
|
||||
- `overflow:hidden` clips children visually without changing normal-flow y unexpectedly.
|
||||
|
||||
### 4. CSS Display Defaults And Element Creation
|
||||
|
||||
The old `breeze.css` dependency has been removed from the old Reddit smoke test. HTML base defaults are now injected automatically when HTML content is attached to a scene.
|
||||
|
||||
Old Reddit relies on browser defaults for:
|
||||
|
||||
- `html`, `body`, `div`, `p`, `form`, `ul`, `li`, `h1`, `hr`
|
||||
- inline anchors and spans
|
||||
- replaced/form controls such as `input`, `textarea`
|
||||
- hidden inputs and `display:none` nodes
|
||||
|
||||
Remaining plan:
|
||||
|
||||
- Audit `UIWidgetCreator` and HTML element constructors for browser-like default display.
|
||||
- Keep theme-specific visual styling in CSS, but move semantic defaults into element creation.
|
||||
- Add tests that load minimal HTML without `breeze.css` and verify display, margins, and intrinsic sizes for the core elements old Reddit uses.
|
||||
- Keep input state semantics in `UIHTMLInput`, not in the child theme widgets. Boolean HTML attributes such as `checked` need HTML truthiness (`checked="checked"` is true), while `value` remains the submitted value.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- `p` has block display and browser-like top/bottom margins.
|
||||
- `ul`/`li` default list layout remains correct.
|
||||
- `form` is block but does not add unexpected margins.
|
||||
- `input type=hidden` is not visible and does not affect layout.
|
||||
- `textarea rows/cols` has intrinsic size before external CSS.
|
||||
|
||||
### 5. CSS Selector And Stylesheet Coverage
|
||||
|
||||
The old Reddit CSS is minified and selector-heavy. The fixture exercises:
|
||||
|
||||
- chained classes: `.thing.id-t3_...odd.link.self`
|
||||
- descendant selectors: `.entry .buttons li`
|
||||
- child selectors in inline page CSS: `body > .content .link .midcol`
|
||||
- pseudo-classes/pseudo-elements in many rules
|
||||
- media queries and vendor-prefixed declarations
|
||||
|
||||
Priority:
|
||||
|
||||
- Confirm selectors that affect the visible reference image are parsed and matched.
|
||||
- It is acceptable to ignore unsupported dynamic pseudo-classes initially if the default state renders correctly.
|
||||
- Pseudo-elements are not critical for the first old Reddit pass except where old Reddit uses `:before`/`:after` for visual icons.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- `body > .content .link .midcol` applies to the fixture structure.
|
||||
- Multiple class selectors match correctly.
|
||||
- Unsupported pseudo-class rules do not invalidate the whole selector list if another selector is valid.
|
||||
|
||||
### 6. Sprite Background Rendering
|
||||
|
||||
The reference depends on a sprite image for:
|
||||
|
||||
- Reddit logo/header icon.
|
||||
- Vote arrows.
|
||||
- Submit button nubs and gradients.
|
||||
- Mail/preference/user icons.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `background-image: url(sprite-reddit.13AvZYXRW_4.png)` resolves relative to the CSS file or document URI as browsers do.
|
||||
- `background-position: -42px -1678px` draws the correct sprite sub-rectangle.
|
||||
- `background-repeat: no-repeat` is honored.
|
||||
- Backgrounds are clipped to the padding/border box correctly.
|
||||
- CSS custom properties that expand to image values should keep working where used by the bundled or older copied CSS.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- Background sprite with negative `background-position`.
|
||||
- CSS-file-relative URL resolution.
|
||||
- `background-repeat: repeat-x` button strip rendering.
|
||||
- Element with explicit width/height and sprite background draws nonblank pixels.
|
||||
|
||||
### 7. Auto Margins And Centering
|
||||
|
||||
Vote arrows use:
|
||||
|
||||
- `.arrow { display: block; width: 15px; margin-left: auto; margin-right: auto; }`
|
||||
|
||||
Required behavior:
|
||||
|
||||
- Block-level auto horizontal margins center fixed-width children inside their containing block.
|
||||
- Auto margin recomputation must happen after parent width and child width are known.
|
||||
- This must work inside floated containers and BFC blocks.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- Fixed-width block centered with `margin-left:auto; margin-right:auto`.
|
||||
- Same case inside a float.
|
||||
- Same case after parent width changes.
|
||||
|
||||
### 8. Text Layout, Paragraph Metrics, And Form Controls
|
||||
|
||||
The screenshot is text-heavy. Issues here may look like float bugs even when positioning is correct.
|
||||
|
||||
Needs:
|
||||
|
||||
- Old Reddit font sizing: `font-size: x-small`, percentages, and inherited line-height.
|
||||
- Inline-block `vertical-align` must align the atomic inline-block box in the parent line without changing the inline-block's own internal text line metrics.
|
||||
- Paragraph margin collapse or equivalent spacing close enough for old Reddit.
|
||||
- `textarea` dimensions from CSS and attributes: comment box should be large and aligned.
|
||||
- `input type=text` shortlink field width/height and border.
|
||||
- Link color, visited link color, bold text, and small metadata text.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- Absolute `font-size` keywords use the browser scale, while `smaller`/`larger` remain relative to the parent.
|
||||
- Inline-block with `vertical-align: middle` has the same own text line height as the baseline-aligned inline-block, while the parent line still applies the middle alignment.
|
||||
- `font-size:x-small` and percentage font-size inheritance.
|
||||
- Paragraph margins inside `.md`.
|
||||
- `textarea` with CSS width/height and `rows/cols`.
|
||||
- `input readonly type=text` fixed width.
|
||||
- `input type=checkbox checked value=...` initializes checked state and submits the explicit value.
|
||||
- `input type=radio checked` initializes the radio active state.
|
||||
|
||||
### 9. Page Width, Scroll View, And Viewport Semantics
|
||||
|
||||
The fixture includes `<meta name="viewport" content="width=1024">`. The `UIWebView` path should behave like a 1024px viewport for this test.
|
||||
|
||||
Needs:
|
||||
|
||||
- `UIWebView` document container width should match the viewport and propagate into `html`/`body`.
|
||||
- Body min-height should not force incorrect content placement.
|
||||
- The full page should scroll vertically without clipping or changing layout widths.
|
||||
- Screenshot tests should be explicit about viewport size.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- `UIWebView` file load sets document width equal to viewport width.
|
||||
- Body/html min-height follows viewport height but content can exceed it.
|
||||
- Right-floated sidebar remains at the same x before and after a scroll update.
|
||||
|
||||
### 10. Layout Invalidation And Performance
|
||||
|
||||
The full old Reddit fixture is expensive in debug/ASAN. Release builds are expected to be much faster, but the fixture still appears to trigger excessive invalidation and repeated layout recomputation. Treat this as a separate investigation track from visual correctness so we do not hide spec bugs behind caching.
|
||||
|
||||
Likely sources:
|
||||
|
||||
- `UIWebView::loadDocumentData()` loads a large DOM, external CSS, inline CSS, and then adjusts `html`/`body` min-height, causing follow-up invalidations.
|
||||
- CSS application may update many widget properties one by one, each producing layout invalidation instead of batching style changes per element.
|
||||
- `UIRichText` rebuilds RichText content from children and can be invalidated repeatedly while descendants are still loading/applying styles.
|
||||
- Auto-size, match-parent, min/max intrinsic measurement, and float-aware layout can recursively query child sizes and trigger re-entry.
|
||||
- Image/background/style resource resolution can mutate widget state after the first layout pass.
|
||||
|
||||
Plan:
|
||||
|
||||
- Add counters around dirty layout enqueue, `UIWidget::updateLayout()`, `UILayouter::layout()`, `UIRichText::rebuildRichText()`, RichText layout, and CSS property application.
|
||||
- Add an opt-in diagnostic mode for the old Reddit smoke test that prints per-frame and total counts.
|
||||
- Measure debug without ASAN, debug with ASAN, and release to separate sanitizer overhead from algorithmic churn.
|
||||
- Batch style application where possible: apply all matched CSS properties, then invalidate layout once per widget.
|
||||
- Avoid relayout when a property setter receives the same computed value.
|
||||
- Ensure `UIWebView` viewport/html/body size synchronization happens once after document load when possible, not as a cascade of independent size changes.
|
||||
- Cache intrinsic width/height results during a layout pass and invalidate them only on relevant style/content changes.
|
||||
|
||||
Reduced tests:
|
||||
|
||||
- Loading a generated page with many styled block children should produce a bounded number of layout passes.
|
||||
- Reapplying the same style sheet should not dirty unchanged widget layout repeatedly.
|
||||
- Measuring min/max intrinsic widths during a single parent layout should not rebuild the same child RichText multiple times.
|
||||
|
||||
## Suggested Phase Order
|
||||
|
||||
### Phase 1: Measurement Harness
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Done. `UIHTML.redditOldThreadWebViewSmoke` passes when `EEPP_REDDIT_OLD_THREAD_VISUAL=1` is set.
|
||||
- Done. A current eepp screenshot is produced at `bin/unit_tests/output/eepp-reddit-old-thread-current.webp`.
|
||||
- Done. The smoke dumps important node rects for `.side`, `.content`, `.midcol`, `.entry`, `.arrow`, `.usertext-body`, `.commentarea`, topbar/header nodes, and comment form controls.
|
||||
|
||||
Validation:
|
||||
|
||||
```sh
|
||||
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.redditOldThreadWebViewSmoke"
|
||||
```
|
||||
|
||||
### Phase 2: Float/BFC Correctness
|
||||
|
||||
Implement a shared CSS float context that block and inline layout can both see. Keep `RichText` responsible for line placement, but do not make it the only owner of float state when block children participate in the same formatting context.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Mostly done for the old Reddit fixture. Reduced float/BFC tests cover the active `.side`, `.content`, `.midcol`, `.entry`, nested BFC, clear, and right-float textarea cases.
|
||||
- `.side`, `.content`, `.midcol`, and `.entry` now match the reference broad geometry closely enough for later visual details to be meaningful.
|
||||
- Keep this phase open only for newly found generic float regressions.
|
||||
|
||||
Validation:
|
||||
|
||||
```sh
|
||||
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*"
|
||||
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.redditOldThreadWebViewSmoke"
|
||||
```
|
||||
|
||||
### Phase 3: CSS Visual Features
|
||||
|
||||
Prioritize sprite backgrounds, URL resolution, background-position, repeat modes, auto margins, and header/topbar CSS behaviors. These are the biggest remaining visual gap after layout boxes are placed correctly.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Vote arrows draw from the sprite and are centered in `.midcol`. (Reduced and old Reddit smoke coverage exists.)
|
||||
- Header/logo sprite regions render.
|
||||
- Submit button strips and nubs render close to reference.
|
||||
- The `#sr-header-area .sr-list` BFC sits in one clipped topbar row beside the floated redesign button and subreddit dropdown, instead of overlapping `#header-bottom-left` or wrapping through later header rows.
|
||||
|
||||
### Phase 4: Defaults And Form Controls
|
||||
|
||||
Keep semantic HTML defaults independent from the app theme. `breeze.css` should remain a visual UI theme, not a behavior crutch for HTML content.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Minimal HTML default-display tests pass without loading `breeze.css` for the covered elements.
|
||||
- The old Reddit smoke test passes without loading `breeze.css`.
|
||||
- Form controls in the comment box and sidebar match expected broad geometry; checkbox/radio checked state is now wired. Remaining work here is visual polish such as native checkbox/radio appearance, textarea resize affordance, and exact input/button paint.
|
||||
|
||||
### Phase 5: Full-Page Visual Gate
|
||||
|
||||
Once the page is close enough that screenshot diffs are meaningful, convert the smoke baseline into a real visual comparison against a curated expected image.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Store an eepp expected output image only after review.
|
||||
- Keep a separate Chrome reference image for human comparison.
|
||||
- Pixel tolerance should be explicit and justified, because font rasterization and renderer backends may differ.
|
||||
|
||||
### Phase 6: Performance Gate
|
||||
|
||||
After the main geometry is correct, optimize invalidation and recomputation using the old Reddit fixture as a stress test.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- The opt-in old Reddit render reports stable, bounded layout/rebuild counts.
|
||||
- Debug/ASAN remains tolerable enough for targeted investigation.
|
||||
- Release render time is documented before and after optimization.
|
||||
- Any caching preserves all reduced correctness tests.
|
||||
|
||||
## Standard Validation Gate
|
||||
|
||||
Before considering a phase complete:
|
||||
|
||||
```sh
|
||||
make -C make/linux -j$(nproc)
|
||||
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTMLFloat.*"
|
||||
projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="UIHTML.redditOldThreadWebViewSmoke"
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Run the full suite before landing broad layout changes.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Resource ownership follow-up plan
|
||||
|
||||
Status: proposed follow-up after Stage 7 completion, 2026-07-24.
|
||||
|
||||
The shared-resource ownership migration is complete. This plan is intentionally limited to
|
||||
validation, documentation, diagnostics, and compatibility-era naming cleanup. It must not reopen
|
||||
the established `ResourceScope` / `ResourceCatalog` ownership model without a concrete defect.
|
||||
|
||||
## 1. Scene lifetime coverage
|
||||
|
||||
- Add focused tests proving resources published only into a scene scope are released when its
|
||||
`UISceneNode` and `ResourceScope` are destroyed.
|
||||
- Cover textures, drawables, fonts, themes, icons, and shader programs where practical.
|
||||
- Verify explicitly imported catalogs remain alive only through their actual external owners.
|
||||
- Assert against registry/catalog contents and retained handles, not only destructor side effects.
|
||||
|
||||
## 2. Public ownership documentation
|
||||
|
||||
- Audit public resource APIs and consistently document whether parameters and return values are:
|
||||
owning, retaining, borrowing, or observing.
|
||||
- Document the required lifetime for borrowed raw pointers returned by UI and GPU APIs.
|
||||
- Keep hot-path raw pointers where ownership is established elsewhere; do not imply ownership by
|
||||
converting those APIs to shared handles.
|
||||
- Add short ownership examples to `ResourceScope`, `ResourceCatalog`, and family-specific services.
|
||||
|
||||
## 3. Compatibility-era naming cleanup
|
||||
|
||||
- Rename non-owning `ShaderProgramManager`, `VertexBufferManager`, and `FrameBufferManager`
|
||||
concepts to `ShaderProgramRegistry`, `VertexBufferRegistry`, and `FrameBufferRegistry` throughout
|
||||
filenames, includes, build files, and documentation.
|
||||
- Review other `*Manager` names only when their current role is genuinely a registry or service.
|
||||
- Keep this as an isolated public API cleanup so downstream include breakage is easy to review.
|
||||
|
||||
## 4. GPU borrowed-lifetime diagnostics
|
||||
|
||||
- Add debug-only validation that borrowed frame buffers, vertex buffers, shaders, and programs are
|
||||
not used after their owning OpenGL context or renderer has been destroyed.
|
||||
- Prefer cheap generation/context identity checks at API boundaries over reference counting in hot
|
||||
rendering paths.
|
||||
- Do not add OpenGL context-loss recreation support; current supported platforms do not require it.
|
||||
|
||||
## 5. Static initialization audit
|
||||
|
||||
- Build a Clang diagnostic configuration with `-Wglobal-constructors` and
|
||||
`-Wexit-time-destructors` to identify C++ work performed before `main()` and after its return.
|
||||
- Produce a linker-level inventory of `.init_array` entries for representative executables to find
|
||||
constructors hidden in libraries or translation units excluded from Clang diagnostics.
|
||||
- Prioritize globals that allocate memory, register callbacks/resources, depend on singleton order,
|
||||
or retain graphics objects. Constant-initialized POD data is not a migration target.
|
||||
- Move executable state into `main()` scopes and callback lambdas. Replace necessary library
|
||||
globals with function-local statics only when process lifetime is intentional and documented.
|
||||
- Track the baseline count and prevent new non-trivial global constructors in CI once existing
|
||||
cases have been classified.
|
||||
|
||||
## Validation
|
||||
|
||||
- Run the complete unit-test suite and all normal platform CI jobs after each focused change.
|
||||
- Keep `git diff --check` clean and run examples affected by shutdown-order changes under ASan.
|
||||
- Confirm GPU/resource handles are destroyed before `Engine::destroySingleton()` in examples and
|
||||
tools that own them locally.
|
||||
|
||||
@@ -1,901 +0,0 @@
|
||||
# eepp shared-resource ownership architecture
|
||||
|
||||
Status: implementation complete through Stage 7, 2026-07-24.
|
||||
|
||||
This document freezes the contracts that must be true before the public texture API is changed. The
|
||||
implementation may refine names and small mechanics, but changing an invariant below requires an
|
||||
explicit architecture revision.
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Replace raw manager ownership, global load side effects, manual deletion, and `ownIt` flags with
|
||||
explicit shared ownership throughout eepp and all in-repository consumers.
|
||||
|
||||
The final model is:
|
||||
|
||||
- Consumers, immutable source objects, catalogs, and caches own resources with strong handles.
|
||||
- A live registry observes resources weakly for diagnostics, accounting, and leak
|
||||
reporting. It is never searched for semantic names.
|
||||
- Catalogs define names and persistence.
|
||||
- Scopes define which catalogs and typed caches are visible.
|
||||
- GPU resources remain graphics-thread-affine. A final owning release may happen on a worker, but
|
||||
the texture deleter only performs a thread-safe handoff to TextureFactory. Actual destruction runs
|
||||
through the graphics/display lifecycle.
|
||||
- UI drawable resolution is layered over Graphics resource lookup; browser caching and navigation
|
||||
remain outside Graphics.
|
||||
- A UISceneNode can own a scope and resolver, but neither texture lifetime nor pure Graphics usage
|
||||
requires a UISceneNode.
|
||||
|
||||
This is an intentional repository-wide API break. There will be no compatibility API, no `Shared`
|
||||
suffixes, and no retained `ownIt` overloads.
|
||||
|
||||
## 2. Confirmed hazards in the current code
|
||||
|
||||
These are not hypothetical migration risks:
|
||||
|
||||
- `TextureAtlasLoader` queues `TextureFactory::loadFromFile()` and `loadFromPack()` calls, discards
|
||||
their results, then later resolves the textures globally by name. Immediately switching the
|
||||
factory to weak, unpinned retention would compile and destroy each loaded texture at the end of
|
||||
the lambda.
|
||||
- `TextureLoader` stores and returns `Texture*`; its callback and unload behavior depend on the
|
||||
factory owning the object.
|
||||
- `TextureRegion` stores `Texture*`, and constructors taking a texture ID resolve that raw pointer
|
||||
through the factory.
|
||||
- `FrameBuffer` stores `Texture*` and deletes it directly in its destructor.
|
||||
- `TextureAtlas`, glyph drawables, SVG icon raster caches, GIF loading, and font caches retain raw
|
||||
texture pointers.
|
||||
- `Texture::~Texture()` performs GL deletion and reaches `Engine::instance()` and
|
||||
`TextureFactory::instance()`. Shared ownership must replace this singleton-dependent destruction
|
||||
with TextureFactory-controlled deferred release on the graphics thread.
|
||||
- `TextureRegion::isStateful()` and `DrawableGroup::isStateful()` return false despite draw/update
|
||||
methods mutating destination size, position, or child state.
|
||||
- `UIImage`, `StateListDrawable`, and `DrawableGroup` temporarily mutate drawable color, alpha,
|
||||
size, position, or children while drawing.
|
||||
- `Models::Variant` copies both an owning flag and the same raw drawable pointer, allowing two
|
||||
copies to believe they exclusively own one allocation.
|
||||
- `EE_MEMORY_MANAGER` requires allocation registration through `eeNew` and removal through
|
||||
`eeDelete`; default `shared_ptr` deletion of an `eeNew` allocation would leave tracking invalid.
|
||||
- `Engine::~Engine()` destroys `Renderer` before `ShaderProgramManager`, while
|
||||
`ShaderProgram::~ShaderProgram()` calls `GLi->deleteProgram()` directly.
|
||||
- The global HTTP pool is cleared after Graphics resources and managers, allowing asynchronous
|
||||
producers to outlive systems they can mutate.
|
||||
|
||||
## 3. Frozen ownership vocabulary
|
||||
|
||||
### 3.1 Public handle representation
|
||||
|
||||
eepp will expose `std::shared_ptr` and `std::weak_ptr` through consistent aliases:
|
||||
|
||||
```cpp
|
||||
template <typename T> using ResourcePtr = std::shared_ptr<T>;
|
||||
template <typename T> using ResourceWeakPtr = std::weak_ptr<T>;
|
||||
|
||||
using TexturePtr = ResourcePtr<Texture>;
|
||||
using TextureWeakPtr = ResourceWeakPtr<Texture>;
|
||||
```
|
||||
|
||||
This is an explicit API/ABI choice. eepp accepts that users can use standard shared-pointer
|
||||
operations. The library nevertheless exposes no API for adopting an arbitrary resource raw pointer,
|
||||
and resource constructors remain protected/private where practical.
|
||||
|
||||
Requirements:
|
||||
|
||||
- A resource allocation has exactly one control block.
|
||||
- Owning APIs return handles. They do not return a raw pointer plus an ownership convention.
|
||||
- Long-lived borrowed raw pointers are forbidden unless the field or API documents the owner that
|
||||
dominates the borrow. Local `.get()` views inside a call are allowed.
|
||||
- Shared-library and static-library builds must test the chosen handle/deleter behavior.
|
||||
|
||||
### 3.2 Centralized creation and deletion
|
||||
|
||||
All ref-counted eepp resource allocations go through one internal creation path with an
|
||||
eepp-compatible deleter:
|
||||
|
||||
```cpp
|
||||
template <typename T> struct ResourceDeleter {
|
||||
void operator()( T* resource ) const noexcept { eeDelete( resource ); }
|
||||
};
|
||||
|
||||
template <typename T, typename... Args>
|
||||
ResourcePtr<T> makeResource( Args&&... args );
|
||||
```
|
||||
|
||||
Factories use an equivalent private helper for protected constructors. `std::make_shared` is not
|
||||
used for tracked eepp resources unless the memory manager is redesigned to understand its combined
|
||||
allocation. No second control block may be created from `handle.get()`.
|
||||
|
||||
Texture is the deliberate exception to immediate `eeDelete`: its factory-controlled deleter may
|
||||
queue the final raw object from any thread. `TextureFactory::collectReleasedTextures()` performs the
|
||||
eventual `eeDelete` on the graphics thread after queued rendering has been flushed. This is the same
|
||||
deferred destruction contract used by scene nodes; it is not a general GPU disposal system.
|
||||
|
||||
### 3.3 Identity, keys, and labels
|
||||
|
||||
These concepts are distinct:
|
||||
|
||||
- `ResourceId` is immutable and process-unique across Engine recreation in tests. For textures it
|
||||
is the value returned by `Texture::getTextureId()`; there is no separate factory-internal texture
|
||||
ID.
|
||||
- `ResourceKey` is the immutable canonical semantic lookup key. Equality compares the complete key,
|
||||
never only a hash.
|
||||
- `displayName` is diagnostic text and may change without changing identity or catalog indexes.
|
||||
- Aliases are catalog entries, not mutable fields used as registry indexes.
|
||||
|
||||
A process-wide monotonic ID source must not reset when an Engine singleton is recreated by tests.
|
||||
|
||||
## 4. Graphics ownership layers
|
||||
|
||||
```text
|
||||
Engine
|
||||
├── Renderer and contexts
|
||||
├── TextureFactory
|
||||
│ ├── weak live-texture registry
|
||||
│ └── deferred released-texture queue
|
||||
├── GlobalResourceCatalog (intentional strong persistence)
|
||||
└── Default ResourceScope (local catalog + explicit imports)
|
||||
|
||||
Application/scene ResourceScope
|
||||
├── local ResourceCatalog
|
||||
├── explicitly imported catalogs
|
||||
└── typed caches
|
||||
|
||||
UI::DrawableResolver
|
||||
├── CSS/image/icon/glyph parsing
|
||||
├── node and UISceneNode context
|
||||
└── delegates texture/source lookup to Graphics::ResourceScope
|
||||
|
||||
UI/Network::WebResourceCache
|
||||
├── request/cache partitioning
|
||||
├── in-flight request coalescing
|
||||
├── TTL/LRU/byte-budget retention
|
||||
└── per-document leases and subscribers
|
||||
```
|
||||
|
||||
Engine coordinates the Graphics lifetime roots directly. TextureFactory coordinates texture
|
||||
creation, weak observation, and deferred destruction, but does not provide semantic lookup or
|
||||
normal strong retention. No Graphics class depends on UI.
|
||||
|
||||
### 4.1 LiveResourceRegistry
|
||||
|
||||
The registry observes every instantiated texture weakly:
|
||||
|
||||
```cpp
|
||||
struct TextureRecord {
|
||||
ResourceId id;
|
||||
ResourceKey creationKey;
|
||||
std::string displayName;
|
||||
TextureWeakPtr texture;
|
||||
std::size_t memoryBytes;
|
||||
ResourceFlags flags;
|
||||
};
|
||||
```
|
||||
|
||||
It has no strong resource field, no `ownerScope`, no semantic name resolution, and no public
|
||||
`unregister()` operation.
|
||||
|
||||
Primary operations are snapshots, expiration purging, and live iteration. Diagnostic snapshots
|
||||
return metadata plus weak handles, not a vector of owning texture handles:
|
||||
|
||||
```cpp
|
||||
TextureRegistrySnapshot snapshotTextures() const;
|
||||
void purgeExpired();
|
||||
```
|
||||
|
||||
Opening `UITextureViewer` must not retain all textures. The viewer may lock a weak handle for one
|
||||
render operation and may strongly retain only a user-selected texture.
|
||||
|
||||
Destruction, callbacks, and GL operations never occur while a registry lock is held.
|
||||
|
||||
Snapshot metadata, including texture memory usage, is copied from the texture while the registry
|
||||
temporarily locks its weak handle. Texture keeps its existing memory size as the single source of
|
||||
truth; accounting does not require separate shared state or callbacks into the factory.
|
||||
|
||||
### 4.2 ResourceCatalog
|
||||
|
||||
A catalog maps complete canonical keys/aliases to strong handles. It provides semantic lookup and
|
||||
intentional persistence:
|
||||
|
||||
```cpp
|
||||
class ResourceCatalog {
|
||||
public:
|
||||
void publish( ResourceKey key, TexturePtr texture );
|
||||
TexturePtr findTexture( const ResourceKey& key ) const;
|
||||
bool erase( const ResourceKey& key );
|
||||
void clear();
|
||||
};
|
||||
```
|
||||
|
||||
Publishing is the final global pin model. The low-level TextureFactory has no `ResourcePin` argument
|
||||
and no factory-owned strong pin. Independent catalogs/caches naturally provide independent pins.
|
||||
If temporary explicit pinning is needed, `ResourceCatalog::pin()` returns an independent RAII token;
|
||||
there is no shared boolean or one global `strong` field.
|
||||
|
||||
A higher-level scope convenience may accept a retention option and publish into that scope's catalog,
|
||||
but texture creation and decoding remain unpinned operations.
|
||||
|
||||
### 4.3 ResourceScope
|
||||
|
||||
`Graphics::ResourceScope` performs Graphics-only lookup and loading:
|
||||
|
||||
```cpp
|
||||
class ResourceScope {
|
||||
public:
|
||||
TexturePtr findTexture( const ResourceKey& key ) const;
|
||||
TexturePtr loadTexture( const TextureRequest& request );
|
||||
void publishLocal( ResourceKey key, TexturePtr texture );
|
||||
void importCatalog( ResourceCatalogPtr catalog );
|
||||
};
|
||||
```
|
||||
|
||||
Frozen lookup rules:
|
||||
|
||||
- Search the local catalog, then explicitly imported catalogs in deterministic order.
|
||||
- Never search the live registry.
|
||||
- Never implicitly search a parent, host scene, sibling scene, or every live resource.
|
||||
- The default Graphics scope imports the global catalog explicitly.
|
||||
- A UI/application scene imports the default resource catalog automatically for the common case;
|
||||
callers can disable this at construction for strict isolation and then import only the catalogs
|
||||
they deliberately expose.
|
||||
- A Web document receives the same default-catalog baseline unless created with automatic import
|
||||
disabled. It never implicitly inherits host, sibling, or other document-local catalogs.
|
||||
- Scopes import catalogs, not arbitrary scopes. This avoids recursive lookup and import cycles.
|
||||
|
||||
Pure `EE::Graphics` users may use TextureFactory for unpinned creation or Engine's default Graphics
|
||||
scope/catalog for named persistent resources. No UISceneNode is involved.
|
||||
|
||||
### 4.4 TextureFactory final API role
|
||||
|
||||
TextureFactory creates, decodes, uploads, and updates textures. Creation names return `TexturePtr`
|
||||
directly and do not retain it:
|
||||
|
||||
```cpp
|
||||
TexturePtr createEmptyTexture( ... );
|
||||
TexturePtr loadFromPixels( ... );
|
||||
TexturePtr loadFromPack( ... );
|
||||
TexturePtr loadFromMemory( ... );
|
||||
TexturePtr loadFromStream( ... );
|
||||
TexturePtr loadFromFile( ... );
|
||||
```
|
||||
|
||||
The factory registers every result with its weak live registry. It has no semantic `getByName()` or
|
||||
`getByHash()` API, no public deletion/removal API, no public registry detachment, and no owning
|
||||
`getTextures()` API. Named lookup belongs to a catalog/scope; diagnostics use registry snapshots.
|
||||
|
||||
The `TexturePtr` control block uses a factory-controlled deleter. Final release appends the raw
|
||||
texture to TextureFactory's released queue; it does not execute `Texture::~Texture()` immediately.
|
||||
`Window::display()` flushes pending batches and then calls
|
||||
`TextureFactory::collectReleasedTextures()` while the active context is current. Engine shutdown
|
||||
performs the same collection explicitly because no later display is guaranteed.
|
||||
|
||||
## 5. GPU lifetime and threading
|
||||
|
||||
### 5.1 Graphics-thread lifetime contract
|
||||
|
||||
GPU operations remain graphics-context-affine and follow the existing graphics/update or explicitly
|
||||
shared-context rules. Releasing the final `TexturePtr` is different: it performs no GPU operation and
|
||||
may enqueue the raw texture from any thread. Only collection and actual destruction require the
|
||||
graphics thread and a current context.
|
||||
|
||||
Debug builds assert the collection boundary. The design does not add a generic device state, epoch,
|
||||
or disposal mechanism for other GPU resource families; TextureFactory's small deferred-release queue
|
||||
is the texture-specific lifetime boundary already required by batched rendering.
|
||||
|
||||
### 5.2 Texture deferred destruction
|
||||
|
||||
Final `TexturePtr` release queues the Texture object in TextureFactory. It remains allocated until a
|
||||
safe collection point:
|
||||
|
||||
```cpp
|
||||
void Window::display( bool clear ) {
|
||||
GlobalBatchRenderer::instance()->draw();
|
||||
TextureFactory::instance()->collectReleasedTextures();
|
||||
swapBuffers();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Batch flushing precedes collection because the current renderer still keeps borrowed texture state.
|
||||
Long-lived render queues must eventually retain TexturePtr themselves. The released queue is also
|
||||
drained during Engine shutdown after consumers are released and before TextureFactory, Renderer or
|
||||
contexts are destroyed.
|
||||
|
||||
Other self-contained GPU classes retain their direct, graphics-thread destruction model. They are
|
||||
not routed through TextureFactory and do not acquire generic lifetime machinery unless a later
|
||||
ownership migration demonstrates a concrete need.
|
||||
|
||||
### 5.3 Resource mutation and loading threads
|
||||
|
||||
- Decode and network work may run off-thread.
|
||||
- GPU create/upload/reload/mutation and final ownership release obey the graphics-thread/shared-
|
||||
context contract.
|
||||
- Registries and caches are thread-safe at their boundaries.
|
||||
- UI mutation executes on the scene/main thread and remains protected by scene generation tokens.
|
||||
- A generation token controls whether a subscriber may mutate a scene; it does not own a resource or
|
||||
a shared request.
|
||||
- No callback, resource destruction, or device command executes while a registry/cache mutex is held.
|
||||
|
||||
## 6. Engine shutdown and restart contract
|
||||
|
||||
Engine teardown must be reordered around producer shutdown, consumer release and valid GL contexts:
|
||||
|
||||
1. Mark Engine and Web cache/resource delivery services as shutting down; reject new work.
|
||||
2. Invalidate scene/document async subscribers and stop accepting main-thread resource deliveries.
|
||||
3. Cancel/stop and join resource/network/decode producers that can create resources or callbacks.
|
||||
4. Destroy scenes, documents, UI resolvers, document leases, scene scopes, and application caches.
|
||||
5. Clear application/global catalogs and remaining manager-owned handles.
|
||||
6. Flush/discard pending rendering submissions, then collect TextureFactory's released textures.
|
||||
7. Inspect the weak texture registry. Debug/tests assert that no unexpected strong TexturePtr
|
||||
remains; a defensive shutdown sweep may release the GPU payload of reported survivors.
|
||||
8. Destroy device-dependent managers in audited order while Renderer and contexts remain valid.
|
||||
9. Destroy TextureFactory, Renderer, windows/contexts and backend state in that order.
|
||||
|
||||
The exact manager list will be produced by the Stage 1 GPU audit, but these order constraints are
|
||||
fixed:
|
||||
|
||||
- HTTP/decode producers stop before resource consumers and GPU systems are dismantled.
|
||||
- `ShaderProgramManager` releases programs before Renderer/GL dispatch is destroyed.
|
||||
- TextureFactory's deferred released queue is empty before its context disappears.
|
||||
- A TexturePtr surviving Engine destruction is a project-contract violation, reported by debug
|
||||
builds and tests rather than supported through a second device-lifetime architecture.
|
||||
- Destructors cannot recreate singletons.
|
||||
- Tests release all resource handles before recreating Engine state from zero.
|
||||
|
||||
## 7. Drawable model
|
||||
|
||||
Shared lifetime and shareable instance state are separate concerns. `isStateful()` is not a sharing
|
||||
contract and will not be used as one.
|
||||
|
||||
### 7.1 Source/instance split
|
||||
|
||||
Resource resolution caches immutable source data. UI consumers own per-consumer drawable instances:
|
||||
|
||||
```cpp
|
||||
using DrawablePtr = ResourcePtr<Drawable>;
|
||||
|
||||
DrawablePtr Drawable::clone() const;
|
||||
DrawablePtr DrawableResolver::createDrawable( const DrawableRequest& request );
|
||||
```
|
||||
|
||||
Stage 4 established this contract without introducing a parallel `DrawableSource` class hierarchy.
|
||||
Existing drawable resource types serve as source prototypes while retained by an atlas, theme,
|
||||
icon, catalog, or resolver. A prototype is never handed directly to an unrelated consumer:
|
||||
`clone()` returns independently mutable presentation state while sharing underlying
|
||||
texture/resource handles. This is simpler than duplicating every drawable type into source and
|
||||
instance classes and remains compatible with introducing immutable source-only types later when a
|
||||
concrete resource requires one.
|
||||
|
||||
eepp continues to use `Drawable::Type` for runtime drawable dispatch. Generic handle conversion
|
||||
checks that tag and then uses `static_pointer_cast`; cloning code for a statically known concrete
|
||||
type also uses `static_pointer_cast`. The ownership migration does not introduce RTTI casts.
|
||||
|
||||
Representative split:
|
||||
|
||||
- `Texture` is shared GPU/resource data, not a globally shared mutable drawable instance.
|
||||
- `TextureRegion` prototypes and instances retain a TexturePtr; instances copy rectangle, offset,
|
||||
intrinsic size, destination size, tint, and position.
|
||||
- `NinePatch` instances clone their nine mutable region children while sharing the textures.
|
||||
- `TextureDrawable` holds per-consumer destination size, tint, alpha, and position while retaining
|
||||
the shared TexturePtr.
|
||||
- `StateListDrawable`, `DrawableGroup`, and `Sprite` are per-consumer state machines/instances that
|
||||
refer to source handles or private child instances.
|
||||
|
||||
`DrawableImageParser::createDrawable()` always returns a fresh consumer instance for CSS-generated
|
||||
or resolved content, even when its immutable source came from a cache.
|
||||
|
||||
`UIIcon`, `UIGlyphIcon`, and `UISVGIcon` expose the split directly:
|
||||
|
||||
```cpp
|
||||
const DrawablePtr& UIIcon::getSource( int size ) const;
|
||||
DrawablePtr UIIcon::createDrawable( int size ) const;
|
||||
```
|
||||
|
||||
`getSource()` supports lookup, measurement, and immediate rendering without cloning an existing
|
||||
prototype. Glyph and SVG icons may materialize and cache a missing size source once.
|
||||
`createDrawable()` is the explicit consumer-instance boundary for callers that retain the drawable
|
||||
or need persistent independent state.
|
||||
|
||||
Immediate, single-threaded render paths may borrow an icon source and temporarily change
|
||||
presentation state when they restore every changed value before returning and never retain the raw
|
||||
pointer. Retained widget, menu, model, animated, or otherwise independently stateful consumers must
|
||||
create and own an instance. Shared child mutation remains forbidden where drawing can be reentrant
|
||||
or where the complete state cannot be restored locally.
|
||||
|
||||
No rendering callback may call `clone()`, `UIIcon::createDrawable()`, or an API that performs either
|
||||
operation internally. It must render either a previously retained instance or a borrowed source
|
||||
under the temporary-state contract above. The Stage 4 call-site audit classifies all remaining
|
||||
direct `clone()` calls as:
|
||||
|
||||
- implementations recursively cloning their private child state;
|
||||
- constructors and setters adopting a private region/sprite/map instance;
|
||||
- theme, skin, icon, CSS, and name-resolution source-to-instance boundaries;
|
||||
- widget deserialization and one-time assignment; or
|
||||
- focused ownership tests.
|
||||
|
||||
The code editor lock icon and ecode debugger, linter, LSP breadcrumb, and autocomplete icon paths
|
||||
borrow their per-size icon sources at the point of immediate rendering and restore temporary color
|
||||
changes before returning. Icons assigned to widgets, menus, or models still use owned instances.
|
||||
|
||||
### 7.2 Consumer API
|
||||
|
||||
Consumers store a strong per-consumer instance:
|
||||
|
||||
```cpp
|
||||
UIImage* UIImage::setDrawable( DrawablePtr drawable );
|
||||
DrawablePtr UIImage::getDrawable() const;
|
||||
```
|
||||
|
||||
The same rule applies to CSS layers, buttons, skins, icon instances, state lists, and groups. All
|
||||
`ownIt` flags and manual drawable deletion paths are removed in the same drawable migration stage.
|
||||
|
||||
`Models::Variant` will be structurally migrated to `std::variant` (or an equivalently safe
|
||||
non-union representation) with `DrawablePtr` as a normal non-trivial member. Its old owning flag and
|
||||
raw drawable alternative are removed.
|
||||
|
||||
### 7.3 Resource notifications
|
||||
|
||||
`DrawableResource::Unload` is removed as a consumer lifetime mechanism. A strong owner cannot be
|
||||
notified that its object vanished, and destructor callbacks into a partially destroyed most-derived
|
||||
object are unsafe.
|
||||
|
||||
Mutable/reloadable source data uses a typed change/invalidation signal with RAII connection tokens.
|
||||
Callbacks capture weak lifetime tokens rather than raw consumer `this` pointers. Registry expiration
|
||||
is observed through weak handles and snapshots, not an object destructor callback.
|
||||
|
||||
## 8. UI resolution layering
|
||||
|
||||
`DrawableSearcher` is replaced, but not by putting all of its behavior into Graphics::ResourceScope.
|
||||
|
||||
`UI::DrawableResolver` owns UI-specific interpretation:
|
||||
|
||||
- CSS gradients and functions
|
||||
- `url(...)` and scene-relative URI resolution
|
||||
- icons, glyphs, sprites, and generated drawable instances
|
||||
- node/scene context and UI source-to-instance creation
|
||||
|
||||
It delegates texture/source lookup and creation to the scene's `Graphics::ResourceScope`. A default
|
||||
UI resolver can use the default Graphics scope for UI applications without custom scenes, but pure
|
||||
Graphics does not depend on it.
|
||||
|
||||
Browser request policy, cookies, navigation, and cache leases are not responsibilities of either
|
||||
DrawableResolver or Graphics::ResourceScope.
|
||||
|
||||
## 9. Web document ownership and shared cache
|
||||
|
||||
### 9.1 Topology
|
||||
|
||||
Each UIWebView document has a distinct `DocumentSessionId`, document scope, and cache lease. Multiple
|
||||
documents may use one shared WebResourceCache:
|
||||
|
||||
```text
|
||||
Document scope/session A ─┐
|
||||
├── shared WebResourceCache partition
|
||||
Document scope/session B ─┘
|
||||
```
|
||||
|
||||
Navigation changes/releases only that document's lease. It never directly purges entries required
|
||||
by another document. Widgets retain their currently displayed resource/source through ordinary
|
||||
strong handles independently of cache retention.
|
||||
|
||||
### 9.2 Cache key and isolation
|
||||
|
||||
```cpp
|
||||
struct OriginKey {
|
||||
std::string scheme;
|
||||
std::string normalizedHost;
|
||||
Uint16 effectivePort;
|
||||
};
|
||||
|
||||
struct WebResourceKey {
|
||||
CachePartitionId partition;
|
||||
CanonicalURI uri;
|
||||
ResourceKind kind;
|
||||
DecodeOptions decode;
|
||||
RequestVariant requestVariant;
|
||||
};
|
||||
```
|
||||
|
||||
`CachePartitionId` represents the intentionally shared HTTP/cookie/authentication context. Different
|
||||
cookie jars or credential contexts do not share entries merely because a URI matches. Request
|
||||
variants account for content-affecting headers until full HTTP `Vary` support exists.
|
||||
|
||||
Canonicalization rules are fixed at the cache boundary:
|
||||
|
||||
- Same-origin means normalized scheme, host, and effective port all match.
|
||||
- URI fragments are removed; query strings remain part of the key.
|
||||
- Redirect metadata records both request URI and canonical final URI without merging partitions.
|
||||
- File paths use platform-aware canonicalization with explicit symlink/case behavior.
|
||||
- Data URIs use a content hash plus decode options and enforce resource-size limits.
|
||||
- Hashes accelerate lookup but full keys determine equality.
|
||||
|
||||
### 9.3 Entry state and request coalescing
|
||||
|
||||
```cpp
|
||||
enum class LoadState { Empty, Loading, Ready, Failed, Cancelled };
|
||||
|
||||
struct WebCacheEntry {
|
||||
WebResourceKey key;
|
||||
LoadState state;
|
||||
TexturePtr retainedResource;
|
||||
TextureWeakPtr liveResource;
|
||||
MonotonicTime lastUsed;
|
||||
MonotonicTime expiresAt;
|
||||
std::size_t retainedBytes;
|
||||
UnorderedSet<DocumentSessionId> activeLeases;
|
||||
std::vector<WeakSubscriber> subscribers;
|
||||
};
|
||||
```
|
||||
|
||||
Concurrent requests for one key share one fetch/decode/upload operation. Each subscriber has its own
|
||||
document session and scene generation. A stale subscriber is removed without cancelling delivery to
|
||||
other current subscribers. Cancellation of the shared operation occurs only when policy permits and
|
||||
no subscriber/cache requirement remains.
|
||||
|
||||
Retention uses monotonic TTL, LRU information, and per-partition/global byte budgets. Same-origin
|
||||
navigation may renew a document lease; cross-origin navigation releases that document's old-origin
|
||||
lease. External consumer handles remain valid regardless of cache eviction.
|
||||
|
||||
Failure entries define retry/backoff and do not become permanent accidental cache hits.
|
||||
|
||||
## 10. Migration strategy
|
||||
|
||||
No externally released intermediate state is required. Temporary duplicated retention is allowed on
|
||||
the feature branch to keep behavior valid while the repository-wide API break is assembled.
|
||||
|
||||
### Stage 0: contract freeze and inventories
|
||||
|
||||
Status: complete. Its accepted conclusions are incorporated into this architecture baseline.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- This architecture document accepted or amended.
|
||||
- Complete inventory of GPU resource classes and direct GL deletion sites.
|
||||
- Complete inventory of stored raw `Texture*`, texture-ID lookup, ignored texture-load returns,
|
||||
loader callback signatures, and factory deletion calls.
|
||||
- Complete drawable mutation/shareability inventory.
|
||||
- Build-matrix decision for shared/static libraries and `EE_MEMORY_MANAGER`.
|
||||
- Concrete shutdown dependency graph for Engine-owned producers, consumers, managers, Renderer, and
|
||||
contexts.
|
||||
|
||||
Exit criterion: no unresolved ownership, lookup, last-release-thread, Engine restart, scope import,
|
||||
or drawable-sharing contract blocks substrate implementation.
|
||||
|
||||
### Stage 0.5: prerequisite bug fixes
|
||||
|
||||
Status: complete, 2026-07-14. Concrete defects discovered by the ownership audit were fixed with
|
||||
focused regression coverage while preserving current raw factory ownership:
|
||||
|
||||
- HTTP Pool clears clients outside its mutex so joined callbacks can re-enter without deadlock.
|
||||
- Externally executed HTTP tasks cannot retain a dangling raw Http after Pool destruction.
|
||||
- TextureAtlasLoader joins/stops ResourceLoader work before callback-visible loader state is
|
||||
destroyed.
|
||||
- Engine clears TextLayout before destroying the default ResourceScope and its FontService.
|
||||
- Engine stops asynchronous resource producers before resource consumers and GPU managers.
|
||||
- UISceneNode's static async delivery queue has an explicit shutdown purge/rejection boundary.
|
||||
- Obsolete FrameBuffer context-loss reload APIs were removed.
|
||||
|
||||
TextureLoader's static callback registry is deliberately removed with Stage 1 live observation.
|
||||
Drawable ownership defects remain assigned to their structural Stage 4 replacement.
|
||||
|
||||
### Stage 1: texture lifetime scaffolding, with old factory retention still active
|
||||
|
||||
Status: complete, 2026-07-15. Stable process-wide ResourceId, eepp-compatible resource
|
||||
aliases/deleter, and TextureFactory's weak live-texture registry are
|
||||
implemented.
|
||||
`Texture::getTextureId()` now returns that ResourceId directly, and every identity-based texture
|
||||
API and stored consumer uses ResourceId; the only other texture identifier is the OpenGL handle.
|
||||
The factory now retains the single TexturePtr control block internally while public texture APIs
|
||||
still return raw pointers, preserving its old strong-retention behavior until Stage 2. Texture
|
||||
memory updates no longer reach the factory singleton, and texture destruction no longer unregisters
|
||||
itself through a singleton callback. Final handle release now queues Texture destruction in the
|
||||
factory; `Window::display()` collects only after batch flush, and Engine shutdown performs a final
|
||||
collection while its context remains valid. Debug assertions enforce graphics-thread release and
|
||||
collection, while shutdown diagnostics report and defensively release GPU payloads from surviving
|
||||
external handles. TextureLoader's static callback registry is removed. UITextureViewer reconciles
|
||||
weak snapshots only when the atomic live-registry generation changes and strongly retains only the
|
||||
currently enlarged texture.
|
||||
|
||||
Implement stable ResourceId, centralized eepp-compatible handle creation, the weak TextureFactory
|
||||
live registry, TextureFactory's deferred released-texture queue, shutdown diagnostics,
|
||||
and graphics-thread assertions. Integrate collection into Window::display() after batch flush and
|
||||
into Engine shutdown before Renderer/context destruction. Reorder Engine teardown using the audited
|
||||
dependency graph. Do not generalize this substrate to self-contained GPU resource classes.
|
||||
|
||||
Remove TextureLoader's static callback registry in the same change and migrate UITextureViewer to
|
||||
the weak live registry. Do not add an intermediate synchronization/reset contract to the old API.
|
||||
|
||||
The old raw factory ownership remains temporarily so this internal stage cannot make resources
|
||||
disappear. Public texture APIs have not switched yet; the deferred shared-pointer deleter becomes
|
||||
active in the complete Stage 2 TexturePtr cut.
|
||||
|
||||
Exit tests:
|
||||
|
||||
- Released textures are deleted only at display/final shutdown collection points.
|
||||
- Pending batches flush before texture collection.
|
||||
- Engine teardown leaves no pending released textures or unexpected live registry entries.
|
||||
- Repeated test-only Engine create/destroy cycles start with empty resource state.
|
||||
- Worker-thread final release only queues the texture; it does not run GL or destruction work.
|
||||
- `EE_MEMORY_MANAGER` accurately removes texture allocations through the factory-controlled deleter.
|
||||
|
||||
### Stage 2: one complete TexturePtr ownership cut
|
||||
|
||||
Status: complete, 2026-07-19. TextureFactory creation and acquisition APIs now return TexturePtr,
|
||||
and TextureLoader exposes handle-based state with `reset()` replacing destructive `unload()`
|
||||
semantics. TextureRegion, atlases/loaders, framebuffers, font pages and glyphs, nine-patches,
|
||||
sprites, particle systems, SVG caches, UI image/background paths, maps, tools, tests, ecode, and
|
||||
eeiv now retain texture handles. Atlas worker loads store their returned handles directly instead
|
||||
of depending on later global lookup. BatchRenderer retains handle-aware submissions until flush;
|
||||
its raw overload is limited to Texture's immediate draw path, whose queued object lifetime is
|
||||
protected by display-time deferred destruction. Sprite's obsolete texture-owner flag and public
|
||||
factory texture-removal APIs are removed. Factory-wide strong retention remains only as the
|
||||
planned temporary bridge to Stage 3.
|
||||
|
||||
Change creation/acquisition APIs to return TexturePtr and migrate every required holder in the same
|
||||
repository-wide cut. During conversion, TextureFactory temporarily retains strong handles so an
|
||||
unclassified ignored result cannot silently expire.
|
||||
|
||||
At minimum migrate:
|
||||
|
||||
- TextureLoader state, return types, callbacks, unload behavior, and async captures
|
||||
- Texture GIF frame ownership
|
||||
- TextureRegion and region-source texture ownership
|
||||
- TextureAtlas, TextureAtlasLoader, and texture vectors
|
||||
- FrameBuffer attachment ownership
|
||||
- fonts and glyph texture caches
|
||||
- nine-patches and region chains
|
||||
- SVG raster caches and UI icons
|
||||
- sprites and particle systems that retain textures/regions
|
||||
- UIImage and UINodeDrawable texture paths
|
||||
- debug texture viewer and live-resource diagnostics
|
||||
- eepp, ecode, modules, examples, tools, and tests
|
||||
|
||||
Every stored raw Texture pointer is classified as strong, weak, or a short borrow dominated by a
|
||||
documented owner. Add a source audit/clang-tidy check where practical.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Regions, atlases, framebuffers, fonts, glyphs, UI consumers, and async work retain dependencies.
|
||||
- No ignored factory load result is relied upon for later global lookup.
|
||||
- No public texture delete/remove API remains.
|
||||
- Diagnostic snapshots do not pin resources.
|
||||
- Temporary factory retention can be removed without failing ownership tests.
|
||||
|
||||
### Stage 3: catalog and scope ownership cutover
|
||||
|
||||
Status: complete, 2026-07-19. Engine now owns the global catalog and default Graphics scope;
|
||||
UISceneNode owns an isolated scope that can be shared explicitly. TextureFactory is an unpinned
|
||||
creator and weak live registry with no semantic name/hash lookup. Atlas, map, UI image/background,
|
||||
DrawableSearcher, ecode, tests, and other name-based consumers publish to and resolve through their
|
||||
explicit scope. Catalog aliases and imports provide intentional persistence and deterministic
|
||||
sharing. Worker-thread final TexturePtr release is handed to the factory's thread-safe queue and
|
||||
actual deletion remains display/shutdown-bound. The full cut also corrected TextureLoader's decoder
|
||||
pixel allocator provenance, which asynchronous scoped loading exposed.
|
||||
|
||||
Implement the global catalog, default Graphics scope, application/scene catalogs, explicit imports,
|
||||
immutable keys, and aliases. Move intended persistent resources from temporary factory retention into
|
||||
catalogs/caches. Remove factory-wide strong retention and activate final unpinned creation.
|
||||
|
||||
Remove semantic TextureFactory name/hash lookup and migrate every lookup to a scope/catalog.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Dropping the last real consumer/catalog/cache handle expires a texture.
|
||||
- Duplicate names in unrelated scopes resolve independently.
|
||||
- Sibling/document resources are invisible without explicit catalog import.
|
||||
- Global resources persist only because the global catalog owns them.
|
||||
- Live diagnostic records cannot be resolved semantically.
|
||||
- Scope destruction does not invalidate externally retained resources.
|
||||
|
||||
### Stage 4: drawable source/instance conversion
|
||||
|
||||
Status: complete, 2026-07-20. Drawable ownership
|
||||
now uses `DrawablePtr`; textures create private
|
||||
`TextureDrawable` wrappers; mutable prototypes implement `clone()`; sprites, state lists,
|
||||
skins, groups, nine-patches, regions, glyphs, gradients, and primitive drawables clone their
|
||||
presentation state. UIImage, UINodeDrawable, menus/icons/themes, parsers, editor/tool consumers,
|
||||
maps, physics, ecode, and eeiv were migrated in the same API cut.
|
||||
|
||||
`DrawableResource::Unload` and callback IDs were replaced by Change-only RAII connections.
|
||||
Callback state is allocated lazily on the first connection, so ordinary drawable resources carry
|
||||
no callback allocation. Callback storage and notification snapshots use small inline buffers;
|
||||
snapshotting preserves safe self-disconnection and reentrant mutation during notification without
|
||||
allocating in the common case.
|
||||
`Variant` stores DrawablePtr outside its scalar union. UITextureRegion and ScrollParallax render
|
||||
with local geometry rather than temporarily resizing shared source regions; region-based map
|
||||
objects retain private instances. `DrawableSearcher` already returns fresh instances as a safe
|
||||
bridge, but its replacement by the layered UI resolver remains Stage 5.
|
||||
|
||||
`UIIcon::getSource()` now returns a cached source/prototype for lookup, measurement, and immediate
|
||||
single-threaded rendering under the temporary-state restoration contract, while
|
||||
`UIIcon::createDrawable()` explicitly creates one private consumer instance. `UIGlyphIcon` and
|
||||
`UISVGIcon` cache their lazily materialized sources under the same contract. The complete `clone()`
|
||||
call-site audit found no remaining render-loop cloning. The code editor, debugger, linter, LSP
|
||||
breadcrumb, and autocomplete draw-only paths borrow sources directly; retained widget and menu
|
||||
icons continue to own instances.
|
||||
|
||||
Introduce source types and per-consumer instances, remove shared draw-state mutation, replace manual
|
||||
ownership with DrawablePtr, remove Unload lifetime callbacks, add RAII change connections, and
|
||||
migrate Variant's storage.
|
||||
|
||||
Convert UIImage, UINodeDrawable layers, StateListDrawable, DrawableGroup, Sprite, UIPushButton,
|
||||
UISkin, UIIcon, themes, and DrawableImageParser in one coherent cut.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Two consumers using one source have independent tint, alpha, size, position, state, and animation.
|
||||
- Nested/reentrant drawing leaves no shared source or child modified.
|
||||
- Drawable groups do not reposition shared child instances.
|
||||
- Variant copying cannot duplicate exclusive ownership.
|
||||
- No drawable `ownIt` or manual child deletion remains.
|
||||
|
||||
### Stage 5: layered UI resolution
|
||||
|
||||
Status: complete, 2026-07-21. `DrawableSearcher` was removed from Graphics. `ResourceScope` now
|
||||
provides scoped drawable lookup for textures, atlas regions, nine-patches, and sprites, preserving a
|
||||
pure Graphics entry point with no UI dependency. Each `UISceneNode` owns an allocation-free
|
||||
`UI::DrawableResolver` that reads the scene's current scope and referer when resolving file, data,
|
||||
HTTP, and named drawable references. UI images, sprites, menus, CSS parsing, and tests use the scene
|
||||
resolver; callers without a scene explicitly construct one over `defaultResourceScope()`.
|
||||
|
||||
CSS icon resolution now uses the requesting node's scene instead of the process-global scene.
|
||||
Texture names remain isolated by local/imported catalogs and become visible across scenes only when
|
||||
their scopes or catalogs are shared intentionally. Atlas, nine-patch, and sprite manager migration
|
||||
to scoped catalogs remains Stage 7 work for those resource families.
|
||||
|
||||
Implement UI::DrawableResolver and replace DrawableSearcher. Scene resolvers delegate Graphics work
|
||||
to their explicit scope. Keep CSS/icon/glyph interpretation in UI and cookie/navigation concerns in
|
||||
Web services.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Pure Graphics works without UI.
|
||||
- UI resolves through its scene/application scope.
|
||||
- Embedded documents cannot see sibling resources accidentally.
|
||||
- Host/application assets require explicit export/import.
|
||||
|
||||
### Stage 6: WebResourceCache and document leases
|
||||
|
||||
Status: complete, 2026-07-21. Each UISceneNode now owns a WebResourceCache document session with
|
||||
an explicit cache partition and navigation generation. UIWebView advances that session when the
|
||||
replacement document is installed, after the previous document has been detached. Document,
|
||||
stylesheet, remote font, image, and CSS background image
|
||||
requests share canonical fragment-free keys that include the partition, request method/body and
|
||||
headers, resource kind, and image decode options.
|
||||
|
||||
Concurrent requests coalesce into one fetch and one image decode/upload. Subscribers retain their
|
||||
own document generation, so navigation or destruction removes stale delivery without cancelling a
|
||||
request needed by another session. Applications can install one cache and explicit partition into
|
||||
multiple WebViews to share eligible public resources; distinct partitions and content-affecting
|
||||
headers remain isolated. Scene ResourceScope entries remain an explicit override but fetched Web
|
||||
resources are retained only by consumers, document leases, and the cache.
|
||||
|
||||
Completed entries use monotonic TTL and LRU timestamps plus a configurable byte budget. Active
|
||||
document leases are not evicted; the TTL starts when the final document lease is released, allowing
|
||||
Back/Forward navigation to reuse resources regardless of how long the previous document remained
|
||||
open. Navigation releases only that session's previous leases, and UIWebView performs throttled
|
||||
cache maintenance so expired unleased entries are collected even when no new requests complete. Failed loads
|
||||
retry, redirect/final cookies are delivered only to current subscribers, and image upload is
|
||||
dispatched through the scene's guarded main-thread resource queue. Common lease lists use inline
|
||||
small vectors, and completed entries release starter request bodies, headers, and dispatchers.
|
||||
|
||||
Implement cache partitions, canonical keys/origins, per-document sessions and leases, in-flight
|
||||
coalescing, per-subscriber generation guards, retries, TTL/LRU, and byte budgets. Integrate WebView
|
||||
navigation at its existing document replacement boundary.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Shared tabs reuse eligible resources without sharing document ownership.
|
||||
- Navigation in one tab cannot evict another tab's active lease.
|
||||
- Different cookie/auth partitions cannot reuse credential-dependent responses.
|
||||
- Stale subscribers do not block current subscribers.
|
||||
- Same-origin retention and cross-origin lease release follow policy.
|
||||
- Cache memory remains within configured budgets.
|
||||
|
||||
### Stage 7: remaining resource families
|
||||
|
||||
Status: complete, 2026-07-24. Nine-patches and texture atlases are migrated. `NinePatch::New()`,
|
||||
`TextureRegion::New()`, and `TextureAtlas::New()` return handles. Atlases retain region and texture
|
||||
handles; loaders retain and publish atlas handles; theme-owned catalogs retain their named atlas
|
||||
sources; and scene `ResourceScope` imports make those sources visible intentionally.
|
||||
`NinePatchManager`, `TextureAtlasManager`, and `GlobalTextureAtlas` were removed. Removing a catalog
|
||||
entry releases only catalog ownership and leaves separately retained consumers valid.
|
||||
|
||||
This is the required pattern for the remaining process-wide singleton resource managers. A
|
||||
singleton must not be modernized into another process-global semantic namespace. Each family moves
|
||||
to ordinary catalogs owned by its application, scene, theme, document, or other natural lifetime
|
||||
boundary. `globalResourceCatalog()` is reserved for resources deliberately published process-wide;
|
||||
scene scopes see non-global resources only through their local catalog or explicit imports.
|
||||
|
||||
Migrate fonts, font faces/fallback caches, themes/icons, shader programs/shaders, and every remaining
|
||||
raw-owning ResourceManager subclass one family at a time. Their self-contained GPU objects retain
|
||||
the established graphics-thread destruction contract unless a concrete migration requires
|
||||
otherwise.
|
||||
|
||||
For fonts, ownership is separate from rendering policy. Font handles and semantic lookup live in
|
||||
naturally owned catalogs: application defaults use the default scope, while author `@font-face`
|
||||
resources are owned by their document scene. Every `ResourceScope` owns an inline `FontService` for
|
||||
its rendering configuration, emoji fonts, configured fallbacks, and system fallback cache. Fonts
|
||||
retain only a borrowed service pointer while published in that scope and are detached when removed
|
||||
or when the scope is destroyed. Raw `Font*` values in text/layout/style structures remain borrowed
|
||||
views whose enclosing application, scene, theme, or fallback service retains the corresponding
|
||||
handle. The former process-wide `FontManager` singleton and compatibility namespace were removed.
|
||||
Publishing a font with an existing local key replaces that catalog binding; the legacy manager
|
||||
behavior that silently suffixed duplicate font names is intentionally not preserved.
|
||||
|
||||
Themes, skins, icon themes, and icons now use shared resource handles. Each scene's
|
||||
`UIThemeManager` owns its themes and imports their catalogs into that scene's scope; it is not a
|
||||
process-wide semantic namespace. Shader and shader-program factories return handles, programs own
|
||||
their constituent shaders, and catalogs/scopes can publish shader programs under semantic keys.
|
||||
`ShaderProgramRegistry`, `VertexBufferRegistry`, and `FrameBufferRegistry` remain process-wide only
|
||||
as non-owning inventories of live OpenGL-context objects used for reload and diagnostics.
|
||||
|
||||
The raw-owning `ResourceManager<T>` and `ResourceManagerMulti<T>` templates were removed after their
|
||||
last consumers migrated.
|
||||
|
||||
## 11. Required validation matrix
|
||||
|
||||
### Ownership and registry
|
||||
|
||||
- Unpinned texture expires after its last real owner releases it.
|
||||
- Region/source retains its texture; atlas retains its sources/textures; framebuffer retains its
|
||||
attachment.
|
||||
- Catalog publication retains; erasing one catalog entry does not invalidate other owners.
|
||||
- Independent catalog/pin leases do not interfere.
|
||||
- Registry snapshot and UITextureViewer do not retain all resources.
|
||||
- Registry creation, snapshot, expiration, and purging are race-safe.
|
||||
|
||||
### GPU/thread lifetime
|
||||
|
||||
- Final texture release on the graphics thread queues rather than immediately deleting.
|
||||
- Worker-thread final release safely queues; display performs the destruction with a current context.
|
||||
- Display flushes batches before collecting released textures under the current context.
|
||||
- Engine shutdown performs a final collection before TextureFactory/Renderer/context destruction.
|
||||
- No deletion/callback occurs while registry/cache locks are held.
|
||||
- Repeated test-only Engine creation starts with no prior texture handles or registry state.
|
||||
- Relevant suites run under TSAN as well as ASAN/LSAN.
|
||||
|
||||
### Scope isolation
|
||||
|
||||
- Identical keys in sibling scopes resolve independently.
|
||||
- Explicit imported catalog resolves; missing import does not fall back.
|
||||
- Global catalog is visible only to scopes importing it.
|
||||
- Registry records are never semantically resolvable.
|
||||
- Scope/catalog destruction leaves externally retained resources alive.
|
||||
- Catalog import order is deterministic and graph cycles are structurally impossible.
|
||||
|
||||
### Drawable independence
|
||||
|
||||
- One source displayed by two widgets with different tint, alpha, and size.
|
||||
- Two region instances draw at different sizes without shared mutation.
|
||||
- State-list and sprite instances maintain independent state/time.
|
||||
- Nested/reentrant drawing restores nothing because shared objects were not mutated.
|
||||
- DrawableGroup owns/private-instantiates mutable children.
|
||||
- Variant copy/move/reset is safe with DrawablePtr.
|
||||
|
||||
### Web cache
|
||||
|
||||
- Same-origin navigation reuses resources.
|
||||
- Cross-origin navigation releases only the navigating document lease.
|
||||
- Two tabs share eligible entries and survive independent navigation.
|
||||
- Different cookie/auth partitions do not share.
|
||||
- URI plus different decode/request options produces distinct entries.
|
||||
- Concurrent requests coalesce while subscribers retain independent generation guards.
|
||||
- Failed requests retry according to explicit policy.
|
||||
- TTL uses a monotonic clock; byte-budget eviction works independently.
|
||||
|
||||
### Teardown and repeatable tests
|
||||
|
||||
- Pending HTTP, decode, and upload work during scene and Engine destruction.
|
||||
- No destructor recreates Engine, Renderer, TextureFactory, or another manager.
|
||||
- Repeated Engine create/destroy cycles in one process.
|
||||
- Each unit test releases its scopes, catalogs, leases, and resources independently.
|
||||
- Test teardown reports unintended catalog pins and live resource provenance.
|
||||
- `EE_MEMORY_MANAGER`, supported debug/release configurations, static builds, and shared-library
|
||||
builds.
|
||||
|
||||
## 12. Next implementation deliverable
|
||||
|
||||
The ownership migration is complete through Stage 7. Follow-up work is validation and API polish:
|
||||
expand scene-lifetime and repeated-Engine coverage, audit public ownership documentation, add
|
||||
debug-only borrowed GPU lifetime checks, audit non-trivial static initialization, and rename
|
||||
compatibility-era manager filenames when the public include transition is scheduled. OpenGL
|
||||
context-loss recreation is explicitly outside the supported lifecycle contract.
|
||||
@@ -46,6 +46,10 @@ Run this **after** all edits and **before** attempting to compile.
|
||||
## Step 2: Compile the Project
|
||||
To compile the project in debug mode, execute the `make` command, ensuring you point to the correct directory for your current Operating System.
|
||||
|
||||
Always use all processors reported by the platform when selecting the parallel job count. On Linux
|
||||
and other systems with `nproc`, use `-j$(nproc)` exactly; do not substitute an arbitrary fixed value
|
||||
such as `-j4`. Use the platform-equivalent processor-count command where `nproc` is unavailable.
|
||||
|
||||
The valid OS directory names are: `windows`, `macosx`, `linux`, `bsd`, `haiku`.
|
||||
|
||||
Run the following command, replacing `<os_name>` with the correct environment:
|
||||
|
||||
@@ -6,7 +6,50 @@ The goal is browser-compatible behavior where implemented, not a parallel eepp-s
|
||||
|
||||
## Spec-First Requirement
|
||||
|
||||
Agents must implement HTML/CSS features by following the official specifications first. Do not add element-specific hacks just to match one fixture if the behavior belongs to a generic CSS concept.
|
||||
HTML/CSS compatibility work is **specification implementation**, even when the request begins with a
|
||||
visual difference in one website fixture. A screenshot identifies a symptom; it does not define the
|
||||
layout rule. Agents must not patch toward fixture pixels until they can name the generic HTML/CSS
|
||||
mechanism that produces those pixels.
|
||||
|
||||
### Mandatory Decision Process
|
||||
|
||||
Before editing layout or rendering code, write down in the working notes or plan:
|
||||
|
||||
1. The generic concept involved, such as inline baseline calculation, margin collapsing, intrinsic
|
||||
sizing, replaced elements, float exclusion, or border painting.
|
||||
2. The specification section or browser-defined user-agent behavior that governs it.
|
||||
3. The engine abstraction that owns the behavior. Prefer parsers, computed style, formatting
|
||||
contexts, line layout, or generic painting over element classes and fixture code.
|
||||
4. A fixture-independent invariant that a focused test can assert.
|
||||
|
||||
If those four points cannot be identified, continue investigating before implementing a fix.
|
||||
|
||||
### Prohibited Fix Patterns
|
||||
|
||||
Do not put website names, fixture IDs/classes, DOM ancestry from one page, or special coordinates
|
||||
from a screenshot into generic layout/rendering code. Do not change an HTML element default when the
|
||||
problem belongs to a CSS formatting rule. Do not introduce unexplained pixel offsets, font-size
|
||||
multipliers, or thresholds chosen only because they improve a golden image.
|
||||
|
||||
Element-specific behavior is allowed only when HTML or a user-agent stylesheet actually defines
|
||||
behavior for that element, such as replaced form controls or `summary { display: list-item; }`. In
|
||||
that case, document the HTML/UA rule being implemented; the triggering website is still not the
|
||||
justification.
|
||||
|
||||
### Required Evidence Before Completion
|
||||
|
||||
- Add a focused regression test that does not depend on the original website and expresses the
|
||||
generic invariant. Keep the real fixture or golden test as integration coverage, not as the only
|
||||
proof.
|
||||
- Explain every new layout constant from a specification, font metric, existing engine convention,
|
||||
or documented user-agent choice. If it has no principled source, do not add it.
|
||||
- Search the generic implementation diff for fixture names, selectors, and unexplained constants.
|
||||
- In the final handoff, state the governing concept, the abstraction where it was fixed, and why the
|
||||
solution applies beyond the original fixture.
|
||||
|
||||
A visually improved fixture is not sufficient evidence of correctness. If the focused generic test
|
||||
and the fixture disagree, investigate the model or document an intentional unsupported subset rather
|
||||
than adding a second compensating adjustment.
|
||||
|
||||
Primary references:
|
||||
|
||||
|
||||
@@ -16,3 +16,10 @@ When working on this project, rely on the following resources to understand exis
|
||||
* **Basic Documentation:** Found in `docs/articles/`.
|
||||
* **Implementation Examples:** A wide variety of examples showing how to use the library are located in `src/examples/`.
|
||||
* **General Context:** The `README.md` at the root directory contains deeper project details.
|
||||
|
||||
## C++ Virtual Method Style
|
||||
Follow the convention already used by the class being edited. In particular, when a class declares
|
||||
virtual methods without the `override` specifier, do not introduce `override` on new methods in that
|
||||
class. Mixing the styles can enable Clang's inconsistent-missing-override warnings for the existing
|
||||
declarations. A class-wide conversion is a separate change and must update all applicable methods
|
||||
together.
|
||||
|
||||
Reference in New Issue
Block a user