blob: 0288ba91655ffcecc581bb6243c1ce6f5ae68e34 [file] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type {Transform1D, Transform2D} from '../geom';
import {
RECT_PATTERN_FADE_RIGHT,
RECT_PATTERN_HATCHED,
type RowLayout,
type SliceBuffers,
SLICE_GAP_PX,
} from '../renderer';
import {createProgram, getUniformLocation} from './gl';
// Static quad geometry shared by all slice batches
const QUAD_CORNERS = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const QUAD_INDICES = new Uint16Array([0, 1, 2, 3]);
interface SliceBatchProgram {
readonly program: WebGLProgram;
readonly quadCornerLoc: number;
readonly leftLoc: number;
readonly rightLoc: number;
readonly depthLoc: number;
readonly colorLoc: number;
readonly patternLoc: number;
readonly resolutionLoc: WebGLUniformLocation;
readonly xTransformLoc: WebGLUniformLocation;
readonly viewTransformLoc: WebGLUniformLocation;
readonly clipRectLoc: WebGLUniformLocation;
readonly firstRowHeightLoc: WebGLUniformLocation;
readonly rowHeightLoc: WebGLUniformLocation;
readonly rowGapLoc: WebGLUniformLocation;
readonly paddingTopLoc: WebGLUniformLocation;
}
function createSliceProgram(gl: WebGL2RenderingContext): SliceBatchProgram {
const vsSource = `#version 300 es
in vec2 a_quadCorner; // (0,0), (1,0), (0,1), (1,1) for the quad corners (per vertex)
in float a_left; // Left in data space (per instance)
in float a_right; // Right in data space (per instance)
in uint a_depth; // Row index (per instance)
in uint a_color; // Packed RGBA color (0xRRGGBBAA) (per instance)
in uint a_pattern; // Bitfield for patterns (per instance)
uniform vec2 u_xTransform; // Data space X -> CSS pixels (scaleX, offsetX)
uniform vec4 u_viewTransform; // CSS pixels -> device pixels (scaleX, scaleY, offsetX, offsetY)
uniform vec2 u_resolution; // Canvas size in device pixels
uniform vec4 u_clipRect; // Clip rect in device pixels (LTRB)
uniform float u_firstRowHeight; // Height of row 0 in CSS pixels
uniform float u_rowHeight; // Height of rows at depth > 0 in CSS pixels
uniform float u_rowGap; // Vertical gap between rows in CSS pixels
uniform float u_paddingTop; // Top padding in CSS pixels
out vec4 v_color;
out vec2 v_localPos;
flat out uint v_pattern;
flat out float v_rectWidth;
// Minimum width in CSS pixels to ensure visibility
const float MIN_WIDTH = 1.0;
// Gap inset from each slice's right edge (see SLICE_GAP_PX).
const float SLICE_GAP = float(${SLICE_GAP_PX});
// Transform a LTRB rect through a scale+translate transform.
vec4 transformRect(vec4 t, vec4 r) {
return vec4(r.xy * t.xy + t.zw, r.zw * t.xy + t.zw);
}
// Clamp an LTRB rect to clip bounds, ensuring valid rect.
vec4 clampRect(vec4 r, vec4 clip) {
r = clamp(r, clip.xyxy, clip.zwzw);
r.zw = max(r.xy, r.zw);
return r;
}
void main() {
// Compute row top/bottom from depth using a two-tier formula.
// Row 0 uses firstRowHeight; deeper rows use rowHeight with stride.
float stride = u_rowHeight + u_rowGap;
float rowTop;
float rowBottom;
if (a_depth == 0u) {
rowTop = u_paddingTop;
rowBottom = rowTop + u_firstRowHeight;
} else {
rowTop = u_paddingTop + u_firstRowHeight + u_rowGap + float(a_depth - 1u) * stride;
rowBottom = rowTop + u_rowHeight;
}
// Transform X from data space to CSS pixels
float leftCss = a_left * u_xTransform.x + u_xTransform.y;
float rightCss = a_right * u_xTransform.x + u_xTransform.y;
// Inset the right edge so adjacent slices stay distinct, clamped to the
// minimum width.
rightCss = max(leftCss + MIN_WIDTH, rightCss - SLICE_GAP);
// Build rect in CSS pixel space (LTRB)
vec4 rectCss = vec4(leftCss, rowTop, rightCss, rowBottom);
// CSS pixels -> device pixels
vec4 rect = transformRect(u_viewTransform, rectCss);
// Clamp to clip rect
rect = clampRect(rect, u_clipRect);
// Interpolate vertex position within clipped rect
vec2 pixelPos = mix(rect.xy, rect.zw, a_quadCorner);
vec2 viewScale = u_viewTransform.xy;
gl_Position = vec4((pixelPos / u_resolution * 2.0 - 1.0) * vec2(1, -1), 0, 1);
v_localPos = (pixelPos - rect.xy) / viewScale;
v_rectWidth = (rect.z - rect.x) / viewScale.x;
v_color = vec4(
float((a_color >> 24) & 0xffu) / 255.0,
float((a_color >> 16) & 0xffu) / 255.0,
float((a_color >> 8) & 0xffu) / 255.0,
float(a_color & 0xffu) / 255.0
);
v_pattern = a_pattern;
}
`;
const fsSource = `#version 300 es
precision mediump float;
in vec4 v_color;
in vec2 v_localPos;
flat in uint v_pattern;
flat in float v_rectWidth;
out vec4 fragColor;
const uint FLAG_HATCHED = ${RECT_PATTERN_HATCHED}u;
const uint FLAG_FADEOUT = ${RECT_PATTERN_FADE_RIGHT}u;
const float HATCH_SPACING = 8.0;
const float HATCH_WIDTH = 2.0;
const float HATCH_MIN_WIDTH = 4.0;
void main() {
fragColor = v_color;
if ((v_pattern & FLAG_FADEOUT) != 0u) {
float fadeProgress = v_localPos.x / v_rectWidth;
float fadeAmount = clamp((fadeProgress - 0.66) / 0.34, 0.0, 1.0);
fragColor.a *= 1.0 - fadeAmount;
}
if ((v_pattern & FLAG_HATCHED) != 0u && v_rectWidth >= HATCH_MIN_WIDTH) {
float diag = mod(v_localPos.x, HATCH_SPACING) + v_localPos.y;
float stripe = mod(diag, HATCH_SPACING);
if (stripe < HATCH_WIDTH) {
fragColor.rgb = mix(fragColor.rgb, vec3(1.0), 0.3);
}
}
// Premultiply alpha for correct blending
fragColor.rgb *= fragColor.a;
}
`;
const program = createProgram(gl, vsSource, fsSource);
return {
program,
quadCornerLoc: gl.getAttribLocation(program, 'a_quadCorner'),
leftLoc: gl.getAttribLocation(program, 'a_left'),
rightLoc: gl.getAttribLocation(program, 'a_right'),
depthLoc: gl.getAttribLocation(program, 'a_depth'),
colorLoc: gl.getAttribLocation(program, 'a_color'),
patternLoc: gl.getAttribLocation(program, 'a_pattern'),
resolutionLoc: getUniformLocation(gl, program, 'u_resolution'),
xTransformLoc: getUniformLocation(gl, program, 'u_xTransform'),
viewTransformLoc: getUniformLocation(gl, program, 'u_viewTransform'),
clipRectLoc: getUniformLocation(gl, program, 'u_clipRect'),
firstRowHeightLoc: getUniformLocation(gl, program, 'u_firstRowHeight'),
rowHeightLoc: getUniformLocation(gl, program, 'u_rowHeight'),
rowGapLoc: getUniformLocation(gl, program, 'u_rowGap'),
paddingTopLoc: getUniformLocation(gl, program, 'u_paddingTop'),
};
}
/**
* A batch renderer for slices using instanced rendering.
* Each slice has a left, right, depth, color, and pattern.
* Row positions are computed in the shader from a simple formula:
* top = paddingTop + depth * rowStride
* bottom = top + rowHeight
*/
export class SliceBatch {
private readonly gl: WebGL2RenderingContext;
private readonly prog: SliceBatchProgram;
// GPU buffers
private readonly quadCornerBuffer: WebGLBuffer;
private readonly quadIndexBuffer: WebGLBuffer;
private readonly leftBuffer: WebGLBuffer;
private readonly rightBuffer: WebGLBuffer;
private readonly depthBuffer: WebGLBuffer;
private readonly colorBuffer: WebGLBuffer;
private readonly flagsBuffer: WebGLBuffer;
constructor(gl: WebGL2RenderingContext) {
this.gl = gl;
this.prog = createSliceProgram(gl);
// Create static quad buffers
this.quadCornerBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadCornerBuffer);
gl.bufferData(gl.ARRAY_BUFFER, QUAD_CORNERS, gl.STATIC_DRAW);
this.quadIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, QUAD_INDICES, gl.STATIC_DRAW);
// Create dynamic instance buffers
this.leftBuffer = gl.createBuffer();
this.rightBuffer = gl.createBuffer();
this.depthBuffer = gl.createBuffer();
this.colorBuffer = gl.createBuffer();
this.flagsBuffer = gl.createBuffer();
}
/**
* Draw slices using instanced WebGL rendering.
*
* Each slice is a horizontal rectangle positioned by its left/right edges
* (in data space) and its depth (row index). The vertical position and
* height are computed in the shader from the row layout formula:
*
* ```
* depth == 0:
* top = paddingTop
* bottom = top + firstRowHeight
* depth > 0:
* stride = rowHeight + rowGap
* top = paddingTop + firstRowHeight + rowGap + (depth - 1) * stride
* bottom = top + rowHeight
* ```
*
* The coordinate pipeline is:
* 1. X: data space → CSS pixels (via xTransform)
* 2. Full rect: CSS pixels → device pixels (via viewTransform)
* 3. Clamp to clipRect
* 4. Convert to NDC for rasterization
*
* @param buffers Columnar slice data:
* - `starts`/`ends`: left/right edges in data space (e.g. nanoseconds
* relative to trace start). Transformed to CSS pixels by xTransform.
* - `depths`: row index per slice (uint16). Used to look up vertical
* position from the row layout.
* - `colors`: packed RGBA per slice (0xRRGGBBAA).
* - `patterns`: bitfield per slice for visual effects
* (RECT_PATTERN_HATCHED, RECT_PATTERN_FADE_RIGHT).
* - `count`: number of valid slices in the arrays.
* @param rowLayout Defines the vertical geometry of rows:
* - `rowHeight`: height of rows in CSS pixels (required).
* - `paddingTop`: offset from the top of the track to row 0 (default 0).
* - `firstRowHeight`: height of row 0, can differ from other rows
* (default: rowHeight).
* - `rowGap`: vertical gap between rows in CSS pixels (default 0).
* @param xTransform Scale+translate to convert data-space X coordinates to
* CSS pixels: `cssPx = value * scale + offset`.
* @param viewTransform Scale+translate to convert CSS pixels to device
* pixels (accounts for DPR and any scroll/pan offset).
* @param clipRect Axis-aligned clip rectangle in device pixels (LTRB).
* Slices are clamped to this region.
* @param clipRect.left Left boundary of clip rectangle.
* @param clipRect.top Top boundary of clip rectangle.
* @param clipRect.right Right boundary of clip rectangle.
* @param clipRect.bottom Bottom boundary of clip rectangle.
*/
draw(
buffers: SliceBuffers,
rowLayout: RowLayout,
xTransform: Transform1D,
viewTransform: Transform2D,
clipRect: {left: number; top: number; right: number; bottom: number},
): void {
const {starts, ends, depths, colors, patterns, count} = buffers;
if (count === 0) return;
const gl = this.gl;
const prog = this.prog;
gl.useProgram(prog.program);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
// Set uniforms
gl.uniform2f(prog.resolutionLoc, gl.canvas.width, gl.canvas.height);
gl.uniform2f(prog.xTransformLoc, xTransform.scale, xTransform.offset);
gl.uniform4f(
prog.viewTransformLoc,
viewTransform.scaleX,
viewTransform.scaleY,
viewTransform.offsetX,
viewTransform.offsetY,
);
gl.uniform4f(
prog.clipRectLoc,
clipRect.left,
clipRect.top,
clipRect.right,
clipRect.bottom,
);
gl.uniform1f(
prog.firstRowHeightLoc,
rowLayout.firstRowHeight ?? rowLayout.rowHeight,
);
gl.uniform1f(prog.rowHeightLoc, rowLayout.rowHeight);
gl.uniform1f(prog.rowGapLoc, rowLayout.rowGap ?? 0);
gl.uniform1f(prog.paddingTopLoc, rowLayout.paddingTop ?? 0);
// Bind static quad
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadCornerBuffer);
gl.enableVertexAttribArray(prog.quadCornerLoc);
gl.vertexAttribPointer(prog.quadCornerLoc, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(prog.quadCornerLoc, 0);
// Upload per-instance buffers
this.bindFloatBuffer(prog.leftLoc, this.leftBuffer, starts, count);
this.bindFloatBuffer(prog.rightLoc, this.rightBuffer, ends, count);
// Depth (uint16)
gl.bindBuffer(gl.ARRAY_BUFFER, this.depthBuffer);
gl.bufferData(gl.ARRAY_BUFFER, depths.subarray(0, count), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(prog.depthLoc);
gl.vertexAttribIPointer(prog.depthLoc, 1, gl.UNSIGNED_SHORT, 0, 0);
gl.vertexAttribDivisor(prog.depthLoc, 1);
// Colors (uint32)
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors.subarray(0, count), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(prog.colorLoc);
gl.vertexAttribIPointer(prog.colorLoc, 1, gl.UNSIGNED_INT, 0, 0);
gl.vertexAttribDivisor(prog.colorLoc, 1);
// Patterns (uint8)
gl.bindBuffer(gl.ARRAY_BUFFER, this.flagsBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
patterns.subarray(0, count),
gl.DYNAMIC_DRAW,
);
gl.enableVertexAttribArray(prog.patternLoc);
gl.vertexAttribIPointer(prog.patternLoc, 1, gl.UNSIGNED_BYTE, 0, 0);
gl.vertexAttribDivisor(prog.patternLoc, 1);
// Draw
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadIndexBuffer);
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_SHORT, 0, count);
// Reset divisors
gl.vertexAttribDivisor(prog.leftLoc, 0);
gl.vertexAttribDivisor(prog.rightLoc, 0);
gl.vertexAttribDivisor(prog.depthLoc, 0);
gl.vertexAttribDivisor(prog.colorLoc, 0);
gl.vertexAttribDivisor(prog.patternLoc, 0);
}
private bindFloatBuffer(
loc: number,
buffer: WebGLBuffer,
data: Float32Array,
count: number,
): void {
const gl = this.gl;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, count), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 1, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(loc, 1);
}
}