mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Merge pull request #929 from neobrain/geoshader_definitions
Pica/Shader: Add geometry shader definitions.
This commit is contained in:
		
						commit
						aa6dfdb827
					
				
					 6 changed files with 169 additions and 156 deletions
				
			
		|  | @ -259,7 +259,7 @@ void GraphicsVertexShaderModel::OnUpdate() | |||
|     for (auto pattern : Pica::g_state.vs.swizzle_data) | ||||
|         info.swizzle_info.push_back({pattern}); | ||||
| 
 | ||||
|     info.labels.insert({ Pica::g_state.regs.vs_main_offset, "main" }); | ||||
|     info.labels.insert({ Pica::g_state.regs.vs.main_offset, "main" }); | ||||
| 
 | ||||
|     endResetModel(); | ||||
| } | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | |||
|     if (GPU::g_skip_frame && id != PICA_REG_INDEX(trigger_irq)) | ||||
|         return; | ||||
| 
 | ||||
|     // TODO: Figure out how register masking acts on e.g. vs_uniform_setup.set_value
 | ||||
|     // TODO: Figure out how register masking acts on e.g. vs.uniform_setup.set_value
 | ||||
|     u32 old_value = regs[id]; | ||||
|     regs[id] = (old_value & ~mask) | (value & mask); | ||||
| 
 | ||||
|  | @ -282,7 +282,7 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | |||
|                                                                    &geometry_dumper, _1, _2, _3)); | ||||
| 
 | ||||
|                 // Send to vertex shader
 | ||||
|                 VertexShader::OutputVertex output = VertexShader::RunShader(input, attribute_config.GetNumTotalAttributes()); | ||||
|                 VertexShader::OutputVertex output = VertexShader::RunShader(input, attribute_config.GetNumTotalAttributes(), g_state.regs.vs, g_state.vs); | ||||
| 
 | ||||
|                 if (is_indexed) { | ||||
|                     // TODO: Add processed vertex to vertex cache!
 | ||||
|  | @ -321,35 +321,35 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | |||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         case PICA_REG_INDEX(vs_bool_uniforms): | ||||
|         case PICA_REG_INDEX(vs.bool_uniforms): | ||||
|             for (unsigned i = 0; i < 16; ++i) | ||||
|                 g_state.vs.uniforms.b[i] = (regs.vs_bool_uniforms.Value() & (1 << i)) != 0; | ||||
|                 g_state.vs.uniforms.b[i] = (regs.vs.bool_uniforms.Value() & (1 << i)) != 0; | ||||
| 
 | ||||
|             break; | ||||
| 
 | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_int_uniforms[0], 0x2b1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_int_uniforms[1], 0x2b2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_int_uniforms[2], 0x2b3): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_int_uniforms[3], 0x2b4): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[0], 0x2b1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[1], 0x2b2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[2], 0x2b3): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[3], 0x2b4): | ||||
|         { | ||||
|             int index = (id - PICA_REG_INDEX_WORKAROUND(vs_int_uniforms[0], 0x2b1)); | ||||
|             auto values = regs.vs_int_uniforms[index]; | ||||
|             int index = (id - PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[0], 0x2b1)); | ||||
|             auto values = regs.vs.int_uniforms[index]; | ||||
|             g_state.vs.uniforms.i[index] = Math::Vec4<u8>(values.x, values.y, values.z, values.w); | ||||
|             LOG_TRACE(HW_GPU, "Set integer uniform %d to %02x %02x %02x %02x", | ||||
|                       index, values.x.Value(), values.y.Value(), values.z.Value(), values.w.Value()); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[0], 0x2c1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[1], 0x2c2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[2], 0x2c3): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[3], 0x2c4): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[4], 0x2c5): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[5], 0x2c6): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[6], 0x2c7): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_uniform_setup.set_value[7], 0x2c8): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[0], 0x2c1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[1], 0x2c2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[2], 0x2c3): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[3], 0x2c4): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[4], 0x2c5): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[5], 0x2c6): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[6], 0x2c7): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.uniform_setup.set_value[7], 0x2c8): | ||||
|         { | ||||
|             auto& uniform_setup = regs.vs_uniform_setup; | ||||
|             auto& uniform_setup = regs.vs.uniform_setup; | ||||
| 
 | ||||
|             // TODO: Does actual hardware indeed keep an intermediate buffer or does
 | ||||
|             //       it directly write the values?
 | ||||
|  | @ -392,32 +392,32 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) { | |||
|         } | ||||
| 
 | ||||
