Skip to content

[Impeller][GLES] Link programs in parallel on ANGLE with GL_KHR_parallel_shader_compile - #193066

Closed
krll-kov wants to merge 12 commits into
flutter:masterfrom
krll-kov:impeller-gles-parallel-shader-compile
Closed

krll-kov wants to merge 12 commits into
flutter:masterfrom
krll-kov:impeller-gles-parallel-shader-compile

Conversation

@krll-kov

@krll-kov krll-kov commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Design doc and testing: https://docs.google.com/document/d/1eLrXEWoyWm8AZQBioqcHrQeVM2NAPDYbaEB4z_HOQs0

Windows starts Impeller on ANGLE, and ANGLE cross compiles every program to HLSL, so linking the built in programs dominates startup. PipelineLibraryGLES links them one at a time on the IO task runner: each job calls glLinkProgram and then asks for GL_LINK_STATUS, and that query blocks until the driver finishes. The driver can link several programs at once when it advertises GL_KHR_parallel_shader_compile, and this change lets it.

The link is split in two. A compile queue job compiles both shaders and calls glLinkProgram without asking for any status, and a later job on the same task runner checks the statuses and finishes the pipelines. The deferred path runs only inside a PipelineCompileQueueGLES job, only for asynchronous requests, and only when the driver is ANGLE and advertises the extension. Every other case links as before, including synchronous GetPipeline. The compile queue itself changes on every GLES backend: it counts the jobs it runs and reports when it runs out of them, which costs one weak pointer lock per job.

