diff --git a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
index 1ad33107d9..47bfa5538f 100644
--- a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
@@ -165,6 +165,7 @@ public:
         DeclareConstantBuffers();
         DeclareGlobalMemory();
         DeclareSamplers();
+        DeclarePhysicalAttributeReader();
 
         code.AddLine("void execute_" + suffix + "() {");
         ++code.scope;
@@ -330,8 +331,7 @@ private:
 
     void DeclareInputAttributes() {
         if (ir.HasPhysicalAttributes()) {
-            const u32 num_inputs{stage == ShaderStage::Vertex ? GetNumPhysicalAttributes()
-                                                              : GetNumPhysicalVaryings()};
+            const u32 num_inputs{GetNumPhysicalInputAttributes()};
             for (u32 i = 0; i < num_inputs; ++i) {
                 DeclareInputAttribute(ToGenericAttribute(i));
             }
@@ -374,7 +374,7 @@ private:
     }
 
     void DeclareOutputAttributes() {
-        if (ir.HasPhysicalAttributes()) {
+        if (ir.HasPhysicalAttributes() && stage != ShaderStage::Fragment) {
             for (u32 i = 0; i < GetNumPhysicalVaryings(); ++i) {
                 DeclareOutputAttribute(ToGenericAttribute(i));
             }
@@ -461,6 +461,31 @@ private:
             code.AddNewLine();
     }
 
+    void DeclarePhysicalAttributeReader() {
+        if (!ir.HasPhysicalAttributes()) {
+            return;
+        }
+        code.AddLine("float readPhysicalAttribute(uint physical_address) {");
+        ++code.scope;
+        code.AddLine("switch (physical_address) {");
+
+        // Just declare generic attributes for now.
+        const auto num_attributes{static_cast<u32>(GetNumPhysicalAttributes())};
+        for (u32 index = 0; index < num_attributes; ++index) {
+            for (u32 element = 0; element < 4; ++element) {
+                code.AddLine(fmt::format("case 0x{:x}: return {};", 0x80 + index * 16 + element * 4,
+                                         ReadAttribute(ToGenericAttribute(index), element)));
+            }
+        }
+
+        code.AddLine("default: return 0;");
+
+        code.AddLine('}');
+        --code.scope;
+        code.AddLine('}');
+        code.AddNewLine();
+    }
+
     void VisitBlock(const NodeBlock& bb) {
         for (const Node node : bb) {
             if (const std::string expr = Visit(node); !expr.empty()) {
@@ -515,69 +540,12 @@ private:
             return value;
 
         } else if (const auto abuf = std::get_if<AbufNode>(node)) {
-            const auto attribute = abuf->GetIndex();
-            const auto element = abuf->GetElement();
-
-            const auto GeometryPass = [&](const std::string& name) {
-                if (stage == ShaderStage::Geometry && abuf->GetBuffer()) {
-                    // TODO(Rodrigo): Guard geometry inputs against out of bound reads. Some games
-                    // set an 0x80000000 index for those and the shader fails to build. Find out why
-                    // this happens and what's its intent.
-                    return "gs_" + name + "[ftou(" + Visit(abuf->GetBuffer()) +
-                           ") % MAX_VERTEX_INPUT]";
-                }
-                return name;
-            };
-
-            switch (attribute) {
-            case Attribute::Index::Position:
-                if (stage != ShaderStage::Fragment) {
-                    return GeometryPass("position") + GetSwizzle(element);
-                } else {
-                    return element == 3 ? "1.0f" : "gl_FragCoord" + GetSwizzle(element);
-                }
-            case Attribute::Index::PointCoord:
-                switch (element) {
-                case 0:
-                    return "gl_PointCoord.x";
-                case 1:
-                    return "gl_PointCoord.y";
-                case 2:
-                case 3:
-                    return "0";
-                }
-                UNREACHABLE();
-                return "0";
-            case Attribute::Index::TessCoordInstanceIDVertexID:
-                // TODO(Subv): Find out what the values are for the first two elements when inside a
-                // vertex shader, and what's the value of the fourth element when inside a Tess Eval
-                // shader.
-                ASSERT(stage == ShaderStage::Vertex);
-                switch (element) {
-                case 2:
-                    // Config pack's first value is instance_id.
-                    return "uintBitsToFloat(config_pack[0])";
-                case 3:
-                    return "uintBitsToFloat(gl_VertexID)";
-                }
-                UNIMPLEMENTED_MSG("Unmanaged TessCoordInstanceIDVertexID element={}", element);
-                return "0";
-            case Attribute::Index::FrontFacing:
-                // TODO(Subv): Find out what the values are for the other elements.
-                ASSERT(stage == ShaderStage::Fragment);
-                switch (element) {
-                case 3:
-                    return "itof(gl_FrontFacing ? -1 : 0)";
-                }
-                UNIMPLEMENTED_MSG("Unmanaged FrontFacing element={}", element);
-                return "0";
-            default:
-                if (IsGenericAttribute(attribute)) {
-                    return GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element);
-                }
-                break;
+            UNIMPLEMENTED_IF_MSG(abuf->IsPhysicalBuffer() && stage == ShaderStage::Geometry,
+                                 "Physical attributes in geometry shaders are not implemented");
+            if (abuf->IsPhysicalBuffer()) {
+                return "readPhysicalAttribute(ftou(" + Visit(abuf->GetPhysicalAddress()) + "))";
             }
-            UNIMPLEMENTED_MSG("Unhandled input attribute: {}", static_cast<u32>(attribute));
+            return ReadAttribute(abuf->GetIndex(), abuf->GetElement(), abuf->GetBuffer());
 
         } else if (const auto cbuf = std::get_if<CbufNode>(node)) {
             const Node offset = cbuf->GetOffset();
@@ -629,6 +597,69 @@ private:
         return {};
     }
 
+    std::string ReadAttribute(Attribute::Index attribute, u32 element, Node buffer = {}) {
+        const auto GeometryPass = [&](std::string name) {
+            if (stage == ShaderStage::Geometry && buffer) {
+                // TODO(Rodrigo): Guard geometry inputs against out of bound reads. Some games
+                // set an 0x80000000 index for those and the shader fails to build. Find out why
+                // this happens and what's its intent.
+                return "gs_" + std::move(name) + "[ftou(" + Visit(buffer) + ") % MAX_VERTEX_INPUT]";
+            }
+            return name;
+        };
+
+        switch (attribute) {
+        case Attribute::Index::Position:
+            if (stage != ShaderStage::Fragment) {
+                return GeometryPass("position") + GetSwizzle(element);
+            } else {
+                return element == 3 ? "1.0f" : "gl_FragCoord" + GetSwizzle(element);
+            }
+        case Attribute::Index::PointCoord:
+            switch (element) {
+            case 0:
+                return "gl_PointCoord.x";
+            case 1:
+                return "gl_PointCoord.y";
+            case 2:
+            case 3:
+                return "0";
+            }
+            UNREACHABLE();
+            return "0";
+        case Attribute::Index::TessCoordInstanceIDVertexID:
+            // TODO(Subv): Find out what the values are for the first two elements when inside a
+            // vertex shader, and what's the value of the fourth element when inside a Tess Eval
+            // shader.
+            ASSERT(stage == ShaderStage::Vertex);
+            switch (element) {
+            case 2:
+                // Config pack's first value is instance_id.
+                return "uintBitsToFloat(config_pack[0])";
+            case 3:
+                return "uintBitsToFloat(gl_VertexID)";
+            }
+            UNIMPLEMENTED_MSG("Unmanaged TessCoordInstanceIDVertexID element={}", element);
+            return "0";
+        case Attribute::Index::FrontFacing:
+            // TODO(Subv): Find out what the values are for the other elements.
+            ASSERT(stage == ShaderStage::Fragment);
+            switch (element) {
+            case 3:
+                return "itof(gl_FrontFacing ? -1 : 0)";
+            }
+            UNIMPLEMENTED_MSG("Unmanaged FrontFacing element={}", element);
+            return "0";
+        default:
+            if (IsGenericAttribute(attribute)) {
+                return GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element);
+            }
+            break;
+        }
+        UNIMPLEMENTED_MSG("Unhandled input attribute: {}", static_cast<u32>(attribute));
+        return "0";
+    }
+
     std::string ApplyPrecise(Operation operation, const std::string& value) {
         if (!IsPrecise(operation)) {
             return value;
@@ -1677,6 +1708,10 @@ private:
         return name + '_' + std::to_string(index) + '_' + suffix;
     }
 
+    u32 GetNumPhysicalInputAttributes() const {
+        return stage == ShaderStage::Vertex ? GetNumPhysicalAttributes() : GetNumPhysicalVaryings();
+    }
+
     u32 GetNumPhysicalAttributes() const {
         return std::min<u32>(device.GetMaxVertexAttributes(), Maxwell::NumVertexAttributes);
     }
diff --git a/src/video_core/shader/shader_ir.h b/src/video_core/shader/shader_ir.h
index 3a1164d4f6..a4bb0c41cf 100644
--- a/src/video_core/shader/shader_ir.h
+++ b/src/video_core/shader/shader_ir.h
@@ -885,7 +885,7 @@ private:
     std::set<Sampler> used_samplers;
     std::array<bool, Tegra::Engines::Maxwell3D::Regs::NumClipDistances> used_clip_distances{};
     std::map<GlobalMemoryBase, GlobalMemoryUsage> used_global_memory;
-    bool use_physical_attributes = true; // Shader uses AL2P
+    bool use_physical_attributes{}; // Shader uses AL2P or physical attribute read/writes
 
     Tegra::Shader::Header header;
 };