Repository navigation
Adds design doc for parallel gles compilation #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
060c38e
670ab34
4827f85
b745ec8
084dd05
524a817
39f53c1
c50a220
06e2d16
fd7e242
877b2eb
99d6346
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| --- | ||
| type: rfc | ||
| rfc: '210.0001' | ||
| title: Parallel OpenGLES Linking in Impeller | ||
| description: Outlines how Impeller will implement parallel shader compilation with the OpenGLES backend. | ||
| status: draft | ||
| created: 2026-10-09T00:00:00Z | ||
| updated: 2026-10-09T23:07:13.777951Z | ||
| tags: | ||
| - 210-graphics | ||
| authors: | ||
| - https://github.com/gaaclarke | ||
| - https://github.com/walley892 | ||
| --- | ||
|
|
||
|
gaaclarke marked this conversation as resolved.
|
||
| # RFC 210.0001: Parallel OpenGLES Linking in Impeller | ||
|
|
||
| ## Description | ||
|
|
||
| This design doc lays out a refactoring of the pipeline compilation process on | ||
| Impeller OpenGLES that allows supporting backends to parallelize shader | ||
| compilation which will increase the likelihood that a shader is immediately | ||
| availble for use in the first frames of rendering. | ||
|
|
||
| ## Background | ||
|
|
||
| On backends like Vulkan and Metal we are able to precompile shaders at | ||
| compile-time. For the OpenGLES backend we need to compile them at runtime. The | ||
| API provides a few functions to help make that happen: | ||
|
|
||
| - [glCompileShader](https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glCompileShader.xhtml) | ||
| - [glLinkProgram](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glLinkProgram.xhtml) | ||
| - [GL_LINK_STATUS](https://registry.khronos.org/OpenGL-Refpages/gl4/html/glLinkProgram.xhtml) | ||
|
|
||
| The OpenGLES standard doesn't specify exactly when the linking needs to be done, | ||
| it just has to be realized before it's actually used. This allows some drivers | ||
| to allow parallel compilation jobs to happen at the same time. Currently | ||
| Impeller is not taking advantage of this. It is instead serially issuing | ||
| compilation jobs and querying the `GL_LINK_STATUS` which forces synchronization | ||
| on the compilation job. | ||
|
|
||
| In Impeller we work with Pipelines, but from the OpenGLES perspective there are | ||
| programs and there are pipelines. A program represents a compiled bundle of | ||
| shaders, for example a vertex shader and a fragment shader. A pipeline | ||
| represents a variant of those compiled shaders, for example with specialization | ||
| constants set. Programs can be shared across pipelines and they have their own | ||
| cache. | ||
|
|
||
| ANGLE supports parallel compilation, which is what Impeller uses on Windows. On | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just curious, how much parallel compilation does Impeller Windows actually do? I assume Windows will be able to do even more parallel compilation after this change?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't do any before this change.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, I misunderstood this bit as "Impeller on Windows does parallel compilation thanks to ANGLE". I'd consider rewording this sentence to "Impeller uses ANGLE on Windows, which supports parallel compilation" to avoid that confusion :) |
||
| Android it doesn't seem to be as common. There isn't a direct check for support | ||
| but we can infer from the presence of the `KHR_parallel_shader_compile` | ||
| extension that some devices do support it. | ||
|
|
||
| ## Technical Details | ||
|
|
||
| The PipelineLibraryGLES design as it stands today is not well suited to handle | ||
| parallel compilation jobs. For one thing, the logic for the PipelineLibraryGLES | ||
| class is split among 3 different classes: PipelineLibraryGLES, | ||
| PipelineCompileQueue, PipelineCompileQueueGLES. This makes it difficult for us | ||
| to reason about and make the necessary changes. The following refactors will | ||
| first need to be redone: | ||
|
|
||
| - Delete PipelineCompileQueue - It's artificially making the Vulkan compile | ||
| queue and the OpenGLES queue behaves similarly for no reason. There is one | ||
| place in the code that grabs the queue from the library and talks to it | ||
| directly to eagerly perform a job. That request should come through the | ||
| library directly. | ||
| - Create a class to represent pending programs - This will allow us to maintain | ||
| the concept of a compiling program when it no longer is a synchronous | ||
| operation. | ||
|
|
||
| ### Class diagram | ||
|
|
||
| Here's what things will look like after the refactoring. | ||
|
|
||
| ```mermaid | ||
| classDiagram | ||
|
|
||
| class CompileJob { | ||
| -descriptor | ||
| +Start()* | ||
| +Finish()* | ||
| } | ||
|
|
||
| class BasicTaskRunner { | ||
| +PostTask(task)* | ||
| } | ||
|
|
||
| class ReactorGLES { | ||
| +React() | ||
| } | ||
|
|
||
| class PipelineCompileQueueGLES { | ||
| -supports_parallel_compile | ||
| -max_inflight_compile_jobs | ||
| -atomic~int~ outstanding_jobs | ||
| } | ||
|
|
||
| PipelineCompileQueueGLES *-- BasicTaskRunner | ||
| PipelineCompileQueueGLES *-- "*" CompileJob | ||
| PipelineCompileQueueGLES *-- "* compiling" CompileJob | ||
|
|
||
| class PipelineLibrary { | ||
| +GetPipeline(descriptor, bool async) PipelineFuture | ||
| +GetPipeline(descriptor, bool async, bool threadsafe)* PipelineFuture | ||
| } | ||
|
|
||
| class PipelineLibraryGLES { | ||
| -ProgramMap programs | ||
| -PipelineMap pipelines | ||
| -CreatePipeline(library, descriptor, vert, frag, bool threadsafe) PipelineGLES | ||
| -LinkProgram(reactor, pipeline, vert, frag) bool | ||
| } | ||
|
|
||
| PipelineLibrary <|-- PipelineLibraryGLES | ||
| PipelineLibraryGLES *-- ReactorGLES | ||
| PipelineLibraryGLES *-- "*" PipelineFuture | ||
| PipelineLibraryGLES *-- PipelineCompileQueueGLES | ||
| PipelineLibraryGLES *-- "*" PendingProgram | ||
|
|
||
| class PendingProgram { | ||
| +Compile() | ||
| +Wait() | ||
| } | ||
|
|
||
| class PipelineFuture { | ||
| +descriptor | ||
| +future | ||
| +Get() Pipeline | ||
| } | ||
| ``` | ||
|
|
||
| ### Sequence diagram | ||
|
|
||
| This shows how managing jobs will happen with the PipelineCompileQueueGLES and | ||
| the io thread. | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant raster as (raster) | ||
| participant io as (io) | ||
| participant library as :PipelineLibraryGLES | ||
| participant queue as :PipelineCompileQueueGLES | ||
| participant joba as a:Job | ||
| participant jobb as b:Job | ||
| participant jobc as c:Job | ||
| participant jobd as d:Job | ||
|
|
||
| raster->>+library: GetPipelineAsync | ||
| library->>+queue: PostJobForDescriptor | ||
| queue->>+io: ScheduleRun | ||
| io-->>-queue: return | ||
| queue-->>-library: return | ||
| library-->>-raster: return | ||
|
|
||
| io->>+queue: Run | ||
| queue->>+queue: FinishOldestJob | ||
| Note right of queue: Happens because max pending links is hit | ||
| queue->>+jobd: Wait | ||
| Note right of jobd: glGetStatus(LINK_STATUS) | ||
| jobd-->>-queue: return | ||
| queue-->>-queue: return | ||
| queue->>+io: ScheduleRun | ||
| io-->>-queue: return | ||
| queue-->>-io: return | ||
|
|
||
| io->>+queue: Run | ||
| queue->>+joba: Start | ||
| joba-->>-queue: return | ||
| queue->>+io: ScheduleRun | ||
| io-->>-queue: return | ||
| queue-->>-io: return | ||
|
|
||
| raster->>+library: GetPipelineSync | ||
| library->>+queue: PerformJobEagerly | ||
| Note right of queue: Job is started but not completed | ||
| queue->>+jobb: Wait | ||
|
|
||
| io->>+jobb: Wait() | ||
| jobb-->>-io: return | ||
|
|
||
| jobb-->>-queue: return | ||
| queue-->>-library: return | ||
| library-->>-raster: return | ||
|
|
||
| raster->>+library: GetPipelineSync | ||
| library->>+queue: PerformJobEagerly | ||
| Note right of queue: Job is not started | ||
| queue->>+jobc: ExecSync | ||
| jobc-->>-queue: return | ||
| queue-->>-library: return | ||
| library-->>-raster: return | ||
|
|
||
| raster->>+library: Delete | ||
| library->>+queue: Delete | ||
| queue->>+queue: FinishPendingJobs | ||
| io->>+joba: Wait | ||
| joba-->>-io: return | ||
| queue-->>-queue: return | ||
| queue-->>-library: return | ||
| library-->>-raster: return | ||
| ``` | ||
|
|
||
| ### Pseudocode | ||
|
|
||
| Here is the logic that will happen inside of the queue. It works recursively on | ||
| the io thread, doing one operation at a time and allows out of order | ||
|
gaaclarke marked this conversation as resolved.
|
||
| synchronization when an eager request happens from the raster thread. | ||
|
|
||
| ```dart | ||
| class PipelineQueue { | ||
| // Raster thread | ||
| void StartJob(Job job) { | ||
| _inactive_jobs.Add(job) | ||
| ScheduleRun() | ||
| } | ||
|
|
||
|
|
||
| // io thread | ||
| void Run() { | ||
| if (_eager_request){ | ||
| Job job = _active_jobs.Find(_eager_request) | ||
| _eager_request = null | ||
| if (job) { | ||
| job.Synchronize() | ||
| } | ||
| _condition_variable.Signal() | ||
| ScheduleRun() | ||
| } else if (_active_jobs.count < LIMIT) { | ||
| Job job = _inactive_jobs.Pop() | ||
| job.Start() | ||
| _active_jobs.Add(job) | ||
| ScheduleRun() | ||
| } else { | ||
| Job job = _active_jobs.Pop() | ||
| if (job) { | ||
| job.Synchronize() | ||
| ScheduleRun() | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // Raster thread | ||
| void RunEager(Desc desc) { | ||
| Job job = _inactive_jobs.Remove(desc) | ||
| if (job) { | ||
| job.Start() | ||
| job.Synchronize() | ||
| } else { | ||
| _eager_request = desc | ||
| ScheduleRun() | ||
| _condition_variable.Wait() | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The LIMIT value will be informed by the number of cores avaiable to the machine. | ||
| The driver itself is managing its own thread pools and the LIMIT just controls | ||
| how much we make visible to the io thread in order to limit the latency when | ||
| performing a compilation eagerly. | ||
|
|
||
| ## Testing | ||
|
|
||
| We already have the benefit of many high level integration tests. Since this | ||
| change is happening under the covers and at a critical point in rendering that | ||
| 100% of renders will hit that will provide a lot of coverage. | ||
|
|
||
| The new logic, parallel execution, will be exercised in unit tests on | ||
| PipelineCompileQueueGLES. The following cases should be be covered: | ||
|
|
||
| - Destruction with outstanding pending jobs and active jobs | ||
| - Eager execution swaps the priority of synchronization of active jobs | ||
| - Hammering the queue enforces a limited number of jobs | ||
|
|
||
| ## Alternatives considered | ||
|
|
||
| ### Use the `KHR_parallel_shader_compile` extension | ||
|
|
||
| There is an extension that directly deals with parallel compilation of shaders. | ||
| It opens up an API that allows one to query the status of a compile job without | ||
| blocking. Previous attempts at fixing this problem used that API and guarded | ||
| parallel execution on the presence of that extension. | ||
|
|
||
| That API is a polling API, there is no callback when compilation is complete. No | ||
| good heuristic could be thought up that does polling efficently without some | ||
| tradeoffs. It is also possible that a driver supports parallel compilation but | ||
| does not support the extension. By implementing the code with the assumption of | ||
| parallel compilation, the code is cleaner and we capture more potential benefits | ||
| if the drivers support it. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can choose to have one sentence on a line or one paragraph on a line. 80col formatting leads to reflow and large line diffs if you edit it later.