Two things bound the work. At most std::clamp(hardware_concurrency / 2, 1, 4) links are started before the library checks them, the formula ContextVK::ChooseThreadCountForWorkers uses for the same reason (#143540). The checks run when the compile queue reports that it has no jobs left, so the programs still queued reach the driver before the first status query waits for one, and earlier when that limit is reached.

Towards #191860. Also, this does not fix 193048, where a FragmentProgram that an application preloads is linked after the window is shown: the links run in parallel here, and the work still happens where it happens now, not before the first frame. Program linking is one part of the startup gap: on the mini PC the application with 8 shaders reaches Dart main in 159 to 162 ms with this change and in 163 to 166 ms with the artifact, so whatever happens before the first pipeline request is untouched.

Results

flutter_gallery and a local application with 8 FragmentProgram shaders, both in profile, on two machines: a mini PC with a Ryzen AI 9 HX 370 and a Radeon 890M, 24 logical cores, Windows 11 at 120 Hz, and a Windows 11 virtual machine on Parallels on Apple silicon, where x64 runs through emulation and the GPU is virtual. Artifact is the engine artifact of the same commit. The median of 15 rounds unless the row says otherwise:

mini PC, artifact mini PC, this change VM, artifact VM, this change
gallery, first frame 1068 ms 630 ms 680 ms 507 ms
8 shaders, first frame 2568 ms 1752 ms 1799 ms 1279 ms
8 shaders, frames drawn 3 s after start 2 135 115 181
8 shaders, longest gap between frames in the first 5 s 243 ms 65 ms 127 ms 70 ms

The builds of this change are instrumented lab builds, see How it is measured. The gallery row comes from 5 rounds. On the mini PC it is the median time to the first visible window, which the Windows runner shows with the first frame. On the VM it is timeToFirstFrameRasterized from start_up_info.json, averaged the way devicelab averages flutter_gallery_win_desktop__start_up, and counted from the engine start, so the two machines are not comparable with each other in that row.

The mini PC shows the effect most clearly. The artifact starts in two modes there: 3 of 15 launches reach the first frame in 1619 to 1772 ms and the other 12 in 2540 to 2669 ms. With this change 13 of 15 launches take the fast mode. The VM, where x64 runs through emulation and the GPU is virtual, shows a smaller effect with a wider spread.

The Dart side pays for the change: on the VM frameworkInit rises by 8 ms and timeToFirstFrame by 30 ms, most likely because the driver threads now run next to the isolate while it starts.

The fallback path shows no clear regression, though a small one is not ruled out. With the new path disabled, the same binary reaches the first frame before the artifact on both machines. Counted from the process start on the mini PC, it draws slightly fewer frames than the artifact in 5 of 7 paired rounds, 57 against 61 by 3.5 s and 238 against 245 by 5 s at the median, and two of those 5 rounds are fast launches of the artifact.

How it is measured
  • Every comparison runs the same executable against different engine builds. this change and off are the same lab build: it adds timing logs and diagnostic switches that do nothing unless their environment variable is set, and in the Windows compositor it carries the OpenGL ES 2 backing store format of [Windows] Use an unsized texture format for backing stores on OpenGL ES 2 #192917 and extra framebuffer and glGetError checks on the first 5 presents. All runs here got an OpenGL ES 3 context. off disables the new path through an environment variable, so this change against off isolates the parallel link, while a comparison with the artifact also includes those extras. base is a local build of the same tree without the change. Variants alternate inside every round, and the first launch after a dll swap is discarded.
  • start_up_info.json: flutter run --profile --trace-startup --use-application-binary, the way devicelab's StartupTest runs the startup benchmark, 5 iterations averaged.
  • First frame and frame counts: the 8 shader application takes its process creation time from GetProcessTimes and places every frame by its FramePhase.rasterFinish timestamp relative to it. Counting from the process start and not from the first frame keeps a variant that moves work across the first frame from looking better or worse than it is.
  • Window visible: wall clock from the moment the process start call returns to its first visible window, polled every 20 ms. In the 8 shader application the window becomes visible 8 to 43 ms after the first frame finished rasterizing.
Per round results

Gallery, first frame, milliseconds. The mini PC columns are the time to the first visible window, the VM columns timeToFirstFrameRasterized:

Round mini PC, this change mini PC, off mini PC, artifact VM, this change VM, off VM, base VM, artifact
1 631 852 1068 489 749 772 555
2 630 849 1069 533 757 549 737
3 630 854 1122 501 464 800 651
4 624 967 961 512 441 777 727
5 636 976 996 502 740 774 730

On the VM the variants without the change scatter from 441 to 800 ms, and a few of their rounds come as early as the new path. The other fields of start_up_info.json there are frameworkInit 73, 67, 64 and 65 ms and timeToFirstFrame 276, 250, 247 and 246 ms for this change, off, base and the artifact.

The 8 shader application, 15 rounds, the median with the middle half of the rounds in brackets:

mini PC, artifact mini PC, this change VM, artifact VM, this change
first frame 2568 ms (2540 to 2606) 1752 ms (1693 to 1786) 1799 ms (1263 to 2081) 1279 ms (1226 to 1789)
rounds with the first frame under 2 s 3 of 15 13 of 15 11 of 15 13 of 15
frames drawn by 3.0 s 2 135 115 181
frames drawn by 5.0 s 240 376 342 383
longest gap between frames in the first 5 s 243 ms 65 ms 127 ms 70 ms

The same application through start_up_info.json on the VM, 5 rounds averaged: 1190 ms with this change, 1614 ms with the path off and 1683 ms with the artifact. The rounds of the new path, 1135 to 1322 ms, do not overlap the rounds of the other two, 1587 to 1787 ms.

The fallback path against the artifact on the mini PC, 7 rounds: the first frame at 2252 ms against 2569 ms, 57 against 61 frames drawn by 3.5 s and 238 against 245 by 5 s. On the VM its timeToFirstFrameRasterized is 630 ms against 734 ms for the base build and 680 ms for the artifact.

Correctness

flutter_gallery and 8 scenes of a local bench (shader, multiply, savelayer, backdrop, imagefilter, colorfilter, atlas, text) render pixel identical screenshots before and after, compare -metric RMSE is 0 with the animation frozen at a fixed phase.

Closing the window while programs are still linking exits cleanly: 30 runs of the gallery on each machine, 10 of them with every link slowed by 300 ms so that the close lands on a pending link, and every run exits by itself with code 0 and a clean log.

Shutdown runs

10 runs per variant, the close moment sweeping 100 to 1000 ms across the ten iterations:

Variant mini PC, closed in mini PC, pending VM, closed in VM, pending
new path 399 ms 0 of 10, 0 links 264 ms 1 of 10, 1 link
new path off 940 ms 0 of 10, 0 links 356 ms 0 of 10, 0 links
new path, every link slowed by 300 ms 1114 ms 10 of 10, 24 links 1053 ms 10 of 10, 24 links

closed in is the median time from the close message until the harness has seen the process exit and saved its log, so it includes that bookkeeping. pending is the iterations that closed with links still unchecked and how many links they abandoned. The slowed variant closes later only because every pending link sleeps 300 ms before it is checked. On the mini PC the window also closes sooner with the new path; on the VM the difference is within the spread of its runs, 153 to 662 ms.

A promise is always resolved. The library fails its pending pipelines when the reactor refuses the operation, when the reactor accepted it but could not react on this thread, and in the destructor. That matters because PipelineFuture::WaitAndGet blocks the raster thread, so a promise nobody sets would hang it. ReactorGLES keeps an operation on the thread that added it, and a thread that has given up its context may never react again, which is why the list of pending pipelines is checked rather than the return value of AddOperation. The job a request posts holds the queue weakly, so a task runner that drops its tasks lets the queue go with the engine, and the queue runs what it still holds from its destructor through FinishAllJobs, which resolves those promises.

A program that is still linking is already in the cache. CreatePipeline stores it under its key right after glLinkProgram, so a second pipeline with the same shaders reuses it instead of compiling its own copy. Such a pipeline owns no shaders and checks no compile status: it asks for the GL_LINK_STATUS of the shared program, and that query waits for the link when it is still running. The synchronous path asks the same question, since a cached program can be linking in a compile queue job at that moment. When the link fails, or when the library gives up on a link it can no longer check, the pipeline that started it removes the entry, and only when the entry still holds that program, so a failed program stays out of the cache and every pipeline that reused it fails with it.

The job counters live in the queue instance, not in globals, so two engines in one process do not see each other's jobs.

Tests

impeller_unittests, OpenGL ES set without the playground suites: 106 tests, 104 passed, 2 skipped, the same 2 that are skipped without this change. These tests drive the changed code through the GLES mock, so they need no GL context. Every wait in the new queue and deferred library tests has a 30 second timeout, so a regression fails instead of holding a bot. The two new playground tests wait on the pipeline future as the existing ones do.

New tests
  • PipelineCompileQueueGLESTest.ReportsProcessingAndCallsOnDrained covers the drain notification and that a cleared callback is not called again.
  • PipelineCompileQueueGLESTest.ReportsRunningJobOnlyInsideJobs covers the job flag inside jobs, inside descriptor jobs and in a plain task on the same thread.
  • PipelineCompileQueueGLESTest.JobsOfOneQueueAreInvisibleToAnother covers two queues in one process. Against a build with the counters in globals it fails on both assertions.
  • PipelineLibraryGLESDeferredTest.FailsPendingPipelinesWhenTheReactorCannotReact covers the pending pipeline whose link nobody can check. Against a build without that guard it hangs and fails on its timeout.
  • PipelineLibraryGLESDeferredTest.DefersStatusAndSharesPendingProgram drives two descriptors that share a program through the deferred path and counts the status queries, so the deferred path is covered through the public API and not only through the mock.
  • PipelineLibraryGLESDeferredTest.FailedCompileRejectsBothSharedPipelines and FailedLinkRejectsBothSharedPipelines cover a failure of each kind reaching both pipelines that share the program.
  • PipelineLibraryGLESDeferredTest.AbandonedFailedLinkDoesNotPoisonProgramCache covers a link that nobody checks because the reactor refused, and then a later request for the same shaders. Against a build where the abandoned pipelines keep their cache entry, the later request gets the failed program and returns null.
  • PipelineLibraryGLESDeferredTest.ResolvesUnstartedPipelinesWhenTheRunnerDiscardsJobs covers a task runner that drops its tasks, which is what ConditionalBasicTaskRunner does once the IO manager is gone. Against a build where the job holds its queue, the queue outlives the engine and the future is never resolved.

The four tests that share a program drive the existing MockGLES through a resolver of their own, and that resolver falls back to kMockResolverGLES for everything it does not name. It replaces the entry points whose results the deferred path reads: the compile status, the link status, the program and shader names, and the info logs. The mock as it stands leaves the value of glGetShaderiv and glGetProgramiv untouched, so neither a link that succeeds nor a link that fails on purpose can be expressed through it. Teaching MockGLESImpl those methods, the way #192196 added IsProgram, would remove the resolver and belongs in a change of its own.

Scope and limits

Correct me if this should be done by platform only (Windows) check instead, since right now the deferred path is enabled only when the driver reports itself as ANGLE and advertises the extension. That is a driver check and not a platform check: an Android device whose GLES driver is ANGLE takes the same path, and no Android device was available to measure it. The Android GLES drivers have known trouble with runtime effect programs (#186186). Every other driver keeps the previous behaviour, and widening the condition to one of them is one line in HasParallelShaderCompile once it is measured.

glMaxShaderCompilerThreadsKHR is not called, so the number of threads inside the driver is left at its default. This change only bounds how many links start before the library checks them.

The *OpenGLES playground variants run on Windows, on the mini PC, where the driver gives the ES context through WGL. With the emoji, text and glyph groups excluded, 1236 of the remaining 1319 tests pass and 67 skip, none fail, the two new tests among them, and the run still stops 16 tests short, on TypographerTest.LazyAtlasTracksColor. Those groups are excluded because a test whose text reaches a color glyph through DirectWrite aborts in Skia with No PNG decoder registered (SkScalerContext_win_dw.cpp), which this change does not touch. Upstream runs the set on Linux under xvfb and excludes it from the macOS unit test run as covered by the golden tests, so CI keeps that coverage.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making.
  • All existing and new tests are passing.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Sep 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for parallel shader compilation and deferred program linking in the Impeller GLES backend. It updates PipelineCompileQueueGLES to track running jobs and notify when the queue is drained, and modifies PipelineLibraryGLES to manage pending pipelines and resolve them asynchronously. Unit tests are added to verify these changes. Feedback is provided regarding a potential dangling pointer issue in the unit tests where the global pointer deferred_links is left pointing to a destroyed local variable, suggesting an RAII helper to reset it.

DeferredLinkState state;
state.fail_compile = fail_compile;
state.fail_link = fail_link;
deferred_links = &state;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The global pointer deferred_links is set to point to the local variable state. When RunDeferredLink exits, state is destroyed, leaving deferred_links as a dangling pointer. This can cause undefined behavior or crashes if subsequent tests or teardown procedures invoke mock GL functions that dereference it. Consider using a local RAII helper to automatically reset deferred_links to nullptr upon exiting the function.

  deferred_links = &state;
  struct ScopedReset {
    ~ScopedReset() { deferred_links = nullptr; }
  } reset;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with local variant

  fml::ScopedCleanupClosure reset_deferred_links(
      []() { deferred_links = nullptr; });

@gaaclarke
gaaclarke requested a review from walley892 September 21, 2026 18:10
@gaaclarke

Copy link
Copy Markdown
Member

@walley892 can you please review this? It has to do with speeding up shader compilation time with ANGLE.

if (pending_count >= library.max_pending_links_ ||
!library.compile_queue_->IsProcessingJobs()) {
library.FinishPendingPipelines(*reactor);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this batched finishing can cause the following situation:

  1. Pipeline A is popped from pending_jobs_ in pipeline_compile_queue.cc, starts linking, and is put into pending_pipelines_

  2. Pipeline B is popped from pending_jobs_, starts linking, and is put into pending_pipelines_

  3. Pipeline A is requested for immediate compilation and use (e.g., Pipeline::WaitAndGet -> PipelineCompileQueue::PerformJobEagerly)

In this case, WaitAndGet will call PerformJobEagerly. Before, this would result in the pipeline being scheduled for immediate compilation. With these changes, PerformJobEagerly will return as a no-op, since Pipeline A isn't found in pending_jobs_. The upstream caller of WaitAndGet will block until pending_count >= library.max_pending_links and everything in pending_pipelines_ is finished (e.g., the compilation of a few more pipelines).

I can think of two ways to resolve this - there are maybe better ways :)

  1. Wire up PipelineCompileQueue::PerformJobEagerly to be able to finish pipelines from pending_pipelines_ directly. This seems kind of ugly, but maybe there's a nice way to do it.

  2. GL_KHR_parallel_shader_compile comes with GL_COMPLETION_STATUS_KHR, a non-blocking version of GL_LINK_STATUS. We can use that to scan through pending_pipelines_ each time this function is called, and immediately call FinishProgram on them if GL_COMPLETION_STATUS_KHR == TRUE.

2 seems nice because faster pipelines and pipelines that have already completed compilation don't have to wait for pending_count >= library.max_pending_links, it may speed things up overall.

thoughts on this?

@krll-kov krll-kov Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once link is started, the pipeline is no longer in pending_jobs_, so PerformJobEagerly can't help and WaitAndGet waits for the whole batch.

I went with option 2, thanks for this good finding, but it's overall win ended in very random ~-1-10% of startup time, which is 4% faster on average on real mini pc. Before each compile job adds its pipeline, it asks GL_COMPLETION_STATUS_KHR for the pending links and finishes the ones that completed. If no job runs afterwards, the drain check finishes them as before. If a driver rejects the query, the value stays GL_FALSE, and those links are checked at the limit or on drain like before. Tests: FinishesCompletedLinkBeforeLimit, KeepsLinkPendingWhenCompletionQueryFails.

Tested this again on both mini pc and vm:
Same two profile apps, flutter_gallery, timed from process start to its first visible window, and app with 8 FragmentProgram shaders, timed from process start to its first rasterized frame. Every comparison uses one engine binary with environment switches, 9 to 10 rounds per variant in rotating order.

The case you mentioned happens rarely. The raster thread waited on a started link in 2 of 8 gallery runs on the mini PC, 33 to 34 ms each, and in 1 of 5 on the VM, 17 ms. I never saw it in the 8 shader app no matter how many times i launched it (in more than 100 launches).

Results, median first frame o 8 shader app, ms:

before with the poll
mini PC, 4 sessions 1750 / 1701 / 1767 / 1665 1683 / 1675 / 1587 / 1645
VM, 2 sessions 1178 / 1152 1176 / 1167

On mini PC first frame comes 1-10% earlier, depending on the session, and never later. On VM the difference was too small to tell it apart from noise. The gallery start time did not change on either machine. After the first frame both versions run at about 120 FPS.

Also retested (again) closing the window while programs are still linking: 60 runs per machine, some of them with links slowed down on purpose. Every run exited normally with code 0 and no errors in the lo.

One more finding that I did not include. My first draft of this change was 3 to 8% faster than the final version. The reason was in debug log line in every job. Regardless of how ridiculous it sounds, writing it took about 0.4 ms, and that short pause of IO thread made the startup faster. Adding pause on purpose gives the same effect.

pause when none of the pending links has finished yet mini PC VM
no pause (this PR) 1638 ms 1129 ms
std::this_thread::yield() 1562 ms 1024 ms
sleep_for(100us), which Windows turns into about 1.5 ms 1446 ms 1042 ms

My guess why: without a pause, the IO thread checks again before the driver has finished any link. The pending list then reaches the limit, and the IO thread has to wait for the slowest link. With that pause, more links are already done when it checks. With the sleep, on mini PC the time spent waiting at the limit dropped down from 906 ms to 560 ms.

The pause looks awkwardly hacky, so I didn't include it, but I can add it if you want.

My guess is just a guess so I don't fully understand why it helps, and its effect depends on OS: on Windows sleep_for(100us) actually sleeps 1 to 2 ms, and yield() helped more on the VM than on my mini PC. It also makes the first frame faster to appear, while some links are still running, so right after the first frame there is one longer gap: 139 ms instead of 91 on the mini PC. The same gap appears without the pause whenever the first frame happens to come too early. In total more frames are shown by 3 s with the pause, 162 instead of 138.

Mini PC and VM are the same mentioned in description Ryzen AI 9 HX 370 with Radeon 890M, and Windows VM on Parallels on Macbook M4 Max

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I'm glad that this sped things up.

@gaaclarke, Thoughts on the behavior change in WaitAndGet -> PerformJobEagerly? IMO with the addition of the continuous polling, the behavior change is negligible.

@krll-kov
krll-kov requested a review from walley892 September 29, 2026 10:11

@walley892 walley892 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few nits, thanks for addressing the feedback!

@walley892 walley892 added the CICD Run CI/CD label Sep 29, 2026
@walley892

Copy link
Copy Markdown
Contributor

@gaaclarke, LGTM - can I get a second set of eyes?

@walley892
walley892 requested a review from gaaclarke September 29, 2026 20:16
@walley892

Copy link
Copy Markdown
Contributor

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements deferred pipeline compilation and linking for the GLES backend in Impeller using the GL_KHR_parallel_shader_compile extension. It updates PipelineCompileQueueGLES to track running jobs and support drain notifications, and modifies PipelineLibraryGLES to defer program linking and check status asynchronously. Comprehensive unit tests are added to verify these behaviors. The review feedback suggests simplifying the initialization of weak_compile_queue in PipelineLibraryGLES by replacing a ternary operator with a conditional assignment to improve readability.

Comment on lines +672 to +675
std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue =
async && supports_parallel_shader_compile_
? compile_queue_
: std::shared_ptr<PipelineCompileQueueGLES>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The initialization of weak_compile_queue can be simplified and made more readable by default-constructing the std::weak_ptr and conditionally assigning it, rather than using a ternary operator with an explicit temporary std::shared_ptr construction. This avoids unnecessary temporary object creation and improves code clarity.

  std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue;
  if (async && supports_parallel_shader_compile_) {
    weak_compile_queue = compile_queue_;
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know the ternary makes a temporary shared_ptr copy 3 atomic ops instead of 1 with if / else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because your last term is std::shared_ptr<PipelineCompileQueueGLES>(), not std::weak_ptr<PipelineCompileQueueGLES>(). Otherwise they are the same: https://godbolt.org/z/967Wo81nc

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 29, 2026
@krll-kov
krll-kov requested a review from walley892 September 29, 2026 20:51
@krll-kov

krll-kov commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

@walley892 cicd label dropped after push with requested changes, can you add it again?

@walley892 walley892 added the CICD Run CI/CD label Sep 29, 2026
walley892
walley892 previously approved these changes Sep 29, 2026

@walley892 walley892 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM pending clarke's call on the WaitAndGet change and the CI run

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, i've looked through all the code. This is looking really cool. In the future it would be helpful if something this level of complexity was preceeded with a small design doc. Wally and I both ended up having to draw up stuff to make sure we are good. I still have to do the design review part of the review.

// deferred link also flags the shaders for deletion right away. GL frees them
// when they are detached or when the program is deleted, so they do not leak
// if FinishProgramLink never runs.
static bool StartProgramLink(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch this to return absl::StatusOr please.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// deferred link also flags the shaders for deletion right away. GL frees them
// when they are detached or when the program is deleted, so they do not leak
// if FinishProgramLink never runs.
static bool StartProgramLink(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you switch the comment to standard docstring format. Spelling out what "deferred" means would be helpful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +231 to +236
if (deferred) {
gl.DeleteShader(vert_shader);
gl.DeleteShader(frag_shader);
}
delete_vert_shader.Release();
delete_frag_shader.Release();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment about where these are deleted is worth mentioning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Checks the result of StartProgramLink, then detaches the shaders. A shader
// of a deferred link is already flagged for deletion, so detaching frees it.
static bool FinishProgramLink(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return absl::Status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return true;
}

static bool LinkProgram(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return absl::status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (async && supports_parallel_shader_compile_) {
weak_compile_queue = compile_queue_;
} else {
weak_compile_queue = std::shared_ptr<PipelineCompileQueueGLES>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: based on what we learned previously from godbolt

Suggested change
weak_compile_queue = std::shared_ptr<PipelineCompileQueueGLES>();
weak_compile_queue = std::weak_ptr<PipelineCompileQueueGLES>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// The queue whose job this thread runs right now. Two engines in one process
// have a queue each. With a flag instead of the queue, a job of one queue
// would make IsRunningJobOnCurrentThread return true for the other queue.
thread_local const PipelineCompileQueueGLES* tls_running_queue = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was thinking that we have the information we need, it just isn't getting plumbed down to the callback and we are recreating that information with the thread_local storage.

I think if we change the callback to PostJobForDescriptor to get called with a bool eager argument we'll have the same information.

using Job = std::function<void(bool eager)>;
bool PostJobForDescriptor(const PipelineDescriptor& desc, const Job& job);

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 30, 2026
@krll-kov
krll-kov requested a review from gaaclarke September 30, 2026 19:19
@krll-kov

Copy link
Copy Markdown
Contributor Author

Okay, i've looked through all the code. This is looking really cool. In the future it would be helpful if something this level of complexity was preceeded with a small design doc. Wally and I both ended up having to draw up stuff to make sure we are good. I still have to do the design review part of the review.

Makes sense, a change of this size is hard to follow from the code alone. Here is the flow, in case it helps with the design review, if it helps, visualization:

image

Comment on lines +40 to +41
// which is what makes a link expensive, and it is the only GLES driver the
// change was measured on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can you remove the specific reference to "the change" here. Comments should just reflect the current state of the codebase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed, just wanted to make sure it's noticed since angle is not just Windows related
image

};

//------------------------------------------------------------------------------
/// @brief Compiles both shaders and starts the program link.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: drop the word "both"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased "both" into what it means in 2 places

Comment on lines +697 to +730
std::weak_ptr<PipelineCompileQueueGLES> weak_compile_queue;
if (async && supports_parallel_shader_compile_) {
weak_compile_queue = compile_queue_;
} else {
weak_compile_queue = std::weak_ptr<PipelineCompileQueueGLES>();
}
auto generation_task = [promise, weak_this, descriptor, vert_function,
frag_function, threadsafe, reactor]() {
frag_function, threadsafe, reactor,
weak_compile_queue](bool eager) {
auto thiz = weak_this.lock();
if (!thiz) {
promise->set_value(nullptr);
return;
}
const bool result = reactor->AddOperation([promise, //
weak_this, //
descriptor, //
vert_function, //
frag_function, //
threadsafe //
const bool result = reactor->AddOperation([promise, //
weak_this, //
descriptor, //
vert_function, //
frag_function, //
threadsafe, //
weak_compile_queue, //
eager //
](const ReactorGLES& reactor) {
// The job that runs this holds the queue, so the lock succeeds while a
// job of it runs on this thread.
auto compile_queue = weak_compile_queue.lock();
if (!eager && compile_queue) {
// The promise is set later unless the pipeline fails right away.
if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function,
threadsafe, promise)) {
promise->set_value(nullptr);
}
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic here and in CreatePipeline is difficult to follow because of the dependencies between variables and the particular logic of std::weak_ptr that makes this work.

IIUC, (!eager && compile_queue) checks that we're (1) not eagerly compiling and

(2a): - We have the lock on the compile_queue_

(2b): - We support_parallel_shader_compile

2a is immediately derivable from the surrounding code, but 2b only works because we've set weak_compile_queue ~30 lines before this (because async && supports_parallel_shader_compile_ == true) and because calling .lock() on an empty std::weak_ptr returns nullptr.

Then, in CreatePipeline, the logic that actually does the async linking is only active if deferred_promise != nullptr - which in turn depends on the complicated chain above.

I recommend breaking the logic here up a bit to be more clear to reason about what logic is being executed when we support parallel shader compile vs not. I'd also recommend adding a check and comment inside of CreatePipeline that ensures deferred_promise == true only if we support parallel shader compile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, do we still need compile_queue now that we have eager if not, the check inside the job lambda could just be:

const bool defer_link =                                                                                    
 !eager && library.SupportsParallelShaderCompile();                                                     
if (defer_link) {                                                                                          
// The promise is resolved later in FinishCompletedPipelines or                                          
// FinishPendingPipelines unless starting the link fails right away.                                     
    if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function,                                 
        threadsafe, promise)) {                                                              
    promise->set_value(nullptr);                                                                           
    }                                                                                                        
    return;                                                                                                  
}

That + the comments and checks in CreatePipeline would make this a lot clearer

@krll-kov krll-kov Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, do we still need compile_queue now that we have eager if not, the check inside the job lambda could just be:

const bool defer_link =                                                                                    
 !eager && library.SupportsParallelShaderCompile();                                                     
if (defer_link) {                                                                                          
// The promise is resolved later in FinishCompletedPipelines or                                          
// FinishPendingPipelines unless starting the link fails right away.                                     
    if (!CreatePipeline(weak_this, descriptor, vert_function, frag_function,                                 
        threadsafe, promise)) {                                                              
    promise->set_value(nullptr);                                                                           
    }                                                                                                        
    return;                                                                                                  
}

That + the comments and checks in CreatePipeline would make this a lot clearer

The lock only fails once the queue is gone, which shouldn't happen while the library lives, but we'd rather keep it as a guard than rely on that, i'll add a commit soon with requested changes + extra test

@krll-kov krll-kov Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since design doc was requested, I kept this small so we don't change the same code twice, the extension check is now an explicit may_defer_link in the condition, CreatePipeline DCHECK it with comment, and AsyncJobWithoutExtensionLinksSynchronously covers this scenario

The confusing part was left over from the eager change, the weak_ptr lock used to tell whether we were inside a queue job

@walley892 walley892 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, accidental approve

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay @krll-kov I've looked through this and I believe I'm beginning to have an understanding of what's going on. My initial instincts on how to handle this are slightly different. This is a valuable idea and has a good payoff. Ultimately though I don't want to land something into the engine that is complex to the level that it takes people multiple hours to digest what is happening. It just isn't clear from the code. Sometimes things are just complex too, there is no good way to write them in code to correct that.

I think we are going to have to step back and request a design doc. That serves 2 purposes:

  1. That allows us to make sure we are solving this problem the best way possible. (maybe we are already there)
  2. It documents the process to future developers.

The important sections to include for me in the design doc are:

  1. Alternatives considered
  2. Testing

Here's information on writing design docs: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md

@krll-kov

krll-kov commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Okay @krll-kov I've looked through this and I believe I'm beginning to have an understanding of what's going on. My initial instincts on how to handle this are slightly different. This is a valuable idea and has a good payoff. Ultimately though I don't want to land something into the engine that is complex to the level that it takes people multiple hours to digest what is happening. It just isn't clear from the code. Sometimes things are just complex too, there is no good way to write them in code to correct that.

I think we are going to have to step back and request a design doc. That serves 2 purposes:

1. That allows us to make sure we are solving this problem the best way possible. (maybe we are already there)

2. It documents the process to future developers.

The important sections to include for me in the design doc are:

1. Alternatives considered

2. Testing

Here's information on writing design docs: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md

Okay, I'll work on the design document, thanks for the guide

@krll-kov

krll-kov commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Okay @krll-kov I've looked through this and I believe I'm beginning to have an understanding of what's going on. My initial instincts on how to handle this are slightly different. This is a valuable idea and has a good payoff. Ultimately though I don't want to land something into the engine that is complex to the level that it takes people multiple hours to digest what is happening. It just isn't clear from the code. Sometimes things are just complex too, there is no good way to write them in code to correct that.

I think we are going to have to step back and request a design doc. That serves 2 purposes:

1. That allows us to make sure we are solving this problem the best way possible. (maybe we are already there)

2. It documents the process to future developers.

The important sections to include for me in the design doc are:

1. Alternatives considered

2. Testing

Here's information on writing design docs: https://github.com/flutter/flutter/blob/master/docs/contributing/Design-Documents.md

https://docs.google.com/document/d/1eLrXEWoyWm8AZQBioqcHrQeVM2NAPDYbaEB4z_HOQs0

This is my first design doc, correct me if something is wrong or missing so that i fix it. I added what was asked for, all the tests explanations and how i verified them, alternatives and what i tried within this approach, how to code works, explanation of terms, basic visualization, numbers, why something was chosen the way it's in code, WaitAndGet cost, etc.

Do i need to create doc related issue and pr for it or you asked it for internal usage and it's fine like this?

@gaaclarke
gaaclarke self-requested a review October 2, 2026 20:01
@gaaclarke

Copy link
Copy Markdown
Member

Do i need to create doc related issue and pr for it or you asked it for internal usage and it's fine like this?

We can just link to it in #191860

@gaaclarke

Copy link
Copy Markdown
Member

Thanks @krll-kov, I've given it a look and gave some feedback. My hope is that the design doc will give us a higher level description that will inform how we can structure the code to make it more clear, or if we can't, provide guidance on it. I'm pretty sure this is technically correct. I have to make sure everything is clear though, not just technically correct.

Let's give this design doc a chance, if this gets to the limit of what you are able to contribute at this time, maybe one of us on the team can take it over. Just let me know. You've done valuable work already pointing this out and prototyping it, I'd like to find a way to get a PR by you landed, but it just may become easier to pass it off so we can iterate on it quicker inside the team.

@krll-kov

krll-kov commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @krll-kov, I've given it a look and gave some feedback. My hope is that the design doc will give us a higher level description that will inform how we can structure the code to make it more clear, or if we can't, provide guidance on it. I'm pretty sure this is technically correct. I have to make sure everything is clear though, not just technically correct.

Let's give this design doc a chance, if this gets to the limit of what you are able to contribute at this time, maybe one of us on the team can take it over. Just let me know. You've done valuable work already pointing this out and prototyping it, I'd like to find a way to get a PR by you landed, but it just may become easier to pass it off so we can iterate on it quicker inside the team.

Thanks for feedback, i've fixed what you asked for in docs and added code structure block as well that you mentioned here. It also includes pseudo-code for this solution and easier navigation with headings.

As for the second part, I understand that this change may look big, and the team may see a simpler way to do it, since you know the engine much better than I do. My goal was never to get my own PR landed. I assumed this is not the top priority for the team right now, so I just wanted to help get the startup problem fixed sooner and reliably. I am ready to keep working on it through as many review rounds as it takes. But if I am slowing this down, or if reviewing code from someone outside the team takes you more time than writing it yourselves, I am completely fine with you taking it over, even if this solution will be used in the end with some tweaks. Authorship does not matter much to me, the result does. In that case I still will be able to answer questions about the design doc and test any version on my machines, if that's needed.

@gaaclarke

Copy link
Copy Markdown
Member

Okay I spent some time thinking about how we can make this more clear and correct. Here's what I came up with.

class

parallel-shader-link

sequence

parallel-shader-link-seq

overview

  1. Splitting logic between PipelineCompileQueue and PipelineCompileQueueGLES makes everything more complicated to follow. We should make PipelineCompileQueue an interface. When PipelineCompileQueueGLES holds the jobs and their states it makes it easier to verify it is handling them correctly.
  2. The draining logic in the PR now complicates things. I don't think it's necessary. This approach lazy synchronizes, only synchronizing on one thing to make space for a new job or when it is asked for. Otherwise we just clean it up at deletion time.
  3. I think the void lambdas are overly abstracted and makes the code harder to follow. I'm replacing them with objects.

I plan on implementing these changes in a separate PR forked from this one. I don't want to start editing this without being sure I'm happy with the final results (cc @walley892 )

@krll-kov

krll-kov commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Okay I spent some time thinking about how we can make this more clear and correct. Here's what I came up with.

class

1. Splitting logic between PipelineCompileQueue and PipelineCompileQueueGLES makes everything more complicated to follow.  We should make PipelineCompileQueue an interface.  When PipelineCompileQueueGLES holds the jobs and their states it makes it easier to verify it is handling them correctly.

2. The draining logic in the PR now complicates things.  I don't think it's necessary.  This approach lazy synchronizes, only synchronizing on one thing to make space for a new job or when it is asked for.  Otherwise we just clean it up at deletion time.

3. I think the void lambdas are overly abstracted and makes the code harder to follow.  I'm replacing them with objects.

I plan on implementing these changes in a separate PR forked from this one. I don't want to start editing this without being sure I'm happy with the final results (cc @walley892 )

Looks interesting, how would this work for flutter_gpu by the way? lib/gpu/render_pass.cc requests the pipeline with async=true and waits on the future directly, without PerformJobEagerly. If no other job is queued after it, nothing checks that link and the raster thread keeps waiting

@gaaclarke

Copy link
Copy Markdown
Member

Looks interesting, how would this work for flutter_gpu by the way? lib/gpu/render_pass.cc requests the pipeline with async=true and waits on the future directly, without PerformJobEagerly. If no other job is queued after it, nothing checks that link and the raster thread keeps waiting

Yea, I think there are cases we'll have to route through the PerformEagerly logic. I haven't pulled on that thread yet but I don't think there is a problem unifying access in that way.

@gaaclarke gaaclarke mentioned this pull request Oct 8, 2026
11 tasks done
@gaaclarke

gaaclarke commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Okay, I have an alternative queued up and hopefully we can get landed. It's split between a design doc, a refactor PR and the actual implementation. Thanks for all your help, we couldn't have done it without your investigative work. Give it a check.

@gaaclarke gaaclarke closed this Oct 9, 2026
opensource-fork-sync Bot pushed a commit to cla7aye15I4nd/opensource-flutter__flutter that referenced this pull request Oct 10, 2026
This PR is just a refactor that lays down the groundwork for the
parallel linking work as started in
flutter#193066

Refactors:
1) Deleted PipelineLibraryQueue class, unifying the logic into their
implementations.
1) Split out caching programs from creating pipelines, clearing up the
logic.
1) Created a PendingProgram class to manage programs that are in the
process of being linked.
1) Made queues private implementation details to the pipeline libraries,
so they can be modified freely.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@krll-kov

Copy link
Copy Markdown
Contributor Author

Okay, I have an alternative queued up and hopefully we can get landed. It's split between a design doc, a refactor PR and the actual implementation. Thanks for all your help, we couldn't have done it without your investigative work. Give it a check.

* design doc: [Adds design doc for parallel gles compilation rfc#34](https://github.com/flutter/rfc/pull/34)

* refactor: [Refactored PipelineLibraryGLES #193985](https://github.com/flutter/flutter/pull/193985)

* implementation: [Parallelize compiles #194022](https://github.com/flutter/flutter/pull/194022)

I'm glad this helped and shaders problem is improved!

Rusino pushed a commit to Rusino/flutter that referenced this pull request Oct 15, 2026
> [!NOTE]
> This PR depended on flutter#193985 which refactored code to make way for this
change.

This PR was authored in lieu of
flutter#193066 and was informed by
@krll-kov's great investigative work.

design doc:
https://github.com/flutter/rfc/blob/main/rfc/210.0001-parallel-gles-linking.md
issue: flutter#191860

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
This PR is just a refactor that lays down the groundwork for the
parallel linking work as started in
flutter#193066

Refactors:
1) Deleted PipelineLibraryQueue class, unifying the logic into their
implementations.
1) Split out caching programs from creating pipelines, clearing up the
logic.
1) Created a PendingProgram class to manage programs that are in the
process of being linked.
1) Made queues private implementation details to the pipeline libraries,
so they can be modified freely.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
> [!NOTE]
> This PR depended on flutter#193985 which refactored code to make way for this
change.

This PR was authored in lieu of
flutter#193066 and was informed by
@krll-kov's great investigative work.

design doc:
https://github.com/flutter/rfc/blob/main/rfc/210.0001-parallel-gles-linking.md
issue: flutter#191860

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants