Browse Source

LibGL+LibGPU+LibSoftGPU: Implement and expose glClipPlane

This commit implements glClipPlane and its supporting calls, backed
by new support for user-defined clip planes in the software GPU clipper.

This fixes some visual bugs seen in the Quake III port, in which mirrors
would only reflect correctly from close distances.
RKBethke 3 years ago
parent
commit
0836912a6d

+ 2 - 1
Userland/Libraries/LibGL/CMakeLists.txt

@@ -1,10 +1,11 @@
 set(SOURCES
+    ClipPlanes.cpp
     ContextParameter.cpp
     GLAPI.cpp
     GLContext.cpp
-    Matrix.cpp
     Lighting.cpp
     Lists.cpp
+    Matrix.cpp
     Stencil.cpp
     Tex/NameAllocator.cpp
     Tex/Texture2D.cpp

+ 57 - 0
Userland/Libraries/LibGL/ClipPlanes.cpp

@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
+ * Copyright (c) 2021, Stephan Unverwerth <s.unverwerth@serenityos.org>
+ * Copyright (c) 2022, Jelle Raaijmakers <jelle@gmta.nl>
+ * Copyright (c) 2022, Ryan Bethke <ryanbethke11@gmail.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Debug.h>
+#include <LibGL/GLContext.h>
+
+namespace GL {
+
+void GLContext::gl_clip_plane(GLenum plane, GLdouble const* equation)
+{
+    APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_clip_plane, plane, equation);
+
+    RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
+    RETURN_WITH_ERROR_IF((plane < GL_CLIP_PLANE0) || (plane > GL_CLIP_PLANE5), GL_INVALID_ENUM);
+
+    auto plane_idx = static_cast<size_t>(plane) - GL_CLIP_PLANE0;
+
+    auto eqn = FloatVector4(equation[0], equation[1], equation[2], equation[3]);
+    m_clip_plane_attributes.eye_clip_plane[plane_idx] = m_model_view_matrix * eqn;
+    m_clip_planes_dirty = true;
+}
+
+void GLContext::gl_get_clip_plane(GLenum plane, GLdouble* equation)
+{
+    RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
+    RETURN_WITH_ERROR_IF((plane < GL_CLIP_PLANE0) || (plane > GL_CLIP_PLANE5), GL_INVALID_ENUM);
+
+    auto plane_idx = static_cast<size_t>(plane) - GL_CLIP_PLANE0;
+    equation[0] = static_cast<GLdouble>(m_clip_plane_attributes.eye_clip_plane[plane_idx][0]);
+    equation[1] = static_cast<GLdouble>(m_clip_plane_attributes.eye_clip_plane[plane_idx][1]);
+    equation[2] = static_cast<GLdouble>(m_clip_plane_attributes.eye_clip_plane[plane_idx][2]);
+    equation[3] = static_cast<GLdouble>(m_clip_plane_attributes.eye_clip_plane[plane_idx][3]);
+}
+
+void GLContext::sync_clip_planes()
+{
+    if (!m_clip_planes_dirty)
+        return;
+    m_clip_planes_dirty = false;
+
+    // TODO: Replace magic number 6 with device-dependent constant
+    Vector<FloatVector4, 6> user_clip_planes;
+    for (size_t plane_idx = 0; plane_idx < 6; ++plane_idx) {
+        if ((m_clip_plane_attributes.enabled & (1 << plane_idx)) != 0u) {
+            user_clip_planes.append(m_clip_plane_attributes.eye_clip_plane[plane_idx]);
+        }
+    }
+    m_rasterizer->set_clip_planes(user_clip_planes);
+}
+
+}

+ 24 - 0
Userland/Libraries/LibGL/ContextParameter.cpp

@@ -52,6 +52,8 @@ Optional<ContextParameter> GLContext::get_context_parameter(GLenum name)
         return ContextParameter { .type = GL_BOOL, .is_capability = true, .value = { .boolean_value = m_lighting_enabled } };
     case GL_LINE_SMOOTH:
         return ContextParameter { .type = GL_BOOL, .is_capability = true, .value = { .boolean_value = m_line_smooth } };
+    case GL_MAX_CLIP_PLANES:
+        return ContextParameter { .type = GL_INT, .value = { .integer_value = static_cast<GLint>(m_device_info.max_clip_planes) } };
     case GL_MAX_LIGHTS:
         return ContextParameter { .type = GL_INT, .value = { .integer_value = static_cast<GLint>(m_device_info.num_lights) } };
     case GL_MAX_MODELVIEW_STACK_DEPTH:
