diff --git a/.mypy.ini b/.mypy.ini index d223627..bcc0ffe 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,6 +1,10 @@ [mypy] exclude = lglpy/timeline/protos/.*\.py ignore_missing_imports = True +disable_error_code = annotation-unchecked + +[mypy-lglpy.timeline.data.raw_trace] +disable_error_code = attr-defined [mypy-google.*] ignore_missing_imports = True diff --git a/.pylintrc b/.pylintrc index 55eab54..186aef6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -64,7 +64,7 @@ ignore-patterns=^\.# # manipulated during runtime and thus existing member attributes cannot be # deduced by static analysis). It supports qualified module names, as well as # Unix pattern matching. -ignored-modules=cairo +ignored-modules=cairo,protos # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -293,7 +293,7 @@ ignored-parents= max-args=5 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=12 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 @@ -342,7 +342,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=80 # Maximum number of lines in a module. max-module-lines=1000 @@ -436,7 +436,8 @@ disable=raw-checker-failed, use-implicit-booleaness-not-comparison-to-string, use-implicit-booleaness-not-comparison-to-zero, use-symbolic-message-instead, - duplicate-code + duplicate-code, + arguments-differ # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -456,8 +457,7 @@ timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests. # List of note tags to take in consideration, separated by a comma. notes=FIXME, - XXX, - TODO + XXX # Regular expression of note tags to take in consideration. notes-rgx= @@ -521,7 +521,7 @@ ignore-imports=yes ignore-signatures=yes # Minimum lines number of a similarity. -min-similarity-lines=4 +min-similarity-lines=10 [SPELLING] diff --git a/lglpy/comms/service_gpu_timeline.py b/lglpy/comms/service_gpu_timeline.py index 700e71a..b7ca806 100644 --- a/lglpy/comms/service_gpu_timeline.py +++ b/lglpy/comms/service_gpu_timeline.py @@ -112,7 +112,7 @@ def handle_render_pass(self, msg: Any) -> None: # If this is a continuation then merge records if last_render_pass and (last_render_pass['tid'] == msg['tid']): - # Don't accumulate if tagID is flagged as ambiguous + # Don't accumulate if tag_id is flagged as ambiguous if last_render_pass['drawCallCount'] != -1: last_render_pass['drawCallCount'] += msg['drawCallCount'] diff --git a/lglpy/timeline/data/processed_trace.py b/lglpy/timeline/data/processed_trace.py index c96074f..f41cbcc 100644 --- a/lglpy/timeline/data/processed_trace.py +++ b/lglpy/timeline/data/processed_trace.py @@ -21,87 +21,49 @@ # SOFTWARE. # ----------------------------------------------------------------------------- ''' -TODO +This module implements the processed trace that stores the formatted workloads +that merge data from the Perfetto data and GPU Timeline layer data into a +single combined representation. ''' -import enum -import lglpy.timeline.data.raw_trace as raw +from typing import Optional, Union +from .raw_trace import RawTrace, RenderstageEvent, MetadataWork, \ + MetadataRenderPass, MetadataDispatch, MetadataBufferTransfer, \ + MetadataImageTransfer, GPUStreamID -class GPUStreamID(enum.Enum): - ''' - Symbolic mapping of known GPU scheduling stream IDs. - ''' - COMPUTE = 0 - NONFRAGMENT = 1 - FRAGMENT = 2 - BINNING = 3 - MAIN = 4 - TRANSFER = 5 - - @classmethod - def get_ui_name(self, streamID) -> str: - ''' - Get presentable name for a stream. - ''' - HUMAN_NAMES = { - self.COMPUTE: 'Compute', - self.NONFRAGMENT: 'Non-fragment', - self.FRAGMENT: 'Fragment', - self.BINNING: 'Binning', - self.MAIN: 'Main', - self.TRANSFER: 'Transfer' - } - - return HUMAN_NAMES[streamID] - -class GPUStageID(enum.Enum): +class GPUWorkload: ''' - Symbolic mapping of known GPU workload stage IDs. + Base class holding command data for GPU workloads in a trace. + + Attributes: + tag_id: The unique tag ID assigned by the layer. + start_time: The event start time in nanoseconds. + duration: The event duration in nanoseconds. + stream: The scheduling stream of this workload. + stage: The render stage of this workload. + frame: The frame index in the application. + label_stack: Application debug label stack. ''' - COMPUTE = 0 - ADVANCED_GEOMETRY = 1 - VERTEX = 2 - FRAGMENT = 3 - BINNING = 4 - MAIN = 5 - IMAGE_TRANSFER = 6 - BUFFER_TRANSFER = 7 - @classmethod - def get_ui_name(self, streamID) -> str: + def __init__( + self, event: RenderstageEvent, metadata: Optional[MetadataWork]): ''' - Get presentable name for a stream. - ''' - HUMAN_NAMES = { - self.COMPUTE: 'Compute', - self.ADVANCED_GEOMETRY: 'Advanced geometry', - self.VERTEX: 'Vertex', - self.FRAGMENT: 'Fragment', - self.BINNING: 'Binning', - self.MAIN: 'Main', - self.IMAGE_TRANSFER: 'Image transfer', - self.BUFFER_TRANSFER: 'Buffer transfer' - } - - return HUMAN_NAMES[streamID] - + Base class for workloads in a trace. -class GPUWorkload: - ''' - Base class for workloads in a trace. - ''' - - def __init__(self, event, metadata): - # Data we from the Perfetto event + Args: + event: Parsed render stage event. + metadata: Parsed metadata annotation. + ''' + # Common data we from the Perfetto event self.tag_id = event.user_label self.start_time = event.start_time self.duration = event.duration self.stream = event.stream self.stage = event.stage - # Common data we get from the metadata + # Common data we get from the layer metadata self.frame = None self.label_stack = None if metadata: @@ -109,48 +71,75 @@ def __init__(self, event, metadata): self.label_stack = metadata.label_stack def get_long_label(self) -> str: - return 'Long label' - - def get_short_label(self) -> str: - return 'Short' + ''' + Get the long form label for this workload. + Returns: + Returns the label for use in the UI. + ''' + assert False, 'Subclass must implement this' + return '' -class GPURenderPassAttachment: - ''' - Workload class representing a render pass attachment. - ''' + def get_short_label(self) -> str: + ''' + Get the short form label for this workload. - def __init__(self, metadata): - self.binding = metadata.binding - self.is_loaded = metadata.is_loaded - self.is_stored = metadata.is_stored - self.is_resolved = metadata.is_resolved + Returns: + Returns the label for use in the UI. + ''' + assert False, 'Subclass must implement this' + return '' class GPURenderPass(GPUWorkload): ''' Workload class representing a render pass. + + Attributes: + width: The workload width, in pixels. + height: The workload height, in pixels. + draw_call_count: The number of draw calls in the render pass. + attachments: The list of framebuffer attachments. ''' - def __init__(self, event, metadata): + def __init__(self, event: RenderstageEvent, metadata: MetadataRenderPass): + ''' + Render pass workload in a trace. + + Args: + event: Parsed render stage event. + metadata: Parsed metadata annotation. + ''' # Populate common data super().__init__(event, metadata) # We must have metadata so no need to check - self.workload_x = metadata.width - self.workload_y = metadata.height + self.width = metadata.width + self.height = metadata.height self.draw_call_count = metadata.draw_call_count - - self.attachments = [] - for attachment in metadata.attachments.attachments: - self.attachments.append(GPURenderPassAttachment(attachment)) + self.attachments = list(metadata.attachments.attachments) @classmethod - def get_compact_string(cls, bindings): + def get_compact_string(cls, bindings: list[str]) -> str: + ''' + Get the compact UI string for a set of attachment bind points. + + Args: + bindings: The list of binding names for the binding type. + + Returns: + A binding string of the form, e.g. "C0124DS". + ''' merge = ''.join(bindings) return ''.join([j for i, j in enumerate(merge) if j not in merge[:i]]) - def get_attachment_long_label(self): + def get_attachment_long_label(self) -> str: + ''' + Get the long label showing use of render pass attachments. + + Returns: + A string showing loadOp, attachment, storeOp. + ''' bindings = [x.binding for x in self.attachments] present = self.get_compact_string(bindings) @@ -168,25 +157,37 @@ def get_attachment_long_label(self): return f'{loaded}[{present}]{stored}' - def get_attachment_short_label(self): + def get_attachment_short_label(self) -> str: + ''' + Get the short label showing use of render pass attachments. + + Returns: + A string showing attachments without load/storeOp usage. + ''' bindings = [x.binding for x in self.attachments] present = f'[{self.get_compact_string(bindings)}]' return present - def get_long_label(self): + def get_long_label(self) -> str: + ''' + Get the long form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] if self.label_stack: lines.append(self.label_stack[-1]) if self.draw_call_count < 0: - drawStr = 'Unknown draws' + draw_str = 'Unknown draws' elif self.draw_call_count == 1: - drawStr = '1 draw' + draw_str = '1 draw' else: - drawStr = f'{self.draw_call_count} draws' + draw_str = f'{self.draw_call_count} draws' - line = f'{self.workload_x}x{self.workload_y} ({drawStr})' + line = f'{self.width}x{self.height} ({draw_str})' lines.append(line) line = self.get_attachment_long_label() @@ -194,10 +195,16 @@ def get_long_label(self): return '\n'.join(lines) - def get_short_label(self): + def get_short_label(self) -> str: + ''' + Get the short form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] - line = f'{self.workload_x}x{self.workload_y}' + line = f'{self.width}x{self.height}' lines.append(line) line = self.get_attachment_short_label() @@ -211,16 +218,29 @@ class GPUDispatch(GPUWorkload): Workload class representing a render pass. ''' - def __init__(self, event, metadata): + def __init__(self, event: RenderstageEvent, metadata: MetadataDispatch): + ''' + Compute dispatch workload in a trace. + + Args: + event: Parsed render stage event. + metadata: Parsed metadata annotation. + ''' # Populate common data super().__init__(event, metadata) # We must have metadata so no need to check - self.workload_x = metadata.xGroups - self.workload_y = metadata.yGroups - self.workload_z = metadata.zGroups + self.groups_x = metadata.groups_x + self.groups_y = metadata.groups_y + self.groups_z = metadata.groups_z - def get_long_label(self): + def get_long_label(self) -> str: + ''' + Get the long form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] if self.label_stack: @@ -229,20 +249,28 @@ def get_long_label(self): lines.append(self.get_short_label()) return '\n'.join(lines) - def get_short_label(self): + def get_short_label(self) -> str: + ''' + Get the short form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] # If indirect then show a placeholder - if self.workload_x == -1: + if self.groups_x == -1: line = "?x?x? groups" - # If not 3D then show a 2D dimension + + # Else show the actual dimension else: - dims = [self.workload_x, self.workload_y] - # If 3D then show the Z dimension, else skip it - if self.workload_z > 1: - dims.append(self.workload_z) + dims = [self.groups_x, self.groups_y] + + # Hide Z dimension unless greater than 1 + if self.groups_z > 1: + dims.append(self.groups_z) - line = f'{"x".join([str(x) for x in dims])} groups' + line = f'{"x".join([str(dim) for dim in dims])} groups' lines.append(line) return '\n'.join(lines) @@ -251,9 +279,20 @@ def get_short_label(self): class GPUImageTransfer(GPUWorkload): ''' Workload class representing an image transfer. + + Image transfers are any transfer that writes to an image. Source may be + an image or a buffer. ''' - def __init__(self, event, metadata): + def __init__( + self, event: RenderstageEvent, metadata: MetadataImageTransfer): + ''' + Image transfer workload in a trace. + + Args: + event: Parsed render stage event. + metadata: Parsed metadata annotation. + ''' # Populate common data super().__init__(event, metadata) @@ -261,7 +300,13 @@ def __init__(self, event, metadata): self.transfer_type = metadata.subtype self.pixel_count = metadata.pixel_count - def get_long_label(self): + def get_long_label(self) -> str: + ''' + Get the long form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] if self.label_stack: @@ -277,16 +322,33 @@ def get_long_label(self): return '\n'.join(lines) - def get_short_label(self): + def get_short_label(self) -> str: + ''' + Get the short form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' return self.transfer_type class GPUBufferTransfer(GPUWorkload): ''' - Workload class representing a buffer transfer transfer. + Workload class representing a buffer transfer. + + Buffer transfers are any transfer that writes to a buffer. Source may be + an image or a buffer. ''' - def __init__(self, event, metadata): + def __init__( + self, event: RenderstageEvent, metadata: MetadataBufferTransfer): + ''' + Buffer transfer workload in a trace. + + Args: + event: Parsed render stage event. + metadata: Parsed metadata annotation. + ''' # Populate common data super().__init__(event, metadata) @@ -294,7 +356,13 @@ def __init__(self, event, metadata): self.transfer_type = metadata.subtype self.byte_count = metadata.byte_count - def get_long_label(self): + def get_long_label(self) -> str: + ''' + Get the long form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' lines = [] if self.label_stack: @@ -310,20 +378,47 @@ def get_long_label(self): return '\n'.join(lines) - def get_short_label(self): + def get_short_label(self) -> str: + ''' + Get the short form label for this workload. + + Returns: + Returns the label for use in the UI. + ''' return self.transfer_type +# Helper for typing all workload subclasses of MetadataWorkload +GPUWork = Union[ + # Generic workload if no metadata + GPUWorkload, + + # Specific workload if metadata + GPURenderPass, + GPUDispatch, + GPUImageTransfer, + GPUBufferTransfer +] + + class GPUTrace: ''' Decoded GPU trace, combining data sources into a single representation. + + Attributes: + streams: Mapping of stream name to list of events in that stream. ''' - def __init__(self, raw_trace): - self.streams = {} + def __init__(self, raw_trace: RawTrace): + ''' + Create a processed trace, combining raw trace data sources. + + Args: + raw_trace: The raw parsed file data. + ''' + self.streams = {} # type: dict[GPUStreamID, list[GPUWork]] for event in raw_trace.events: - # Find the metadata record for this event if we have one event_meta = None if event.user_label in raw_trace.metadata: @@ -332,18 +427,26 @@ def __init__(self, raw_trace): # Generic event if not event_meta: workload = GPUWorkload(event, None) + # Specific event - elif isinstance(event_meta, raw.MetadataRenderPass): + elif isinstance(event_meta, MetadataRenderPass): workload = GPURenderPass(event, event_meta) - elif isinstance(event_meta, raw.MetadataDispatch): + + elif isinstance(event_meta, MetadataDispatch): workload = GPUDispatch(event, event_meta) - elif isinstance(event_meta, raw.MetadataImageTransfer): + + elif isinstance(event_meta, MetadataImageTransfer): workload = GPUImageTransfer(event, event_meta) - elif isinstance(event_meta, raw.MetadataBufferTransfer): + + elif isinstance(event_meta, MetadataBufferTransfer): workload = GPUBufferTransfer(event, event_meta) + else: assert False, 'Unknown metadata type' + # All streams must have been decoded + assert workload.stream is not None + # Pack the workload into scheduling streams if workload.stream not in self.streams: self.streams[workload.stream] = [] diff --git a/lglpy/timeline/data/raw_trace.py b/lglpy/timeline/data/raw_trace.py index e7b7403..b2ceab9 100644 --- a/lglpy/timeline/data/raw_trace.py +++ b/lglpy/timeline/data/raw_trace.py @@ -21,23 +21,391 @@ # SOFTWARE. # ----------------------------------------------------------------------------- ''' -TODO +This module implements the raw traces that store parsed, but not process, +data loaded from Perfetto data and GPU Timeline layer data. These traces are +subsequently parsed into a single combined representation for tool use. ''' -import collections + +import enum import struct import json +from typing import Any, Optional, Union + +from lglpy.timeline.protos.perfetto.trace import trace_pb2 + + +# Helper for typing JSON payloads +JSONType = Any + + +class GPUStreamID(enum.Enum): + ''' + Symbolic mapping of known GPU scheduling stream IDs. + + Attributes: + COMPUTE: Compute and advanced geometry stream on all GPUs. + NONFRAGMENT: Non-fragment stream on Bifrost and Valhall GPUs. + FRAGMENT: Fragment stream on Bifrost and Valhall GPUs. + BINNING: Binning phase stream on 5th Generation GPUs. + MAIN: Main phase stream on 5th Generation GPUs. + TRANSFER: Transfer stream on all GPUs. + ''' + COMPUTE = 0 + NONFRAGMENT = 1 + FRAGMENT = 2 + BINNING = 3 + MAIN = 4 + TRANSFER = 5 + + @classmethod + def get_ui_name(cls, stream_id) -> str: + ''' + Get presentable name for a stream. + + Args: + stream_id: The enum value to convert. + + Returns: + Pretty name for use in user interfaces. + ''' + human_names = { + cls.COMPUTE: 'Compute', + cls.NONFRAGMENT: 'Non-fragment', + cls.FRAGMENT: 'Fragment', + cls.BINNING: 'Binning', + cls.MAIN: 'Main', + cls.TRANSFER: 'Transfer' + } + + return human_names[stream_id] + + +class GPUStageID(enum.Enum): + ''' + Symbolic mapping of known GPU workload stage IDs. + + Attributes: + COMPUTE: Compute shaders. + ADVANCED_GEOMETRY: Tessellation or Geometry shaders. + VERTEX: Vertex shaders from a render pass. + FRAGMENT: Fragment shaders from a render pass. + BINNING: Binning subset of vertex shaders from a render pass. + MAIN: Main phase vertex and fragment shaders from a render pass. + IMAGE_TRANSFER: Transfers writing an image output. + BUFFER_TRANSFER: Transfer writing a buffer output. + ''' + COMPUTE = 0 + ADVANCED_GEOMETRY = 1 + VERTEX = 2 + FRAGMENT = 3 + BINNING = 4 + MAIN = 5 + IMAGE_TRANSFER = 6 + BUFFER_TRANSFER = 7 + + @classmethod + def get_ui_name(cls, stage_id) -> str: + ''' + Get presentable name for a stage. + + Args: + stage_id: The enum value to convert. + + Returns: + Pretty name for use in user interfaces. + ''' + human_names = { + cls.COMPUTE: 'Compute', + cls.ADVANCED_GEOMETRY: 'Advanced geometry', + cls.VERTEX: 'Vertex', + cls.FRAGMENT: 'Fragment', + cls.BINNING: 'Binning', + cls.MAIN: 'Main', + cls.IMAGE_TRANSFER: 'Image transfer', + cls.BUFFER_TRANSFER: 'Buffer transfer' + } + + return human_names[stage_id] + + +class MetadataAttachment: + ''' + Parsed GPU Timeline layer payload for a single render pass attachment. + + Attributes: + binding: The name of the attachment point. + is_loaded: Is this attachment loaded from memory at start of pass? + is_store: Is this attachment stored to memory at end of pass? + is_resolved: Is this attachment a multi-sample resolve attachment? + ''' + + def __init__(self, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single render pass attachment. + + Attributes: + metadata: JSON payload from the layer. + ''' + self.binding = str(metadata['binding']) + self.is_loaded = bool(metadata.get('load', False)) + self.is_stored = bool(metadata.get('store', True)) + self.is_resolved = bool(metadata.get('resolve', True)) + + +class MetadataAttachments: + ''' + Parsed GPU Timeline layer payload for a set of render pass attachments. + + Attributes: + binding: The name of the attachment point. + is_loaded: Is this attachment loaded from memory at start of pass? + is_store: Is this attachment stored to memory at end of pass? + is_resolved: Is this attachment a multi-sample resolve attachment? + ''' + + def __init__(self, metadata: JSONType): + ''' + Parsed GPU Timeline layer attachment payload for a single render pass. + + Attributes: + metadata: JSON payload from the layer. + ''' + self.attachments = [] # type: list[MetadataAttachment] + + for attach_meta in metadata['attachments']: + attachment = MetadataAttachment(attach_meta) + self.attachments.append(attachment) + + self.attachments.sort(key=lambda x: x.binding) + + +class MetadataWorkload: + ''' + Baseclass for a parsed GPU Timeline layer payload for a workload. + + Attributes: + frame: The frame index in the application. + tag_id: The unique workload tag ID to cross-reference with Perfetto. + label_stack: Debug label stack, or None if no user labels. + ''' + + def __init__(self, frame: int, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single render pass. + + Attributes: + frame: The frame index in the application. + metadata: JSON payload from the layer. + ''' + self.frame = frame + self.tag_id = int(metadata['tid']) -from lglpy.timeline.data.processed_trace import * + self.label_stack = None + label_stack = metadata.get('label', None) + if label_stack: + self.label_stack = label_stack.split('|') + + def get_perfetto_tag_id(self) -> str: + ''' + Get the tag ID formatted to match the Perfetto data. + + Returns: + The Perfetto-formatted tag ID. + ''' + return f't{self.tag_id}' + + +class MetadataRenderPass(MetadataWorkload): + ''' + Parsed GPU Timeline layer payload for a render pass workload. + + Attributes: + width: Width of the render pass in pixels. + height: Height of the render pass in pixels. + draw_call_count: Number of draw calls in the render pass. + attachments: List of render pass attachments. + ''' + + def __init__(self, frame: int, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single render pass. + + Attributes: + frame: The frame index in the application. + metadata: JSON payload from the layer. + ''' + super().__init__(frame, metadata) + + self.width = int(metadata['width']) + self.height = int(metadata['height']) + self.draw_call_count = int(metadata['drawCallCount']) + + self.attachments = MetadataAttachments(metadata) + + +class MetadataDispatch(MetadataWorkload): + ''' + Parsed GPU Timeline layer payload for a compute dispatch workload. + + Attributes: + groups_x: Width of the dispatch in work groups, or -1 if unknown. + groups_y: Height of the dispatch in work groups, or -1 if unknown. + groups_z: Depth of the dispatch in work groups, or -1 if unknown. + ''' + + def __init__(self, frame: int, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single dispatch. + + Attributes: + frame: The frame index in the application. + metadata: JSON payload from the layer. + ''' + super().__init__(frame, metadata) + + self.groups_x = int(metadata['xGroups']) + self.groups_y = int(metadata['yGroups']) + self.groups_z = int(metadata['zGroups']) + + def get_perfetto_tag_id(self) -> str: + ''' + Get the tag ID formatted to match the Perfetto data. + + Returns: + The Perfetto-formatted tag ID. + ''' + return f't{self.tag_id}' + + +class MetadataImageTransfer(MetadataWorkload): + ''' + Parsed GPU Timeline layer payload for a transfer that writes an image. + + Attributes: + subtype: Specific type of the transfer. + pixel_count: Number of pixels written, or -1 if unknown. + ''' -import lglpy.timeline.protos.perfetto.trace.trace_pb2 as trace_pb2 + def __init__(self, frame: int, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single image transfer. + + Attributes: + frame: The frame index in the application. + metadata: JSON payload from the layer. + ''' + super().__init__(frame, metadata) + + self.subtype = str(metadata['subtype']) + + if 'pixelCount' in metadata: + self.pixel_count = int(metadata['pixelCount']) + # Remove this when we re-record our test traces + else: + self.pixel_count = int(metadata['pixels']) + + +class MetadataBufferTransfer(MetadataWorkload): + ''' + Parsed GPU Timeline layer payload for a transfer that writes a buffer. + + Attributes: + frame: The frame index in the application. + tag_id: The unique workload tag ID to cross-reference with Perfetto. + label_stack: Debug label stack, or None if no user labels. + subtype: Specific type of the transfer. + byte_count: Number of bytes written, or -1 if unknown. + ''' + + def __init__(self, frame: int, metadata: JSONType): + ''' + Parsed GPU Timeline layer payload for a single buffer transfer. + + Attributes: + frame: The frame index in the application. + metadata: JSON payload from the layer. + ''' + super().__init__(frame, metadata) + + self.subtype = str(metadata['subtype']) + + if 'byteCount' in metadata: + self.byte_count = int(metadata['byteCount']) + # Remove this when we re-record our test traces + else: + self.byte_count = int(metadata['bytes']) + + +class RenderstageEvent: + ''' + Parsed Perfetto trace payload for a render stages event. + + The GPU Timeline layers embeds its tag_id used for cross-referencing in to + the user_label field, otherwise this is the user debug label. + + Attributes: + start_time: The event start time, in nanoseconds. + duration: The event duration, in nanoseconds. + stream_iid: The interned ID of the stream. + stream: The type of the stream after resolving interning. + stage_iid: The interned ID of the stage. + stream: The type of the stage after resolving interning. + user_label: The user debug label, if specified. + ''' + + def __init__(self, start_time: int, spec: Any): + ''' + Parsed Perfetto trace payload for a render stages event. + + Args: + start_time: The event start time, in nanoseconds. + spec: The event payload. + ''' + self.start_time = start_time + self.duration = int(spec.duration) + + # Decode the interned stream and stage types + # Interning is resolved later as packets may be out-of-order + self.stream_iid = int(spec.hw_queue_iid) + self.stream = None # type: Optional[GPUStreamID] + + self.stage_iid = int(spec.stage_iid) + self.stage = None # type: Optional[GPUStageID] + + # Decode the user label if we have one + self.user_label = None + for item in spec.extra_data: + if item.name != 'Labels': + continue + + self.user_label = str(item.value) + + +# Helper for typing all workload subclasses of MetadataWorkload +MetadataWork = Union[ + MetadataRenderPass, + MetadataDispatch, + MetadataImageTransfer, + MetadataBufferTransfer +] class PerfettoConfig: ''' This class persists settings found in the Perfetto trace that are needed to fully decode later event packets in the trace. - ''' + Attributes: + STREAM_REMAP: Mapping of Perfetto stream names to enum values. + STAGE_REMAP: Mapping of Perfetto stage names to enum values. + clock_sync: Mapping of clock sync information to allow correction + for clock drift across different clock sources. + raw_iids: Mapping of numeric IDs to interned string labels. + stream_iids: Mapping of numeric IDs to interned string labels that + have been verified as belonging to GPU stream names. + stage_iids: Mapping of numeric IDs to interned string labels that + have been verified as belonging to GPU render stage names. + ''' # Known Perfetto streams and their remapped names STREAM_REMAP = { "compute": GPUStreamID.COMPUTE, @@ -69,26 +437,33 @@ def __init__(self): Initialize an empty configuration to be populated incrementally. ''' # Clock sync information - self.clock_sync = {} + self.clock_sync = {} # type: dict[int, int] - # Interned data references - self.raw_iids = {} - self.stream_iids = {} - self.stage_iids = {} + # Interned data reference IDs + self.raw_iids = {} # type: dict[int, str] + self.stream_iids = {} # type: dict[int, GPUStreamID] + self.stage_iids = {} # type: dict[int, GPUStageID] - def add_interned_data(self, spec): + def add_interned_data(self, spec) -> None: ''' Add raw interned string data which we can reference later. + + Args: + spec: The Perfetto interned data specification object. ''' iid = spec.iid if spec.HasField('iid') else 0 name = spec.name if spec.HasField('name') else None assert iid and name, 'ERROR: Interned data missing expected fields' self.raw_iids[iid] = name - def add_clock_sync_data(self, event): + def add_clock_sync_data(self, event) -> None: ''' Add raw clock sync data which we can use to correct timing later. + + Args: + spec: The Perfetto clock sync snapshot object. ''' + # Default clock domain is clock 5, BUILTIN_CLOCK_MONOTONIC_RAW root_clock = 5 root_time = None @@ -110,22 +485,33 @@ def add_clock_sync_data(self, event): correction = clock.timestamp - root_time self.clock_sync[clock.clock_id] = correction - def get_event_time(self, event): + def get_event_time(self, event) -> int: ''' - Get the event time in a unified clock domain applying clock sync. + Get the event time in a unified clock domain. + + Args: + event: The Perfetto event to extract the timestamp from. + + Returns: + Event start time in a unified clock domain. ''' - clock_id = 6 + # Determine the clock this event it using + clock_id = 6 # Default is BUILTIN_CLOCK_BOOTTIME if event.HasField('timestamp_clock_id'): clock_id = event.timestamp_clock_id + # Correct for clock offset and skew across the clock sources return event.timestamp - self.clock_sync[clock_id] - def replace_interned_stream(self, event): + def replace_interned_stream(self, event: RenderstageEvent) -> None: ''' Replaced interned data stream references with the real data. + + Args: + event: The Perfetto event to rewrite ''' # Rewrite the hardware stream - stream = event.stream + stream = event.stream_iid # Interned ID has been found and remapped already if stream in self.stream_iids: @@ -145,12 +531,15 @@ def replace_interned_stream(self, event): else: assert False, 'ERROR: Unknown stream interned data ID' - def replace_interned_stage(self, event): + def replace_interned_stage(self, event: RenderstageEvent) -> None: ''' - Replaced interned data stage references with the real data. + Replaced interned data render stage references with the real data. + + Args: + event: The Perfetto event to rewrite ''' # Rewrite the hardware stage - stage = event.stage + stage = event.stage_iid # Interned ID has been found and remapped already if stage in self.stage_iids: @@ -171,232 +560,83 @@ def replace_interned_stage(self, event): assert False, "Stage IID not found" -class MetadataAttachment: - - def __init__(self, metadata): - self.binding = metadata['binding'] - self.is_loaded = metadata.get('load', False) - self.is_stored = metadata.get('store', True) - self.is_resolved = metadata.get('resolve', True) - - -class MetadataAttachments: - - def __init__(self, metadata): - self.attachments = [] - - for attach_meta in metadata['attachments']: - attachment = MetadataAttachment(attach_meta) - self.attachments.append(attachment) - - self.attachments.sort(key=lambda x: x.binding) - - -class MetadataRenderPass: - - def __init__(self, frame, metadata): - self.frame = frame - self.tagID = metadata['tid'] - - self.label_stack = None - label_stack = metadata.get('label', None) - if label_stack: - self.label_stack = label_stack.split('|') - - self.width = metadata['width'] - self.height = metadata['height'] - self.draw_call_count = metadata['drawCallCount'] - - self.attachments = MetadataAttachments(metadata) - - def getKey(self): - return f't{self.tagID}' - - -class MetadataDispatch: - - def __init__(self, frame, metadata): - self.frame = frame - self.tagID = metadata['tid'] - - self.label_stack = None - label_stack = metadata.get('label', None) - if label_stack: - self.label_stack = label_stack.split('|') - - self.xGroups = metadata['xGroups'] - self.yGroups = metadata['yGroups'] - self.zGroups = metadata['zGroups'] - - def getKey(self): - return f't{self.tagID}' - - -class MetadataImageTransfer: - - def __init__(self, frame, metadata): - self.frame = frame - self.tagID = metadata['tid'] - - self.label_stack = None - label_stack = metadata.get('label', None) - if label_stack: - self.label_stack = label_stack.split('|') - - self.subtype = metadata['subtype'] - self.pixel_count = metadata['pixels'] # TODO: pixelCount - - def getKey(self): - return f't{self.tagID}' - - def get_long_label(self): - lines = [] - - if self.label_stack: - lines.append(self.label_stack[-1]) - - # If indirect then show a placeholder - if self.pixelCount == -1: - line = f'{self.subtype} (? pixels)' - elif self.pixelCount == 1: - line = f'{self.subtype} (1 pixel)' - # If 3D then show a 3D dimension - else: - line = f'{self.subtype} ({self.pixelCount} pixels)' - - lines.append(line) - return '\n'.join(lines) - - def get_short_label(self): - if None is self.subtype: - return "Missing subtype" - return self.subtype - - -class MetadataBufferTransfer: - - def __init__(self, frame, metadata): - self.frame = frame - self.tagID = metadata['tid'] - - self.label_stack = None - label_stack = metadata.get('label', None) - if label_stack: - self.label_stack = label_stack.split('|') - - self.subtype = metadata['subtype'] - self.byte_count = metadata['bytes'] # TODO: byteCount - - def getKey(self): - return f't{self.tagID}' - - -class RenderstageEvent: - - def __init__(self, start_time, spec): - self.start_time = start_time - self.duration = spec.duration - self.stream = spec.hw_queue_iid - self.stage = spec.stage_iid - - # Decode the user label if we have one - self.user_label = None - for i, item in enumerate(spec.extra_data): - if item.name != 'Labels': - continue - - self.user_label = item.value - - self.submission_id = spec.submission_id - - self.command_buffer_handle = None - if spec.HasField('command_buffer_handle'): - self.command_buffer_handle = spec.command_buffer_handle - self.command_buffer_handle &= 0x00FFFFFFFFFFFFFF - - self.framebuffer_handle = None - if spec.HasField('render_target_handle'): - self.framebuffer_handle = spec.render_target_handle - self.framebuffer_handle &= 0x00FFFFFFFFFFFFFF - - self.framebuffer_handle = None - if spec.HasField('render_pass_handle'): - self.render_pass_handle = spec.render_pass_handle - self.render_pass_handle &= 0x00FFFFFFFFFFFFFF - - assert self.stream and self.stage - - def get_long_label(self): - # Get metadata - if self.metadata: - return self.metadata.get_long_label() - elif self.user_label: - return f'L: {self.user_label}\n{self.stage}' - return f'S: {self.submission_id}\n{self.stage}' - - def get_short_label(self): - # Get metadata - if self.metadata: - return self.metadata.get_short_label() - elif self.user_label: - return f'L: {self.user_label}' - return f'S: {self.submission_id}' - - class RawTrace: + ''' + A raw trace pair loaded from the file system. + + Attributes: + events: The perfetto event data. + metadata: The layer metadata for the events. + ''' @classmethod - def load_metadata_from_file(cls, metadata_file): + def load_metadata_from_file( + cls, metadata_file: str) -> dict[str, MetadataWork]: ''' Load the raw metadata from file. + + Args: + metadata_file: The file path of the metadata payload. ''' - metadata = {} + metadata = {} # type: dict[str, MetadataWork] with open(metadata_file, 'rb') as handle: while True: - # Read frame header and exit on partial read - frame_size = handle.read(4) - if len(frame_size) != 4: + # End decoding if we get a partial frame header + bin_data = handle.read(4) + if len(bin_data) != 4: break - frame_size = struct.unpack(' list[RenderstageEvent]: ''' - Load the raw metadata from file. + Load the raw Perfetto trace from file. + + Args: + perfetto_file: The file path of the Perfetto trace payload. ''' config = PerfettoConfig() trace_events = [] # Open the Perfetto protobuf trace file - protoc = trace_pb2.Trace() - with open(file_path, 'rb') as handle: + protoc = trace_pb2.Trace() # pylint: disable=no-member + with open(perfetto_file, 'rb') as handle: protoc.ParseFromString(handle.read()) # Extract render stages events from Perfetto data @@ -451,6 +691,16 @@ def load_perfetto_from_file(cls, file_path): return trace_events - def __init__(self, trace_file, metadata_file): - self.metadata = self.load_metadata_from_file(metadata_file) + def __init__(self, trace_file: str, metadata_file: str): + ''' + Load a trace from file. + + Args: + trace_file: The file path of the Perfetto trace payload. + metadata_file: The file path of the Timeline layer trace payload. + ''' self.events = self.load_perfetto_from_file(trace_file) + + self.metadata = None + if metadata_file: + self.metadata = self.load_metadata_from_file(metadata_file) diff --git a/lglpy/timeline/drawable/canvas_drawable.py b/lglpy/timeline/drawable/canvas_drawable.py index 92db5de..231ece7 100644 --- a/lglpy/timeline/drawable/canvas_drawable.py +++ b/lglpy/timeline/drawable/canvas_drawable.py @@ -63,7 +63,7 @@ def __init__(self, pos, dim, style, label=None): class CanvasDrawableRect(CanvasDrawable): ''' - A canvas-space rectangle with fill, stroke, and label. Any of these may be + A canvas-space rectangle with fill, and stroke. Any of these may be skipped by setting its color to None in the style. ''' @@ -89,17 +89,11 @@ def draw(self, gc): gc.rectangle(x, y, w, h) gc.stroke() - # Draw label, centered in object - if self.label and self.style.bind_font(gc): - lw, lh = self.get_label_extents(gc) - gc.move_to(x + w * 0.5 - lw * 0.5, y + h * 0.5 + lh * 0.5) - gc.show_text(self.label) - class CanvasDrawableRectFill(CanvasDrawable): ''' - A canvas-space rectangle with fill, and label. Any of these may be skipped - by setting its color to None in the style. + A canvas-space rectangle with fill. Any of these may be skipped by setting + its color to None in the style. This is useful for uses rendering a composite shape that contains a mixture of fill-only rectangles as well as lines or full rectangles with a stroke. @@ -124,12 +118,6 @@ def draw(self, gc): gc.rectangle(x, y, w, h) gc.fill() - # Draw label, centered in object - if self.label and self.style.bind_font(gc): - lw, lh = self.get_label_extents(gc) - gc.move_to(x + w * 0.5 - lw * 0.5, y + h * 0.5 + lh * 0.5) - gc.show_text(self.label) - class CanvasDrawableLine(CanvasDrawable): ''' diff --git a/lglpy/timeline/drawable/drawable.py b/lglpy/timeline/drawable/drawable.py index cddfc41..a91b116 100644 --- a/lglpy/timeline/drawable/drawable.py +++ b/lglpy/timeline/drawable/drawable.py @@ -21,7 +21,7 @@ # SOFTWARE. # ----------------------------------------------------------------------------- ''' -This module contains a number of basic abstract_rendering constants and +This module contains a number of basic abstract rendering constants and primitives. ''' @@ -73,7 +73,7 @@ def __init__(self, style): ''' self.style = style - def set_style(self, style): + def set_style(self, style) -> None: ''' Change the style of this object. @@ -95,7 +95,7 @@ def draw(self, gc): assert False, f'Draw function for {self.__class__.__name__} missing' @staticmethod - def rt00(x): + def rt00(x: float) -> float: ''' Round x down to an integer boundary. @@ -108,7 +108,7 @@ def rt00(x): return float(int(x)) @staticmethod - def rt05(x): + def rt05(x: float) -> float: ''' Round x to the nearest n.5 boundary. @@ -150,7 +150,7 @@ def __init__(self, style, label): Args: style: Rendering style for this object. - label: Full label text string. + label: Full label text string, may be None ''' super().__init__(style) @@ -178,7 +178,7 @@ def set_style(self, style): super().set_style(style) - def compute_label_extents(self, gc): + def compute_label_extents(self, gc) -> None: ''' Compute the canvas-space extents of the text label. @@ -198,9 +198,9 @@ def compute_label_extents(self, gc): self.line_extents = [gc.text_extents(x)[2] for x in self.label] self.total_extents = (max(self.line_extents), h) - def fits_centered(self, gc, width, padding): + def fits_centered(self, gc, width, padding) -> bool: ''' - Get the label that fits in the given on-screen width. + Test of the label that fits in the given on-screen width. Args: gc: Cairo graphics context @@ -208,14 +208,13 @@ def fits_centered(self, gc, width, padding): padding: Desired padding in pixels. Returns: - Tuple of string and extents for a label that fits. - Tuple of None if no label fits. + True if the label fits, False otherwise. ''' target_width = width - 2 * padding self.compute_label_extents(gc) return self.total_extents[0] <= target_width - def draw_centered(self, gc, pos, dim): + def draw_centered(self, gc, pos, dim) -> None: ''' Draw the label on the screen. @@ -224,7 +223,6 @@ def draw_centered(self, gc, pos, dim): pos: Origin on canvas pixels. dim: Space in canvas pixels. ''' - line_count = len(self.label) lh = self.line_height pad_y = (dim[1] - self.total_extents[1]) * 0.5 - 1 diff --git a/lglpy/timeline/drawable/timeline_base.py b/lglpy/timeline/drawable/timeline_base.py index e942e4b..acf4194 100644 --- a/lglpy/timeline/drawable/timeline_base.py +++ b/lglpy/timeline/drawable/timeline_base.py @@ -35,7 +35,8 @@ from lglpy.timeline.drawable.drawable import Drawable from lglpy.timeline.drawable.style import Style -from lglpy.timeline.drawable.canvas_drawable import * +from lglpy.timeline.drawable.canvas_drawable import CanvasDrawableRect, \ + CanvasDrawableRectFill, CanvasDrawableLabel, CanvasDrawableLine from lglpy.timeline.drawable.timeline_viewport import TimelineViewport from lglpy.timeline.drawable.entry_dialog import get_entry_dialog @@ -96,6 +97,8 @@ def __init__(self, trace, cs_pos, cs_dim, css, prefix): css: CSS stylesheet to load styles from. prefix: the widget prefix for the stylesheet. ''' + self.parent = None + self.drawable_trace = trace # Initial bounds are the size of the whole trace, but may be no data diff --git a/lglpy/timeline/drawable/world_drawable.py b/lglpy/timeline/drawable/world_drawable.py index 5ec7067..c0c5f61 100644 --- a/lglpy/timeline/drawable/world_drawable.py +++ b/lglpy/timeline/drawable/world_drawable.py @@ -172,42 +172,3 @@ def draw(self, gc, vp): gc.move_to(line_from[0], line_from[1]) gc.line_to(line_to[0], line_to[1]) gc.stroke() - - -class WorldDrawableLabel(WorldDrawable): - ''' - A world-space label, drawn left-aligned. - ''' - - __slots__ = ("cs_offset_x",) - - def __init__(self, pos, style, label, cs_offset_x=0): - ''' - Args: - pos: X and Y coordinate in world-space. - style: visual style. - label: text label. - cs_offset_x: canvas-space x offset, e.g. for padding. - ''' - assert label - dim = (200, 100) # TODO this is a hack - needs improving - super().__init__(pos, dim, style, label) - self.cs_offset_x = cs_offset_x - - def draw(self, gc, vp): - ''' - Render this object. - - Args: - gc: Cairo graphics context. - vp: viewport configuration. - ''' - if (not vp.is_object_visible(self)) or (not self.style.font_color): - return - - x, y = vp.transform_ws_to_cs(self.ws.min_x, self.ws.min_y, 2) - x = x + self.cs_offset_x - - if self.style.bind_font(gc): - gc.move_to(x, y) - gc.show_text(self.label) diff --git a/lglpy/timeline/gui/timeline/timeline_widget.py b/lglpy/timeline/gui/timeline/timeline_widget.py index 10f370d..ba1b784 100644 --- a/lglpy/timeline/gui/timeline/timeline_widget.py +++ b/lglpy/timeline/gui/timeline/timeline_widget.py @@ -29,7 +29,6 @@ # pylint: disable=wrong-import-position from gi.repository import Gtk -from lglpy.timeline.drawable.world_drawable import WorldDrawableLabel from lglpy.timeline.drawable.world_drawable import WorldDrawableLine from lglpy.timeline.drawable.timeline_base import TimelineWidgetBase @@ -132,24 +131,6 @@ class for documentation. return True - # Is using control then show the grouping save/restore menu - if mod == 'c': - menu = Gtk.Menu() - - menui = Gtk.MenuItem('Save active objects') - menui.connect_object('activate', self.on_norc5, clicked) - menu.append(menui) - - if None is not self.active_objects_stash: - menui = Gtk.MenuItem('Restore active objects') - menui.connect_object('activate', self.on_norc6, clicked) - menu.append(menui) - - menu.show_all() - menu.popup_at_pointer(event) - - return True - return False def on_orc1(self, clicked_object): @@ -164,6 +145,7 @@ def on_norc1(self, clicked_object): ''' Right click menu handler -> clear range ''' + del clicked_object self.active_time_range = [] self.parent.queue_draw() @@ -171,6 +153,7 @@ def on_norc2(self, clicked_object): ''' Right click menu handler -> clear selection ''' + del clicked_object self.clear_active_objects() self.parent.queue_draw() @@ -178,28 +161,13 @@ def on_norc3(self, clicked_object): ''' Right click menu handler -> clear clamp limits ''' + del clicked_object self.ws_clamp_min_x = self.original_trace_ws_min_x - 100 self.trace_ws_min_x = self.ws_clamp_min_x self.ws_clamp_max_x = self.original_trace_ws_max_x + 100 self.trace_ws_max_x = self.ws_clamp_max_x self.parent.queue_draw() - def on_norc5(self, clicked_object): - ''' - Right click menu handler -> save active objects - ''' - self.active_objects_stash = self.active_objects.copy() - - def on_norc6(self, clicked_object): - ''' - Right click menu handler -> restore active objects - ''' - self.clear_active_objects() - for drawable in self.active_objects_stash: - self.add_to_active_objects(drawable) - self.active_objects_stash = None - self.parent.queue_draw() - def on_jump_bookmark(self, name): ''' Right click menu handler -> jump to bookmark diff --git a/lglpy/timeline/gui/timeline/view.py b/lglpy/timeline/gui/timeline/view.py index 2ab194d..bf03c4c 100644 --- a/lglpy/timeline/gui/timeline/view.py +++ b/lglpy/timeline/gui/timeline/view.py @@ -25,7 +25,6 @@ ''' import time -import random import gi gi.require_version('Gtk', '3.0') @@ -235,30 +234,30 @@ def add_style(self, css, style_name, variant): style_set = self.get_style_set(style_name) style_set.add_style(variant, style) - def get_style(self, channel, rotation=0, types='all'): + def get_style(self, name, variant=0, types='all'): ''' Add a new style to the timeline style library ''' # Determining the correct style is quite slow and called frequently, so # build a hash table of styles matching requested keys and use that ... - key = (channel, rotation, types) + key = (name, variant, types) if key in self.cache: return self.cache[key] - channel = channel.split('.') - channel = channel[0:2] - channel = '.'.join(channel) + name = name.split('.') + name = name[0:2] + name = '.'.join(name) # Find the style specified for the given channel, object, and rotation def test(x): - return x.channel == channel and x.types == types + return x.channel == name and x.types == types cmap = [x for x in self.color_map if test(x)] - assert len(cmap) == 1, f'{channel}, {len(cmap)}' + assert len(cmap) == 1, f'{name}, {len(cmap)}' style = cmap[0].style_code if cmap[0].rotation: - style = f'{style}{rotation}' + style = f'{style}{variant}' # Call the parent class method to fetch the correct style style = super().get_style(style) @@ -402,7 +401,7 @@ def on_clear_bookmarks(self, _unused): self.timeline_widget.bookmarks = {} self.parent.queue_draw() - def load(self, data=None): + def load(self, trace_data=None): ''' Populate this view with a loaded data file. @@ -415,7 +414,7 @@ def load(self, data=None): trace = DrawableTrace(style) self.timeline_trace = trace - if not data: + if not trace_data: return # TODO: Channel names need to be made dynamic @@ -429,7 +428,7 @@ def load(self, data=None): channel.label_visible = tl.label # Add scheduling channels - for name, stream in data.streams.items(): + for name, stream in trace_data.streams.items(): name = name.get_ui_name(name) channel = trace.get_channel(name) diff --git a/lglpy/timeline/gui/window.py b/lglpy/timeline/gui/window.py index f7389ee..4455b3c 100644 --- a/lglpy/timeline/gui/window.py +++ b/lglpy/timeline/gui/window.py @@ -27,6 +27,7 @@ import os import sys import time +import traceback import gi gi.require_version('Gtk', '3.0') @@ -333,11 +334,26 @@ def load_file(self, trace_file, metadata_file=None): Handle the underlying data processing related to opening a file. Args: - file_name: The trace file to load. + trace_file: The perfetto trace file. + metadata_file: The layer metadata file. ''' assert self.loaded_file_path is None assert self.trace_data is None + # Derive the metadata file if we can + if not metadata_file: + postfix = '.perfetto' + if trace_file.endswith(postfix): + metadata_file = f'{trace_file[:-len(postfix)]}.gputl' + + if not os.path.exists(metadata_file): + metadata_file = None + + # Notify the plugin views - this may take some time so defer it; change + # the cursor so the user knows that we're thinking ... + watch_cursor = Gdk.Cursor(Gdk.CursorType.WATCH) + self.window.get_root_window().set_cursor(watch_cursor) + self.status.log('File loading. Please wait ...', None) # Notify the plugin views - this may take some time so defer it; change @@ -359,7 +375,6 @@ def deferred_load(): self.loaded_file_path = trace_file except Exception: self.status.log('Open cancelled (failed to load)') - import traceback traceback.print_exc() return diff --git a/source_common/trackers/layer_command_stream.cpp b/source_common/trackers/layer_command_stream.cpp index 3a54bc0..48a62ae 100644 --- a/source_common/trackers/layer_command_stream.cpp +++ b/source_common/trackers/layer_command_stream.cpp @@ -272,7 +272,7 @@ std::string LCSImageTransfer::getMetadata( { "type", "imagetransfer" }, { "tid", tagID }, { "subtype", transferType }, - { "pixels", pixelCount } + { "pixelCount", pixelCount } }; if (debugLabel && debugLabel->size()) @@ -307,7 +307,7 @@ std::string LCSBufferTransfer::getMetadata( { "type", "buffertransfer" }, { "tid", tagID }, { "subtype", transferType }, - { "bytes", byteCount } + { "byteCount", byteCount } }; if (debugLabel && debugLabel->size())