From 1bbf39e18b1e6e2a26adfb3fca5503a234dc9990 Mon Sep 17 00:00:00 2001 From: Jaswant Panchumarti <jaswant.panchumarti@kitware.com> Date: Wed, 5 Feb 2025 08:07:15 -0500 Subject: [PATCH] Remove duplicate `FragmentInput` structure. - Refactor the `fragmentMain` function to directly accept the output of a `vertexMain` entry point. - This refactor ensures additions/deletions in `VertexOutput` are automatically visible in `fragmentMain` and do not require an extra step by changing the `FragmentInput` struct. --- Rendering/WebGPU/wgsl/LineFragmentShader.wgsl | 26 ++++--------- Rendering/WebGPU/wgsl/LineGlyphShader.wgsl | 26 ++++--------- Rendering/WebGPU/wgsl/PointGlyphShader.wgsl | 34 ++++++---------- Rendering/WebGPU/wgsl/PointShader.wgsl | 34 ++++++---------- Rendering/WebGPU/wgsl/PolyData2D.wgsl | 6 +-- .../WebGPU/wgsl/SurfaceMeshGlyphShader.wgsl | 39 +++++++------------ Rendering/WebGPU/wgsl/SurfaceMeshShader.wgsl | 39 +++++++------------ 7 files changed, 67 insertions(+), 137 deletions(-) diff --git a/Rendering/WebGPU/wgsl/LineFragmentShader.wgsl b/Rendering/WebGPU/wgsl/LineFragmentShader.wgsl index 368c92827dc..d1c307534e9 100644 --- a/Rendering/WebGPU/wgsl/LineFragmentShader.wgsl +++ b/Rendering/WebGPU/wgsl/LineFragmentShader.wgsl @@ -1,15 +1,3 @@ -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) @interpolate(flat) cell_id: u32, - @location(5) distance_from_centerline: f32, -} - //------------------------------------------------------------------- struct FragmentOutput { @location(0) color: vec4<f32>, @@ -18,7 +6,7 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain(vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); @@ -26,10 +14,10 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { var opacity: f32; - let distance_from_centerline = abs(fragment.distance_from_centerline); + let distance_from_centerline = abs(vertex.distance_from_centerline); // adjust z component of normal in order to emulate a tube if necessary. - var normal_vc: vec3<f32> = normalize(fragment.normal_vc); + var normal_vc: vec3<f32> = normalize(vertex.normal_vc); let render_lines_as_tubes = getRenderLinesAsTubes(actor.render_options.flags); if (render_lines_as_tubes) { normal_vc.z = 1.0 - 2.0 * distance_from_centerline; @@ -44,9 +32,9 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { diffuse_color = mesh.override_colors.diffuse_color.rgb; opacity = mesh.override_colors.opacity; } else if (has_mapped_colors) { - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; } else { ambient_color = actor.color_options.ambient_color; diffuse_color = actor.color_options.diffuse_color; @@ -98,6 +86,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/LineGlyphShader.wgsl b/Rendering/WebGPU/wgsl/LineGlyphShader.wgsl index 593952ba177..82134cf3d26 100644 --- a/Rendering/WebGPU/wgsl/LineGlyphShader.wgsl +++ b/Rendering/WebGPU/wgsl/LineGlyphShader.wgsl @@ -270,18 +270,6 @@ fn vertexMain(vertex: VertexInput) -> VertexOutput { return output; } -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) @interpolate(flat) cell_id: u32, - @location(5) distance_from_centerline: f32, -} - //------------------------------------------------------------------- struct FragmentOutput { @location(0) color: vec4<f32>, @@ -290,7 +278,7 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain(vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); @@ -298,18 +286,18 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { var opacity: f32; - let distance_from_centerline = abs(fragment.distance_from_centerline); + let distance_from_centerline = abs(vertex.distance_from_centerline); // adjust z component of normal in order to emulate a tube if necessary. - var normal_vc: vec3<f32> = normalize(fragment.normal_vc); + var normal_vc: vec3<f32> = normalize(vertex.normal_vc); let render_lines_as_tubes = getRenderLinesAsTubes(actor.render_options.flags); if (render_lines_as_tubes) { normal_vc.z = 1.0 - 2.0 * distance_from_centerline; } - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; ///------------------------/// // Lights @@ -356,6 +344,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/PointGlyphShader.wgsl b/Rendering/WebGPU/wgsl/PointGlyphShader.wgsl index 07b130f1e00..fdee632b235 100644 --- a/Rendering/WebGPU/wgsl/PointGlyphShader.wgsl +++ b/Rendering/WebGPU/wgsl/PointGlyphShader.wgsl @@ -192,18 +192,6 @@ fn vertexMain(vertex: VertexInput) -> VertexOutput { return output; } -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) @interpolate(flat) cell_id: u32, - @location(5) local_position: vec2<f32>, -} - //------------------------------------------------------------------- struct FragmentOutput { @builtin(frag_depth) frag_depth: f32, @@ -213,7 +201,7 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain(vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); @@ -231,13 +219,13 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { diffuse_color = actor.color_options.vertex_color; opacity = actor.color_options.opacity; } else { - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; } - let d = length(fragment.local_position); // distance of fragment from the input vertex. + let d = length(vertex.local_position); // distance of fragment from the input vertex. let point_2d_shape = getPoint2DShape(actor.render_options.flags); let render_points_as_spheres = getRenderPointsAsSpheres(actor.render_options.flags); if (((point_2d_shape == POINT_2D_ROUND) || render_points_as_spheres) && (d > 1)) { @@ -245,12 +233,12 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } let point_size = clamp(actor.render_options.point_size, 1.0f, 100000.0f); - var normal_vc = normalize(fragment.normal_vc); + var normal_vc = normalize(vertex.normal_vc); if (render_points_as_spheres) { if (d > 1) { discard; } - normal_vc = normalize(vec3f(fragment.local_position, 1)); + normal_vc = normalize(vec3f(vertex.local_position, 1)); normal_vc.z = sqrt(1.0f - d * d); // Pushes the fragment in order to fake a sphere. // See Rendering/OpenGL2/PixelsToZBufferConversion.txt for the math behind this. Note that, @@ -259,17 +247,17 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { let r = point_size / (scene_transform.viewport.z * scene_transform.projection[0][0]); if (getUseParallelProjection(scene_transform.flags)) { let s = scene_transform.projection[2][2]; - output.frag_depth = fragment.frag_coord.z + normal_vc.z * r * s; + output.frag_depth = vertex.position.z + normal_vc.z * r * s; } else { let s = -scene_transform.projection[2][2]; - output.frag_depth = (s - fragment.frag_coord.z) / (normal_vc.z * r - 1.0) + s; + output.frag_depth = (s - vertex.position.z) / (normal_vc.z * r - 1.0) + s; } } else { - output.frag_depth = fragment.frag_coord.z; + output.frag_depth = vertex.position.z; } ///------------------------/// @@ -317,6 +305,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/PointShader.wgsl b/Rendering/WebGPU/wgsl/PointShader.wgsl index 7bc92652415..45c9da664fd 100644 --- a/Rendering/WebGPU/wgsl/PointShader.wgsl +++ b/Rendering/WebGPU/wgsl/PointShader.wgsl @@ -189,18 +189,6 @@ fn vertexMain(vertex: VertexInput) -> VertexOutput { return output; } -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) @interpolate(flat) cell_id: u32, - @location(5) local_position: vec2<f32>, -} - //------------------------------------------------------------------- struct FragmentOutput { @builtin(frag_depth) frag_depth: f32, @@ -210,7 +198,7 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain(vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); @@ -233,16 +221,16 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { diffuse_color = mesh.override_colors.diffuse_color.rgb; opacity = mesh.override_colors.opacity; } else if (has_mapped_colors) { - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; } else { ambient_color = actor.color_options.ambient_color; diffuse_color = actor.color_options.diffuse_color; opacity = actor.color_options.opacity; } - let d = length(fragment.local_position); // distance of fragment from the input vertex. + let d = length(vertex.local_position); // distance of fragment from the input vertex. let point_2d_shape = getPoint2DShape(actor.render_options.flags); let render_points_as_spheres = getRenderPointsAsSpheres(actor.render_options.flags); if (((point_2d_shape == POINT_2D_ROUND) || render_points_as_spheres) && (d > 1)) { @@ -250,12 +238,12 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } let point_size = clamp(actor.render_options.point_size, 1.0f, 100000.0f); - var normal_vc = normalize(fragment.normal_vc); + var normal_vc = normalize(vertex.normal_vc); if (render_points_as_spheres) { if (d > 1) { discard; } - normal_vc = normalize(vec3f(fragment.local_position, 1)); + normal_vc = normalize(vec3f(vertex.local_position, 1)); normal_vc.z = sqrt(1.0f - d * d); // Pushes the fragment in order to fake a sphere. // See Rendering/OpenGL2/PixelsToZBufferConversion.txt for the math behind this. Note that, @@ -264,17 +252,17 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { let r = point_size / (scene_transform.viewport.z * scene_transform.projection[0][0]); if (getUseParallelProjection(scene_transform.flags)) { let s = scene_transform.projection[2][2]; - output.frag_depth = fragment.frag_coord.z + normal_vc.z * r * s; + output.frag_depth = vertex.position.z + normal_vc.z * r * s; } else { let s = -scene_transform.projection[2][2]; - output.frag_depth = (s - fragment.frag_coord.z) / (normal_vc.z * r - 1.0) + s; + output.frag_depth = (s - vertex.position.z) / (normal_vc.z * r - 1.0) + s; } } else { - output.frag_depth = fragment.frag_coord.z; + output.frag_depth = vertex.position.z; } ///------------------------/// @@ -322,6 +310,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/PolyData2D.wgsl b/Rendering/WebGPU/wgsl/PolyData2D.wgsl index 0e51a9c9123..a785cb84e94 100644 --- a/Rendering/WebGPU/wgsl/PolyData2D.wgsl +++ b/Rendering/WebGPU/wgsl/PolyData2D.wgsl @@ -273,9 +273,9 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(vertex_output: VertexOutput) -> FragmentOutput { +fn fragmentMain(vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; - output.color = vertex_output.color; - output.cell_id = vertex_output.cell_id; + output.color = vertex.color; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/SurfaceMeshGlyphShader.wgsl b/Rendering/WebGPU/wgsl/SurfaceMeshGlyphShader.wgsl index a3816d7bf83..b3a759b48d3 100644 --- a/Rendering/WebGPU/wgsl/SurfaceMeshGlyphShader.wgsl +++ b/Rendering/WebGPU/wgsl/SurfaceMeshGlyphShader.wgsl @@ -211,19 +211,6 @@ fn vertexMain(vertex: VertexInput) -> VertexOutput { return output; } -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) edge_dists: vec3<f32>, - @location(5) @interpolate(flat) cell_id: u32, - @location(6) @interpolate(flat) hide_edge: f32, -} - //------------------------------------------------------------------- struct FragmentOutput { @location(0) color: vec4<f32>, @@ -232,21 +219,23 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain( + @builtin(front_facing) is_front_facing: bool, + vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); var specular_color: vec3<f32> = vec3<f32>(0., 0., 0.); - var normal_vc: vec3<f32> = normalize(fragment.normal_vc); + var normal_vc: vec3<f32> = normalize(vertex.normal_vc); var opacity: f32; ///------------------------/// // Colors are acquired either from a global per-actor color, or from per-vertex colors, or from cell colors. ///------------------------/// - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; ///------------------------/// // Representation: VTK_SURFACE with edge visibility turned on. @@ -257,14 +246,14 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { let use_line_width_for_edge_thickness = getUseLineWidthForEdgeThickness(actor.render_options.flags); let linewidth: f32 = select(actor.render_options.edge_width, actor.render_options.line_width, use_line_width_for_edge_thickness); // Undo perspective correction. - let dist_vec = fragment.edge_dists.xyz * fragment.frag_coord.w; + let dist_vec = vertex.edge_dists.xyz * vertex.position.w; var d: f32 = 0.0; // Compute the shortest distance to the edge - if fragment.hide_edge == 2.0 { + if vertex.hide_edge == 2.0 { d = min(dist_vec[0], dist_vec[2]); - } else if fragment.hide_edge == 1.0 { + } else if vertex.hide_edge == 1.0 { d = dist_vec[0]; - } else if fragment.hide_edge == 0.0 { + } else if vertex.hide_edge == 0.0 { d = min(dist_vec[0], dist_vec[1]); } else { // no edge is hidden d = min(dist_vec[0], min(dist_vec[1], dist_vec[2])); @@ -285,9 +274,9 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { ///------------------------/// // Normals ///------------------------/// - if !fragment.is_front_facing { + if !is_front_facing { if (normal_vc.z < 0.0) { - normal_vc = -fragment.normal_vc; + normal_vc = -vertex.normal_vc; normal_vc = normalize(normal_vc); } } else if normal_vc.z < 0.0 { @@ -331,6 +320,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } diff --git a/Rendering/WebGPU/wgsl/SurfaceMeshShader.wgsl b/Rendering/WebGPU/wgsl/SurfaceMeshShader.wgsl index 1e9772a2830..fc4c0ebb1d7 100644 --- a/Rendering/WebGPU/wgsl/SurfaceMeshShader.wgsl +++ b/Rendering/WebGPU/wgsl/SurfaceMeshShader.wgsl @@ -212,19 +212,6 @@ fn vertexMain(vertex: VertexInput) -> VertexOutput { return output; } -//------------------------------------------------------------------- -struct FragmentInput { - @builtin(position) frag_coord: vec4<f32>, - @builtin(front_facing) is_front_facing: bool, - @location(0) color: vec4<f32>, - @location(1) position_vc: vec4<f32>, // in view coordinate system. - @location(2) normal_vc: vec3<f32>, // in view coordinate system. - @location(3) tangent_vc: vec3<f32>, // in view coordinate system. - @location(4) edge_dists: vec3<f32>, - @location(5) @interpolate(flat) cell_id: u32, - @location(6) @interpolate(flat) hide_edge: f32, -} - //------------------------------------------------------------------- struct FragmentOutput { @location(0) color: vec4<f32>, @@ -233,12 +220,14 @@ struct FragmentOutput { //------------------------------------------------------------------- @fragment -fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { +fn fragmentMain( + @builtin(front_facing) is_front_facing: bool, + vertex: VertexOutput) -> FragmentOutput { var output: FragmentOutput; var ambient_color: vec3<f32> = vec3<f32>(0., 0., 0.); var diffuse_color: vec3<f32> = vec3<f32>(0., 0., 0.); var specular_color: vec3<f32> = vec3<f32>(0., 0., 0.); - var normal_vc: vec3<f32> = normalize(fragment.normal_vc); + var normal_vc: vec3<f32> = normalize(vertex.normal_vc); var opacity: f32; @@ -251,9 +240,9 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { diffuse_color = mesh.override_colors.diffuse_color.rgb; opacity = mesh.override_colors.opacity; } else if (has_mapped_colors) { - ambient_color = fragment.color.rgb; - diffuse_color = fragment.color.rgb; - opacity = fragment.color.a; + ambient_color = vertex.color.rgb; + diffuse_color = vertex.color.rgb; + opacity = vertex.color.a; } else { ambient_color = actor.color_options.ambient_color; diffuse_color = actor.color_options.diffuse_color; @@ -269,14 +258,14 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { let use_line_width_for_edge_thickness = getUseLineWidthForEdgeThickness(actor.render_options.flags); let linewidth: f32 = select(actor.render_options.edge_width, actor.render_options.line_width, use_line_width_for_edge_thickness); // Undo perspective correction. - let dist_vec = fragment.edge_dists.xyz * fragment.frag_coord.w; + let dist_vec = vertex.edge_dists.xyz * vertex.position.w; var d: f32 = 0.0; // Compute the shortest distance to the edge - if fragment.hide_edge == 2.0 { + if vertex.hide_edge == 2.0 { d = min(dist_vec[0], dist_vec[2]); - } else if fragment.hide_edge == 1.0 { + } else if vertex.hide_edge == 1.0 { d = dist_vec[0]; - } else if fragment.hide_edge == 0.0 { + } else if vertex.hide_edge == 0.0 { d = min(dist_vec[0], dist_vec[1]); } else { // no edge is hidden d = min(dist_vec[0], min(dist_vec[1], dist_vec[2])); @@ -297,9 +286,9 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { ///------------------------/// // Normals ///------------------------/// - if !fragment.is_front_facing { + if !is_front_facing { if (normal_vc.z < 0.0) { - normal_vc = -fragment.normal_vc; + normal_vc = -vertex.normal_vc; normal_vc = normalize(normal_vc); } } else if normal_vc.z < 0.0 { @@ -343,6 +332,6 @@ fn fragmentMain(fragment: FragmentInput) -> FragmentOutput { } // pre-multiply colors output.color = vec4(output.color.rgb * opacity, opacity); - output.cell_id = fragment.cell_id; + output.cell_id = vertex.cell_id; return output; } -- GitLab