trace_processor: Route hw_queue GpuUserAnnotationEvent to the queue track

When a GpuUserAnnotationEvent carries a hw_queue_iid, intern it onto that
hardware queue's render stage track (reusing the render stage blueprint) so
it appears alongside the events on the queue. Without a resolvable
specification it falls back to the stream-scoped gpu_user_annotation track.
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.cc b/src/trace_processor/importers/proto/gpu_event_parser.cc
index 8e721a9..5f339c7 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.cc
+++ b/src/trace_processor/importers/proto/gpu_event_parser.cc
@@ -1029,13 +1029,35 @@
   }
 
   auto ugpu = context_->gpu_tracker->GetOrCreateGpu(gpu_id);
-  TrackId track_id = context_->track_compressor->InternScoped(
-      kUserAnnotationBlueprint, tracks::Dimensions(ugpu.value, gpu_id, upid),
-      ts, static_cast<int64_t>(event.duration()), tracks::DynamicName(name_id),
-      [](ArgsTracker::BoundInserter&) {});
+  std::optional<TrackId> track_id;
+  if (event.has_hw_queue_iid()) {
+    // With a hardware queue, place the annotation on that queue's render stage
+    // track, next to the events on the queue.
+    auto* spec = sequence_state->LookupInternedMessage<
+        protos::pbzero::InternedData::kGpuSpecificationsFieldNumber,
+        protos::pbzero::InternedGpuRenderStageSpecification>(
+        event.hw_queue_iid());
+    if (spec) {
+      StringId hwqueue_name = context_->storage->InternString(spec->name());
+      track_id = context_->track_compressor->InternScoped(
+          kRenderStageBlueprint,
+          tracks::Dimensions(ugpu.value, gpu_id, base::StringView("iid"),
+                             static_cast<uint32_t>(event.hw_queue_iid()),
+                             hwqueue_name),
+          ts, static_cast<int64_t>(event.duration()),
+          tracks::DynamicName(hwqueue_name),
+          [](ArgsTracker::BoundInserter&) {});
+    }
+  }
+  if (!track_id) {
+    track_id = context_->track_compressor->InternScoped(
+        kUserAnnotationBlueprint, tracks::Dimensions(ugpu.value, gpu_id, upid),
+        ts, static_cast<int64_t>(event.duration()),
+        tracks::DynamicName(name_id), [](ArgsTracker::BoundInserter&) {});
+  }
 
   auto opt_slice_id = context_->slice_tracker->Scoped(
-      ts, track_id, kNullStringId, name_id,
+      ts, *track_id, kNullStringId, name_id,
       static_cast<int64_t>(event.duration()),
       [&](ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(context_id_id_,
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
index dbc1029..980e56b 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
@@ -416,6 +416,61 @@
         "HostSubmit","annotation1"
         """))
 
+  def test_gpu_user_annotation_hw_queue(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          timestamp: 0
+          incremental_state_cleared: true
+          trusted_packet_sequence_id: 1
+          interned_data {
+            graphics_contexts { iid: 1 pid: 100 api: CUDA }
+            gpu_specifications { iid: 1 name: "queue0" }
+          }
+        }
+        packet {
+          timestamp: 1000
+          trusted_packet_sequence_id: 1
+          gpu_render_stage_event {
+            event_id: 1
+            duration: 100
+            hw_queue_iid: 1
+            gpu_id: 0
+            context: 1
+            name: "kernelA"
+          }
+        }
+        packet {
+          timestamp: 1200
+          trusted_packet_sequence_id: 1
+          gpu_user_annotation_event {
+            name: "annotation1"
+            duration: 800
+            hw_queue_iid: 1
+            gpu_id: 0
+            context: 1
+            args { name: "device" int_value: 0 }
+            args { name: "stream" uint_value: 7 }
+          }
+        }
+        """),
+        query="""
+        SELECT
+          COUNT(DISTINCT s.track_id) AS distinct_tracks,
+          SUM(t.type = 'gpu_render_stage') AS render_stage_slices,
+          MAX(CASE
+                WHEN s.name = 'annotation1'
+                THEN extract_arg(s.arg_set_id, 'stream')
+              END) AS annotation_stream
+        FROM gpu_slice s
+        JOIN track t ON t.id = s.track_id
+        WHERE s.name IN ('kernelA', 'annotation1');
+        """,
+        out=Csv("""
+        "distinct_tracks","render_stage_slices","annotation_stream"
+        1,2,7
+        """))
+
   def test_vulkan_api_events(self):
     return DiffTestBlueprint(
         trace=Path('vulkan_api_events.py'),