From 26c917fe27a967ae8c6368e7f9013cdf19ae96a3 Mon Sep 17 00:00:00 2001 From: "anthony@rabine.fr" Date: Tue, 28 May 2024 11:02:19 +0200 Subject: [PATCH] Chip32 asm: supports hex and binary numbers, audio can be cancelled, add support to event masks --- docs/dev-firmware-micro-vm.md | 52 +++++++++++++++++++++ firmware/chip32/chip32_assembler.cpp | 13 +++++- firmware/chip32/chip32_vm.h | 4 +- shared/audio_player.cpp | 4 ++ shared/resource_manager.cpp | 27 +++++++++-- shared/resource_manager.h | 6 ++- shared/story_project.cpp | 2 +- story-editor/scripts/media.chip32 | 18 ++++--- story-editor/src/emulator_window.cpp | 7 ++- story-editor/src/i_story_manager.h | 1 + story-editor/src/importers/pack_archive.cpp | 2 +- story-editor/src/main_window.cpp | 50 ++++++++++++++++---- story-editor/src/main_window.h | 25 ++++++++-- story-editor/src/node_engine/media_node.cpp | 8 ++-- 14 files changed, 182 insertions(+), 37 deletions(-) diff --git a/docs/dev-firmware-micro-vm.md b/docs/dev-firmware-micro-vm.md index f721f72..6dad7f9 100644 --- a/docs/dev-firmware-micro-vm.md +++ b/docs/dev-firmware-micro-vm.md @@ -14,6 +14,9 @@ Here is a description of that VM. | sp | 21 | stack pointer | Y | | ra | 22 | return address | N | +Registers that are preserved means that the VM will store them on the stack before the `call` operation. Then, after the return (`ret`), theses registers are restored. + +Registers that are not preserved must be saved manually (by writing code) by the user before using them. # Instructions @@ -45,6 +48,55 @@ Here is a description of that VM. | jumpr | 23 | 1 | jump to address contained in a register. | `jumpr t9` | | skipz | 24 | 1 | skip next instruction if zero. | `skipz r0` | | skipnz | 25 | 1 | skip next instruction if not zero. | `skipnz r2` | +| eq | 26 | 2 | compare two registers for equality, result in first | `eq r4, r0, r1 ; equivalent to C code: r4 = (r0 == r1) ? 1 : 0` | +| gt | 27 | 2 | compare if first register is greater than the second, result in first | `gt r4, r0, r1` | +| lt | 28 | 2 | compare if first register is less than the second, result in first | `lt r4, r0, r1` | + + +# System calls + +The story player machine supports the following system calls: + +## SYSCALL 1 (Play media) + +This system call is used to play media. Use R0 and R1 to pass arguments: + +- R0 is for the image: + - if zero, clear the screen + - Otherwise pass the address in ROM or RAM where is stored the image filename + +- R1 is for the sound: + - if zero, do not play anything + - Otherwise pass the address in ROM or RAM where is stored the sound filename + +The file must be stored in the `/assets` subdirectory of the current story. + +The syscall do not blocks. User must wait for `end of sound` event if necessary. + +## SYSCALL 2 (wait event) + +This system call is used to wait for machine events. Use R0 to mask events to wait for (events not selected are ignored). + +| Event | Bit | +|------- |-------- | +| OK button | 0 | +| Previous button | 1 | +| Next button | 2 | +| Up button | 3 | +| Down button | 4 | +| Home button | 5 | +| Select button | 6 | +| Start button | 7 | +| Stop button | 8 | +| Pause button | 9 | +| End of audio | 10 | + + +Example: + +``` + mov r0, +``` # Assembler diff --git a/firmware/chip32/chip32_assembler.cpp b/firmware/chip32/chip32_assembler.cpp index 7e97963..d710148 100644 --- a/firmware/chip32/chip32_assembler.cpp +++ b/firmware/chip32/chip32_assembler.cpp @@ -135,6 +135,17 @@ std::vector Split(std::string line) return result; } +uint32_t convertStringToLong(const std::string& str) { + char* end; + if (str.compare(0, 2, "0x") == 0 || str.compare(0, 2, "0X") == 0) { + return static_cast(strtol(str.c_str(), &end, 16)); + } else if (str.compare(0, 2, "0b") == 0 || str.compare(0, 2, "0B") == 0) { + return static_cast(strtol(str.c_str() + 2, &end, 2)); + } else { + return static_cast(strtol(str.c_str(), &end, 10)); + } +} + // ============================================================================= // ASSEMBLER CLASS // ============================================================================= @@ -187,7 +198,7 @@ bool Assembler::CompileMnemonicArguments(Instr &instr) instr.useLabel = true; leu32_put(instr.compiledArgs, 0); // reserve 4 bytes } else { // immediate value - leu32_put(instr.compiledArgs, static_cast(strtol(instr.args[1].c_str(), NULL, 0))); + leu32_put(instr.compiledArgs, convertStringToLong(instr.args[1])); } break; case OP_POP: diff --git a/firmware/chip32/chip32_vm.h b/firmware/chip32/chip32_vm.h index 70370a2..110290f 100644 --- a/firmware/chip32/chip32_vm.h +++ b/firmware/chip32/chip32_vm.h @@ -93,8 +93,8 @@ typedef enum | name | number | type | preserved | |-------|--------|----------------------------------|-----------| -| r0-r9 | 0-9 | general-purpose | Y | -| t0-t9 | 10-19 | temporary registers | N | +| r0-r9 | 0-9 | general-purpose | N | +| t0-t9 | 10-19 | temporary registers | Y | | pc | 20 | program counter | Y | | sp | 21 | stack pointer | Y | | ra | 22 | return address | N | diff --git a/shared/audio_player.cpp b/shared/audio_player.cpp index 1b726db..724abf3 100644 --- a/shared/audio_player.cpp +++ b/shared/audio_player.cpp @@ -60,6 +60,10 @@ void AudioPlayer::Initialize() void AudioPlayer::Play(const std::string &filename) { + // On coupe le son en cours + g_audioQueue.clear(); + Mix_HaltMusic(); + Mix_FreeMusic(music); g_audioQueue.push({"play", filename}); } diff --git a/shared/resource_manager.cpp b/shared/resource_manager.cpp index 0842dc9..809c04a 100644 --- a/shared/resource_manager.cpp +++ b/shared/resource_manager.cpp @@ -34,7 +34,7 @@ std::string ResourceManager::ExtentionInfo(std::string extension, int info_type) } } -void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, const std::filesystem::path &destAssetsPath, Resource::ImageFormat imageFormat, Resource::SoundFormat soundFormat) +void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, const std::filesystem::path &destAssetsPath, Resource::ImageFormat targetImageFormat, Resource::SoundFormat targetSoundFormat) { auto [b, e] = Items(); for (auto it = b; it != e; ++it) @@ -47,7 +47,7 @@ void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, { if ((*it)->format == "PNG") { - if (imageFormat == Resource::IMG_FORMAT_QOIF) + if (targetImageFormat == Resource::IMG_FORMAT_QOIF) { outputfile += ".qoi"; // FIXME: prendre la config en cours désirée retCode = MediaConverter::ImageToQoi(inputfile.generic_string(), outputfile.generic_string()); @@ -59,7 +59,7 @@ void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, } else if ((*it)->format == "MP3") { - if (soundFormat == Resource::SND_FORMAT_WAV) + if (targetSoundFormat == Resource::SND_FORMAT_WAV) { outputfile += ".wav"; // FIXME: prendre la config en cours désirée retCode = MediaConverter::Mp3ToWav(inputfile.generic_string(), outputfile.generic_string()); @@ -71,7 +71,7 @@ void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, } else if ((*it)->format == "OGG") { - if (soundFormat == Resource::SND_FORMAT_WAV) + if (targetSoundFormat == Resource::SND_FORMAT_WAV) { outputfile += ".wav"; // FIXME: prendre la config en cours désirée retCode = MediaConverter::OggToWav(inputfile.generic_string(), outputfile.generic_string()); @@ -81,9 +81,26 @@ void ResourceManager::ConvertResources(const std::filesystem::path &assetsPath, outputfile += ".ogg"; } } + else if ((*it)->format == "WAV") + { + if (targetSoundFormat == Resource::SND_SAME_FORMAT) + { + outputfile += ".wav"; + } + else if (targetSoundFormat == Resource::SND_FORMAT_WAV) + { + m_log.Log("Skipped: " + inputfile.generic_string() + ", already in WAV format" + outputfile.generic_string(), true); + outputfile += ".wav"; + } + else + { + m_log.Log("Skipped: " + inputfile.generic_string() + ", conversion not supported! " + outputfile.generic_string(), true); + outputfile += ".wav"; + } + } else { - // Log("Skipped: " + inputfile + ", unknown format" + outputfile, true); + m_log.Log("Skipped: " + inputfile.generic_string() + ", unknown format" + outputfile.generic_string(), true); } } diff --git a/shared/resource_manager.h b/shared/resource_manager.h index 413ee79..efd0652 100644 --- a/shared/resource_manager.h +++ b/shared/resource_manager.h @@ -21,8 +21,9 @@ struct Media { class ResourceManager { public: - ResourceManager() - : m_images(filter("image")) + ResourceManager(ILogger &log) + : m_log(log) + , m_images(filter("image")) , m_sounds(filter("sound")) { } @@ -76,6 +77,7 @@ public: private: + ILogger &m_log; std::list> m_items; std::pair m_images; std::pair m_sounds; diff --git a/shared/story_project.cpp b/shared/story_project.cpp index 57a5bd9..3a00e4f 100644 --- a/shared/story_project.cpp +++ b/shared/story_project.cpp @@ -33,7 +33,7 @@ void StoryProject::SetPaths(const std::string &uuid, const std::string &library_ void StoryProject::CopyToDevice(const std::string &outputDir) { - ResourceManager manager; + ResourceManager manager(m_log); Load(manager); diff --git a/story-editor/scripts/media.chip32 b/story-editor/scripts/media.chip32 index 5575517..38231aa 100644 --- a/story-editor/scripts/media.chip32 +++ b/story-editor/scripts/media.chip32 @@ -10,7 +10,8 @@ ; t4: address of the last element in the choice array ; t5: where to jump when OK button is pressed - mov t3, r0 ; on sauvegarde R0 (rx non préservés) + mov t3, r0 ; copie de R0 pour travailler dessus + mov t2, r0 ; sauvegarde de R0 load r0, @t3, 4 ; Le premier élément est le nombre de choix possibles, ex: r0 = 12 lcons t1, 4 mul r0, t1 ; on calcule l'offset: r0 = nb_elements * 4 = 48 @@ -27,19 +28,16 @@ ; Return argument in R0: the address of the node to call whe OK is pressed mov t5, r0 ; save it - ; wait for event (OK or wheel) + ; wait for event + lcons r0, 0b111 ; mask for OK, previous and next buttons syscall 2 ; Event is stored in R0 - ; 1 = ok - ; 2 = previous - ; 4 = next - ; 8 = audio finished ; ----- Test if event is OK button lcons r1, 1 ; mask for OK button and r1, r0 ; r1 = r1 AND r0 skipz r1 ; not OK, skip jump - jumpr t5 ; we do not plan to return here, so a jump is enough + jump .media_wait_event ; test previous event lcons r1, 2 ; mask for previous button @@ -68,3 +66,9 @@ .media_set_last: ; on reboucle sur le dernier élément de la structure mov t0, t4 jump .media_loop + +.media_wait_event: + call t5 ; jump to the node + lcons r0, 0b10000100001 ; mask for end of audio, Ok, and home buttons + syscall 2 ; wait for event (OK, home or end of audio), return to choice loop + jump .media_loop \ No newline at end of file diff --git a/story-editor/src/emulator_window.cpp b/story-editor/src/emulator_window.cpp index 0d065af..f5a343e 100644 --- a/story-editor/src/emulator_window.cpp +++ b/story-editor/src/emulator_window.cpp @@ -56,11 +56,16 @@ void EmulatorWindow::Draw() m_story.Ok(); } ImGui::SameLine(); - if (ImGui::Button(ICON_MDI_STOP_CIRCLE_OUTLINE, ImVec2(50, 50))) + if (ImGui::Button(ICON_MDI_PAUSE, ImVec2(50, 50))) { m_story.Pause(); } ImGui::SameLine(); + if (ImGui::Button(ICON_MDI_HOME, ImVec2(50, 50))) + { + m_story.Home(); + } + ImGui::SameLine(); if (ImGui::Button(ICON_MDI_ARROW_LEFT_BOLD_CIRCLE_OUTLINE, ImVec2(50, 50))) { m_story.Previous(); diff --git a/story-editor/src/i_story_manager.h b/story-editor/src/i_story_manager.h index 6b23d17..22a517a 100644 --- a/story-editor/src/i_story_manager.h +++ b/story-editor/src/i_story_manager.h @@ -62,6 +62,7 @@ public: virtual void Ok() = 0; virtual void Stop() = 0; virtual void Pause() = 0; + virtual void Home() = 0; virtual void Next() = 0; virtual void Previous() = 0; virtual std::string VmState() const = 0; diff --git a/story-editor/src/importers/pack_archive.cpp b/story-editor/src/importers/pack_archive.cpp index b90678f..7f1b9a0 100644 --- a/story-editor/src/importers/pack_archive.cpp +++ b/story-editor/src/importers/pack_archive.cpp @@ -382,7 +382,7 @@ bool PackArchive::ConvertJsonStudioToOst(const std::string &basePath, const std: std::ifstream f(basePath + "/story.json"); nlohmann::json j = nlohmann::json::parse(f); StoryProject proj(m_log); - ResourceManager res; + ResourceManager res(m_log); if (j.contains("title")) { diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index d34b7af..a26980b 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -21,7 +21,8 @@ #include "ImGuiFileDialog.h" MainWindow::MainWindow() - : m_libraryManager(*this) + : m_resources(*this) + , m_libraryManager(*this) , m_emulatorWindow(*this) , m_codeEditorWindow(*this) , m_cpuWindow(*this) @@ -104,6 +105,11 @@ void MainWindow::Pause() } +void MainWindow::Home() +{ + m_eventQueue.push({VmEventType::EvHomeButton}); +} + void MainWindow::Next() { Log("Next button"); @@ -182,23 +188,43 @@ void MainWindow::ProcessStory() } else if (event.type == VmEventType::EvOkButton) { - m_chip32_ctx.registers[R0] = 0x01; - m_dbg.run_result = VM_OK; + if (m_dbg.IsValidEvent(EV_MASK_OK_BUTTON)) + { + m_chip32_ctx.registers[R0] = EV_MASK_OK_BUTTON; + m_dbg.run_result = VM_OK; + } } else if (event.type == VmEventType::EvPreviousButton) { - m_chip32_ctx.registers[R0] = 0x02; - m_dbg.run_result = VM_OK; + if (m_dbg.IsValidEvent(EV_MASK_PREVIOUS_BUTTON)) + { + m_chip32_ctx.registers[R0] = EV_MASK_PREVIOUS_BUTTON; + m_dbg.run_result = VM_OK; + } } else if (event.type == VmEventType::EvNextButton) { - m_chip32_ctx.registers[R0] = 0x04; - m_dbg.run_result = VM_OK; + if (m_dbg.IsValidEvent(EV_MASK_NEXT_BUTTON)) + { + m_chip32_ctx.registers[R0] = EV_MASK_NEXT_BUTTON; + m_dbg.run_result = VM_OK; + } } else if (event.type == VmEventType::EvAudioFinished) { - m_chip32_ctx.registers[R0] = 0x08; - m_dbg.run_result = VM_OK; + if (m_dbg.IsValidEvent(EV_MASK_END_OF_AUDIO)) + { + m_chip32_ctx.registers[R0] = EV_MASK_END_OF_AUDIO; + m_dbg.run_result = VM_OK; + } + } + else if (event.type == VmEventType::EvHomeButton) + { + if (m_dbg.IsValidEvent(EV_MASK_HOME_BUTTON)) + { + m_chip32_ctx.registers[R0] = EV_MASK_HOME_BUTTON; + m_dbg.run_result = VM_OK; + } } else if (event.type == VmEventType::EvStop) { @@ -261,7 +287,7 @@ uint8_t MainWindow::Syscall(chip32_ctx_t *ctx, uint8_t code) Log("Sound: " + soundFile); m_player.Play(soundFile); } - retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause + retCode = SYSCALL_RET_OK; // We continue execution, script must wait for event if necessary (end of audio) } // WAIT EVENT bits: // 0: block @@ -272,7 +298,11 @@ uint8_t MainWindow::Syscall(chip32_ctx_t *ctx, uint8_t code) // 5: rotary right else if (code == 2) // Wait for event { + // Empty event queue + m_eventQueue.clear(); + // Event mask is located in R0 + m_dbg.event_mask = m_chip32_ctx.registers[R0]; // optional timeout is located in R1 // if timeout is set to zero, wait for infinite and beyond retCode = SYSCALL_RET_WAIT_EV; // set the VM in pause diff --git a/story-editor/src/main_window.h b/story-editor/src/main_window.h index 1a03f62..a5fbacf 100644 --- a/story-editor/src/main_window.h +++ b/story-editor/src/main_window.h @@ -23,6 +23,21 @@ #include "library_window.h" #include "cpu_window.h" + + +#define EV_MASK_OK_BUTTON 0b1 +#define EV_MASK_PREVIOUS_BUTTON 0b10 +#define EV_MASK_NEXT_BUTTON 0b100 +#define EV_MASK_UP_BUTTON 0b1000 +#define EV_MASK_DOWN_BUTTON 0b10000 +#define EV_MASK_HOME_BUTTON 0b100000 +#define EV_MASK_SELECT_BUTTON 0b1000000 +#define EV_MASK_START_BUTTON 0b10000000 +#define EV_MASK_STOP_BUTTON 0b100000000 +#define EV_MASK_PAUSE_BUTTON 0b1000000000 +#define EV_MASK_END_OF_AUDIO 0b10000000000 +#define EV_MASK_ALL 0xFFFFFFFFUL + struct DebugContext { uint32_t event_mask{0}; @@ -37,6 +52,10 @@ struct DebugContext run_result = VM_FINISHED; } + bool IsValidEvent(uint32_t event) { + return (event_mask & event) != 0; + } + static void DumpCodeAssembler(Chip32::Assembler & assembler) { for (std::vector::const_iterator iter = assembler.Begin(); @@ -74,7 +93,7 @@ public: void Loop(); private: - enum VmEventType { EvNoEvent, EvStep, EvRun, EvOkButton, EvPreviousButton, EvNextButton, EvAudioFinished, EvStop}; + enum VmEventType { EvNoEvent, EvStep, EvRun, EvOkButton, EvPreviousButton, EvNextButton, EvAudioFinished, EvStop, EvHomeButton}; std::shared_ptr m_story; @@ -82,13 +101,11 @@ private: uint8_t m_rom_data[16*1024]; uint8_t m_ram_data[16*1024]; chip32_ctx_t m_chip32_ctx; - Chip32::Result m_result; DebugContext m_dbg; std::string m_currentCode; - std::vector m_recentProjects; ResourceManager m_resources; @@ -120,7 +137,6 @@ private: ThreadSafeQueue m_eventQueue; - // From IStoryManager (proxy to StoryProject class) virtual void OpenProject(const std::string &uuid) override; virtual void ImportProject(const std::string &fileName, int format); @@ -150,6 +166,7 @@ private: virtual void Ok() override; virtual void Stop() override; virtual void Pause() override; + virtual void Home() override; virtual void Next() override; virtual void Previous() override; virtual std::string VmState() const override; diff --git a/story-editor/src/node_engine/media_node.cpp b/story-editor/src/node_engine/media_node.cpp index 4de0b0b..81d729f 100644 --- a/story-editor/src/node_engine/media_node.cpp +++ b/story-editor/src/node_engine/media_node.cpp @@ -138,13 +138,13 @@ std::string MediaNode::Build(IStoryProject &story, int nb_out_conns) ss << "syscall 1\n"; // Check output connections number - // == 0: end node : generate halt + // == 0: end node : wait for home button or return du choice node at the end of the audio // == 1: transition node : image + sound on demand, jump directly to the other node when OK // > 1 : choice node : call the node choice manager if (nb_out_conns == 0) // End node { - ss << "halt\n"; + ss << "ret\n"; } else if (nb_out_conns == 1) // it is a transition node { @@ -165,7 +165,9 @@ std::string MediaNode::Build(IStoryProject &story, int nb_out_conns) } else // Choice node { - ss << "lcons r0, $" << ChoiceLabel(GetId()) << "\n" + ss << "lcons r0, 0b10000000000\n" // ; mask for end of audio, Ok, and home buttons + << "syscall 2\n" // ; wait for event (OK, home or end of audio), return to choice loop" + << "lcons r0, $" << ChoiceLabel(GetId()) << "\n" << "jump .media ; no return possible, so a jump is enough"; } return ss.str();