diff --git a/src/video_core/pica.h b/src/video_core/pica.h
index fbc95a4b6..f8313d20d 100644
--- a/src/video_core/pica.h
+++ b/src/video_core/pica.h
@@ -162,6 +162,25 @@ struct Regs {
         ETC1A4       = 13,  // compressed
     };
 
+    enum class LogicOp : u32 {
+        Clear        =  0,
+        And          =  1,
+        AndReverse   =  2,
+        Copy         =  3,
+        Set          =  4,
+        CopyInverted =  5,
+        NoOp         =  6,
+        Invert       =  7,
+        Nand         =  8,
+        Or           =  9,
+        Nor          = 10,
+        Xor          = 11,
+        Equiv        = 12,
+        AndInverted  = 13,
+        OrReverse    = 14,
+        OrInverted   = 15,
+    };
+
     static unsigned NibblesPerPixel(TextureFormat format) {
         switch (format) {
         case TextureFormat::RGBA8:
@@ -413,12 +432,8 @@ struct Regs {
         } alpha_blending;
 
         union {
-            enum Op {
-                Set = 4,
-            };
-
-            BitField<0, 4, Op> op;
-        } logic_op;
+            BitField<0, 4, LogicOp> logic_op;
+        };
 
         union {
             BitField< 0, 8, u32> r;
diff --git a/src/video_core/rasterizer.cpp b/src/video_core/rasterizer.cpp
index 8f39e609b..2613e398f 100644
--- a/src/video_core/rasterizer.cpp
+++ b/src/video_core/rasterizer.cpp
@@ -873,8 +873,63 @@ static void ProcessTriangleInternal(const VertexShader::OutputVertex& v0,
                 blend_output     = EvaluateBlendEquation(combiner_output, srcfactor, dest, dstfactor, params.blend_equation_rgb);
                 blend_output.a() = EvaluateBlendEquation(combiner_output, srcfactor, dest, dstfactor, params.blend_equation_a).a();
             } else {
-                LOG_CRITICAL(HW_GPU, "logic op: %x", output_merger.logic_op);
-                UNIMPLEMENTED();
+                static auto LogicOp = [](u8 src, u8 dest, Regs::LogicOp op) -> u8 {
+                    switch (op) {
+                    case Regs::LogicOp::Clear:
+                        return 0;
+
+                    case Regs::LogicOp::And:
+                        return src & dest;
+
+                    case Regs::LogicOp::AndReverse:
+                        return src & ~dest;
+
+                    case Regs::LogicOp::Copy:
+                        return src;
+
+                    case Regs::LogicOp::Set:
+                        return 255;
+
+                    case Regs::LogicOp::CopyInverted:
+                        return ~src;
+
+                    case Regs::LogicOp::NoOp:
+                        return dest;
+
+                    case Regs::LogicOp::Invert:
+                        return ~dest;
+
+                    case Regs::LogicOp::Nand:
+                        return ~(src & dest);
+
+                    case Regs::LogicOp::Or:
+                        return src | dest;
+
+                    case Regs::LogicOp::Nor:
+                        return ~(src | dest);
+
+                    case Regs::LogicOp::Xor:
+                        return src ^ dest;
+
+                    case Regs::LogicOp::Equiv:
+                        return ~(src ^ dest);
+
+                    case Regs::LogicOp::AndInverted:
+                        return ~src & dest;
+
+                    case Regs::LogicOp::OrReverse:
+                        return src | ~dest;
+
+                    case Regs::LogicOp::OrInverted:
+                        return ~src | dest;
+                    }
+                };
+
+                blend_output = Math::MakeVec(
+                    LogicOp(combiner_output.r(), dest.r(), output_merger.logic_op),
+                    LogicOp(combiner_output.g(), dest.g(), output_merger.logic_op),
+                    LogicOp(combiner_output.b(), dest.b(), output_merger.logic_op),
+                    LogicOp(combiner_output.a(), dest.a(), output_merger.logic_op));
             }
 
             const Math::Vec4<u8> result = {
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index bacdb7172..b51f8efdf 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -135,6 +135,7 @@ void RasterizerOpenGL::Reset() {
     SyncBlendFuncs();
     SyncBlendColor();
     SyncAlphaTest();
+    SyncLogicOp();
     SyncStencilTest();
     SyncDepthTest();
 
@@ -249,6 +250,11 @@ void RasterizerOpenGL::NotifyPicaRegisterChanged(u32 id) {
         SyncDepthTest();
         break;
 
+    // Logic op
+    case PICA_REG_INDEX(output_merger.logic_op):
+        SyncLogicOp();
+        break;
+
     // TEV stage 0
     case PICA_REG_INDEX(tev_stage0.color_source1):
         SyncTevSources(0, regs.tev_stage0);
@@ -633,6 +639,10 @@ void RasterizerOpenGL::SyncAlphaTest() {
     glUniform1f(uniform_alphatest_ref, regs.output_merger.alpha_test.ref / 255.0f);
 }
 
+void RasterizerOpenGL::SyncLogicOp() {
+    state.logic_op = PicaToGL::LogicOp(Pica::g_state.regs.output_merger.logic_op);
+}
+
 void RasterizerOpenGL::SyncStencilTest() {
     // TODO: Implement stencil test, mask, and op
 }
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h
index 9896f8d04..d7d422b1f 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer.h
@@ -125,6 +125,9 @@ private:
     /// Syncs the alpha test states to match the PICA register
     void SyncAlphaTest();
 
+    /// Syncs the logic op states to match the PICA register
+    void SyncLogicOp();
+
     /// Syncs the stencil test states to match the PICA register
     void SyncStencilTest();
 
diff --git a/src/video_core/renderer_opengl/gl_state.cpp b/src/video_core/renderer_opengl/gl_state.cpp
index 0d7ba1983..9c5f38f94 100644
--- a/src/video_core/renderer_opengl/gl_state.cpp
+++ b/src/video_core/renderer_opengl/gl_state.cpp
@@ -32,6 +32,8 @@ OpenGLState::OpenGLState() {
     blend.color.blue = 0.0f;
     blend.color.alpha = 0.0f;
 
+    logic_op = GL_COPY;
+
     for (auto& texture_unit : texture_units) {
         texture_unit.enabled_2d = false;
         texture_unit.texture_2d = 0;
@@ -99,8 +101,13 @@ void OpenGLState::Apply() {
     if (blend.enabled != cur_state.blend.enabled) {
         if (blend.enabled) {
             glEnable(GL_BLEND);
+
+            cur_state.logic_op = GL_COPY;
+            glLogicOp(cur_state.logic_op);
+            glDisable(GL_COLOR_LOGIC_OP);
         } else {
             glDisable(GL_BLEND);
+            glEnable(GL_COLOR_LOGIC_OP);
         }
     }
 
@@ -118,6 +125,10 @@ void OpenGLState::Apply() {
         glBlendFuncSeparate(blend.src_rgb_func, blend.dst_rgb_func, blend.src_a_func, blend.dst_a_func);
     }
 
+    if (logic_op != cur_state.logic_op) {
+        glLogicOp(logic_op);
+    }
+
     // Textures
     for (unsigned texture_index = 0; texture_index < ARRAY_SIZE(texture_units); ++texture_index) {
         if (texture_units[texture_index].enabled_2d != cur_state.texture_units[texture_index].enabled_2d) {
diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h
index 63dba2761..6b97721d6 100644
--- a/src/video_core/renderer_opengl/gl_state.h
+++ b/src/video_core/renderer_opengl/gl_state.h
@@ -42,6 +42,8 @@ public:
         } color; // GL_BLEND_COLOR
     } blend;
 
+    GLenum logic_op; // GL_LOGIC_OP_MODE
+
     // 3 texture units - one for each that is used in PICA fragment shader emulation
     struct {
         bool enabled_2d; // GL_TEXTURE_2D
diff --git a/src/video_core/renderer_opengl/pica_to_gl.h b/src/video_core/renderer_opengl/pica_to_gl.h
index f8763e71b..e566f9f7a 100644
--- a/src/video_core/renderer_opengl/pica_to_gl.h
+++ b/src/video_core/renderer_opengl/pica_to_gl.h
@@ -71,6 +71,37 @@ inline GLenum BlendFunc(Pica::Regs::BlendFactor factor) {
     return blend_func_table[(unsigned)factor];
 }
 
+inline GLenum LogicOp(Pica::Regs::LogicOp op) {
+    static const GLenum logic_op_table[] = {
+        GL_CLEAR,           // Clear
+        GL_AND,             // And
+        GL_AND_REVERSE,     // AndReverse
+        GL_COPY,            // Copy
+        GL_SET,             // Set
+        GL_COPY_INVERTED,   // CopyInverted
+        GL_NOOP,            // NoOp
+        GL_INVERT,          // Invert
+        GL_NAND,            // Nand
+        GL_OR,              // Or
+        GL_NOR,             // Nor
+        GL_XOR,             // Xor
+        GL_EQUIV,           // Equiv
+        GL_AND_INVERTED,    // AndInverted
+        GL_OR_REVERSE,      // OrReverse
+        GL_OR_INVERTED,     // OrInverted
+    };
+
+    // Range check table for input
+    if ((unsigned)op >= ARRAY_SIZE(logic_op_table)) {
+        LOG_CRITICAL(Render_OpenGL, "Unknown logic op %d", op);
+        UNREACHABLE();
+
+        return GL_COPY;
+    }
+
+    return logic_op_table[(unsigned)op];
+}
+
 inline GLenum CompareFunc(Pica::Regs::CompareFunc func) {
     static const GLenum compare_func_table[] = {
         GL_NEVER,    // CompareFunc::Never