From ea51bca51837dc2a3cb8c5116fca2cdd8d967bd7 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Thu, 23 May 2024 11:46:39 -0500 Subject: [PATCH] example/maple_storage: initial example --- base.mk | 1 + common.mk | 2 +- example/example.mk | 3 +- example/maple_storage.cpp | 483 ++++++++++++++++++++++++---- maple/maple_bus_commands.hpp | 3 + maple/maple_bus_ft1.hpp | 7 +- maple/maple_host_command_writer.hpp | 21 +- maple/storage.hpp | 66 ++++ notes/storage-notes.txt | 403 +++++++++++++++++++++++ regs/gen/maple_data_format.py | 34 +- regs/maple_bus_ft1.csv | 4 +- regs/maple_bus_ft1.ods | Bin 18921 -> 18824 bytes 12 files changed, 932 insertions(+), 95 deletions(-) create mode 100644 maple/storage.hpp create mode 100644 notes/storage-notes.txt diff --git a/base.mk b/base.mk index 82f8536..be80c0a 100644 --- a/base.mk +++ b/base.mk @@ -7,6 +7,7 @@ CFLAGS += -Wall -Werror -Wfatal-errors CFLAGS += -Wno-array-bounds #CFLAGS += -Wno-error=narrowing -Wno-error=unused-variable -Wno-error=array-bounds= CFLAGS += -Wno-error=maybe-uninitialized +CFLAGS += -Wno-error=unused-but-set-variable CXXFLAGS += -fno-exceptions -fno-non-call-exceptions -fno-rtti -fno-threadsafe-statics diff --git a/common.mk b/common.mk index b5fd50c..16cfe68 100644 --- a/common.mk +++ b/common.mk @@ -2,7 +2,7 @@ MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) DIR := $(dir $(MAKEFILE_PATH)) LIB ?= . -OPT ?= -Os +OPT ?= -O3 GENERATED ?= AARCH = --isa=sh4 --little diff --git a/example/example.mk b/example/example.mk index 1a30778..19d5ecc 100644 --- a/example/example.mk +++ b/example/example.mk @@ -290,7 +290,8 @@ MAPLE_STORAGE_OBJ = \ example/maple_storage.o \ holly/video_output.o \ sh7091/serial.o \ - maple/maple.o + maple/maple.o \ + $(LIBGCC) example/maple_storage.elf: LDSCRIPT = $(LIB)/main.lds example/maple_storage.elf: $(START_OBJ) $(MAPLE_STORAGE_OBJ) diff --git a/example/maple_storage.cpp b/example/maple_storage.cpp index 3fbb4d3..e920bff 100644 --- a/example/maple_storage.cpp +++ b/example/maple_storage.cpp @@ -10,10 +10,281 @@ #include "maple/maple_bus_commands.hpp" #include "maple/maple_bus_ft1.hpp" #include "maple/maple_host_command_writer.hpp" +#include "maple/storage.hpp" #include "sh7091/serial.hpp" #include "systembus.hpp" +struct storage_state { + uint32_t * send_buf; + uint32_t * recv_buf; + uint32_t host_port_select; + uint32_t destination_ap; + struct { + uint8_t partitions; + uint16_t bytes_per_block; + uint8_t write_accesses; + uint8_t read_accesses; + } device_status; + struct { + struct { + uint16_t block_number; + } system_area; + struct { + uint16_t block_number; + uint16_t number_of_blocks; + } fat_area; + struct { + uint16_t block_number; + uint16_t number_of_blocks; + } file_information; + } media_info; + storage::system_area system_area; + storage::fat_area * fat_area; + storage::file_information * file_information; // array + + uint32_t bytes_per_read_access() + { + // divide rounding up + return (device_status.bytes_per_block + (device_status.read_accesses - 1)) / device_status.read_accesses; + } + + uint32_t bytes_per_write_access() + { + // divide rounding up + return (device_status.bytes_per_block + (device_status.write_accesses - 1)) / device_status.write_accesses; + } + + uint32_t system_area_size() + { + return device_status.bytes_per_block * 1; + } + + uint32_t fat_area_size() + { + return device_status.bytes_per_block * media_info.fat_area.number_of_blocks; + } + + uint32_t file_information_size() + { + return device_status.bytes_per_block * media_info.file_information.number_of_blocks; + } + + uint32_t file_information_entries() + { + return file_information_size() / (sizeof (struct storage::file_information)); + } +}; + +void parse_storage_function_definition(const uint32_t fd, storage_state& state) +{ + state.device_status.partitions = ((fd >> 24) & 0xff) + 1; + state.device_status.bytes_per_block = (((fd >> 16) & 0xff) + 1) * 32; + state.device_status.write_accesses = (fd >> 12) & 0xf; + state.device_status.read_accesses = (fd >> 8) & 0xf; + + serial::string(" function_definition:\n"); + serial::string(" partitions: "); + serial::integer(state.device_status.partitions); + serial::string(" bytes_per_block: "); + serial::integer(state.device_status.bytes_per_block); + serial::string(" write_accesses: "); + serial::integer(state.device_status.write_accesses); + serial::string(" read_accesses: "); + serial::integer(state.device_status.read_accesses); +} + +template +inline void copy(T * dst, const T * src, const int32_t n) noexcept +{ + int32_t n_t = n / (sizeof (T)); + while (n_t > 0) { + *dst++ = *src++; + n_t--; + } +} + +struct block_read_response { + union responses { + struct maple::file_error::data_fields file_error; + struct maple::data_transfer::data_fields data_transfer; + }; + + using data_fields = union responses; +}; + +bool do_block_read(storage_state& state, uint16_t block_number, uint8_t * dest) +{ + using command_type = maple::block_read; + using response_type = block_read_response; + + auto writer = maple::host_command_writer(state.send_buf, state.recv_buf); + + maple::host_response * host_responses[state.device_status.read_accesses]; + for (int32_t phase = 0; phase < state.device_status.read_accesses; phase++) { + bool end_flag = phase == (state.device_status.read_accesses - 1); + auto [host_command, host_response] + = writer.append_command(state.host_port_select, + state.destination_ap, + end_flag, + 0, // send_trailing + state.bytes_per_read_access() // recv_trailing + ); + + auto& data_fields = host_command->bus_data.data_fields; + data_fields.function_type = std::byteswap(function_type::storage); + data_fields.pt = 0; + data_fields.phase = phase; + data_fields.block_number = std::byteswap(block_number); + + host_responses[phase] = host_response; + + //serial::string("read phase: "); + //serial::integer(phase); + //serial::integer(writer.recv_offset); + } + + maple::dma_start(state.send_buf, writer.send_offset, + state.recv_buf, writer.recv_offset); + + for (uint32_t phase = 0; phase < state.device_status.read_accesses; phase++) { + auto& bus_data = host_responses[phase]->bus_data; + if (bus_data.command_code != maple::data_transfer::command_code) { + auto& file_error = bus_data.data_fields.file_error; + serial::string("lm did not reply block_read: "); + serial::integer(bus_data.command_code); + if (bus_data.command_code == maple::file_error::command_code) { + serial::string("error: "); + serial::hexlify(&file_error.function_error_code, 4); + serial::character('\n'); + } + return false; + } else { + auto& data_transfer = bus_data.data_fields.data_transfer; + copy(dest, data_transfer.data.block_data, state.bytes_per_read_access()); + dest += state.bytes_per_read_access(); + } + } + + return true; +} + +struct get_last_error_response { + union responses { + struct maple::device_reply device_reply; + struct maple::file_error file_error; + }; + + using data_fields = union responses; +}; + +bool do_block_write(storage_state& state, uint16_t block_number, uint8_t * src) +{ + auto writer = maple::host_command_writer(state.send_buf, state.recv_buf); + + uint32_t phase; + for (phase = 0; phase < state.device_status.write_accesses; phase++) { + using command_type = maple::block_write; + using response_type = maple::device_reply; + + auto [host_command, host_response] + = writer.append_command(state.host_port_select, + state.destination_ap, + false, // end_flag + state.bytes_per_write_access(), // send_trailing + 0 // recv_trailing + ); + + auto& data_fields = host_command->bus_data.data_fields; + data_fields.function_type = std::byteswap(function_type::storage); + data_fields.pt = 0; + data_fields.phase = phase; + data_fields.block_number = std::byteswap(block_number); + + copy(data_fields.written_data, src, state.bytes_per_write_access()); + src += state.bytes_per_write_access(); + //serial::string("write phase: "); + //serial::integer(phase); + //serial::integer(writer.send_offset); + } + + using command_type = maple::get_last_error; + using response_type = get_last_error_response; + + auto [host_command, host_response] + = writer.append_command(state.host_port_select, + state.destination_ap, + true); // end_flag + + auto& data_fields = host_command->bus_data.data_fields; + data_fields.function_type = std::byteswap(function_type::storage); + data_fields.pt = 0; + data_fields.phase = phase; + data_fields.block_number = std::byteswap(block_number); + //serial::string("write phase: "); + //serial::integer(phase); + //serial::integer(writer.send_offset); + + maple::dma_start(state.send_buf, writer.send_offset, + state.recv_buf, writer.recv_offset); + + serial::string("block write status: "); + serial::integer(host_response->bus_data.command_code); + + return true; +} + +uint16_t allocate_fat_chain(storage_state& state, uint16_t blocks) +{ + int32_t first_block = -1; + int32_t last_block; + // + for (int32_t i = (state.fat_area_size() / (sizeof (uint16_t))) - 1; i >= 0; i--) { + if (state.fat_area->fat_number[i] == storage::fat_area::data::unused) { + if (first_block == -1) { + first_block = i; + } else { + state.fat_area->fat_number[last_block] = i; + } + last_block = i; + blocks -= 1; + } + + if (blocks == 0) { + state.fat_area->fat_number[last_block] = storage::fat_area::data::data_end; + break; + } + } + for (uint32_t i = 0; i < (state.fat_area_size() / (sizeof (uint16_t))); i++) { + //serial::integer(i, ' '); + //serial::integer(state.fat_area->fat_number[i]); + } + + return first_block; +} + +bool allocate_file_information_data(storage_state& state, + uint16_t start_fat, + uint8_t const * const file_name, + uint16_t block_size) +{ + for (uint32_t i = 0; i < state.file_information_entries(); i++) { + if (state.file_information[i].status == storage::file_information::status::no_data_file) { + + state.file_information[i].status = storage::file_information::status::data_file; + state.file_information[i].copy = 0; + state.file_information[i].start_fat = start_fat; + copy(state.file_information[i].file_name, file_name, 12); + state.file_information[i].block_size = block_size; + state.file_information[i].header = 0; + state.file_information[i]._reserved = 0; + + return true; + } + } + return false; +} + void do_lm_request(uint8_t port, uint8_t lm) { uint32_t send_buf[1024] __attribute__((aligned(32))); @@ -22,9 +293,15 @@ void do_lm_request(uint8_t port, uint8_t lm) const uint32_t host_port_select = host_instruction_port_select(port); const uint32_t destination_ap = ap_port_select(port) | ap::de::expansion_device | lm; + storage_state state; + state.send_buf = send_buf; + state.recv_buf = recv_buf; + state.host_port_select = host_port_select; + state.destination_ap = destination_ap; + { - using command_type = device_request; - using response_type = device_status; + using command_type = maple::device_request; + using response_type = maple::device_status; auto writer = maple::host_command_writer(send_buf, recv_buf); @@ -60,11 +337,13 @@ void do_lm_request(uint8_t port, uint8_t lm) if ((std::byteswap(data_fields.device_id.ft) & function_type::storage) == 0) { return; } + + parse_storage_function_definition(std::byteswap(data_fields.device_id.fd[2]), state); } { - using command_type = get_media_info; - using response_type = data_transfer; + using command_type = maple::get_media_info; + using response_type = maple::data_transfer; auto writer = maple::host_command_writer(send_buf, recv_buf); @@ -87,84 +366,174 @@ void do_lm_request(uint8_t port, uint8_t lm) serial::integer(port, ' '); serial::integer(lm); } else { - serial::string(" total_size: "); + serial::string(" media_info:\n"); + serial::string(" total_size: "); serial::integer(data.total_size); - serial::string(" partition_number: "); + serial::string(" partition_number: "); serial::integer(data.partition_number); - serial::string(" system_area_block_number: "); + serial::string(" system_area_block_number: "); serial::integer(data.system_area_block_number); - serial::string(" fat_area_block_number: "); + serial::string(" fat_area_block_number: "); serial::integer(data.fat_area_block_number); - serial::string(" number_of_fat_area_blocks: "); + serial::string(" number_of_fat_area_blocks: "); serial::integer(data.number_of_fat_area_blocks); - serial::string(" file_information_block_number: "); + serial::string(" file_information_block_number: "); serial::integer(data.file_information_block_number); - serial::string(" number_of_file_information_blocks: "); + serial::string(" number_of_file_information_blocks: "); serial::integer(data.number_of_file_information_blocks); - serial::string(" volume_icon: "); + serial::string(" volume_icon: "); serial::integer(data.volume_icon); - serial::string(" save_area_block_number: "); + serial::string(" save_area_block_number: "); serial::integer(data.save_area_block_number); - serial::string(" number_of_save_area_blocks: "); + serial::string(" number_of_save_area_blocks: "); serial::integer(data.number_of_save_area_blocks); - serial::string(" reserved_for_execution_file: "); + serial::string(" reserved_for_execution_file: "); serial::integer(data.reserved_for_execution_file); } + + state.media_info.system_area.block_number = data.system_area_block_number; + state.media_info.fat_area.block_number = data.fat_area_block_number; + state.media_info.fat_area.number_of_blocks = data.number_of_fat_area_blocks; + state.media_info.file_information.block_number = data.file_information_block_number; + state.media_info.file_information.number_of_blocks = data.number_of_file_information_blocks; + } + + uint8_t fat_area_data[state.fat_area_size()]; + uint8_t file_information_data[state.file_information_size()]; + + { + do_block_read(state, state.media_info.system_area.block_number, + reinterpret_cast(&state.system_area)); + serial::string(" system_area:\n"); + serial::string(" format_information: "); + serial::hexlify(state.system_area.format_information, 16); + serial::string(" volume_label: "); + serial::hexlify(state.system_area.volume_label, 32); + serial::string(" date_and_time_created: "); + serial::hexlify(state.system_area.date_and_time_created, 8); + serial::string(" total_size: "); + serial::integer(state.system_area.total_size); + } + + { + uint8_t * bufi = fat_area_data; + uint16_t chain = state.media_info.fat_area.block_number; + state.fat_area = reinterpret_cast(fat_area_data); + do { + do_block_read(state, chain, bufi); + bufi += state.device_status.bytes_per_block; + chain = state.fat_area->fat_number[chain]; + } while (chain != storage::fat_area::data::data_end); + serial::string(" read fat_area bytes: "); + serial::integer((uint32_t)(bufi - fat_area_data)); + + uint32_t count = 0; + for (uint32_t i = 0; i < (bufi - fat_area_data) / (sizeof (uint16_t)); i++) { + if (state.fat_area->fat_number[i] == 0xfffc) { + count += 1; + } + //serial::integer(i, ' '); + //serial::integer(state.fat_area->fat_number[i]); + } + serial::string(" free blocks: "); + serial::integer(count); + } + + { + uint8_t * bufi = file_information_data; + uint16_t chain = state.media_info.file_information.block_number; + state.file_information = reinterpret_cast(file_information_data); + do { + do_block_read(state, chain, bufi); + bufi += state.device_status.bytes_per_block; + chain = state.fat_area->fat_number[chain]; + } while (chain != storage::fat_area::data::data_end); + serial::string(" read file_information bytes: "); + serial::integer((uint32_t)(bufi - file_information_data)); + + for (uint32_t i = 0; i < state.file_information_entries(); i++) { + if (state.file_information[i].status == storage::file_information::status::data_file + || state.file_information[i].status == storage::file_information::status::execution_file) { + serial::string(" file_name: "); + serial::string(state.file_information[i].file_name, 12); + serial::character('\n'); + serial::string(" status: "); + serial::integer(state.file_information[i].status); + serial::string(" start_fat: "); + serial::integer(state.file_information[i].start_fat); + serial::string(" block_size: "); + serial::integer(state.file_information[i].block_size); + } + } } { - using command_type = block_read; - using response_type = data_transfer>; + uint16_t block_size = 3; + uint16_t start_fat = allocate_fat_chain(state, block_size); + char const * file_name = "HELLOANA.SUP"; + allocate_file_information_data(state, start_fat, + reinterpret_cast(file_name), + block_size); - auto writer = maple::host_command_writer(send_buf, recv_buf); - - auto [host_command, host_response] - = writer.append_command(host_port_select, - destination_ap, - true); // end_flag - host_command->bus_data.data_fields.function_type = std::byteswap(function_type::storage); - host_command->bus_data.data_fields.pt = 0; - host_command->bus_data.data_fields.phase = 0; - host_command->bus_data.data_fields.block_number = std::byteswap(0xff); - - maple::dma_start(send_buf, writer.send_offset, - recv_buf, writer.recv_offset); - - auto& bus_data = host_response->bus_data; - auto& data_fields = bus_data.data_fields; - auto& data = data_fields.data; - serial::integer(std::byteswap(host_command->bus_data.data_fields.block_number)); - if (bus_data.command_code != response_type::command_code) { - serial::string("lm did not reply block_read: "); - serial::integer(port, ' '); - serial::integer(lm, ' '); - serial::integer(bus_data.command_code); - auto error = reinterpret_cast(&data_fields); - serial::hexlify(&error->function_error_code, 4); - } else { - for (int i = 0; i < 512; i++) { - serial::hexlify(data.block_data[i]); - serial::character(' '); - if (i % 16 == 15) { - serial::character('\n'); - } - } + { + uint16_t chain = state.media_info.fat_area.block_number; + uint8_t * buf = reinterpret_cast(state.fat_area); + do { + do_block_write(state, chain, buf); + buf += state.device_status.bytes_per_block; + chain = state.fat_area->fat_number[chain]; + } while (chain != storage::fat_area::data::data_end); } + + { + uint16_t chain = state.media_info.file_information.block_number; + uint8_t * buf = reinterpret_cast(state.file_information); + do { + do_block_write(state, chain, buf); + buf += state.device_status.bytes_per_block; + chain = state.fat_area->fat_number[chain]; + } while (chain != storage::fat_area::data::data_end); + } + } + + { + uint8_t * bufi = fat_area_data; + uint16_t chain = state.media_info.fat_area.block_number; + state.fat_area = reinterpret_cast(fat_area_data); + do { + do_block_read(state, chain, bufi); + bufi += state.device_status.bytes_per_block; + chain = state.fat_area->fat_number[chain]; + } while (chain != storage::fat_area::data::data_end); + serial::string(" read fat_area bytes: "); + serial::integer((uint32_t)(bufi - fat_area_data)); + + uint32_t count = 0; + for (uint32_t i = 0; i < (bufi - fat_area_data) / (sizeof (uint16_t)); i++) { + if (state.fat_area->fat_number[i] == 0xfffc) { + count += 1; + } + serial::integer(i, ' '); + serial::integer(state.fat_area->fat_number[i]); + } + serial::string(" free blocks: "); + serial::integer(count); + allocate_fat_chain(state, 98); } } void do_lm_requests(uint8_t port, uint8_t lm) { if (lm & ap::lm_bus::_0) - do_lm_request(port, lm & ap::lm_bus::_0); + do_lm_request(port, ap::lm_bus::_0); if (lm & ap::lm_bus::_1) - do_lm_request(port, lm & ap::lm_bus::_1); + do_lm_request(port, ap::lm_bus::_1); if (lm & ap::lm_bus::_2) - do_lm_request(port, lm & ap::lm_bus::_2); + do_lm_request(port, ap::lm_bus::_2); if (lm & ap::lm_bus::_3) - do_lm_request(port, lm & ap::lm_bus::_3); + do_lm_request(port, ap::lm_bus::_3); if (lm & ap::lm_bus::_4) - do_lm_request(port, lm & ap::lm_bus::_4); + do_lm_request(port, ap::lm_bus::_4); } void do_device_request() @@ -174,8 +543,8 @@ void do_device_request() auto writer = maple::host_command_writer(send_buf, recv_buf); - using command_type = device_request; - using response_type = device_status; + using command_type = maple::device_request; + using response_type = maple::device_status; auto [host_command, host_response] = writer.append_command_all_ports(); diff --git a/maple/maple_bus_commands.hpp b/maple/maple_bus_commands.hpp index b76ec22..7658c00 100644 --- a/maple/maple_bus_commands.hpp +++ b/maple/maple_bus_commands.hpp @@ -2,6 +2,8 @@ #include +namespace maple { + struct device_id { uint32_t ft; uint32_t fd[3]; @@ -276,3 +278,4 @@ struct ar_error { static_assert((sizeof (struct ar_error::data_fields)) == 4); +} diff --git a/maple/maple_bus_ft1.hpp b/maple/maple_bus_ft1.hpp index 4a7151b..0b849bc 100644 --- a/maple/maple_bus_ft1.hpp +++ b/maple/maple_bus_ft1.hpp @@ -20,15 +20,14 @@ namespace ft1 { } namespace block_read_data_transfer { - template struct data_format { uint8_t pt; uint8_t phase; uint16_t block_number; - uint8_t block_data[n]; + uint8_t block_data[0]; }; - static_assert((sizeof (struct data_format<0>)) % 4 == 0); - static_assert((sizeof (struct data_format<0>)) == 4); + static_assert((sizeof (struct data_format)) % 4 == 0); + static_assert((sizeof (struct data_format)) == 4); } } diff --git a/maple/maple_host_command_writer.hpp b/maple/maple_host_command_writer.hpp index d45fa98..b3e9c09 100644 --- a/maple/maple_host_command_writer.hpp +++ b/maple/maple_host_command_writer.hpp @@ -20,20 +20,21 @@ struct host_command_writer { : send_buf(send_buf), recv_buf(recv_buf), send_offset(0), recv_offset(0) { } - template + template constexpr inline std::tuple *, maple::host_response *> append_command(uint32_t host_port_select, uint32_t destination_ap, - bool end_flag) + bool end_flag, + uint32_t send_trailing = 0, + uint32_t recv_trailing = 0) { using command_type = maple::host_command; using response_type = maple::host_response; - constexpr uint32_t data_size = (sizeof (typename C::data_fields)) + data_fields_trailing; + const uint32_t data_size = (sizeof (typename C::data_fields)) + send_trailing; static_assert((sizeof (command_type)) % 4 == 0); static_assert((sizeof (response_type)) % 4 == 0); - static_assert(data_size % 4 == 0); auto host_command = reinterpret_cast(&send_buf[send_offset / 4]); auto host_response = reinterpret_cast(&recv_buf[recv_offset / 4]); @@ -50,8 +51,8 @@ struct host_command_writer { host_command->bus_data.source_ap = destination_ap & ap::port_select::bit_mask; host_command->bus_data.data_size = data_size / 4; - send_offset += (sizeof (command_type)) + data_fields_trailing; - recv_offset += (sizeof (response_type)); + send_offset += (sizeof (command_type)) + send_trailing; + recv_offset += (sizeof (response_type)) + recv_trailing; return {host_command, host_response}; } @@ -61,10 +62,10 @@ struct host_command_writer { maple::host_response *> append_command_all_ports() { - auto ret = append_command(host_instruction::port_select::a, ap::de::device | ap::port_select::a, false); - append_command(host_instruction::port_select::b, ap::de::device | ap::port_select::b, false); - append_command(host_instruction::port_select::c, ap::de::device | ap::port_select::c, false); - append_command(host_instruction::port_select::d, ap::de::device | ap::port_select::d, true); + auto ret = append_command(host_instruction::port_select::a, ap::de::device | ap::port_select::a, false); + append_command(host_instruction::port_select::b, ap::de::device | ap::port_select::b, false); + append_command(host_instruction::port_select::c, ap::de::device | ap::port_select::c, false); + append_command(host_instruction::port_select::d, ap::de::device | ap::port_select::d, true); return ret; } }; diff --git a/maple/storage.hpp b/maple/storage.hpp new file mode 100644 index 0000000..6f8f5b5 --- /dev/null +++ b/maple/storage.hpp @@ -0,0 +1,66 @@ +#include +#include + +namespace storage { + +struct system_area { + uint8_t format_information[16]; + uint8_t volume_label[32]; + uint8_t date_and_time_created[8]; + uint8_t _reserved0[8]; + uint16_t total_size; + uint16_t partition_number; + uint16_t system_area_block_number; + uint16_t fat_area_block_number; + uint16_t number_of_fat_area_blocks; + uint16_t file_information_block_number; + uint16_t number_of_file_information_blocks; + uint8_t volume_icon; + uint8_t reserved; + uint16_t save_area_block_number; + uint16_t number_of_save_area_blocks; + uint8_t reserved_for_execution_file[4]; + uint8_t _reserved1[8]; + uint8_t _reserved2[416]; +}; + +static_assert((sizeof (struct system_area)) == 0x200); +static_assert((offsetof (struct system_area, format_information)) == 0x000); +static_assert((offsetof (struct system_area, volume_label)) == 0x010); +static_assert((offsetof (struct system_area, date_and_time_created)) == 0x030); +static_assert((offsetof (struct system_area, total_size)) == 0x040); +static_assert((offsetof (struct system_area, save_area_block_number)) == 0x050); +static_assert((offsetof (struct system_area, _reserved2)) == 0x060); + +struct fat_area { + uint16_t fat_number[0]; + + struct data { + static constexpr uint16_t data_end = 0xfffa; + static constexpr uint16_t unused = 0xfffc; + static constexpr uint16_t block_damaged = 0xffff; + }; +}; + +static_assert((sizeof (struct fat_area)) == 0); + +struct file_information { + uint8_t status; + uint8_t copy; + uint16_t start_fat; + uint8_t file_name[12]; + uint8_t date[8]; + uint16_t block_size; + uint16_t header; + uint32_t _reserved; + + struct status { + static constexpr uint16_t no_data_file = 0x00; + static constexpr uint16_t data_file = 0x33; + static constexpr uint16_t execution_file = 0xcc; + }; +}; + +static_assert((sizeof (struct file_information)) == 32); + +} diff --git a/notes/storage-notes.txt b/notes/storage-notes.txt new file mode 100644 index 0000000..35e00f3 --- /dev/null +++ b/notes/storage-notes.txt @@ -0,0 +1,403 @@ + ft: 0x0000000e + fd[0]: 0x7e7e3f40 timer + fd[1]: 0x00051000 lcd + fd[2]: 0x000f4100 storage + +storage: + pt: 0x00 → 1 partition + bb: 0x0f → (0x0f + 1) * 32 = 512 bytes per block + wa: 0x4 → 1 block = 4 accesses, 128 bytes per access + ra: 0x1 → 1 block = 1 accesss, 512 bytes per access + rm: 0 → fixed + crc: 0 → CRC not needed + fd: 0 → reserved + +---- block ff (system area) + +format information: 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 +volume label: 01 8f af ff ff 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +date and time: 19 98 11 26 01 37 59 03 +reserved: 00 00 00 00 00 00 00 00 +total size: ff 00 +partition number: 00 00 +system area block number: ff 00 +fat area block number: fe 00 +number of fat area blocks: 01 00 +file information block number: fd 00 +number of file information blocks: 0d 00 +volume icon: 74 +reserved: 00 +save area block number: c8 00 +number of save area blocks: 1f 00 +reserved: 00 00 80 00 + +00 00 00 00 00 00 00 00 + +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + +----- block fe (fat area) + +00: fc ff +01: fc ff +02: fc ff +03: fc ff +04: fc ff +05: fc ff +06: fc ff +07: fc ff +08: fc ff +09: fc ff +0a: fc ff +0b: fc ff +0c: fc ff +0d: fc ff +0e: fc ff +0f: fc ff +10: fc ff +11: fc ff +12: fc ff +13: fc ff +14: fc ff +15: fc ff +16: fc ff +17: fc ff +18: fc ff +19: fc ff +1a: fc ff +1b: fc ff +1c: fc ff +1d: fc ff +1e: fc ff +1f: fc ff +20: fc ff +21: fc ff +22: fc ff +23: fc ff +24: fc ff +25: fc ff +26: fc ff +27: fc ff +28: fc ff +29: fc ff +2a: fc ff +2b: fc ff +2c: fc ff +2d: fc ff +2e: fc ff +2f: fc ff +30: fc ff +31: fc ff +32: fc ff +33: fc ff +34: fc ff +35: fc ff +36: fc ff +37: fc ff +38: fc ff +39: fc ff +3a: fc ff +3b: fc ff +3c: fc ff +3d: fc ff +3e: fc ff +3f: fc ff +40: fc ff +41: fc ff +42: fc ff +43: fc ff +44: fc ff +45: fc ff +46: fc ff +47: fc ff +48: fc ff +49: fc ff +4a: fc ff +4b: fc ff +4c: fc ff +4d: fc ff +4e: fc ff +4f: fc ff +50: fc ff +51: fc ff +52: fc ff +53: fc ff +54: fc ff +55: fc ff +56: fc ff +57: fc ff +58: fc ff +59: fc ff +5a: fc ff +5b: fc ff +5c: fc ff +5d: fa ff +5e: 5d 00 +5f: 5e 00 +60: fa ff +61: 60 00 +62: 61 00 +63: 62 00 +64: 63 00 +65: 64 00 +66: 65 00 +67: 66 00 +68: 67 00 +69: 68 00 +6a: 69 00 +6b: 6a 00 +6c: 6b 00 +6d: fa ff +6e: 6d 00 +6f: fa ff +70: 6f 00 +71: 70 00 +72: 71 00 +73: 72 00 +74: 73 00 +75: 74 00 +76: 75 00 +77: 76 00 +78: 77 00 +79: 78 00 +7a: 79 00 +7b: 7a 00 +7c: 7b 00 +7d: 7c 00 +7e: 7d 00 +7f: 7e 00 +80: 7f 00 +81: 80 00 +82: 81 00 +83: 82 00 +84: 83 00 +85: 84 00 +86: 85 00 +87: 86 00 +88: 87 00 +89: 88 00 +8a: 89 00 +8b: 8a 00 +8c: 8b 00 +8d: 8c 00 +8e: 8d 00 +8f: 8e 00 +90: fa ff +91: 90 00 +92: 91 00 +93: 92 00 +94: fa ff +95: 94 00 +96: 95 00 +97: 96 00 +98: 97 00 +99: 98 00 +9a: 99 00 +9b: 9a 00 +9c: 9b 00 +9d: 9c 00 +9e: 9d 00 +9f: 9e 00 +a0: 9f 00 +a1: a0 00 +a2: a1 00 +a3: a2 00 +a4: a3 00 +a5: a4 00 +a6: a5 00 +a7: fa ff +a8: a7 00 +a9: a8 00 +aa: a9 00 +ab: aa 00 +ac: ab 00 +ad: ac 00 +ae: ad 00 +af: ae 00 +b0: af 00 +b1: b0 00 +b2: b1 00 +b3: b2 00 +b4: b3 00 +b5: b4 00 +b6: b5 00 +b7: b6 00 +b8: b7 00 +b9: fa ff +ba: b9 00 +bb: ba 00 +bc: bb 00 +bd: fa ff +be: bd 00 +bf: be 00 +c0: bf 00 +c1: c0 00 +c2: c1 00 +c3: fa ff +c4: c3 00 +c5: fa ff +c6: c5 00 +c7: c6 00 +c8: fc ff +c9: fc ff +ca: fc ff +cb: fc ff +cc: fc ff +cd: fc ff +ce: fc ff +cf: fc ff +d0: fc ff +d1: fc ff +d2: fc ff +d3: fc ff +d4: fc ff +d5: fc ff +d6: fc ff +d7: fc ff +d8: fc ff +d9: fc ff +da: fc ff +db: fc ff +dc: fc ff +dd: fc ff +de: fc ff +df: fc ff +e0: fc ff +e1: fc ff +e2: fc ff +e3: fc ff +e4: fc ff +e5: fc ff +e6: fc ff +e7: fc ff +e8: fc ff +e9: fc ff +ea: fc ff +eb: fc ff +ec: fc ff +ed: fc ff +ee: fc ff +ef: fc ff +f0: fc ff +f1: fa ff +f2: f1 00 +f3: f2 00 +f4: f3 00 +f5: f4 00 +f6: f5 00 +f7: f6 00 +f8: f7 00 +f9: f8 00 +fa: f9 00 +fb: fa 00 +fc: fb 00 +fd: fc 00 +fe: fa ff +ff: fa ff + +----- block fd (file information) + +status: 33 +copy: 00 +start fat: c7 00 +file name: 47 41 55 4e 54 4c 45 54 2e 30 30 31 "GAUNTLET.001" +date: 19 98 11 26 00 10 12 03 +block size: 03 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: c4 00 +file name: 45 43 43 4f 44 4f 54 46 2e 5a 41 43 "ECCODOTF.ZAC" +date: 19 98 11 26 00 15 17 03 +block size: 02 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: c2 00 +file name: 45 43 43 4f 44 4f 54 46 2e 5f 5f 5f "ECCODOTF.___" +date: 19 98 11 26 00 12 35 03 +block size: 06 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: bc 00 +file name: 53 48 45 4e 4d 55 45 32 5f 53 59 53 "SHENMUE2_SYS" +date: 19 98 11 27 07 41 15 04 +block size: 04 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: b8 00 +file name: 53 48 45 4e 4d 55 45 32 5f 30 30 31 "SHENMUE2_001" +date: 19 98 11 27 07 41 16 04 +block size: 12 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: a6 00 +file name: 54 4f 4d 42 43 48 52 4e 2e 30 30 30 "TOMBCHRN.000" +date: 20 23 09 30 18 29 06 00 +block size: 13 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: 93 00 +file name: 54 4f 4d 42 43 48 52 4e 2e 53 59 53 "TOMBCHRN.SYS" +date: 20 23 09 30 18 29 06 00 +block size: 04 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: 8f 00 +file name: 54 4f 4d 42 52 41 49 44 2e 30 30 30 "TOMBRAID.000" +date: 20 23 09 30 22 09 06 00 +block size: 21 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: 6e 00 +file name: 54 4f 4d 42 52 41 49 44 2e 53 59 53 "TOMBRAID.SYS" +date: 20 23 09 30 22 09 06 00 +block size: 02 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: 00 +start fat: 6c 00 +file name: 4c 45 4d 41 4e 53 32 34 2e 4f 50 54 "LEMANS24.OPT" +date: 20 23 10 11 19 30 14 02 +block size: 0d 00 +header: 00 00 +reserved: 00 00 00 00 + +status: 33 +copy: ff +start fat: 5f 00 +file name: 43 48 55 5f 43 48 55 5f 5f 52 43 54 "CHU_CHU__RCT" +date: 20 23 10 11 16 59 43 02 +block size: 03 00 +header: 00 00 +reserved: 00 00 00 00 + + diff --git a/regs/gen/maple_data_format.py b/regs/gen/maple_data_format.py index 31b7fc2..2716745 100644 --- a/regs/gen/maple_data_format.py +++ b/regs/gen/maple_data_format.py @@ -35,6 +35,14 @@ def parse_bits(bits: list[str]): position=min(indicies), ) +def parse_format_name(name): + if '<' in name: + head, middle, tail = re.split('[<>]', name) + assert tail == "", name + return head, middle + else: + return name, None + def parse_data_format(ix, rows): if ix >= len(rows): return None @@ -61,7 +69,9 @@ def parse_data_format(ix, rows): assert len(bits) in {0, 8}, bits fields[field_name].append(Field(field_name, list(parse_bits(bits)))) - size += 1 + _, variable = parse_format_name(field_name) + if not variable: + size += 1 if field_name not in field_order: field_order.append(field_name) @@ -81,17 +91,8 @@ def parse(rows): assert len(formats) > 0 return formats -def parse_format_name(name): - if '<' in name: - head, middle, tail = re.split('[<>]', name) - assert tail == "", name - return head, middle - else: - return name, None - def render_format(format): - format_name, variable = parse_format_name(format.name) - yield f"namespace {format_name} {{" + yield f"namespace {format.name} {{" for field_name in format.field_order: subfields = format.fields[field_name] if not any(field.bits != [] for field in subfields): @@ -110,8 +111,6 @@ def render_format(format): yield "}" yield "" - if variable is not None: - yield f"template " yield f"struct data_format {{" for _field_name in format.field_order: @@ -133,13 +132,8 @@ def render_format(format): assert False, (len(subfields), field_name) yield "};" - format_name, variable = parse_format_name(format.name) - if variable is not None: - yield f"static_assert((sizeof (struct data_format<0>)) % 4 == 0);" - yield f"static_assert((sizeof (struct data_format<0>)) == {format.size - 1});" - else: - yield f"static_assert((sizeof (struct data_format)) % 4 == 0);" - yield f"static_assert((sizeof (struct data_format)) == {format.size});" + yield f"static_assert((sizeof (struct data_format)) % 4 == 0);" + yield f"static_assert((sizeof (struct data_format)) == {format.size});" yield "}" def render_formats(name, formats): diff --git a/regs/maple_bus_ft1.csv b/regs/maple_bus_ft1.csv index 1bf81a1..1f45cc3 100644 --- a/regs/maple_bus_ft1.csv +++ b/regs/maple_bus_ft1.csv @@ -24,9 +24,9 @@ "reserved_for_execution_file",,,,,,,, "reserved_for_execution_file",,,,,,,, ,,,,,,,, -"block_read_data_transfer",7,6,5,4,3,2,1,0 +"block_read_data_transfer",7,6,5,4,3,2,1,0 "pt",,,,,,,, "phase",,,,,,,, "block_number",,,,,,,, "block_number",,,,,,,, -"block_data",,,,,,,, +"block_data<0>",,,,,,,, diff --git a/regs/maple_bus_ft1.ods b/regs/maple_bus_ft1.ods index e574186a6a7e07e0c7ed56988f06e2fe1db221df..544f44f276ab1db5216a6c4cee77c7852dc40c98 100644 GIT binary patch delta 14944 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 delta 15222 zcma*O2UL?y*EUKA>4NmodzYdTS_GsEp?8oXARR(4CiJ2dA@mlJ5<1cZX(}!BB29W1 z0qGzCqMZ1=-}j&QIqR&mPRN?MGk5l$xx=2>dtcW~JRElyj!U7ZjZbh72Zsa)C%mIM zl|mmMTfu8^AN*5q zNO8hd@aX?uH(KGz97zU)dEn*sdDJ^cpAIHSjcOw zfn#Nu(9@rX4GP}Fk8(~%!hD8qh6kU3*DMcyQr2g`BdPY&T6Bu*Is$D_wAHO-9G>WF zQx6Ru5TmQIN@kf_}v-nlm0ezY;ow90p9^mz#nE`D)2uZYw0ZJWdopS*Y-77sF? zdYM-G9%=y$M_$e`3+C(yhvxoi>27GR=sm^QuivShJxI2$g1Cw(OxxM6*@f!)3>iL^ z8ksM2x1PR(i}{W~rruj@3NE9%J}gaqISWGgpFc z7yjd*(s}2xPok7wjCC=mDbf=reW0I&g&*hS7Gl@qAHHDH2`Wz^Qhe`MK+&@@Nv?I@ zsqd>j?y^tyr|x^Hl{|HdcRyap8NqP%w24Wo%6S~#;^N>u#mB+vV=C;TeiGdtaC;6C? z>4KNFQp3L&P9AGLTPDxaDwP_ytebOoyG?vOaD#?@k@D5}q8420$3tS*Ucz6TdZfzs zg|wJ1<|3uE->G7wfO)g3N?06G^Muz!LgCk+u1}#PnbZzXXe+#FKAXIjuQ9$8XVu%F z3f-9BHOM}0ooUE?{QfJMh34?%38M>&xQqaNJFCl?hYhu!i8Bq3NywGX!V%Enkydb? z&C5CRH)oTbxC1rHt-&bfIuq^v$e^{(IYuD@tGo_He6Y{tY=9xPN9>&*5GEK&2QBFvuk`Vi25 z3gLVd`SK-`xwWVD+b8IfPi(olhe1^jCbh7QkQ=RonniV?3F`a~Uo@qj`^8>Ku zhb}ACPKWka79ULB_r42E_g%Mmm923;c6aHRHLCk%MpAXvFq+|Kru`@@S?wUtYX*)G zepr;^k>a8xC!yMPUv{zCLwxt9nvcp;E)1A;k9n4Fv`<229+!s_+}psRjtC!IbR(-~ zX8YzAwdsVOiu=MI7_};4d=5Qc9Pf$HO*W7&`+4GoCPG?uxed&JS%2%aFi=cFqw{o5 z_DX=R_d#lUMVQ0eAFzvme&kZt6nYo7?TK*xCdiS5v6|M9~5Wo6Lnn#M=FZhDwpg64 zMb;vv3>JL?@OB|7@Lw*z0g~VD;f>qEW-_P8pBoFlEVKeTjJ!za7?iRLxq0KMwb@39 zYqk7jPjIBf@TAELa3^1k#qGlbR}G$EubMaq5w2VgqN2g)?J+hxZ8yZ94lgo}Ex=Ls z_Fo1!KSXHwZ*xNKy!EU*$b1+`#Wb8E&ZCr5V0HMbX7mjofkX!mpSD&^qM#Wb7tBM6 zWBCWkn=-F?-sZAI0yFB5HBJGNw;}`{GHm3ZeJXwu4Hb36psLQCbW1$giYf|L^?Uh6 z?a|%iurEQ!yS-N&z&Nu45_W03*S=#t!3Aydsfzc%ayq_QZGI(l%enc&0P`Y2&ZW!u z4yEjJN-kyyXSxKxF#iSgQkvK1i;q7q3Ke8o%e+L@rpwCjcGh2EhMTj`?Lx^y(tt6O z+?h`J_4n^q^X5*V>1yxt92EA{1*~(%*#0N}l)~^R?NMq-V2DZZKkq9#iGo67w#~*N zqt3m?pQS%D&AJYIuB!m>IGAs+mAnq>?46qPc&Ah2m0OqdoWB1j-jy~V(H)Cl&vf=N z9!vgd>?vbNhCk=+)_8#>(ymL?m)k}=<8;jo_QOgyL+-&d7?nfz1K3|r5$Dz?#7l&O zGb#7~@Dz<>WOGEOsYUwtDvtytO*Gc__{W(=I^y4zUe_ibYK^tMTz%5Nuof*A6~gkt zLd1$l;n#(f{7;*T?nBr!%im;wl?Tj9iv7YW|HJqHP!7k?u){13_a9P2kLqkOlpnpv zACEnjNpCuTlYs9ro@ehxL(wqHJtzbG)Fmuu$AK#QQlA|V%l}^VWxK`E_M-n-*9dq0 z8G)8>`i_lT-R?m+`TZ?O$l-EIZ6LTqeEHd!x8ypmpE7wGNy+?-ExG8VT$A;lZaAdd zMdjUd;+*eo+~zQXHit;ceh$+`S#i@qNxQ3pk87tVOL0(96wF9Jy!t=eAq=#wO4)Zsw%jdAM;47Vt4* zV8O{bc#%}@Qe%6JHtu}h)n>A5E|jhHy8|u*={FJwuZszoh<(UF=}FsYWB)kE5nfkJ zf=?8NpK7$4r0wN|yy6n`A0o7$NqNZr+M2h+MIbl*5rnB5(Cu&jhW=BjJ5_L^h5I{+ zlHcAsIv)2vhR-!qPKIQa(fjF)-{pRd$$MAVf|kOWTKJ3~n((!psQhCQ4!TNBJva#y zc=r_A&U~&4vqjUQ>(`tHw(iX)fm9qj-!r4NHgh+f2<1e4_e6d6N7;p^wWW!QyBX^k z*H-Tq-~Dp$Q{$^WiwR=BH2-O zIP!sP>SSBb!}10lg0hzTj$htAosGU@t#3y8)3AZo8#c^qRV*!7sZRVZM1YroXr`0s zSwoNp16}XErLdgCVK(RB;Hl|!$De6wZ;;4Jpet{f=4fF65 z{rO>1bN&JJaqg!o-`MU9wyC)av%bCLw(pr}Xc#TG5%4K0 z&D@NNhAEq8cwCp@C5h-CfwuL0TH2bRjue$j2AHi`+Tf+IGB;V=Jr9k9o`~<}HAELl zVz1(4l9D8dM2P%r?sIu*&hj@s*s~xRmj4}xtEMGzv(I*rO+a+vg+oj8orOHAD#{W%(V!0NPBHw@#<@B(!VNwX{5#a!$B zf|z~j+ZB!*dxh#pgue0>Cz1c<1dJY}&GR20m<-Z|s~S*5u#z9YVf@pSe}EBdl**7z z=yeSc`M!`@6vA$0xVtdu^qOnPc*j46RaY`RFR}9Ur}vH(DSx>>Wrr4NN=_jg!ZHvYc*}$0fzK*$K|+g znRv3$4%OS(NT42M?^uY$&Do4umwP-Bo31slq@nK>kp~K!K3lub{xajy_DBM7G0f!4 z&jBLf_o|ZD9!K@xRxv3PaqgZ>4zd1tFXdRmY)`^x0}MZf z81K@P``XIQHKPC`DutyNU8J5gg7ea@1Q#Zt*BPuqg+`C&T#f_yOo56H%LIEm!$LeA-#gZ}C=$hK;YgDg$)Rr17v%M)b&PPx|=ETMo!fzMKx3=h6ACutyK> zY`^@>zdpJ`<^IWO(h$;gY{huo!bZ&5n**5Fq+FqbO%XK9TC>#jY>mEv)>DeceMnZN z4D8Iog`9Js!ZIGlBmsqHZ#^3yiH9AF1Qv!4J<`dwca_ZEWo&GWi7oacxrIzWUbUslz{BI-hnF#a4STtUdT06$=O`M!TM_RQDdRLhZFF2~YFL;o zX-Ul85DjH=9;E;X`ot=J*X#@xX>0myeide1w9xcPx#$NennBP6g&;^yky`@rOEbs04%>|gto`VZn&m;ohk ztfv0et@UK7GUN@w2>KepFzp~v(flU{253liyA@ybI5skS7E7D~uDKB3%Fds{nOm{R zq<=65H1tXX+N=4pQ={f<5CJDU1kvQO1Qzn`7?Wwk=vs$}2aGpdjtOn)wguJQ6B3w$0OFdbgFrlS25N+mW6EuB zJReWiiJq(vHB{2MVJv;piPM3Y?8daAB?)n`cPrhr%7fwne!|+B}Ri1Q*_F*^+s4MwGtvPa_F6SNB_!vKfBF_fhIhk zZcr&^>n-GIvNq<4!y=4Zox4S8#sTw$^@eI#qVRa?33~TaCjQA(xz=--E*TW!wT>h` zG1Nrws@H7dftp*6r}U-?`;~mU!61H=DMos^2dub(%s-x@8ag2aT~VFDvESw2)_Zky z3>RnL;8#NL5*N$}qZ6DsK|Cl*(1jHA^k@zFmIHI%IQ6&}yqSn_?FN7HO^qIc384fV zTnHg^HpJDq9=V7xsUJG1&`Tj zZpTemA7wav(W~o25Z?8dS$$JHD#QR+rHFW4_D0&~E(9i2IVowMjU0q6}J;GY(T%E96bMaWOQ(D>o~X^y70*+k=kvp?w{AkU$OjTUt>e3bbjmQ!PLQu>_3A*%-55&-u=GG$NH66%Ze4!#*TW3~-^Q z_!;&|63gF*@p?SE)B&b8hBO?#NAak_?(IN^P(*ok$tX&-)?gvGphaFma_4sXoS&Ht zproX)m~L>T-&STO41jtd(4 zD6Ut@P55UymyRwiigfSxgk!k>M(cMHWTJa zz)2(^8Uek@rOWulOFU0$PCV#SECR&qol`oT)IUPeR9ixtQXU3$EY^lFJ*~t=hTpup zo|_|EvNPTo;~}>Bh7S`HOr2!Kac}G3@62!+9MNI$qvr$Y+kBMx|1@G>wQ~`98o{(9>66LAjj-_T4qmJaW5f2FUtl`{^ zZDR?%21(zH)gEs3Pd7YG><4u9MvAT5<$1pTI23ptnd z*I?vXwMo(Pb~OUclZq5kAjhgo<%XPS&S-C$aQoy~ui<^P8K1$FN5Nvr;)d51@GEMNlu|%kql|t+B z*_Ae$hTZSGiceaxKOiM8gs#Old6SWi?KIeLHZZ8(R!h9mdF-nTd2A<`OC8t9wEa{O zmD!YgZ_#jCk-Zf64?m3B4V{)5RR?)0pH)uy;hCrICdUWt?;$mbw~u4TGaxahQkmMP z+i>yq-eZ5AWYV_5g=lBSIl_yvhJc7MUbz@?o&FVB>JSoY>czSqR2Qo^fw1muQ9`Eq z9kGzGZB=GF!i(NM?^F4I#J_1s{d`79Y^uPl#FjmQ-(J%MTkg*5$#QaTPJUUsU8Xb; z+8ZS#-N<;2GaRL&);PiXMph-{ByQmIK<70^ljT-buQ0rlo^Y9r6IlZ zz`J)&3|&0JMYooV5;||q+ih;$o06w|6q@*aS#vof+J9&Ul^!7%g5)L+c79+7;=oKo zi5RMTgJ0MM8+^dVE5z23BcI_6yq;#s+!>L5gPJ;4X27ye^0z-=& zrrj$;@5dB|aG8N?&Gk8fM&kq%Tay1HeE-j%{H z|04=zPT53`H&Z4d78r@!nL$@9&{NMfB$^4cRSQE`%%EL@NI}8T2wn7@o~*L0lPQ^# zqjlss+1s|yUaAHA1Z$Otme80s~umeEPu2JyP|j$qG=&(K>%{H&_y7 zmB|8SM)ZQ&86NvnfC$6V1&BBJeF#AbacGx2* z2Q(wTfz;pR=etA;K4H6%hE{@DKv&O>r<_>%G0#J&P7LGGcdow1A`DikgWd))gI;F( z%fCi!jpV)}Mhg~v*F>{{68KNxJeVijpsY=Ls4Prq{A6lQ%K$6@M(+kxup;ov+00RL z$5Zz=k$?WY6bRqjeIP7^;3H-tHiw~ZBLEI7pCB#@<3wV5lS%rGQAL^IaiAYnPP5p4%n-F93vHPqwVEbc*?nQBfdrih1Fs+1-uoSkTpKms6 zI~@Vs-LhzHqiTX1I9I03IFyBz;}$oe3fKHMa;flo2V%M;_ovu$lz+rndF4~vZxO{ z4d|VGPPR?07HDrCMM*o5A2FZoz#<#Wl+h;(cs4RyKbV?L`{(>|BN424!bdOR!2wD| zKppjd=Q~}SajK+g`HQb?Lthwx*Dem`RRHe&GP|_g06it(=gG~p19Pb>n_P*= ze)~oM+QMkK2o6XM@LsE4B8f@86YCjL4qk&cCx7h~>ilLl%fT2{#%>EScpx3bk{kB| zNKKPtPJL!?yZ)Zslnj;`ThHX;_9HmkawTklbk?NtC(kA@V4F3?_lG9m6gaqYu(H|C zcbeLW%{-gvTZY%HgrxZEqD=vrCBEgIr)>~x=1~GK9 zy2h^sdZ|L$3-%p>yKhHvjvrD#MV>4(EcnUzt~{30v_fQjibqsyS(oa>9b;#SpLH@J zD0uF83QYi|iH0Fo>{+Ftt|2!xAoI%;!{B6utWA?iIPu{of7!|3NVTl7Ee6qhP7WnK zw>Lhf2HEj_vg-p^^JDzPP>F~UZA^5B1Ntte_YP>eo)VQ6&l3Q7f$3cdN4%>-4{~7q zw3At(lH<7&4jXisdxd{TaNY#K2B^-MY4K4RSYz379{LV**aymPu=@75O{CBY+aGK| zTyF(Wz6*W#pz0GhN(!yHVgpVs(njx6K**1}!M>cBC;#~g!m)@EwDB>R+EbX8C_wso2=CoSkd+L z0pfA;LpOP(fdheIQniUD?P3gxgzySp_8T!?BE-f6j3yQjRduJG^LS`Epg?<-jHI z`J~emz!Y+$)1vadL_UYrd_EkpT1n3OjSSReQin-yU#6UYWdbIQj^J1OxVSkwbIQT>yHiP=4INc4Ovn7o9t)fPAMZQ3jk8y zxn;hV!`=|GpWH;iD-eWT%>b?cDM0 zO(vs|U9g~^1xfwL^RJdL{g+0jwJqRUz6Yg)6Thxo3Li*2OL5q~@=ga%A?sIkzB{co zEmCQ57%9ikgguJY+Cn?Wnz@NR&LuLqs@rb;owsx`E?DML)4oyp+XSp`U)~`{t!*PR z_r~8!lq_U{!oOBsTOqkYYQ*!p@B%q3_lrzV;MiGmGkcyhDdA1tnCk?vT$v(X&7oj3$;!4?9!BWvtylH^SR=LzPfnk%qc%scxuTaGUNX5p z#cszYT=izyvC3a&AVY2BJ#sv&0M}Z*NHTEg0MAP&ui7s5!o zudA$s-)`T3bq9CS7ziOlDSzzOw=o&mA();yr4mXyEDQ)_ zlMT(|CQZVomF|<<9jBus0nN8(%QLYyMYb>qv)S&w;M~iR@E185roIc;+kiyPy9Dd^ zn3V4QdGrO=9IM%oA}i1Jsx^JlQCp?uB})%E?Aw?d0j~rnFTiYcGniD(%>-ujrv`BR zV|MIm?6nHn`_ti^q%FRDmD{IeM71{Nxf{l4sT(}`0CP_a z?Q(O4x6a>;0DPht-5rd57 zFimvb;5|H0g6RqErxe6A8>m$F697Wf4W8SUS?52IhYmet4#LjxB&}B3pq7z0&$c-* zO(^g$GV~y6-zx-01uZy%7R35l4`#G1w2~f^UPTAmo;xx0V*%B068@d`do9B!($F@Y z*k9;fUH@+IWlhvEEl^m9hZxHh_yhNxe?ggVx#KU8L|s2uUTE_7x0{Z^kCwyDrb32U zsNEj9hQJ8z>1L5;D6&w{^5Tvx`fj}O9|>TsHv$z3+I&DNNyhk{h+#S81Ux()vusO3gYmtzuN1 z!mpz- zKZ5yd8+5_}zMqk?X3ZVxn-aYn3+8=mnFL-`$|5p)-kd(mil?`c{vFw2NBb*aX9=S} zE3%Zz)wT&mxg&4izx**or6eNrR@oA$n~6!N#V8D39&h`{^QI_^Bo?_!ywmzjL5b2^ z7k$oYbeI44vmbKh7z1G8>dmfNZf6$fYz7QVv6WA++8Tn=!;T85-Nbr!%!&U8S`kFS zA`peT>yM@pTqxgAP$z$EIH-7R4#DQyK5`pub{=7T~P@mY-Z;fkFMaw zocFGPDT#Dz&=o=I z`nQ+XYuwm?Y)_?NW?eb&VXA*v2|)dY?vz2#Z(3mTWWG$Nx7Ah`v|sj*pK|^!KCq0$ zX+V-$T0eI}7RGEx|QwwtCHH+tb;!&DW&)9GdC)}=z(nWWZ1 zAgz;;V1Ax19a5r3>o(nsnhmdMB zv%F#V5AO(;3N}nlwu4;%2d&q7Dpa3Cti+FUNL9jgARVQ`fv*>A;?w%&1c+s-3IL_R zq~bTBK)-?Dlms%lG8nfqq(d{go2(&2U~uRw$2$jngW^FPU?T1#Hv^APz9wdJDRgxg z0WJ%&&iJm3nqbp>?5wP+H ztDt|z8uXq8YlF|6SfdgDGd>}&bcZ`A_GimE6=jsWseqP;uGb0EK~9<$9h0(MdCcP_ znp;BZdwb7=fu8d`-Gpk!H{>C%B-Eh65`Wj3n^vhQ$PXUW@^ny@b|~-oYTRI|x%JKy zp&cDBb=eflQRBW#rgK;cQxcFp!_j$api$ysspLCO&^9>az0{O}&EVOk3*?7Y06aHZ z*CqoPg8N%Os<7MXk&xr(Nk-L%*B2A6te1wH9*_a#8Rlg!0PfH_k(yk%ne&UZ_^BI0 z3B3Jbzg|lP8Q#NGSR0oqX1QbEp22)n&94OfM!Wd*rH$#zjy%jjmqU8j%+^TaK=Q`} z0rDosp><+=#=d|b(;35-6(@UV$-f8XVF*6;2S2LazD*Z4cMCj?<9}otEz{}PEa|*J zF!$p5w}Y>hE1F5pPoHfE@r|z{2k)}DIPqlN*7xrKgudG=N^fsU z3U(lvy%xqN5kqI{Q;&sin*paocqFyKOCxVvj7cd=``bq*F7J=Co1+|q^OcSySyr*5 z`?3GSxfn|@Q2zBu^}?&mISI#8m)qYcBC(jR&|iClJp_8LBhlyQM<<31t-?^(K*&-L zcs~gX=w@QE9Ofkj=-28&$X6HX22P2XuW=u))HKk$I)4r9${3R_h8`4}ev4HW4Vc5C zzvR%r|2lK9GJ03v6iNq5kitxBCLwZJp}AC|+~uKiQg@*tYRP$M&HQy_^6?ZX6}HI% zB|Tc>S4Ho>9NS}rre&cuYtfoCe=|>zFiHaB#|vCdM1*yNZ+m4|UL!)fv4n}D7UipH zo_(|)kWH#c5atH+CbNGz^7*nBa9;-=sIL6=!%8mwNjvp^%J9jHjL@wk$I8_hGpv)m zc`C4yt(SL@t+yLo{|47LZwj`w!O_08< zT@x^+lYce`GN=NP)?+CWiC7_tQt1te%aSQ&92~MJ%_mAm3Z>V<)GFIoDjw4?Yf7;~ zC*|x4ujf(UREs}OtZY#CN^a8jDho?KBn!oRu*KFC1ISu6PX|IMO&iG zP@MED0WoGwY_79nH=g7fMl~+iHGlJIRz9AQjmSGb1r>F4H?O&7S2p(ut~jAb#kBnmdw;fHQQqm)LfqTlGtA7>f75`vR+q)_~wzJBW01IS2DY|A*)K;SwlYXJ09A+SzmPJ8vGPW zN4VolvCg;R)2DDuiNDp?GW_Se*3|n`bJf@gl7~3smJG-S+eBg^6lBCPJmg+lbTMFG z4!sM>#=;)8SVrJaCm>EaZ9|+>vsE9P@+li-vrq7qsk{bAjk@e>Fk0_DN8U!FIXI~53JmAL*G(J7W?cc-0L3P6<*S#8RlcLv`^FeTEKgDx%^@lpUsG_^=;SPE|{@a{%wnd&C;)e!s78# z$;beAY2V0?!JHuN#6OxCXNaN=MZ!KY64qTQiH9 zC*R1!_?AFV@(alV8exB>yD{QdVLk*5En!mlRZphEZ+i%zvBHiuXYDpBKd=+j;!>4S ziIrKd#3<4j2aPyv=M}M^THW~aTg%zBHldz`u&VrONqP&MbL-5AMc$gSD&S=gcAF|7uY{=1V9J#;KW9mFzZWD3K zv2^AZPFE2xi2|HFl#yLd{D=&Ei*I69?Z8iLMb1v}h-PONch&9B^hxp7V+>CaQbIc5 zqutvAI&xLmNs)04JZd=J`JN3EI{c_0!+Rh}y3u=6n1XF7=ws z*Uf;~R&SGGBs9M<((|3i2?K}zoEeiJfrlF}vvp4^eejO~xT2KWV6mdz?wIW7qd-xT zt%7{IrRv=^r!K0jnf)4Vn+NiSg;NY=NXf>GwJv3t<+DWgX~ag(C|(5ZfCOysSRdlI zaQVEDxM!L3xC5|KT9~IqT#eE)4o~r54^osLD%ik~WE-R=X+nlPm0>+C2SZEQP=9tV|HC~s0dgUD$`f=ZLxABUI z8=G0(no|yaRG(FXSc%@^fRgO76LA6@jHN6J*a?=;J+Nr&^>u|WhDS5`#?iX*t9GY& z=Jg8L!x>|QWoc+YgpZC5t4<7oqG-X7uvkPT6Nr}#`fBXh@X_poEY|VGAkg?2=@Vjf zf){%5R5wd*G!{YH4URi8#I6j#XHW2;7DN7+k_dPp0CZA^9?TDp!|n$FfC#Z02O0uG zlM%X*wb2DEeB@0CqKig!t?`qeb;Thnr7%shd5_kSSPHcL7~W-mtb=(Dqk)FtfhrLF zV8L~Mp-*bb$YaB>1VowH88#i53dlXCUFV0G7(t&<+Uq@Me8@<*WaSf*91%ECSlj_zqptR5E~tEVT)8O`ltA}_WuhX zQ2u4FvAZ;29Di9fp1&#AUqt=CZ(;8MvQ)(MHBKd&HCHJ07P4G|wksD~Shk6W^&6jl zvbsGS%23Htt$cHF)50tKoncSJ+e35{zn-Sd>vEiT@Hfs-J4{TRW+#FDP!ytpUR7Nj z-k$EEX^=X549u(|Xh-i@17Uu@a=zZL!!)RWINg}Z(;gu2u|4|pv18c13uZwzkb0@v zJRp(b6Hy~U=cB+*quR)%txyJMK5NZ!`>d*!IY@n=SeLL!T2rj^)ka5xOpdooW)%vn zq8fa6)KHqdN2<>sgc8%azZ7LYQ*!MG%;>pr)o8ur#WM@%14DminZfRao_QTIPqctz zkz3yGQ88@?#BlTcmdZ!jqw=lhM5mq*v$Emq2o|x|#^WEO)>+u3Job6ihsYO+fLu;_ z)ox4-;B*-#)W`XAp zRB2E9`Qq-k;>2jtIWu)(=_R%i7b@Ww0r+;-^2M^e>Y>Wn;Gs)Y0N2deU#JSyXp2C<~*)#k2r{4Ik+BMi- zddw4aMb0M^QrHl8^~I7ZSiR4Yd06LpBXOaiN4jWpk$RMW%_~aWBhXk%NyM$|&EdEd z7iSZGPeBTRvgL|EsuV@6W##PeYAEGM-4aiiCns7J3ggP8{cIhr;jSW4ARCB~{N_Fl zSrZ(#L;6G{rNSbJ#?KlGY3M&oEo6N&L8cmmhV`|ap3id;%k*%`W;rN7dUX0xmprpP z@8b!%lk5JEY-=ZD3Zx%7zJpCwX2@HIc0f?5&-|hs?+eo)wJ7A)@g$W_&7nX@ke^48 zT=@b)45WyCtIyo7gK^r(uu8PgQHAU!eo)(tET7YQYYWVCE#d_|kSnNn%9!$_SzO%s zh86K5VEaYrbfx~1{7CLTD1gf0;iYbC> zG*Y+9&dGh3f8mu(*0+KZz zc8J|>6DDI z+lV*2f&r1pvWOJALq8X!mod|KW_EzKuNh_V%g7GJ6W+{9Mo;5bYe$g#r18-AFOg(} zC!4BW5ZyGuZH|D^pN{||wyay+jE(Rf%sq8Y9PCR|Rrg7)vTfwQ4#%0{G?uIQ{O716 zK=nW8AXijb;2x^?;A6Ptg1C71aQ@G6OS8Xf{`bZHzZd`1iSP(qS~wBj-Tw~>z`rC0 zu@amV|I!b~_Kb@UpHRC8x5gzC`q#aG_09M%O>($!6*azpPHSQn;Q8RDF0uXp7OudB zH>>h-{M{G*-%9`8!zym7Hvs-xmGqxZ;OgrC)XVCRDE`X*pOc4hP7OiW-E=OO>+sG`|_pB^FomtZ$mkFQ^_yQA;_o814s+xspK u4*9