|         // Load shader program code
 | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[0], 0x2cc): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[1], 0x2cd): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[2], 0x2ce): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[3], 0x2cf): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[4], 0x2d0): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[5], 0x2d1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[6], 0x2d2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_program.set_word[7], 0x2d3): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[0], 0x2cc): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[1], 0x2cd): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[2], 0x2ce): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[3], 0x2cf): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[4], 0x2d0): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[5], 0x2d1): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[6], 0x2d2): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.program.set_word[7], 0x2d3): | ||||
|         { | ||||
|             g_state.vs.program_code[regs.vs_program.offset] = value; | ||||
|             regs.vs_program.offset++; | ||||
|             g_state.vs.program_code[regs.vs.program.offset] = value; | ||||
|             regs.vs.program.offset++; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         // Load swizzle pattern data
 | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[0], 0x2d6): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[1], 0x2d7): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[2], 0x2d8): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[3], 0x2d9): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[4], 0x2da): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[5], 0x2db): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[6], 0x2dc): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs_swizzle_patterns.set_word[7], 0x2dd): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[0], 0x2d6): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[1], 0x2d7): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[2], 0x2d8): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[3], 0x2d9): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[4], 0x2da): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[5], 0x2db): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[6], 0x2dc): | ||||
|         case PICA_REG_INDEX_WORKAROUND(vs.swizzle_patterns.set_word[7], 0x2dd): | ||||
|         { | ||||
|             g_state.vs.swizzle_data[regs.vs_swizzle_patterns.offset] = value; | ||||
|             regs.vs_swizzle_patterns.offset++; | ||||
|             g_state.vs.swizzle_data[regs.vs.swizzle_patterns.offset] = value; | ||||
|             regs.vs.swizzle_patterns.offset++; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -788,25 +788,27 @@ struct Regs { | |||
|         List   = 0, | ||||
|         Strip  = 1, | ||||
|         Fan    = 2, | ||||
|         ListIndexed = 3, // TODO: No idea if this is correct
 | ||||
|         Shader = 3, // Programmable setup unit implemented in a geometry shader
 | ||||
|     }; | ||||
| 
 | ||||
|     BitField<8, 2, TriangleTopology> triangle_topology; | ||||
| 
 | ||||
|     INSERT_PADDING_WORDS(0x51); | ||||
|     INSERT_PADDING_WORDS(0x21); | ||||
| 
 | ||||
|     struct ShaderConfig { | ||||
|         BitField<0, 16, u32> bool_uniforms; | ||||
| 
 | ||||
|     BitField<0, 16, u32> vs_bool_uniforms; | ||||
|         union { | ||||
|             BitField< 0, 8, u32> x; | ||||
|             BitField< 8, 8, u32> y; | ||||
|             BitField<16, 8, u32> z; | ||||
|             BitField<24, 8, u32> w; | ||||
|     } vs_int_uniforms[4]; | ||||
|         } int_uniforms[4]; | ||||
| 
 | ||||
|         INSERT_PADDING_WORDS(0x5); | ||||
| 
 | ||||
|         // Offset to shader program entry point (in words)
 | ||||
|     BitField<0, 16, u32> vs_main_offset; | ||||
|         BitField<0, 16, u32> main_offset; | ||||
| 
 | ||||
|         union { | ||||
|             BitField< 0, 4, u64> attribute0_register; | ||||
|  | @ -835,8 +837,9 @@ struct Regs { | |||
|                 }; | ||||
|                 return (int)fields[attribute_index]; | ||||
|             } | ||||
|     } vs_input_register_map; | ||||
|         } input_register_map; | ||||
| 
 | ||||
|         // OUTMAP_MASK, 0x28E, CODETRANSFER_END
 | ||||
|         INSERT_PADDING_WORDS(0x3); | ||||
| 
 | ||||
|         struct { | ||||
|  | @ -853,16 +856,16 @@ struct Regs { | |||
|             union { | ||||
|                 // Index of the next uniform to write to
 | ||||
|                 // TODO: ctrulib uses 8 bits for this, however that seems to yield lots of invalid indices
 | ||||
|                 // TODO: Maybe the uppermost index is for the geometry shader? Investigate!
 | ||||
|                 BitField<0, 7, u32> index; | ||||
| 
 | ||||
|                 BitField<31, 1, Format> format; | ||||
|             }; | ||||
| 
 | ||||
|         // Writing to these registers sets the "current" uniform.
 | ||||
|         // TODO: It's not clear how the hardware stores what the "current" uniform is.
 | ||||
|             // Writing to these registers sets the current uniform.
 | ||||
|             u32 set_value[8]; | ||||
| 
 | ||||
|     } vs_uniform_setup; | ||||
|         } uniform_setup; | ||||
| 
 | ||||
|         INSERT_PADDING_WORDS(0x2); | ||||
| 
 | ||||
|  | @ -872,9 +875,8 @@ struct Regs { | |||
|             u32 offset; | ||||
| 
 | ||||
|             // Writing to these registers sets the "current" word in the shader program.
 | ||||
|         // TODO: It's not clear how the hardware stores what the "current" word is.
 | ||||
|             u32 set_word[8]; | ||||
|     } vs_program; | ||||
|         } program; | ||||
| 
 | ||||
|         INSERT_PADDING_WORDS(0x1); | ||||
| 
 | ||||
|  | @ -885,12 +887,17 @@ struct Regs { | |||
|             // Incremented with each instruction write.
 | ||||
|             u32 offset; | ||||
| 
 | ||||
|         // Writing to these registers sets the "current" swizzle pattern in the table.
 | ||||
|         // TODO: It's not clear how the hardware stores what the "current" swizzle pattern is.
 | ||||
|             // Writing to these registers sets the current swizzle pattern in the table.
 | ||||
|             u32 set_word[8]; | ||||
|     } vs_swizzle_patterns; | ||||
|         } swizzle_patterns; | ||||
| 
 | ||||
|     INSERT_PADDING_WORDS(0x22); | ||||
|         INSERT_PADDING_WORDS(0x2); | ||||
|     }; | ||||
| 
 | ||||
|     ShaderConfig gs; | ||||
|     ShaderConfig vs; | ||||
| 
 | ||||
|     INSERT_PADDING_WORDS(0x20); | ||||
| 
 | ||||
|     // Map register indices to names readable by humans
 | ||||
|     // Used for debugging purposes, so performance is not an issue here
 | ||||
|  | @ -937,13 +944,20 @@ struct Regs { | |||
|         ADD_FIELD(vs_default_attributes_setup); | ||||
|         ADD_FIELD(command_buffer); | ||||
|         ADD_FIELD(triangle_topology); | ||||
|         ADD_FIELD(vs_bool_uniforms); | ||||
|         ADD_FIELD(vs_int_uniforms); | ||||
|         ADD_FIELD(vs_main_offset); | ||||
|         ADD_FIELD(vs_input_register_map); | ||||
|         ADD_FIELD(vs_uniform_setup); | ||||
|         ADD_FIELD(vs_program); | ||||
|         ADD_FIELD(vs_swizzle_patterns); | ||||
|         ADD_FIELD(gs.bool_uniforms); | ||||
|         ADD_FIELD(gs.int_uniforms); | ||||
|         ADD_FIELD(gs.main_offset); | ||||
|         ADD_FIELD(gs.input_register_map); | ||||
|         ADD_FIELD(gs.uniform_setup); | ||||
|         ADD_FIELD(gs.program); | ||||
|         ADD_FIELD(gs.swizzle_patterns); | ||||
|         ADD_FIELD(vs.bool_uniforms); | ||||
|         ADD_FIELD(vs.int_uniforms); | ||||
|         ADD_FIELD(vs.main_offset); | ||||
|         ADD_FIELD(vs.input_register_map); | ||||
|         ADD_FIELD(vs.uniform_setup); | ||||
|         ADD_FIELD(vs.program); | ||||
|         ADD_FIELD(vs.swizzle_patterns); | ||||
| 
 | ||||
|         #undef ADD_FIELD | ||||
| 
 | ||||
|  | @ -1015,17 +1029,14 @@ ASSERT_REG_POSITION(trigger_draw_indexed, 0x22f); | |||
| ASSERT_REG_POSITION(vs_default_attributes_setup, 0x232); | ||||
| ASSERT_REG_POSITION(command_buffer, 0x238); | ||||
| ASSERT_REG_POSITION(triangle_topology, 0x25e); | ||||
| ASSERT_REG_POSITION(vs_bool_uniforms, 0x2b0); | ||||
| ASSERT_REG_POSITION(vs_int_uniforms, 0x2b1); | ||||
| ASSERT_REG_POSITION(vs_main_offset, 0x2ba); | ||||
| ASSERT_REG_POSITION(vs_input_register_map, 0x2bb); | ||||
| ASSERT_REG_POSITION(vs_uniform_setup, 0x2c0); | ||||
| ASSERT_REG_POSITION(vs_program, 0x2cb); | ||||
| ASSERT_REG_POSITION(vs_swizzle_patterns, 0x2d5); | ||||
| ASSERT_REG_POSITION(gs, 0x280); | ||||
| ASSERT_REG_POSITION(vs, 0x2b0); | ||||
| 
 | ||||
| #undef ASSERT_REG_POSITION | ||||
| #endif // !defined(_MSC_VER)
 | ||||
| 
 | ||||
| static_assert(sizeof(Regs::ShaderConfig) == 0x30 * sizeof(u32), "ShaderConfig structure has incorrect size"); | ||||
| 
 | ||||
| // The total number of registers is chosen arbitrarily, but let's make sure it's not some odd value anyway.
 | ||||
| static_assert(sizeof(Regs) <= 0x300 * sizeof(u32), "Register set structure larger than it should be"); | ||||
| static_assert(sizeof(Regs) >= 0x300 * sizeof(u32), "Register set structure smaller than it should be"); | ||||
|  | @ -1135,7 +1146,7 @@ struct State { | |||
|     Regs regs; | ||||
| 
 | ||||
|     /// Vertex shader memory
 | ||||
|     struct { | ||||
|     struct ShaderSetup { | ||||
|         struct { | ||||
|             Math::Vec4<float24> f[96]; | ||||
|             std::array<bool, 16> b; | ||||
|  | @ -1146,7 +1157,10 @@ struct State { | |||
| 
 | ||||
|         std::array<u32, 1024> program_code; | ||||
|         std::array<u32, 1024> swizzle_data; | ||||
|     } vs; | ||||
|     }; | ||||
| 
 | ||||
|     ShaderSetup vs; | ||||
|     ShaderSetup gs; | ||||
| 
 | ||||
|     /// Current Pica command list
 | ||||
|     struct { | ||||
|  |  | |||
|  | @ -20,8 +20,9 @@ template<typename VertexType> | |||
| void PrimitiveAssembler<VertexType>::SubmitVertex(VertexType& vtx, TriangleHandler triangle_handler) | ||||
| { | ||||
|     switch (topology) { | ||||
|         // TODO: Figure out what's different with TriangleTopology::Shader.
 | ||||
|         case Regs::TriangleTopology::List: | ||||
|         case Regs::TriangleTopology::ListIndexed: | ||||
|         case Regs::TriangleTopology::Shader: | ||||
|             if (buffer_index < 2) { | ||||
|                 buffer[buffer_index++] = vtx; | ||||
|             } else { | ||||
|  |  | |||
|  | @ -546,20 +546,18 @@ static void ProcessShaderCode(VertexShaderState& state) { | |||
| 
 | ||||
| static Common::Profiling::TimingCategory shader_category("Vertex Shader"); | ||||
| 
 | ||||
| OutputVertex RunShader(const InputVertex& input, int num_attributes) { | ||||
| OutputVertex RunShader(const InputVertex& input, int num_attributes, const Regs::ShaderConfig& config, const State::ShaderSetup& setup) { | ||||
|     Common::Profiling::ScopeTimer timer(shader_category); | ||||
| 
 | ||||
|     const auto& regs = g_state.regs; | ||||
|     const auto& vs = g_state.vs; | ||||
|     VertexShaderState state; | ||||
| 
 | ||||
|     const u32* main = &vs.program_code[regs.vs_main_offset]; | ||||
|     const u32* main = &setup.program_code[config.main_offset]; | ||||
|     state.program_counter = (u32*)main; | ||||
|     state.debug.max_offset = 0; | ||||
|     state.debug.max_opdesc_id = 0; | ||||
| 
 | ||||
|     // Setup input register table
 | ||||
|     const auto& attribute_register_map = regs.vs_input_register_map; | ||||
|     const auto& attribute_register_map = config.input_register_map; | ||||
|     float24 dummy_register; | ||||
|     boost::fill(state.input_register_table, &dummy_register); | ||||
| 
 | ||||
|  | @ -584,16 +582,16 @@ OutputVertex RunShader(const InputVertex& input, int num_attributes) { | |||
|     state.conditional_code[1] = false; | ||||
| 
 | ||||
|     ProcessShaderCode(state); | ||||
|     DebugUtils::DumpShader(vs.program_code.data(), state.debug.max_offset, vs.swizzle_data.data(), | ||||
|                            state.debug.max_opdesc_id, regs.vs_main_offset, | ||||
|                            regs.vs_output_attributes); | ||||
|     DebugUtils::DumpShader(setup.program_code.data(), state.debug.max_offset, setup.swizzle_data.data(), | ||||
|                            state.debug.max_opdesc_id, config.main_offset, | ||||
|                            g_state.regs.vs_output_attributes); // TODO: Don't hardcode VS here
 | ||||
| 
 | ||||
|     // Setup output data
 | ||||
|     OutputVertex ret; | ||||
|     // TODO(neobrain): Under some circumstances, up to 16 attributes may be output. We need to
 | ||||
|     // figure out what those circumstances are and enable the remaining outputs then.
 | ||||
|     for (int i = 0; i < 7; ++i) { | ||||
|         const auto& output_register_map = regs.vs_output_attributes[i]; | ||||
|         const auto& output_register_map = g_state.regs.vs_output_attributes[i]; // TODO: Don't hardcode VS here
 | ||||
| 
 | ||||
|         u32 semantics[4] = { | ||||
|             output_register_map.map_x, output_register_map.map_y, | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ struct OutputVertex { | |||
| static_assert(std::is_pod<OutputVertex>::value, "Structure is not POD"); | ||||
| static_assert(sizeof(OutputVertex) == 32 * sizeof(float), "OutputVertex has invalid size"); | ||||
| 
 | ||||
| OutputVertex RunShader(const InputVertex& input, int num_attributes); | ||||
| OutputVertex RunShader(const InputVertex& input, int num_attributes, const Regs::ShaderConfig& config, const State::ShaderSetup& setup); | ||||
| 
 | ||||
| } // namespace
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue