2014-07-26 14:42:46 +02:00
|
|
|
// Copyright 2014 Citra Emulator Project
|
2014-12-17 06:38:14 +01:00
|
|
|
// Licensed under GPLv2 or any later version
|
2014-07-26 14:42:46 +02:00
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
2016-04-30 17:34:51 +02:00
|
|
|
#include <array>
|
|
|
|
#include <cstddef>
|
|
|
|
#include <memory>
|
|
|
|
#include <utility>
|
|
|
|
#include "common/assert.h"
|
|
|
|
#include "common/logging/log.h"
|
2015-08-17 23:25:21 +02:00
|
|
|
#include "common/microprofile.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "common/vector_math.h"
|
2015-06-21 15:58:59 +02:00
|
|
|
#include "core/hle/service/gsp_gpu.h"
|
|
|
|
#include "core/hw/gpu.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "core/memory.h"
|
|
|
|
#include "core/tracer/recorder.h"
|
2016-09-21 08:52:38 +02:00
|
|
|
#include "video_core/command_processor.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "video_core/debug_utils/debug_utils.h"
|
2016-03-03 04:16:38 +01:00
|
|
|
#include "video_core/pica_state.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "video_core/pica_types.h"
|
2015-09-11 13:20:02 +02:00
|
|
|
#include "video_core/primitive_assembly.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "video_core/rasterizer_interface.h"
|
2017-01-28 22:27:24 +01:00
|
|
|
#include "video_core/regs.h"
|
2017-01-29 00:12:09 +01:00
|
|
|
#include "video_core/regs_pipeline.h"
|
|
|
|
#include "video_core/regs_texturing.h"
|
2015-09-11 13:20:02 +02:00
|
|
|
#include "video_core/renderer_base.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "video_core/shader/shader.h"
|
2016-04-28 19:01:47 +02:00
|
|
|
#include "video_core/vertex_loader.h"
|
2016-04-30 17:34:51 +02:00
|
|
|
#include "video_core/video_core.h"
|
2014-07-26 14:42:46 +02:00
|
|
|
|
|
|
|
namespace Pica {
|
|
|
|
|
|
|
|
namespace CommandProcessor {
|
|
|
|
|
2016-12-17 13:28:59 +01:00
|
|
|
static int vs_float_regs_counter = 0;
|
|
|
|
static u32 vs_uniform_write_buffer[4];
|
2014-07-26 19:17:09 +02:00
|
|
|
|
2016-12-17 13:28:59 +01:00
|
|
|
static int gs_float_regs_counter = 0;
|
|
|
|
static u32 gs_uniform_write_buffer[4];
|
2014-07-26 19:17:09 +02:00
|
|
|
|
2015-04-11 20:53:35 +02:00
|
|
|
static int default_attr_counter = 0;
|
|
|
|
static u32 default_attr_write_buffer[3];
|
|
|
|
|
2015-07-25 22:00:40 +02:00
|
|
|
// Expand a 4-bit mask to 4-byte mask, e.g. 0b0101 -> 0x00FF00FF
|
|
|
|
static const u32 expand_bits_to_bytes[] = {
|
2016-09-18 02:38:01 +02:00
|
|
|
0x00000000, 0x000000ff, 0x0000ff00, 0x0000ffff, 0x00ff0000, 0x00ff00ff, 0x00ffff00, 0x00ffffff,
|
2016-09-19 03:01:46 +02:00
|
|
|
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
|
|
|
|
};
|
2015-07-25 22:00:40 +02:00
|
|
|
|
2015-08-17 23:25:21 +02:00
|
|
|
MICROPROFILE_DEFINE(GPU_Drawing, "GPU", "Drawing", MP_RGB(50, 50, 240));
|
|
|
|
|
2016-12-17 13:28:59 +01:00
|
|
|
static const char* GetShaderSetupTypeName(Shader::ShaderSetup& setup) {
|
|
|
|
if (&setup == &g_state.vs) {
|
|
|
|
return "vertex shader";
|
|
|
|
}
|
|
|
|
if (&setup == &g_state.gs) {
|
|
|
|
return "geometry shader";
|
|
|
|
}
|
|
|
|
return "unknown shader";
|
|
|
|
}
|
|
|
|
|
|
|
|
static void WriteUniformBoolReg(Shader::ShaderSetup& setup, u32 value) {
|
|
|
|
for (unsigned i = 0; i < setup.uniforms.b.size(); ++i)
|
|
|
|
setup.uniforms.b[i] = (value & (1 << i)) != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void WriteUniformIntReg(Shader::ShaderSetup& setup, unsigned index,
|
|
|
|
const Math::Vec4<u8>& values) {
|
|
|
|
ASSERT(index < setup.uniforms.i.size());
|
|
|
|
setup.uniforms.i[index] = values;
|
|
|
|
LOG_TRACE(HW_GPU, "Set %s integer uniform %d to %02x %02x %02x %02x",
|
|
|
|
GetShaderSetupTypeName(setup), index, values.x, values.y, values.z, values.w);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void WriteUniformFloatReg(ShaderRegs& config, Shader::ShaderSetup& setup,
|
|
|
|
int& float_regs_counter, u32 uniform_write_buffer[4], u32 value) {
|
|
|
|
auto& uniform_setup = config.uniform_setup;
|
|
|
|
|
|
|
|
// TODO: Does actual hardware indeed keep an intermediate buffer or does
|
|
|
|
// it directly write the values?
|
|
|
|
uniform_write_buffer[float_regs_counter++] = value;
|
|
|
|
|
|
|
|
// Uniforms are written in a packed format such that four float24 values are encoded in
|
|
|
|
// three 32-bit numbers. We write to internal memory once a full such vector is
|
|
|
|
// written.
|
|
|
|
if ((float_regs_counter >= 4 && uniform_setup.IsFloat32()) ||
|
|
|
|
(float_regs_counter >= 3 && !uniform_setup.IsFloat32())) {
|
|
|
|
float_regs_counter = 0;
|
|
|
|
|
|
|
|
auto& uniform = setup.uniforms.f[uniform_setup.index];
|
|
|
|
|
|
|
|
if (uniform_setup.index >= 96) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid %s float uniform index %d", GetShaderSetupTypeName(setup),
|
|
|
|
(int)uniform_setup.index);
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// NOTE: The destination component order indeed is "backwards"
|
|
|
|
if (uniform_setup.IsFloat32()) {
|
|
|
|
for (auto i : {0, 1, 2, 3})
|
|
|
|
uniform[3 - i] = float24::FromFloat32(*(float*)(&uniform_write_buffer[i]));
|
|
|
|
} else {
|
|
|
|
// TODO: Untested
|
|
|
|
uniform.w = float24::FromRaw(uniform_write_buffer[0] >> 8);
|
|
|
|
uniform.z = float24::FromRaw(((uniform_write_buffer[0] & 0xFF) << 16) |
|
|
|
|
((uniform_write_buffer[1] >> 16) & 0xFFFF));
|
|
|
|
uniform.y = float24::FromRaw(((uniform_write_buffer[1] & 0xFFFF) << 8) |
|
|
|
|
((uniform_write_buffer[2] >> 24) & 0xFF));
|
|
|
|
uniform.x = float24::FromRaw(uniform_write_buffer[2] & 0xFFFFFF);
|
|
|
|
}
|
|
|
|
|
|
|
|
LOG_TRACE(HW_GPU, "Set %s float uniform %x to (%f %f %f %f)",
|
|
|
|
GetShaderSetupTypeName(setup), (int)uniform_setup.index,
|
|
|
|
uniform.x.ToFloat32(), uniform.y.ToFloat32(), uniform.z.ToFloat32(),
|
|
|
|
uniform.w.ToFloat32());
|
|
|
|
|
|
|
|
// TODO: Verify that this actually modifies the register!
|
|
|
|
uniform_setup.index.Assign(uniform_setup.index + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-03 13:21:37 +02:00
|
|
|
static void LoadDefaultVertexAttributes(u32 register_value) {
|
|
|
|
auto& regs = g_state.regs;
|
|
|
|
|
|
|
|
// TODO: Does actual hardware indeed keep an intermediate buffer or does
|
|
|
|
// it directly write the values?
|
|
|
|
default_attr_write_buffer[default_attr_counter++] = register_value;
|
|
|
|
|
|
|
|
// Default attributes are written in a packed format such that four float24 values are encoded
|
|
|
|
// in three 32-bit numbers.
|
|
|
|
// We write to internal memory once a full such vector is written.
|
|
|
|
if (default_attr_counter >= 3) {
|
|
|
|
default_attr_counter = 0;
|
|
|
|
|
|
|
|
auto& setup = regs.pipeline.vs_default_attributes_setup;
|
|
|
|
|
|
|
|
if (setup.index >= 16) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid VS default attribute index %d", (int)setup.index);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Math::Vec4<float24> attribute;
|
|
|
|
|
|
|
|
// NOTE: The destination component order indeed is "backwards"
|
|
|
|
attribute.w = float24::FromRaw(default_attr_write_buffer[0] >> 8);
|
|
|
|
attribute.z = float24::FromRaw(((default_attr_write_buffer[0] & 0xFF) << 16) |
|
|
|
|
((default_attr_write_buffer[1] >> 16) & 0xFFFF));
|
|
|
|
attribute.y = float24::FromRaw(((default_attr_write_buffer[1] & 0xFFFF) << 8) |
|
|
|
|
((default_attr_write_buffer[2] >> 24) & 0xFF));
|
|
|
|
attribute.x = float24::FromRaw(default_attr_write_buffer[2] & 0xFFFFFF);
|
|
|
|
|
|
|
|
LOG_TRACE(HW_GPU, "Set default VS attribute %x to (%f %f %f %f)", (int)setup.index,
|
|
|
|
attribute.x.ToFloat32(), attribute.y.ToFloat32(), attribute.z.ToFloat32(),
|
|
|
|
attribute.w.ToFloat32());
|
|
|
|
|
|
|
|
// TODO: Verify that this actually modifies the register!
|
|
|
|
if (setup.index < 15) {
|
|
|
|
g_state.input_default_attributes.attr[setup.index] = attribute;
|
|
|
|
setup.index++;
|
|
|
|
} else {
|
|
|
|
// Put each attribute into an immediate input buffer. When all specified immediate
|
|
|
|
// attributes are present, the Vertex Shader is invoked and everything is sent to
|
|
|
|
// the primitive assembler.
|
|
|
|
|
|
|
|
auto& immediate_input = g_state.immediate.input_vertex;
|
|
|
|
auto& immediate_attribute_id = g_state.immediate.current_attribute;
|
|
|
|
|
|
|
|
immediate_input.attr[immediate_attribute_id] = attribute;
|
|
|
|
|
|
|
|
if (immediate_attribute_id < regs.pipeline.max_input_attrib_index) {
|
|
|
|
immediate_attribute_id += 1;
|
|
|
|
} else {
|
|
|
|
MICROPROFILE_SCOPE(GPU_Drawing);
|
|
|
|
immediate_attribute_id = 0;
|
|
|
|
|
|
|
|
auto* shader_engine = Shader::GetEngine();
|
|
|
|
shader_engine->SetupBatch(g_state.vs, regs.vs.main_offset);
|
|
|
|
|
|
|
|
// Send to vertex shader
|
|
|
|
if (g_debug_context)
|
|
|
|
g_debug_context->OnEvent(DebugContext::Event::VertexShaderInvocation,
|
|
|
|
static_cast<void*>(&immediate_input));
|
|
|
|
Shader::UnitState shader_unit;
|
|
|
|
Shader::AttributeBuffer output{};
|
|
|
|
|
|
|
|
shader_unit.LoadInput(regs.vs, immediate_input);
|
|
|
|
shader_engine->Run(g_state.vs, shader_unit);
|
|
|
|
shader_unit.WriteOutput(regs.vs, output);
|
|
|
|
|
|
|
|
// Send to geometry pipeline
|
|
|
|
if (g_state.immediate.reset_geometry_pipeline) {
|
|
|
|
g_state.geometry_pipeline.Reconfigure();
|
|
|
|
g_state.immediate.reset_geometry_pipeline = false;
|
|
|
|
}
|
|
|
|
ASSERT(!g_state.geometry_pipeline.NeedIndexInput());
|
|
|
|
g_state.geometry_pipeline.Setup(shader_engine);
|
|
|
|
g_state.geometry_pipeline.SubmitVertex(output);
|
|
|
|
|
|
|
|
// TODO: If drawing after every immediate mode triangle kills performance,
|
|
|
|
// change it to flush triangles whenever a drawing config register changes
|
|
|
|
// See: https://github.com/citra-emu/citra/pull/2866#issuecomment-327011550
|
|
|
|
VideoCore::g_renderer->Rasterizer()->DrawTriangles();
|
|
|
|
if (g_debug_context) {
|
|
|
|
g_debug_context->OnEvent(DebugContext::Event::FinishedPrimitiveBatch, nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void Draw(u32 command_id) {
|
|
|
|
MICROPROFILE_SCOPE(GPU_Drawing);
|
|
|
|
auto& regs = g_state.regs;
|
|
|
|
|
|
|
|
#if PICA_LOG_TEV
|
|
|
|
DebugUtils::DumpTevStageConfig(regs.GetTevStages());
|
|
|
|
#endif
|
|
|
|
if (g_debug_context)
|
|
|
|
g_debug_context->OnEvent(DebugContext::Event::IncomingPrimitiveBatch, nullptr);
|
|
|
|
|
|
|
|
// Processes information about internal vertex attributes to figure out how a vertex is
|
|
|
|
// loaded.
|
|
|
|
// Later, these can be compiled and cached.
|
|
|
|
const u32 base_address = regs.pipeline.vertex_attributes.GetPhysicalBaseAddress();
|
|
|
|
VertexLoader loader(regs.pipeline);
|
|
|
|
|
|
|
|
// Load vertices
|
|
|
|
bool is_indexed = (command_id == PICA_REG_INDEX(pipeline.trigger_draw_indexed));
|
|
|
|
|
|
|
|
const auto& index_info = regs.pipeline.index_array;
|
|
|
|
const u8* index_address_8 = Memory::GetPhysicalPointer(base_address + index_info.offset);
|
|
|
|
const u16* index_address_16 = reinterpret_cast<const u16*>(index_address_8);
|
|
|
|
bool index_u16 = index_info.format != 0;
|
|
|
|
|
|
|
|
PrimitiveAssembler<Shader::OutputVertex>& primitive_assembler = g_state.primitive_assembler;
|
|
|
|
|
|
|
|
if (g_debug_context && g_debug_context->recorder) {
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
const auto texture = regs.texturing.GetTextures()[i];
|
|
|
|
if (!texture.enabled)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
u8* texture_data = Memory::GetPhysicalPointer(texture.config.GetPhysicalAddress());
|
|
|
|
g_debug_context->recorder->MemoryAccessed(
|
|
|
|
texture_data, Pica::TexturingRegs::NibblesPerPixel(texture.format) *
|
|
|
|
texture.config.width / 2 * texture.config.height,
|
|
|
|
texture.config.GetPhysicalAddress());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
DebugUtils::MemoryAccessTracker memory_accesses;
|
|
|
|
|
|
|
|
// Simple circular-replacement vertex cache
|
|
|
|
// The size has been tuned for optimal balance between hit-rate and the cost of lookup
|
|
|
|
const size_t VERTEX_CACHE_SIZE = 32;
|
|
|
|
std::array<u16, VERTEX_CACHE_SIZE> vertex_cache_ids;
|
|
|
|
std::array<Shader::AttributeBuffer, VERTEX_CACHE_SIZE> vertex_cache;
|
|
|
|
Shader::AttributeBuffer vs_output;
|
|
|
|
|
|
|
|
unsigned int vertex_cache_pos = 0;
|
|
|
|
vertex_cache_ids.fill(-1);
|
|
|
|
|
|
|
|
auto* shader_engine = Shader::GetEngine();
|
|
|
|
Shader::UnitState shader_unit;
|
|
|
|
|
|
|
|
shader_engine->SetupBatch(g_state.vs, regs.vs.main_offset);
|
|
|
|
|
|
|
|
g_state.geometry_pipeline.Reconfigure();
|
|
|
|
g_state.geometry_pipeline.Setup(shader_engine);
|
|
|
|
if (g_state.geometry_pipeline.NeedIndexInput())
|
|
|
|
ASSERT(is_indexed);
|
|
|
|
|
|
|
|
for (unsigned int index = 0; index < regs.pipeline.num_vertices; ++index) {
|
|
|
|
// Indexed rendering doesn't use the start offset
|
|
|
|
unsigned int vertex = is_indexed
|
|
|
|
? (index_u16 ? index_address_16[index] : index_address_8[index])
|
|
|
|
: (index + regs.pipeline.vertex_offset);
|
|
|
|
|
|
|
|
// -1 is a common special value used for primitive restart. Since it's unknown if
|
|
|
|
// the PICA supports it, and it would mess up the caching, guard against it here.
|
|
|
|
ASSERT(vertex != -1);
|
|
|
|
|
|
|
|
bool vertex_cache_hit = false;
|
|
|
|
|
|
|
|
if (is_indexed) {
|
|
|
|
if (g_state.geometry_pipeline.NeedIndexInput()) {
|
|
|
|
g_state.geometry_pipeline.SubmitIndex(vertex);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (g_debug_context && Pica::g_debug_context->recorder) {
|
|
|
|
int size = index_u16 ? 2 : 1;
|
|
|
|
memory_accesses.AddAccess(base_address + index_info.offset + size * index, size);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (unsigned int i = 0; i < VERTEX_CACHE_SIZE; ++i) {
|
|
|
|
if (vertex == vertex_cache_ids[i]) {
|
|
|
|
vs_output = vertex_cache[i];
|
|
|
|
vertex_cache_hit = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!vertex_cache_hit) {
|
|
|
|
// Initialize data for the current vertex
|
|
|
|
Shader::AttributeBuffer input;
|
|
|
|
loader.LoadVertex(base_address, index, vertex, input, memory_accesses);
|
|
|
|
|
|
|
|
// Send to vertex shader
|
|
|
|
if (g_debug_context)
|
|
|
|
g_debug_context->OnEvent(DebugContext::Event::VertexShaderInvocation,
|
|
|
|
(void*)&input);
|
|
|
|
shader_unit.LoadInput(regs.vs, input);
|
|
|
|
shader_engine->Run(g_state.vs, shader_unit);
|
|
|
|
shader_unit.WriteOutput(regs.vs, vs_output);
|
|
|
|
|
|
|
|
if (is_indexed) {
|
|
|
|
vertex_cache[vertex_cache_pos] = vs_output;
|
|
|
|
vertex_cache_ids[vertex_cache_pos] = vertex;
|
|
|
|
vertex_cache_pos = (vertex_cache_pos + 1) % VERTEX_CACHE_SIZE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send to geometry pipeline
|
|
|
|
g_state.geometry_pipeline.SubmitVertex(vs_output);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto& range : memory_accesses.ranges) {
|
|
|
|
g_debug_context->recorder->MemoryAccessed(Memory::GetPhysicalPointer(range.first),
|
|
|
|
range.second, range.first);
|
|
|
|
}
|
|
|
|
|
|
|
|
VideoCore::g_renderer->Rasterizer()->DrawTriangles();
|
|
|
|
if (g_debug_context) {
|
|
|
|
g_debug_context->OnEvent(DebugContext::Event::FinishedPrimitiveBatch, nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-25 22:00:40 +02:00
|
|
|
static void WritePicaReg(u32 id, u32 value, u32 mask) {
|
2015-05-14 05:29:27 +02:00
|
|
|
auto& regs = g_state.regs;
|
2014-08-14 23:23:55 +02:00
|
|
|
|
2017-01-28 22:58:51 +01:00
|
|
|
if (id >= Regs::NUM_REGS) {
|
|
|
|
LOG_ERROR(HW_GPU,
|
|
|
|
"Commandlist tried to write to invalid register 0x%03X (value: %08X, mask: %X)",
|
|
|
|
id, value, mask);
|
2014-08-14 23:23:55 +02:00
|
|
|
return;
|
2017-01-28 22:58:51 +01:00
|
|
|
}
|
2014-08-14 23:23:55 +02:00
|
|
|
|
2015-03-22 00:31:40 +01:00
|
|
|
// TODO: Figure out how register masking acts on e.g. vs.uniform_setup.set_value
|
2017-01-28 22:58:51 +01:00
|
|
|
u32 old_value = regs.reg_array[id];
|
2015-07-25 22:00:40 +02:00
|
|
|
|
|
|
|
const u32 write_mask = expand_bits_to_bytes[mask];
|
|
|
|
|
2017-01-28 22:58:51 +01:00
|
|
|
regs.reg_array[id] = (old_value & ~write_mask) | (value & write_mask);
|
2015-07-25 22:00:40 +02:00
|
|
|
|
2016-12-15 07:01:24 +01:00
|
|
|
// Double check for is_pica_tracing to avoid call overhead
|
|
|
|
if (DebugUtils::IsPicaTracing()) {
|
2017-01-28 22:58:51 +01:00
|
|
|
DebugUtils::OnPicaRegWrite({(u16)id, (u16)mask, regs.reg_array[id]});
|
2016-12-15 07:01:24 +01:00
|
|
|
}
|
2014-07-26 14:42:46 +02:00
|
|
|
|
2014-10-25 18:02:26 +02:00
|
|
|
if (g_debug_context)
|
2016-09-18 02:38:01 +02:00
|
|
|
g_debug_context->OnEvent(DebugContext::Event::PicaCommandLoaded,
|
|
|
|
reinterpret_cast<void*>(&id));
|
|
|
|
|
|
|
|
switch (id) {
|
|
|
|
// Trigger IRQ
|
|
|
|
case PICA_REG_INDEX(trigger_irq):
|
2017-10-15 04:18:42 +02:00
|
|
|
//Service::GSP::SignalInterrupt(Service::GSP::InterruptId::P3D);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX(pipeline.triangle_topology):
|
|
|
|
g_state.primitive_assembler.Reconfigure(regs.pipeline.triangle_topology);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX(pipeline.restart_primitive):
|
2016-09-18 02:38:01 +02:00
|
|
|
g_state.primitive_assembler.Reset();
|
|
|
|
break;
|
|
|
|
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX(pipeline.vs_default_attributes_setup.index):
|
2016-09-18 02:38:01 +02:00
|
|
|
g_state.immediate.current_attribute = 0;
|
pica/command_processor: build geometry pipeline and run geometry shader
The geometry pipeline manages data transfer between VS, GS and primitive assembler. It has known four modes:
- no GS mode: sends VS output directly to the primitive assembler (what citra currently does)
- GS mode 0: sends VS output to GS input registers, and sends GS output to primitive assembler
- GS mode 1: sends VS output to GS uniform registers, and sends GS output to primitive assembler. It also takes an index from the index buffer at the beginning of each primitive for determine the primitive size.
- GS mode 2: similar to mode 1, but doesn't take the index and uses a fixed primitive size.
hwtest shows that immediate mode also supports GS (at least for mode 0), so the geometry pipeline gets refactored into its own class for supporting both drawing mode.
In the immediate mode, some games don't set the pipeline registers to a valid value until the first attribute input, so a geometry pipeline reset flag is set in `pipeline.vs_default_attributes_setup.index` trigger, and the actual pipeline reconfigure is triggered in the first attribute input.
In the normal drawing mode with index buffer, the vertex cache is a little bit modified to support the geometry pipeline. Instead of OutputVertex, it now holds AttributeBuffer, which is the input to the geometry pipeline. The AttributeBuffer->OutputVertex conversion is done inside the pipeline vertex handler. The actual hardware vertex cache is believed to be implemented in a similar way (because this is the only way that makes sense).
Both geometry pipeline and GS unit rely on states preservation across drawing call, so they are put into the global state. In the future, the other three vertex shader units should be also placed in the global state, and a scheduler should be implemented on top of the four units. Note that the current gs_unit already allows running VS on it in the future.
2017-08-04 16:03:17 +02:00
|
|
|
g_state.immediate.reset_geometry_pipeline = true;
|
2016-09-18 02:38:01 +02:00
|
|
|
default_attr_counter = 0;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Load default vertex input attributes
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX_WORKAROUND(pipeline.vs_default_attributes_setup.set_value[0], 0x233):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(pipeline.vs_default_attributes_setup.set_value[1], 0x234):
|
2017-10-03 13:21:37 +02:00
|
|
|
case PICA_REG_INDEX_WORKAROUND(pipeline.vs_default_attributes_setup.set_value[2], 0x235):
|
|
|
|
LoadDefaultVertexAttributes(value);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
2015-05-27 16:33:59 +02:00
|
|
|
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX(pipeline.gpu_mode):
|
2017-09-23 17:28:20 +02:00
|
|
|
// This register likely just enables vertex processing and doesn't need any special handling
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX_WORKAROUND(pipeline.command_buffer.trigger[0], 0x23c):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(pipeline.command_buffer.trigger[1], 0x23d): {
|
|
|
|
unsigned index =
|
|
|
|
static_cast<unsigned>(id - PICA_REG_INDEX(pipeline.command_buffer.trigger[0]));
|
|
|
|
u32* head_ptr = (u32*)Memory::GetPhysicalPointer(
|
|
|
|
regs.pipeline.command_buffer.GetPhysicalAddress(index));
|
2016-09-18 02:38:01 +02:00
|
|
|
g_state.cmd_list.head_ptr = g_state.cmd_list.current_ptr = head_ptr;
|
2017-01-28 21:34:31 +01:00
|
|
|
g_state.cmd_list.length = regs.pipeline.command_buffer.GetSize(index) / sizeof(u32);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
2014-12-03 07:04:22 +01:00
|
|
|
|
2016-09-18 02:38:01 +02:00
|
|
|
// It seems like these trigger vertex rendering
|
2017-01-28 21:34:31 +01:00
|
|
|
case PICA_REG_INDEX(pipeline.trigger_draw):
|
2017-10-03 13:21:37 +02:00
|
|
|
case PICA_REG_INDEX(pipeline.trigger_draw_indexed):
|
|
|
|
Draw(id);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
2014-07-26 19:17:09 +02:00
|
|
|
|
2016-09-22 22:42:36 +02:00
|
|
|
case PICA_REG_INDEX(gs.bool_uniforms):
|
2017-05-17 21:14:09 +02:00
|
|
|
WriteUniformBoolReg(g_state.gs, g_state.regs.gs.bool_uniforms.Value());
|
2016-09-22 22:42:36 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.int_uniforms[0], 0x281):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.int_uniforms[1], 0x282):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.int_uniforms[2], 0x283):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.int_uniforms[3], 0x284): {
|
|
|
|
unsigned index = (id - PICA_REG_INDEX_WORKAROUND(gs.int_uniforms[0], 0x281));
|
|
|
|
auto values = regs.gs.int_uniforms[index];
|
|
|
|
WriteUniformIntReg(g_state.gs, index,
|
|
|
|
Math::Vec4<u8>(values.x, values.y, values.z, values.w));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[0], 0x291):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[1], 0x292):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[2], 0x293):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[3], 0x294):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[4], 0x295):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[5], 0x296):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[6], 0x297):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.uniform_setup.set_value[7], 0x298): {
|
|
|
|
WriteUniformFloatReg(g_state.regs.gs, g_state.gs, gs_float_regs_counter,
|
|
|
|
gs_uniform_write_buffer, value);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[0], 0x29c):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[1], 0x29d):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[2], 0x29e):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[3], 0x29f):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[4], 0x2a0):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[5], 0x2a1):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[6], 0x2a2):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.program.set_word[7], 0x2a3): {
|
2017-08-03 00:40:42 +02:00
|
|
|
u32& offset = g_state.regs.gs.program.offset;
|
|
|
|
if (offset >= 4096) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid GS program offset %u", offset);
|
|
|
|
} else {
|
|
|
|
g_state.gs.program_code[offset] = value;
|
|
|
|
offset++;
|
|
|
|
}
|
2016-09-22 22:42:36 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[0], 0x2a6):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[1], 0x2a7):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[2], 0x2a8):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[3], 0x2a9):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[4], 0x2aa):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[5], 0x2ab):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[6], 0x2ac):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(gs.swizzle_patterns.set_word[7], 0x2ad): {
|
2017-08-03 00:40:42 +02:00
|
|
|
u32& offset = g_state.regs.gs.swizzle_patterns.offset;
|
|
|
|
if (offset >= g_state.gs.swizzle_data.size()) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid GS swizzle pattern offset %u", offset);
|
|
|
|
} else {
|
|
|
|
g_state.gs.swizzle_data[offset] = value;
|
|
|
|
offset++;
|
|
|
|
}
|
2016-09-22 22:42:36 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2016-09-18 02:38:01 +02:00
|
|
|
case PICA_REG_INDEX(vs.bool_uniforms):
|
2017-08-03 00:40:42 +02:00
|
|
|
// TODO (wwylele): does regs.pipeline.gs_unit_exclusive_configuration affect this?
|
2017-05-17 21:14:09 +02:00
|
|
|
WriteUniformBoolReg(g_state.vs, g_state.regs.vs.bool_uniforms.Value());
|
2016-09-18 02:38:01 +02:00
|
|
|
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): {
|
2017-08-03 00:40:42 +02:00
|
|
|
// TODO (wwylele): does regs.pipeline.gs_unit_exclusive_configuration affect this?
|
2016-12-17 13:28:59 +01:00
|
|
|
unsigned index = (id - PICA_REG_INDEX_WORKAROUND(vs.int_uniforms[0], 0x2b1));
|
2016-09-18 02:38:01 +02:00
|
|
|
auto values = regs.vs.int_uniforms[index];
|
2016-12-17 13:28:59 +01:00
|
|
|
WriteUniformIntReg(g_state.vs, index,
|
|
|
|
Math::Vec4<u8>(values.x, values.y, values.z, values.w));
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
2014-07-26 19:17:09 +02:00
|
|
|
|
2016-09-18 02:38:01 +02:00
|
|
|
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): {
|
2017-08-03 00:40:42 +02:00
|
|
|
// TODO (wwylele): does regs.pipeline.gs_unit_exclusive_configuration affect this?
|
2016-12-17 13:28:59 +01:00
|
|
|
WriteUniformFloatReg(g_state.regs.vs, g_state.vs, vs_float_regs_counter,
|
|
|
|
vs_uniform_write_buffer, value);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
2014-07-26 19:17:09 +02:00
|
|
|
|
2016-09-18 02:38:01 +02:00
|
|
|
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): {
|
2017-08-03 00:40:42 +02:00
|
|
|
u32& offset = g_state.regs.vs.program.offset;
|
|
|
|
if (offset >= 512) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid VS program offset %u", offset);
|
|
|
|
} else {
|
|
|
|
g_state.vs.program_code[offset] = value;
|
|
|
|
if (!g_state.regs.pipeline.gs_unit_exclusive_configuration) {
|
|
|
|
g_state.gs.program_code[offset] = value;
|
|
|
|
}
|
|
|
|
offset++;
|
|
|
|
}
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
2015-09-13 00:56:12 +02:00
|
|
|
|
2016-09-18 02:38:01 +02:00
|
|
|
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): {
|
2017-08-03 00:40:42 +02:00
|
|
|
u32& offset = g_state.regs.vs.swizzle_patterns.offset;
|
|
|
|
if (offset >= g_state.vs.swizzle_data.size()) {
|
|
|
|
LOG_ERROR(HW_GPU, "Invalid VS swizzle pattern offset %u", offset);
|
|
|
|
} else {
|
|
|
|
g_state.vs.swizzle_data[offset] = value;
|
|
|
|
if (!g_state.regs.pipeline.gs_unit_exclusive_configuration) {
|
|
|
|
g_state.gs.swizzle_data[offset] = value;
|
|
|
|
}
|
|
|
|
offset++;
|
|
|
|
}
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[0], 0x1c8):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[1], 0x1c9):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[2], 0x1ca):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[3], 0x1cb):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[4], 0x1cc):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[5], 0x1cd):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[6], 0x1ce):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(lighting.lut_data[7], 0x1cf): {
|
|
|
|
auto& lut_config = regs.lighting.lut_config;
|
|
|
|
|
|
|
|
ASSERT_MSG(lut_config.index < 256, "lut_config.index exceeded maximum value of 255!");
|
|
|
|
|
|
|
|
g_state.lighting.luts[lut_config.type][lut_config.index].raw = value;
|
|
|
|
lut_config.index.Assign(lut_config.index + 1);
|
|
|
|
break;
|
|
|
|
}
|
2016-05-11 13:39:28 +02:00
|
|
|
|
2017-01-28 05:51:59 +01:00
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[0], 0xe8):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[1], 0xe9):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[2], 0xea):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[3], 0xeb):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[4], 0xec):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[5], 0xed):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[6], 0xee):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.fog_lut_data[7], 0xef): {
|
|
|
|
g_state.fog.lut[regs.texturing.fog_lut_offset % 128].raw = value;
|
|
|
|
regs.texturing.fog_lut_offset.Assign(regs.texturing.fog_lut_offset + 1);
|
2016-09-18 02:38:01 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-04-17 09:01:45 +02:00
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[0], 0xb0):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[1], 0xb1):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[2], 0xb2):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[3], 0xb3):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[4], 0xb4):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[5], 0xb5):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[6], 0xb6):
|
|
|
|
case PICA_REG_INDEX_WORKAROUND(texturing.proctex_lut_data[7], 0xb7): {
|
|
|
|
auto& index = regs.texturing.proctex_lut_config.index;
|
|
|
|
auto& pt = g_state.proctex;
|
|
|
|
|
|
|
|
switch (regs.texturing.proctex_lut_config.ref_table.Value()) {
|
|
|
|
case TexturingRegs::ProcTexLutTable::Noise:
|
|
|
|
pt.noise_table[index % pt.noise_table.size()].raw = value;
|
|
|
|
break;
|
|
|
|
case TexturingRegs::ProcTexLutTable::ColorMap:
|
|
|
|
pt.color_map_table[index % pt.color_map_table.size()].raw = value;
|
|
|
|
break;
|
|
|
|
case TexturingRegs::ProcTexLutTable::AlphaMap:
|
|
|
|
pt.alpha_map_table[index % pt.alpha_map_table.size()].raw = value;
|
|
|
|
break;
|
|
|
|
case TexturingRegs::ProcTexLutTable::Color:
|
|
|
|
pt.color_table[index % pt.color_table.size()].raw = value;
|
|
|
|
break;
|
|
|
|
case TexturingRegs::ProcTexLutTable::ColorDiff:
|
|
|
|
pt.color_diff_table[index % pt.color_diff_table.size()].raw = value;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
index.Assign(index + 1);
|
|
|
|
break;
|
|
|
|
}
|
2016-09-18 02:38:01 +02:00
|
|
|
default:
|
|
|
|
break;
|
2014-07-26 14:42:46 +02:00
|
|
|
}
|
2014-10-25 18:02:26 +02:00
|
|
|
|
2016-03-09 03:31:41 +01:00
|
|
|
VideoCore::g_renderer->Rasterizer()->NotifyPicaRegisterChanged(id);
|
2015-05-19 06:21:33 +02:00
|
|
|
|
2014-10-25 18:02:26 +02:00
|
|
|
if (g_debug_context)
|
2016-09-18 02:38:01 +02:00
|
|
|
g_debug_context->OnEvent(DebugContext::Event::PicaCommandProcessed,
|
|
|
|
reinterpret_cast<void*>(&id));
|
2014-07-26 14:42:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void ProcessCommandList(const u32* list, u32 size) {
|
2015-05-24 06:55:35 +02:00
|
|
|
g_state.cmd_list.head_ptr = g_state.cmd_list.current_ptr = list;
|
|
|
|
g_state.cmd_list.length = size / sizeof(u32);
|
|
|
|
|
|
|
|
while (g_state.cmd_list.current_ptr < g_state.cmd_list.head_ptr + g_state.cmd_list.length) {
|
|
|
|
|
|
|
|
// Align read pointer to 8 bytes
|
|
|
|
if ((g_state.cmd_list.head_ptr - g_state.cmd_list.current_ptr) % 2 != 0)
|
|
|
|
++g_state.cmd_list.current_ptr;
|
|
|
|
|
|
|
|
u32 value = *g_state.cmd_list.current_ptr++;
|
2016-09-18 02:38:01 +02:00
|
|
|
const CommandHeader header = {*g_state.cmd_list.current_ptr++};
|
2015-05-24 06:55:35 +02:00
|
|
|
|
2016-01-17 08:22:51 +01:00
|
|
|
WritePicaReg(header.cmd_id, value, header.parameter_mask);
|
2015-05-24 06:55:35 +02:00
|
|
|
|
|
|
|
for (unsigned i = 0; i < header.extra_data_length; ++i) {
|
|
|
|
u32 cmd = header.cmd_id + (header.group_commands ? i + 1 : 0);
|
2015-07-25 22:00:40 +02:00
|
|
|
WritePicaReg(cmd, *g_state.cmd_list.current_ptr++, header.parameter_mask);
|
2016-09-18 02:38:01 +02:00
|
|
|
}
|
2014-07-26 14:42:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-08 06:05:42 +02:00
|
|
|
} // namespace CommandProcessor
|
2014-07-26 14:42:46 +02:00
|
|
|
|
2017-09-08 06:05:42 +02:00
|
|
|
} // namespace Pica
|