mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
Chip32 asm: supports hex and binary numbers, audio can be cancelled, add support to event masks
This commit is contained in:
parent
fcc3562ecd
commit
26c917fe27
14 changed files with 182 additions and 37 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,17 @@ std::vector<std::string> 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<uint32_t>(strtol(str.c_str(), &end, 16));
|
||||
} else if (str.compare(0, 2, "0b") == 0 || str.compare(0, 2, "0B") == 0) {
|
||||
return static_cast<uint32_t>(strtol(str.c_str() + 2, &end, 2));
|
||||
} else {
|
||||
return static_cast<uint32_t>(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<uint32_t>(strtol(instr.args[1].c_str(), NULL, 0)));
|
||||
leu32_put(instr.compiledArgs, convertStringToLong(instr.args[1]));
|
||||
}
|
||||
break;
|
||||
case OP_POP:
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<std::shared_ptr<Resource>> m_items;
|
||||
std::pair<FilterIterator, FilterIterator> m_images;
|
||||
std::pair<FilterIterator, FilterIterator> m_sounds;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Chip32::Instr>::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<StoryProject> 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<std::string> m_recentProjects;
|
||||
|
||||
ResourceManager m_resources;
|
||||
|
|
@ -120,7 +137,6 @@ private:
|
|||
|
||||
ThreadSafeQueue<VmEvent> 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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue