From 40b7c9d8002c110347bbe7d80d4e6b68b55ff71a Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Fri, 24 May 2024 18:18:29 -0500 Subject: [PATCH] example: add maple_mouse This also updates maple_analog. --- Makefile | 1 + example/example.mk | 13 ++ example/macaw.cpp | 8 +- example/maple_analog.cpp | 37 +++-- example/maple_mouse.cpp | 247 ++++++++++++++++++++++++++++++++++ headers.mk | 3 + maple/maple_bus_ft0.hpp | 1 + maple/maple_bus_ft6.hpp | 3 + maple/maple_bus_ft8.hpp | 3 + maple/maple_bus_ft9.hpp | 53 ++++++++ regs/gen/maple_data_format.py | 9 +- regs/maple_bus_ft9.csv | 21 +++ regs/maple_bus_ft9.ods | Bin 0 -> 12154 bytes 13 files changed, 371 insertions(+), 28 deletions(-) create mode 100644 example/maple_mouse.cpp create mode 100644 maple/maple_bus_ft9.hpp create mode 100644 regs/maple_bus_ft9.csv create mode 100644 regs/maple_bus_ft9.ods diff --git a/Makefile b/Makefile index 46e6b6d..108c2ed 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ build-fonts: include example/example.mk include text_editor/text_editor.mk +include snake/snake.mk include pokemon/pokemon.mk .PHONY: phony diff --git a/example/example.mk b/example/example.mk index 19d5ecc..c19eabc 100644 --- a/example/example.mk +++ b/example/example.mk @@ -329,6 +329,19 @@ MAPLE_ANALOG_OBJ = \ example/maple_analog.elf: LDSCRIPT = $(LIB)/main.lds example/maple_analog.elf: $(START_OBJ) $(MAPLE_ANALOG_OBJ) +MAPLE_MOUSE_OBJ = \ + example/maple_mouse.o \ + holly/video_output.o \ + holly/core.o \ + holly/region_array.o \ + holly/background.o \ + holly/ta_fifo_polygon_converter.o \ + sh7091/serial.o \ + maple/maple.o + +example/maple_mouse.elf: LDSCRIPT = $(LIB)/main.lds +example/maple_mouse.elf: $(START_OBJ) $(MAPLE_MOUSE_OBJ) + SERIAL_TRANSFER_OBJ = \ example/serial_transfer.o \ sh7091/serial.o \ diff --git a/example/macaw.cpp b/example/macaw.cpp index 803ff9a..bf122e2 100644 --- a/example/macaw.cpp +++ b/example/macaw.cpp @@ -30,10 +30,10 @@ struct vertex { const struct vertex strip_vertices[4] = { // [ position ] [ uv coordinates ] [color ] - { -0.5f, 0.5f, 0.f, 0.f , 127.f/128.f, 0x00000000}, // the first two base colors in a - { -0.5f, -0.5f, 0.f, 0.f , 0.f , 0x00000000}, // non-Gouraud triangle strip are ignored - { 0.5f, 0.5f, 0.f, 127.f/128.f, 127.f/128.f, 0x00000000}, - { 0.5f, -0.5f, 0.f, 127.f/128.f, 0.f , 0x00000000}, + { -0.5f, 0.5f, 0.f, 0.f, 1.f, 0x00000000}, // the first two base colors in a + { -0.5f, -0.5f, 0.f, 0.f, 0.f, 0x00000000}, // non-Gouraud triangle strip are ignored + { 0.5f, 0.5f, 0.f, 1.f, 1.f, 0x00000000}, + { 0.5f, -0.5f, 0.f, 1.f, 0.f, 0x00000000}, }; constexpr uint32_t strip_length = (sizeof (strip_vertices)) / (sizeof (struct vertex)); diff --git a/example/maple_analog.cpp b/example/maple_analog.cpp index ac8d926..4257af4 100644 --- a/example/maple_analog.cpp +++ b/example/maple_analog.cpp @@ -17,40 +17,36 @@ #include "holly/background.hpp" #include "holly/texture_memory_alloc.hpp" #include "memorymap.hpp" -#include "sh7091/serial.hpp" #include "geometry/border.hpp" #include "geometry/circle.hpp" #include "math/vec4.hpp" #include "maple/maple.hpp" -#include "maple/maple_impl.hpp" +#include "maple/maple_host_command_writer.hpp" #include "maple/maple_bus_bits.hpp" #include "maple/maple_bus_commands.hpp" #include "maple/maple_bus_ft0.hpp" -uint32_t _command_buf[(1024 + 32) / 4]; -uint32_t _receive_buf[(1024 + 32) / 4]; - static ft0::data_transfer::data_format data[4]; -void do_get_condition(uint32_t * command_buf, - uint32_t * receive_buf) +void do_get_condition() { - using command_type = get_condition; - using response_type = data_transfer; + uint32_t send_buf[1024] __attribute__((aligned(32))); + uint32_t recv_buf[1024] __attribute__((aligned(32))); - get_condition::data_fields data_fields = { - .function_type = std::byteswap(function_type::controller) - }; + using command_type = maple::get_condition; + using response_type = maple::data_transfer; - const uint32_t command_size = maple::init_host_command_all_ports(command_buf, receive_buf, - data_fields); - using host_response_type = struct maple::host_response; - auto host_response = reinterpret_cast(receive_buf); + auto writer = maple::host_command_writer(send_buf, recv_buf); - maple::dma_start(command_buf, command_size, - receive_buf, maple::sizeof_command(host_response)); + auto [host_command, host_response] + = writer.append_command_all_ports(); + + host_command->bus_data.data_fields.function_type = std::byteswap(function_type::controller); + + maple::dma_start(send_buf, writer.send_offset, + recv_buf, writer.recv_offset); for (uint8_t port = 0; port < 4; port++) { auto& bus_data = host_response[port].bus_data; @@ -156,9 +152,6 @@ uint32_t _ta_parameter_buf[((32 * 8192) + 32) / 4]; void main() { - uint32_t * command_buf = align_32byte(_command_buf); - uint32_t * receive_buf = align_32byte(_receive_buf); - video_output::set_mode_vga(); // The address of `ta_parameter_buf` must be a multiple of 32 bytes. @@ -188,7 +181,7 @@ void main() uint32_t frame_ix = 0; while (1) { - do_get_condition(command_buf, receive_buf); + do_get_condition(); ta_polygon_converter_init(opb_size.total(), ta_alloc, diff --git a/example/maple_mouse.cpp b/example/maple_mouse.cpp new file mode 100644 index 0000000..900a34c --- /dev/null +++ b/example/maple_mouse.cpp @@ -0,0 +1,247 @@ +#include +#include + +#include "align.hpp" + +#include "holly/video_output.hpp" +#include "holly/holly.hpp" +#include "holly/core.hpp" +#include "holly/core_bits.hpp" +#include "holly/ta_fifo_polygon_converter.hpp" +#include "holly/ta_parameter.hpp" +#include "holly/ta_vertex_parameter.hpp" +#include "holly/ta_global_parameter.hpp" +#include "holly/ta_bits.hpp" +#include "holly/isp_tsp.hpp" +#include "holly/region_array.hpp" +#include "holly/background.hpp" +#include "holly/texture_memory_alloc.hpp" +#include "memorymap.hpp" +#include "sh7091/serial.hpp" + +#include "geometry/border.hpp" +#include "geometry/circle.hpp" +#include "math/vec4.hpp" + +#include "maple/maple.hpp" +#include "maple/maple_host_command_writer.hpp" +#include "maple/maple_bus_bits.hpp" +#include "maple/maple_bus_commands.hpp" +#include "maple/maple_bus_ft0.hpp" +#include "maple/maple_bus_ft9.hpp" + +static ft9::data_transfer::data_format data; + +void do_get_condition() +{ + uint32_t send_buf[1024] __attribute__((aligned(32))); + uint32_t recv_buf[1024] __attribute__((aligned(32))); + + using command_type = maple::get_condition; + using response_type = maple::data_transfer; + + auto writer = maple::host_command_writer(send_buf, recv_buf); + + auto [host_command, host_response] + = writer.append_command_all_ports(); + + host_command->bus_data.data_fields.function_type = std::byteswap(function_type::pointing); + + maple::dma_start(send_buf, writer.send_offset, + recv_buf, writer.recv_offset); + + for (uint8_t port = 0; port < 4; port++) { + auto& bus_data = host_response[port].bus_data; + if (bus_data.command_code != response_type::command_code) { + continue; + } + + auto& data_fields = bus_data.data_fields; + if ((std::byteswap(data_fields.function_type) & function_type::pointing) == 0) { + continue; + } + + for (uint32_t i = 0; i < 8; i++) { + data.analog_coordinate_axis[i] = data_fields.data.analog_coordinate_axis[i]; + } + + break; + } +} + +void transform(ta_parameter_writer& parameter, + const vec3 * vertices, + const face_vtn& face, + const vec4& color, + const vec3& position, + const float scale + ) +{ + const uint32_t parameter_control_word = para_control::para_type::polygon_or_modifier_volume + | para_control::list_type::opaque + | obj_control::col_type::floating_color + | obj_control::gouraud; + + const uint32_t isp_tsp_instruction_word = isp_tsp_instruction_word::depth_compare_mode::greater + | isp_tsp_instruction_word::culling_mode::cull_if_positive; + + const uint32_t tsp_instruction_word = tsp_instruction_word::src_alpha_instr::one + | tsp_instruction_word::dst_alpha_instr::zero + | tsp_instruction_word::fog_control::no_fog; + + parameter.append() = + ta_global_parameter::polygon_type_0(parameter_control_word, + isp_tsp_instruction_word, + tsp_instruction_word, + 0, // texture_control_word + 0, // data_size_for_sort_dma + 0 // next_address_for_sort_dma + ); + + constexpr uint32_t strip_length = 3; + for (uint32_t i = 0; i < strip_length; i++) { + + // world transform + uint32_t vertex_ix = face[i].vertex; + auto& vertex = vertices[vertex_ix]; + auto point = vertex; + + // rotate 90° around the X axis + float x = point.x; + float y = point.z; + float z = point.y; + + // world transform + x *= scale; // world space + y *= scale; // world space + z *= 10; + + // object transform + x += position.x; // object space + y += position.y; // object space + z += position.z; // object space + + // camera transform + z += 1; + //y -= 10; + + // screen space transform + x *= 240.f; + y *= 240.f; + x += 320.f; + y += 240.f; + z = 1 / z; + + bool end_of_strip = i == strip_length - 1; + + parameter.append() = + ta_vertex_parameter::polygon_type_1(polygon_vertex_parameter_control_word(end_of_strip), + x, y, z, + color.w, // alpha + color.x, // r + color.y, // g + color.z // b + ); + } +} + +void init_texture_memory(const struct opb_size& opb_size) +{ + region_array2(640 / 32, // width + 480 / 32, // height + opb_size + ); + background_parameter(0xff220000); +} + +uint32_t _ta_parameter_buf[((32 * 8192) + 32) / 4]; + +void main() +{ + video_output::set_mode_vga(); + + // The address of `ta_parameter_buf` must be a multiple of 32 bytes. + // This is mandatory for ch2-dma to the ta fifo polygon converter. + uint32_t * ta_parameter_buf = align_32byte(_ta_parameter_buf); + + constexpr uint32_t ta_alloc = ta_alloc_ctrl::pt_opb::no_list + | ta_alloc_ctrl::tm_opb::no_list + | ta_alloc_ctrl::t_opb::no_list + | ta_alloc_ctrl::om_opb::no_list + | ta_alloc_ctrl::o_opb::_16x4byte; + + constexpr struct opb_size opb_size = { .opaque = 16 * 4 + , .opaque_modifier = 0 + , .translucent = 0 + , .translucent_modifier = 0 + , .punch_through = 0 + }; + + holly.SOFTRESET = softreset::pipeline_soft_reset + | softreset::ta_soft_reset; + holly.SOFTRESET = 0; + + core_init(); + init_texture_memory(opb_size); + + uint32_t frame_ix = 0; + float x_pos = 0.0f; + float y_pos = 0.0f; + float z_pos = 0.0f; + + while (1) { + do_get_condition(); + + ta_polygon_converter_init(opb_size.total(), + ta_alloc, + 640 / 32, + 480 / 32); + + x_pos += static_cast(data.analog_coordinate_axis[0] - 0x200) * 0.0015; + y_pos += static_cast(data.analog_coordinate_axis[1] - 0x200) * 0.0015; + z_pos += static_cast(data.analog_coordinate_axis[2] - 0x200) * 0.015; + + auto parameter = ta_parameter_writer(ta_parameter_buf); + for (uint32_t i = 0; i < border::num_faces; i++) { + transform(parameter, + border::vertices, + border::faces[i], + {1.0, 0.0, 0.0, 1.0}, // color + {0.0, 0.0, 0.0}, // position + 0.5f * (1.f / 0.95f) // scale + ); + } + + for (uint32_t i = 0; i < circle::num_faces; i++) { + transform(parameter, + circle::vertices, + circle::faces[i], + {0.0, 1.0, 1.0, 1.0}, // color + {x_pos, y_pos, 0.0}, // position + 0.05f // scale + ); + } + + for (uint32_t i = 0; i < circle::num_faces; i++) { + transform(parameter, + circle::vertices, + circle::faces[i], + {0.0, 1.0, 1.0, 0.0}, // color + {1.0, z_pos, 0.0}, // position + 0.05f // scale + ); + } + + parameter.append() = ta_global_parameter::end_of_list(para_control::para_type::end_of_list); + ta_polygon_converter_transfer(ta_parameter_buf, parameter.offset); + ta_wait_opaque_list(); + core_start_render(frame_ix); + core_wait_end_of_render_video(); + + while (!spg_status::vsync(holly.SPG_STATUS)); + core_flip(frame_ix); + while (spg_status::vsync(holly.SPG_STATUS)); + + frame_ix = (frame_ix + 1) & 1; + } +} diff --git a/headers.mk b/headers.mk index 4217714..4a8f766 100644 --- a/headers.mk +++ b/headers.mk @@ -62,6 +62,9 @@ maple/maple_bus_ft6_key_scan_codes.hpp: regs/maple_bus_ft6_key_scan_codes.csv re maple/maple_bus_ft8.hpp: regs/maple_bus_ft8.csv regs/gen/maple_data_format.py python regs/gen/maple_data_format.py $< > $@ +maple/maple_bus_ft9.hpp: regs/maple_bus_ft9.csv regs/gen/maple_data_format.py + python regs/gen/maple_data_format.py $< > $@ + # AICA aica/aica_channel.hpp: regs/aica_channel_data.csv regs/gen/aica.py diff --git a/maple/maple_bus_ft0.hpp b/maple/maple_bus_ft0.hpp index 999ab82..753a748 100644 --- a/maple/maple_bus_ft0.hpp +++ b/maple/maple_bus_ft0.hpp @@ -62,6 +62,7 @@ namespace ft0 { uint8_t analog_axis_5; uint8_t analog_axis_6; }; + static_assert((sizeof (struct data_format)) % 4 == 0); static_assert((sizeof (struct data_format)) == 8); } diff --git a/maple/maple_bus_ft6.hpp b/maple/maple_bus_ft6.hpp index 4026ff1..06ddb8b 100644 --- a/maple/maple_bus_ft6.hpp +++ b/maple/maple_bus_ft6.hpp @@ -1,4 +1,5 @@ #pragma once + namespace ft6 { namespace data_transfer { namespace modifier_key { @@ -60,6 +61,7 @@ namespace ft6 { uint8_t led_state; uint8_t scan_code_array[6]; }; + static_assert((sizeof (struct data_format)) % 4 == 0); static_assert((sizeof (struct data_format)) == 8); } @@ -70,6 +72,7 @@ namespace ft6 { uint8_t w2_reserved; uint8_t w3_reserved; }; + static_assert((sizeof (struct data_format)) % 4 == 0); static_assert((sizeof (struct data_format)) == 4); } diff --git a/maple/maple_bus_ft8.hpp b/maple/maple_bus_ft8.hpp index 8f9c35f..8b34165 100644 --- a/maple/maple_bus_ft8.hpp +++ b/maple/maple_bus_ft8.hpp @@ -1,4 +1,5 @@ #pragma once + namespace ft8 { namespace data_transfer { namespace vset { @@ -33,6 +34,7 @@ namespace ft8 { uint8_t fm0; uint8_t fm1; }; + static_assert((sizeof (struct data_format)) % 4 == 0); static_assert((sizeof (struct data_format)) == 4); } @@ -43,6 +45,7 @@ namespace ft8 { uint8_t freq; uint8_t inc; }; + static_assert((sizeof (struct data_format)) % 4 == 0); static_assert((sizeof (struct data_format)) == 4); } diff --git a/maple/maple_bus_ft9.hpp b/maple/maple_bus_ft9.hpp new file mode 100644 index 0000000..d2abd1f --- /dev/null +++ b/maple/maple_bus_ft9.hpp @@ -0,0 +1,53 @@ +#pragma once + +namespace ft9 { + namespace data_transfer { + namespace button { + constexpr uint32_t r() { return 0b1 << 7; } + constexpr uint32_t r(uint32_t reg) { return (reg >> 7) & 0b1; } + + constexpr uint32_t l() { return 0b1 << 6; } + constexpr uint32_t l(uint32_t reg) { return (reg >> 6) & 0b1; } + + constexpr uint32_t d() { return 0b1 << 5; } + constexpr uint32_t d(uint32_t reg) { return (reg >> 5) & 0b1; } + + constexpr uint32_t u() { return 0b1 << 4; } + constexpr uint32_t u(uint32_t reg) { return (reg >> 4) & 0b1; } + + constexpr uint32_t s() { return 0b1 << 3; } + constexpr uint32_t s(uint32_t reg) { return (reg >> 3) & 0b1; } + + constexpr uint32_t a() { return 0b1 << 2; } + constexpr uint32_t a(uint32_t reg) { return (reg >> 2) & 0b1; } + + constexpr uint32_t b() { return 0b1 << 1; } + constexpr uint32_t b(uint32_t reg) { return (reg >> 1) & 0b1; } + + constexpr uint32_t c() { return 0b1 << 0; } + constexpr uint32_t c(uint32_t reg) { return (reg >> 0) & 0b1; } + + } + + namespace option { + constexpr uint32_t batt() { return 0b1 << 1; } + constexpr uint32_t batt(uint32_t reg) { return (reg >> 1) & 0b1; } + + constexpr uint32_t wire() { return 0b1 << 0; } + constexpr uint32_t wire(uint32_t reg) { return (reg >> 0) & 0b1; } + + } + + struct data_format { + uint8_t button; + uint8_t option; + uint8_t analog_coordinate_overflow; + uint8_t reserved; + uint16_t analog_coordinate_axis[8]; + }; + static_assert((sizeof (struct data_format)) % 4 == 0); + static_assert((sizeof (struct data_format)) == 20); + } + +} + diff --git a/regs/gen/maple_data_format.py b/regs/gen/maple_data_format.py index 2716745..b084dd1 100644 --- a/regs/gen/maple_data_format.py +++ b/regs/gen/maple_data_format.py @@ -28,6 +28,8 @@ def parse_bits(bits: list[str]): bit_order = [7, 6, 5, 4, 3, 2, 1, 0] by_name = defaultdict(list) for bit_ix, bit in zip(bit_order, bits): + if bit == '': + continue by_name[bit].append(bit_ix) for name, indicies in by_name.items(): yield Bit(name=name, @@ -65,8 +67,8 @@ def parse_data_format(ix, rows): ix += 1 excess_bits = [b for b in _bits[8:] if b != ""] assert excess_bits == [] - bits = [b for b in _bits[:8] if b != ""] - assert len(bits) in {0, 8}, bits + bits = [b for b in _bits[:8]] + assert len(bits) == 8, bits fields[field_name].append(Field(field_name, list(parse_bits(bits)))) _, variable = parse_format_name(field_name) @@ -126,6 +128,9 @@ def render_format(format): yield f"uint16_t {field_name};" elif len(subfields) in {3, 6}: yield f"uint8_t {field_name}[{len(subfields)}];" + elif len(subfields) == 16: + # bleh: hacky + yield f"uint16_t {field_name}[8];" elif len(subfields) == 4: yield f"uint32_t {field_name};" else: diff --git a/regs/maple_bus_ft9.csv b/regs/maple_bus_ft9.csv new file mode 100644 index 0000000..a24c916 --- /dev/null +++ b/regs/maple_bus_ft9.csv @@ -0,0 +1,21 @@ +"data_transfer",7,6,5,4,3,2,1,0 +"button","r","l","d","u","s","a","b","c" +"option",,,,,,,"batt","wire" +"analog_coordinate_overflow",,,,,,,, +"reserved",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, +"analog_coordinate_axis",,,,,,,, diff --git a/regs/maple_bus_ft9.ods b/regs/maple_bus_ft9.ods new file mode 100644 index 0000000000000000000000000000000000000000..1184608836863aa67bfd00472e4723aaa6749e66 GIT binary patch literal 12154 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