@@ -169,6 +171,17 @@ void GLContext::gl_disable(GLenum capability)
     bool update_rasterizer_options = false;
 
     switch (capability) {
+    case GL_CLIP_PLANE0:
+    case GL_CLIP_PLANE1:
+    case GL_CLIP_PLANE2:
+    case GL_CLIP_PLANE3:
+    case GL_CLIP_PLANE4:
+    case GL_CLIP_PLANE5: {
+        auto plane_idx = static_cast<size_t>(capability) - GL_CLIP_PLANE0;
+        m_clip_plane_attributes.enabled &= ~(1 << plane_idx);
+        m_clip_planes_dirty = true;
+        break;
+    }
     case GL_COLOR_MATERIAL:
         m_color_material_enabled = false;
         break;
@@ -308,6 +321,17 @@ void GLContext::gl_enable(GLenum capability)
     bool update_rasterizer_options = false;
 
     switch (capability) {
+    case GL_CLIP_PLANE0:
+    case GL_CLIP_PLANE1:
+    case GL_CLIP_PLANE2:
+    case GL_CLIP_PLANE3:
+    case GL_CLIP_PLANE4:
+    case GL_CLIP_PLANE5: {
+        auto plane_idx = static_cast<size_t>(capability) - GL_CLIP_PLANE0;
+        m_clip_plane_attributes.enabled |= (1 << plane_idx);
+        m_clip_planes_dirty = true;
+        break;
+    }
     case GL_COLOR_MATERIAL:
         m_color_material_enabled = true;
         break;

+ 2 - 0
Userland/Libraries/LibGL/GL/gl.h

@@ -481,6 +481,7 @@ extern "C" {
 #define GL_ADD 0x0104
 
 // User clipping planes
+#define GL_MAX_CLIP_PLANES 0x0D32
 #define GL_CLIP_PLANE0 0x3000
 #define GL_CLIP_PLANE1 0x3001
 #define GL_CLIP_PLANE2 0x3002
@@ -682,6 +683,7 @@ GLAPI void glRecti(GLint x1, GLint y1, GLint x2, GLint y2);
 GLAPI void glGetTexLevelParameteriv(GLenum target, GLint level, GLenum pname, GLint* params);
 GLAPI void glPointSize(GLfloat size);
 GLAPI void glClipPlane(GLenum plane, GLdouble const* equation);
+GLAPI void glGetClipPlane(GLenum plane, GLdouble* equation);
 GLAPI void glArrayElement(GLint i);
 GLAPI void glCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
 

+ 5 - 0
Userland/Libraries/LibGL/GLAPI.cpp

@@ -399,6 +399,11 @@ void glGetBooleanv(GLenum pname, GLboolean* data)
     g_gl_context->gl_get_booleanv(pname, data);
 }
 
+void glGetClipPlane(GLenum plane, GLdouble* equation)
+{
+    g_gl_context->gl_get_clip_plane(plane, equation);
+}
+
 void glGetDoublev(GLenum pname, GLdouble* params)
 {
     g_gl_context->gl_get_doublev(pname, params);

+ 1 - 10
Userland/Libraries/LibGL/GLContext.cpp

@@ -794,16 +794,6 @@ void GLContext::gl_depth_mask(GLboolean flag)
     m_rasterizer->set_options(options);
 }
 
-void GLContext::gl_clip_plane(GLenum plane, [[maybe_unused]] GLdouble const* equation)
-{
-    APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_clip_plane, plane, equation);
-
-    RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
-    RETURN_WITH_ERROR_IF((plane < GL_CLIP_PLANE0) || (plane > GL_CLIP_PLANE5), GL_INVALID_ENUM);
-
-    dbgln_if(GL_DEBUG, "GLContext FIXME: implement gl_clip_plane() (equation = [{} {} {} {}])", equation[0], equation[1], equation[2], equation[3]);
-}
-
 void GLContext::gl_draw_pixels(GLsizei width, GLsizei height, GLenum format, GLenum type, void const* data)
 {
     APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_draw_pixels, width, height, format, type, data);
@@ -1215,6 +1205,7 @@ void GLContext::sync_device_config()
     sync_device_texcoord_config();
     sync_light_state();
     sync_stencil_configuration();
+    sync_clip_planes();
 }
 
 void GLContext::build_extension_string()

+ 9 - 0
Userland/Libraries/LibGL/GLContext.h

@@ -197,6 +197,7 @@ public:
     void gl_get_light(GLenum light, GLenum pname, void* params, GLenum type);
     void gl_get_material(GLenum face, GLenum pname, void* params, GLenum type);
     void gl_clip_plane(GLenum plane, GLdouble const* equation);
+    void gl_get_clip_plane(GLenum plane, GLdouble* equation);
     void gl_array_element(GLint i);
     void gl_copy_tex_sub_image_2d(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
     void gl_point_size(GLfloat size);
@@ -207,6 +208,7 @@ private:
     void sync_device_texcoord_config();
     void sync_light_state();
     void sync_stencil_configuration();
+    void sync_clip_planes();
 
     void build_extension_string();
 
@@ -307,6 +309,13 @@ private:
     GLenum m_current_read_buffer = GL_BACK;
     GLenum m_current_draw_buffer = GL_BACK;
 
+    // User-defined clip planes
+    struct ClipPlaneAttributes {
+        Array<FloatVector4, 6> eye_clip_plane; // TODO: Change to use device-defined constant
+        GLuint enabled { 0 };
+    } m_clip_plane_attributes;
+    bool m_clip_planes_dirty { true };
+
     // Client side arrays
     bool m_client_side_vertex_array_enabled { false };
     bool m_client_side_color_array_enabled { false };

+ 1 - 0
Userland/Libraries/LibGPU/Device.h

@@ -61,6 +61,7 @@ public:
     virtual void set_light_state(unsigned, Light const&) = 0;
     virtual void set_material_state(Face, Material const&) = 0;
     virtual void set_stencil_configuration(Face, StencilConfiguration const&) = 0;
+    virtual void set_clip_planes(Vector<FloatVector4> const&) = 0;
 
     virtual RasterPosition raster_position() const = 0;
     virtual void set_raster_position(RasterPosition const& raster_position) = 0;

+ 1 - 0
Userland/Libraries/LibGPU/DeviceInfo.h

@@ -15,6 +15,7 @@ struct DeviceInfo final {
     String device_name;
     unsigned num_texture_units;
     unsigned num_lights;
+    unsigned max_clip_planes;
     u8 stencil_bits;
     bool supports_npot_textures;
 };

+ 72 - 47
Userland/Libraries/LibSoftGPU/Clipper.cpp

@@ -7,6 +7,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <AK/StdLibExtras.h>
 #include <AK/Vector.h>
 #include <LibGPU/Vertex.h>
 #include <LibGfx/Vector4.h>
@@ -17,40 +18,42 @@ namespace SoftGPU {
 template<Clipper::ClipPlane plane>
 static constexpr bool point_within_clip_plane(FloatVector4 const& vertex)
 {
-    if constexpr (plane == Clipper::ClipPlane::LEFT)
+    if constexpr (plane == Clipper::ClipPlane::Left)
         return vertex.x() >= -vertex.w();
-    else if constexpr (plane == Clipper::ClipPlane::RIGHT)
+    else if constexpr (plane == Clipper::ClipPlane::Right)
         return vertex.x() <= vertex.w();
-    else if constexpr (plane == Clipper::ClipPlane::TOP)
+    else if constexpr (plane == Clipper::ClipPlane::Top)
         return vertex.y() <= vertex.w();
-    else if constexpr (plane == Clipper::ClipPlane::BOTTOM)
+    else if constexpr (plane == Clipper::ClipPlane::Bottom)
         return vertex.y() >= -vertex.w();
-    else if constexpr (plane == Clipper::ClipPlane::NEAR)
+    else if constexpr (plane == Clipper::ClipPlane::Near)
         return vertex.z() >= -vertex.w();
-    else if constexpr (plane == Clipper::ClipPlane::FAR)
+    else if constexpr (plane == Clipper::ClipPlane::Far)
         return vertex.z() <= vertex.w();
     return false;
 }
 
+static bool point_within_user_plane(FloatVector4 const& vertex, FloatVector4 const& user_plane)
+{
+    return vertex.dot(user_plane) >= 0;
+}
+
+template<Clipper::ClipPlane plane>
+bool point_within_plane(GPU::Vertex const& vertex, FloatVector4 const& user_plane)
+{
+    if constexpr (plane == Clipper::ClipPlane::User)
+        return point_within_user_plane(vertex.eye_coordinates, user_plane);
+    else
+        return point_within_clip_plane<plane>(vertex.clip_coordinates);
+}
+
 template<Clipper::ClipPlane plane>
-static constexpr GPU::Vertex clip_intersection_point(GPU::Vertex const& p1, GPU::Vertex const& p2)
+static GPU::Vertex clip_intersection_point(GPU::Vertex const& p1, GPU::Vertex const& p2, FloatVector4 const& plane_normal)
 {
-    constexpr FloatVector4 clip_plane_normals[] = {
-        { 1, 0, 0, 1 },  // Left Plane
-        { -1, 0, 0, 1 }, // Right Plane
-        { 0, -1, 0, 1 }, // Top Plane
-        { 0, 1, 0, 1 },  // Bottom plane
-        { 0, 0, 1, 1 },  // Near Plane
-        { 0, 0, -1, 1 }  // Far Plane
-    };
-    constexpr auto clip_plane_normal = clip_plane_normals[to_underlying(plane)];
-
-    // See https://www.microsoft.com/en-us/research/wp-content/uploads/1978/01/p245-blinn.pdf
-    // "Clipping Using Homogeneous Coordinates" Blinn/Newell, 1978
-    // Clip plane normals have W=1 so the vertices' W coordinates are included in x1 and x2.
-
-    auto const x1 = clip_plane_normal.dot(p1.clip_coordinates);
-    auto const x2 = clip_plane_normal.dot(p2.clip_coordinates);
+    auto p1_coordinates = (plane == Clipper::ClipPlane::User) ? p1.eye_coordinates : p1.clip_coordinates;
+    auto p2_coordinates = (plane == Clipper::ClipPlane::User) ? p2.eye_coordinates : p2.clip_coordinates;
+    auto x1 = plane_normal.dot(p1_coordinates);
+    auto x2 = plane_normal.dot(p2_coordinates);
     auto const a = x1 / (x1 - x2);
 
     GPU::Vertex out;
@@ -65,7 +68,7 @@ static constexpr GPU::Vertex clip_intersection_point(GPU::Vertex const& p1, GPU:
 }
 
 template<Clipper::ClipPlane plane>
-FLATTEN static constexpr void clip_plane(Vector<GPU::Vertex>& input_list, Vector<GPU::Vertex>& output_list)
+FLATTEN static void clip_plane(Vector<GPU::Vertex>& input_list, Vector<GPU::Vertex>& output_list, FloatVector4 const& clip_plane)
 {
     output_list.clear_with_capacity();
 
@@ -74,20 +77,20 @@ FLATTEN static constexpr void clip_plane(Vector<GPU::Vertex>& input_list, Vector
         return;
 
     auto const* prev_vec = &input_list.data()[0];
-    auto is_prev_point_within_clip_plane = point_within_clip_plane<plane>(prev_vec->clip_coordinates);
+    auto is_prev_point_within_plane = point_within_plane<plane>(*prev_vec, clip_plane);
 
     for (size_t i = 1; i <= input_list_size; i++) {
         auto const& curr_vec = input_list[i % input_list_size];
-        auto const is_curr_point_within_clip_plane = point_within_clip_plane<plane>(curr_vec.clip_coordinates);
+        auto const is_curr_point_within_plane = point_within_plane<plane>(curr_vec, clip_plane);
 
-        if (is_curr_point_within_clip_plane != is_prev_point_within_clip_plane)
-            output_list.append(clip_intersection_point<plane>(*prev_vec, curr_vec));
+        if (is_curr_point_within_plane != is_prev_point_within_plane)
+            output_list.append(clip_intersection_point<plane>(*prev_vec, curr_vec, clip_plane));
 
-        if (is_curr_point_within_clip_plane)
+        if (is_curr_point_within_plane)
             output_list.append(curr_vec);
 
         prev_vec = &curr_vec;
-        is_prev_point_within_clip_plane = is_curr_point_within_clip_plane;
+        is_prev_point_within_plane = is_curr_point_within_plane;
     }
 }
 
@@ -97,9 +100,9 @@ void Clipper::clip_points_against_frustum(Vector<GPU::Vertex>& vertices)
 
     for (auto& vertex : vertices) {
         auto const coords = vertex.clip_coordinates;
-        if (point_within_clip_plane<ClipPlane::LEFT>(coords) && point_within_clip_plane<ClipPlane::RIGHT>(coords)
-            && point_within_clip_plane<ClipPlane::TOP>(coords) && point_within_clip_plane<ClipPlane::BOTTOM>(coords)
-            && point_within_clip_plane<ClipPlane::NEAR>(coords) && point_within_clip_plane<ClipPlane::FAR>(coords))
+        if (point_within_clip_plane<ClipPlane::Left>(coords) && point_within_clip_plane<ClipPlane::Right>(coords)
+            && point_within_clip_plane<ClipPlane::Top>(coords) && point_within_clip_plane<ClipPlane::Bottom>(coords)
+            && point_within_clip_plane<ClipPlane::Near>(coords) && point_within_clip_plane<ClipPlane::Far>(coords))
             m_vertex_buffer.append(vertex);
     }
 
@@ -107,39 +110,61 @@ void Clipper::clip_points_against_frustum(Vector<GPU::Vertex>& vertices)
     vertices.extend(m_vertex_buffer);
 }
 
+constexpr FloatVector4 clip_plane_eqns[] = {
+    { 1, 0, 0, 1 },  // Left Plane
+    { -1, 0, 0, 1 }, // Right Plane
+    { 0, -1, 0, 1 }, // Top Plane
+    { 0, 1, 0, 1 },  // Bottom plane
+    { 0, 0, 1, 1 },  // Near Plane
+    { 0, 0, -1, 1 }  // Far Plane
+};
+
 template<Clipper::ClipPlane plane>
 static constexpr bool constrain_line_within_plane(GPU::Vertex& from, GPU::Vertex& to)
 {
+    constexpr auto clip_plane_eqn = clip_plane_eqns[to_underlying(plane)];
+
     auto from_within_plane = point_within_clip_plane<plane>(from.clip_coordinates);
     auto to_within_plane = point_within_clip_plane<plane>(to.clip_coordinates);
     if (!from_within_plane && !to_within_plane)
         return false;
     if (!from_within_plane)
-        from = clip_intersection_point<plane>(from, to);
+        from = clip_intersection_point<plane>(from, to, clip_plane_eqn);
     else if (!to_within_plane)
-        to = clip_intersection_point<plane>(from, to);
+        to = clip_intersection_point<plane>(from, to, clip_plane_eqn);
     return true;
 }
 
 bool Clipper::clip_line_against_frustum(GPU::Vertex& from, GPU::Vertex& to)
 {
-    return constrain_line_within_plane<ClipPlane::LEFT>(from, to)
-        && constrain_line_within_plane<ClipPlane::RIGHT>(from, to)
-        && constrain_line_within_plane<ClipPlane::TOP>(from, to)
-        && constrain_line_within_plane<ClipPlane::BOTTOM>(from, to)
-        && constrain_line_within_plane<ClipPlane::NEAR>(from, to)
-        && constrain_line_within_plane<ClipPlane::FAR>(from, to);
+    return constrain_line_within_plane<ClipPlane::Left>(from, to)
+        && constrain_line_within_plane<ClipPlane::Right>(from, to)
+        && constrain_line_within_plane<ClipPlane::Top>(from, to)
+        && constrain_line_within_plane<ClipPlane::Bottom>(from, to)
+        && constrain_line_within_plane<ClipPlane::Near>(from, to)
+        && constrain_line_within_plane<ClipPlane::Far>(from, to);
 }
 
 void Clipper::clip_triangle_against_frustum(Vector<GPU::Vertex>& input_verts)
 {
     // FIXME C++23. Static reflection will provide looping over all enum values.
-    clip_plane<ClipPlane::LEFT>(input_verts, m_vertex_buffer);
-    clip_plane<ClipPlane::RIGHT>(m_vertex_buffer, input_verts);
-    clip_plane<ClipPlane::TOP>(input_verts, m_vertex_buffer);
-    clip_plane<ClipPlane::BOTTOM>(m_vertex_buffer, input_verts);
-    clip_plane<ClipPlane::NEAR>(input_verts, m_vertex_buffer);
-    clip_plane<ClipPlane::FAR>(m_vertex_buffer, input_verts);
+    clip_plane<ClipPlane::Left>(input_verts, m_vertex_buffer, clip_plane_eqns[0]);
+    clip_plane<ClipPlane::Right>(m_vertex_buffer, input_verts, clip_plane_eqns[1]);
+    clip_plane<ClipPlane::Top>(input_verts, m_vertex_buffer, clip_plane_eqns[2]);
+    clip_plane<ClipPlane::Bottom>(m_vertex_buffer, input_verts, clip_plane_eqns[3]);
+    clip_plane<ClipPlane::Near>(input_verts, m_vertex_buffer, clip_plane_eqns[4]);
+    clip_plane<ClipPlane::Far>(m_vertex_buffer, input_verts, clip_plane_eqns[5]);
+}
+
+void Clipper::clip_triangle_against_user_defined(Vector<GPU::Vertex>& input_verts, Vector<FloatVector4>& user_planes)
+{
+    // FIXME: Also implement user plane support for points and lines
+    auto& in = input_verts;
+    auto& out = m_vertex_buffer;
+    for (auto const& plane : user_planes) {
+        clip_plane<ClipPlane::User>(in, out, plane);
+        swap(in, out);
+    }
 }
 
 }

+ 8 - 6
Userland/Libraries/LibSoftGPU/Clipper.h

@@ -16,12 +16,13 @@ namespace SoftGPU {
 class Clipper final {
 public:
     enum class ClipPlane : u8 {
-        LEFT = 0,
-        RIGHT,
-        TOP,
-        BOTTOM,
-        NEAR,
-        FAR
+        Left = 0,
+        Right,
+        Top,
+        Bottom,
+        Near,
+        Far,
+        User, // Within view space
     };
 
     Clipper() = default;
@@ -29,6 +30,7 @@ public:
     void clip_points_against_frustum(Vector<GPU::Vertex>& vertices);
     bool clip_line_against_frustum(GPU::Vertex& from, GPU::Vertex& to);
     void clip_triangle_against_frustum(Vector<GPU::Vertex>& input_vecs);
+    void clip_triangle_against_user_defined(Vector<GPU::Vertex>& input_verts, Vector<FloatVector4>& user_planes);
 
 private:
     Vector<GPU::Vertex> m_vertex_buffer;

+ 1 - 0
Userland/Libraries/LibSoftGPU/Config.h

@@ -19,6 +19,7 @@ namespace SoftGPU {
 static constexpr bool ENABLE_STATISTICS_OVERLAY = false;
 static constexpr int MILLISECONDS_PER_STATISTICS_PERIOD = 500;
 static constexpr int NUM_LIGHTS = 8;
+static constexpr int MAX_CLIP_PLANES = 6;
 static constexpr int SUBPIXEL_BITS = 6;
 
 // See: https://www.khronos.org/opengl/wiki/Common_Mistakes#Texture_edge_color_problem

+ 9 - 0
Userland/Libraries/LibSoftGPU/Device.cpp

@@ -826,6 +826,7 @@ GPU::DeviceInfo Device::info() const
         .device_name = "SoftGPU",
         .num_texture_units = GPU::NUM_SAMPLERS,
         .num_lights = NUM_LIGHTS,
+        .max_clip_planes = MAX_CLIP_PLANES,
         .stencil_bits = sizeof(GPU::StencilType) * 8,
         .supports_npot_textures = true,
     };
@@ -1175,6 +1176,9 @@ void Device::draw_primitives(GPU::PrimitiveType primitive_type, FloatMatrix4x4 c
         m_clipped_vertices.append(triangle.vertices[2]);
         m_clipper.clip_triangle_against_frustum(m_clipped_vertices);
 
+        if (m_clip_planes.size() > 0)
+            m_clipper.clip_triangle_against_user_defined(m_clipped_vertices, m_clip_planes);
+
         if (m_clipped_vertices.size() < 3)
             continue;
 
@@ -1497,6 +1501,11 @@ void Device::set_raster_position(GPU::RasterPosition const& raster_position)
     m_raster_position = raster_position;
 }
 
+void Device::set_clip_planes(Vector<FloatVector4> const& clip_planes)
+{
+    m_clip_planes = clip_planes;
+}
+
 void Device::set_raster_position(FloatVector4 const& position, FloatMatrix4x4 const& model_view_transform, FloatMatrix4x4 const& projection_transform)
 {
     auto const eye_coordinates = model_view_transform * position;

+ 2 - 0
Userland/Libraries/LibSoftGPU/Device.h

@@ -68,6 +68,7 @@ public:
     virtual void set_light_state(unsigned, GPU::Light const&) override;
     virtual void set_material_state(GPU::Face, GPU::Material const&) override;
     virtual void set_stencil_configuration(GPU::Face, GPU::StencilConfiguration const&) override;
+    virtual void set_clip_planes(Vector<FloatVector4> const&) override;
 
     virtual GPU::RasterPosition raster_position() const override { return m_raster_position; }
     virtual void set_raster_position(GPU::RasterPosition const& raster_position) override;
@@ -107,6 +108,7 @@ private:
     Array<GPU::Light, NUM_LIGHTS> m_lights;
     Array<GPU::Material, 2u> m_materials;
     GPU::RasterPosition m_raster_position;
+    Vector<FloatVector4> m_clip_planes;
     Array<GPU::StencilConfiguration, 2u> m_stencil_configuration;
 };