めのん@ひとりプログラミング同好会

このブログはすべてフィクションであり、実在の人物、団体とは一切関係ありません

Zig版ASP3カーネルのtmevtb_enqueue_reltim

こんにちは、めのんです!

前回から1週間あまり間があいてしまいました。 みなさん、いかがお過ごしでしたでしょうか?

今回見ていくのはdly_tskからよばれていたtmevtb_enqueue_reltim関数です。 サービスコールではなくて、カーネル内部で呼び出している関数になります。

いつものようにtmevtb_enqueue_reltim関数の定義を見ていきますね。 kernel/time_event.zigに定義があります。

///
///  相対時間指定によるタイムイベントの登録
///
///  timeで指定した相対時間が経過した後にコールバック関数が呼び出され
///  るように,p_tmevtbで指定したタイムイベントブロックを登録する.コー
///  ルバック関数,コールバック関数へ渡す引数は,p_tmevtbが指すタイム
///  イベントブロック中に設定しておく.
///
pub fn tmevtb_enqueue_reltim(p_tmevtb: *TMEVTB, time: RELTIM) void {
    // 現在のイベント時刻とタイムイベントの発生時刻を求める[ASPD1026].
    update_current_evttim();
    p_tmevtb.evttim = calc_current_evttim_ub() +% time;

    // タイムイベントブロックをヒープに挿入する[ASPD1030].
    tmevtb_insert(p_tmevtb);

    // 高分解能タイマ割込みの発生タイミングを設定する[ASPD1031][ASPD1034].
    if (!in_signal_time and p_tmevtb.index == 0) {
        set_hrt_event();
    }
}

相対時間というのはdly_tskの引数にも使われていたRELTIM型の時間のことで、単位はmsです。

tmevtb_enqueue_reltim関数をざっと見ていくと、久々にZigらしいところがありました! これだけでも今回は価値がありますね。

何かというと、+%演算子です。

+%演算子を最初に見た私の印象は「剰余算に関係あるのかな?」というものでした。 確かに剰余算にも関係あるのですが、どちらかというと+の方が主で、これは加算のための演算子なんです。

普通の加算と違うのは、評価結果の内部がその型の最大値+1を法とする剰余になることが保証されている点です。 いいかえると、オーバーフロー発生時に2の補数表現のラップアラウンド動作を保証されています。

Cの場合も、普通はそれと同じ動作をするんですが、あくまでも規格上はオーバーフローが発生すると未定義の動作になるんですよね。 ちゃんと言語仕様で振る舞いが保証されていると安心感が違います。

tmevtb_enqueue_reltim関数のその他の部分は別の関数を呼び出しているところぐらいでしょうか。

まずは、update_current_evttim関数を見ていきましょう。 定義は同じくkernel/time_event.zigにあります。

///
///  現在のイベント時刻の更新
///
///  current_evttimとcurrent_hrtcntを,現在の値に更新する.
///
pub fn update_current_evttim() void {
    var new_hrtcnt: HRTCNT = undefined;
    var hrtcnt_advance: HRTCNT = undefined;
    var previous_evttim: EVTTIM = undefined;

    new_hrtcnt = target_timer.hrt.get_current();    //[ASPD1013]
    hrtcnt_advance = new_hrtcnt -% current_hrtcnt;  //[ASPD1014]
    if (TCYC_HRTCNT != null) {
        if (new_hrtcnt < current_hrtcnt) {
            hrtcnt_advance +%= TCYC_HRTCNT.?;
        }
    }
    current_hrtcnt = new_hrtcnt;                    //[ASPD1016]

    previous_evttim = current_evttim;
    current_evttim +%= @intCast(EVTTIM, hrtcnt_advance);    //[ASPD1015]
    boundary_evttim = current_evttim -% BOUNDARY_MARGIN;    //[ASPD1011]

    if (monotonic_evttim -% previous_evttim
                              < @intCast(EVTTIM, hrtcnt_advance)) {
        if (current_evttim < monotonic_evttim) {    //[ASPD1045]
            systim_offset +%= @as(SYSTIM, 1) << @bitSizeOf(EVTTIM);
        }
        monotonic_evttim = current_evttim;          //[ASPD1042]
    }
}

この関数の中でも+%演算子がたくさん使われています。 似た演算子で-%も使われていますが、これは減算を行ってオーバーフローが起きたときに%+と同じように処理するものですね。

@intCastは指定した整数型に変換するためのビルトイン関数です。 このビルトイン関数は以前にも登場したのですが、そのときはスルーしていました。

@intCastは元の値を維持して型だけを変換します。 といっても変換後の型でもとの値を表現できない場合もあり得ます。 その場合には未定義の動作になるのですが、Zigの未定義の動作はsafety-protected Undefined Behavior(=は安全性保護付き未定義動作)です。 「安全性保護付き未定義動作」という日本語はZenから借用しました。

「安全性保護付き未定義動作」が起きたときは、@setRuntimeSafetyビルトイン関数を使って安全性チェックを有効にすることができます。 安全性チェックが失敗するとクラッシュするようです。

@asビルトイン関数は型変換というより型強制です。 値が安全で確実に指定した型にできるときにだけ変換に使うことが推奨されています。

tmevtb_enqueue_reltim関数からはほかにも関数が呼ばれているのですが、Zigとして見るべき点はなさそうなので今回はパスします。

Zig版ASP3カーネルに関しては、Zigの習得目的という意味ではあまり面白みがなくなってしまいました。 今回はこのテーマはいったん終了にしたいと思います。

このブログ自体は別の話題で続けていくつもりですので、今後もよろしくお願いします。

それでは!!

Zig版ASP3カーネルのdly_tsk

こんばんは、めのんです!

しばらく間が開いてしまいましたが、Zig版ASP3カーネルの続きを読んでいきたいと思います。

今回はタスク付属同期機能のサービスコールのひとつdly_tskを見ていくことにします。

一応このサービスコールの説明を簡単にしておきますね。

dly_tskはdelay taskの意味で、指定した時間だけタスクを遅延させます。 遅延している間、タスクは待ち状態になります。 そして指定した時間が経過すると実行可能状態に遷移します。

dly_tskの定義はkernel/task_sync.zigにあります。

///
/// 自タスクの遅延[NGKI1348]
///
pub fn dly_tsk(dlytim: RELTIM) ItronError!void {
    var winfo: WINFO = undefined;
    var tmevtb: TMEVTB = undefined;

    traceLog("dlyTskEnter", .{ dlytim });
    errdefer |err| traceLog("dlyTskLeave", .{ err });
    try checkDispatch();                            //[NGKI1349]
    try checkParameter(validRelativeTime(dlytim));  //[NGKI1351]
    {
        target_impl.lockCpuDsp();
        defer target_impl.unlockCpuDsp();

        var p_selftsk = p_runtsk.?;
        if (p_selftsk.flags.raster) {               //[NGKI3456]
            return ItronError.TerminationRequestRaised;
        }
        else {                                      //[NGKI1353]
            p_selftsk.tstat = TS_WAITING_DLY;
            make_non_runnable(p_selftsk);
            p_selftsk.p_winfo = &winfo;
            winfo.p_tmevtb = &tmevtb;
            tmevtb.callback = wait_tmout_ok;
            tmevtb.arg = @ptrToInt(p_runtsk);
            tmevtb_enqueue_reltim(&tmevtb, dlytim);
            traceLog("taskStateChange", .{ p_selftsk });
            target_impl.dispatch();
            if (winfo.werror) |werror| {
                return werror;
            }
        }
    }
    traceLog("dlyTskLeave", .{ null });
}

Zigの言語仕様としてはとくに見所はありませんが、一通り順に追っていきましょう。

最初は状態や引数のチェックですね。 その次はCPUロック状態に遷移させているようです。

自タスクが終了要求状態、つまりp_selftsk.flags.rasterがtrueであればエラーにしています。

そうでなければ自タスクを待機状態に遷移させています。 ここで、p_winfoというTCBのフィールドを設定しています。 p_winfoはたぶん今回が初めて出てきたように思います。

WINFO型の定義を見てみることにしましょう。 定義はkernel/wait.zigにあります。

///
///  待ち情報ブロック(WINFO)の定義
///
///  タスクが待ち状態の間は,TCBおよびそのp_winfoで指されるWINFOを次の
///  ように設定しなければならない.
///
///  (a) TCBのタスク状態を待ち状態(TS_WAITING_???)にする.
///
///  (b) タイムアウトを監視するために,タイムイベントブロックを登録す
///  る.登録するタイムイベントブロックは,待ちに入るサービスコール処
///  理関数のローカル変数として確保し,それへのポインタをWINFOの
///  p_tmevtbに記憶する.タイムアウトの監視が必要ない場合(永久待ちの
///  場合)には,p_tmevtbをNULLにする.
///
///  同期・通信オブジェクトに対する待ち状態の場合には,標準のWINFOに
///  p_wobjcbフィールドを追加した構造体(WINFO_WOBJ,wait.hで定義)に,
///  待ち対象の同期・通信オブジェクトに依存して記憶することが必要な情
///  報のためのフィールドを追加した構造体(WINFO_???)を定義し,WINFO
///  の代わりに用いる.また,以下の(c)〜(e)の設定を行う必要がある.同
///  期・通信オブジェクトに関係しない待ち(起床待ち,時間経過待ち)の
///  場合には,これらは必要ない.
///
///  (c) TCBを待ち対象の同期・通信オブジェクトの待ちキューにつなぐ.待
///  ちキューにつなぐために,task_queueを使う.
///
///  (d) 待ち対象の同期・通信オブジェクトの管理ブロックへのポインタを,
///  WINFO_WOBJのp_wobjcbに記憶する.
///
///  (e) 待ち対象の同期・通信オブジェクトに依存して記憶することが必要
///  な情報がある場合には,WINFO_???内のフィールドに記憶する.
///
///  待ち状態を解除する際には,待ち解除したタスクに対する返値をWINFOの
///  werrorに設定する.werrorが必要なのは待ち解除以降であるのに対して,
///  p_tmevtbは待ち解除後は必要ないため,メモリ節約のために共用体を使っ
///  ている.そのため,wercdへエラーコードを設定するのは,タイムイベン
///  トブロックを登録解除した後にしなければならない.
///
pub const WINFO = union {
    werror: ?ItronError,    // 待ち解除時のエラー
    p_tmevtb: ?*TMEVTB,     // 待ち状態用のタイムイベントブロック
};

大量のコメントがついていて、私が書くことがなくなっちゃいましたね! でも、なんかこのコメントって、C版のまんまな気がしません?

とりあえずそのあたりはいったん目をつぶりましょう。

結局、WINFO型の肝になっている部分はp_tmevtbのようですから、TMEVTB型を調べればよさそうです。 TMEVTB型の定義はkernel/time_event.zigにあります。

///
///  タイムイベントブロックのデータ型の定義
///
pub const TMEVTB = struct {
    evttim: EVTTIM,     // タイムイベントの発生時刻
    index: usize,       // タイムイベントヒープ中での位置
    callback: CBACK,    // コールバック関数
    arg: usize,         // コールバック関数へ渡す引数
};

どうやら、時刻がevttimになったときにコールバック関数callbackを呼び出すようになっているようです。

dly_tskの定義に戻って、tmevtb_enqueue_reltim関数がきっとTMEVTB型のオブジェクトを組み立ててるんだと思います。

かなり長くなってしまったので、tmevtb_enqueue_reltim関数を追いかけるのは次回回しにしたいと思います。

Zigの言語学習としてはもうネタ切れの感がありますが、こうやってカーネルのソースを見ていくのも悪くないですね。

ちょっと最近はブログの投稿をサボりがちになっていますが、頑張って更新していきますのでどうか見捨てずに応援してください。

それでは!!

Zig版ASP3カーネルのwup_tsk

こんばんは、めのんです!

今日のお昼はタスク付属同期機能のサービスコールslp_tskを見ていきました。

menonfled.hateblo.jp

slp_tskはタスクを起床待ち、もっと簡単な言葉を使えば寝かしつけるためのサービスコールでした。 タスクを寝かしつけたままにするわけにはいきませんので、どこかで起こしてあげないといけませんね。 そのためのサービスコールがwup_tskです。 定義はslp_tskと同じくkernel/task_sync.zigにあります。

///
///  タスクの起床[NGKI3531]
///
pub fn wup_tsk(tskid: ID) ItronError!void {
    var p_tcb: *TCB = undefined;

    traceLog("wupTskEnter", .{ tskid });
    errdefer |err| traceLog("wupTskLeave", .{ err });
    try checkContextUnlock();                   //[NGKI1265]
    if (tskid == TSK_SELF and !target_impl.senseContext()) {
        p_tcb = p_runtsk.?;                     //[NGKI1275]
    }
    else {
        p_tcb = try checkAndGetTCB(tskid);      //[NGKI1267]
    }
    {
        target_impl.lockCpu();
        defer target_impl.unlockCpu();

        if (isDormant(p_tcb.tstat)) {           //[NGKI1270]
            return ItronError.ObjectStateError;
        }
        else if (isWaitingSlp(p_tcb.tstat)) {
            wait_complete(p_tcb);               //[NGKI1271]
            requestTaskDispatch();
        }
        else if (p_tcb.flags.wupque < TMAX_WUPCNT) {
            p_tcb.flags.wupque += 1;            //[NGKI1273]
        }
        else {
            return ItronError.QueueingOverflow; //[NGKI1274]
        }
    }
    traceLog("wupTskLeave", .{ null });
}

今回もZigの言語使用としては見所はとくにありません。 関数の肝になるのはwait_complete関数とrequestTaskDispatch関数のようです。

このうちrequestTaskDispatch関数はターゲット依存部で定義されたディスパッチ関数を読んでいるだけだと思いますし実際そうです。

ですので、今回はwait_complete関数をさらに見ていくことにします。 定義はkernel/wait.zigにあります。

///
///  待ち解除
///
///  p_tcbで指定されるタスクの待ち状態を解除する.具体的には,タイムイ
///  ベントブロックが登録されていれば,それを登録解除する.また,タス
///  ク状態を更新し,待ち解除したタスクからの返値をnull(エラー無し)
///  とする.待ちキューからの削除は行わない.
///
pub fn wait_complete(p_tcb: *TCB) void {
    wait_dequeue_tmevtb(p_tcb);
    p_tcb.p_winfo.* = WINFO{ .werror = null };
    make_non_wait(p_tcb);
}

これはさらにwait_dequeue_tmevtb関数も見る必要がありそうですね。 定義はやはりkernel/wait.zigにあります。

///
///  時間待ちのためのタイムイベントブロックの登録解除
///
///  p_tcbで指定されるタスクに対して,時間待ちのためのタイムイベントブ
///  ロックが登録されていれば,それを登録解除する.
///
pub fn wait_dequeue_tmevtb(p_tcb: *TCB) void {
    if (p_tcb.p_winfo.p_tmevtb) |p_tmevtb| {
        tmevtb_dequeue(p_tmevtb);
    }
}

待ったました! という感じです。 久々に登場する新しいZigの文法です。

ifのあとに||で囲んだ変数には条件式の値が入ります。 どういうことかというとifの条件式にOptional、つまりnullableに評価される式を書いた場合、nullではない(=true)に評価された場合に、値が||で囲んだ変数に格納されます。 上の例でいえば、p_tcb.p_winfo.p_tmevtbがnullでなければその値がp_tmevtbに格納されます。 このあたりはいかにもZigらしいですね。

さらにはmake_non_wait関数も見る必要があります。 これも定義はkernel/wait.zigにあります。

///
///  待ち解除のためのタスク状態の更新
///
///  p_tcbで指定されるタスクを,待ち解除するようタスク状態を更新する.
///  待ち解除するタスクが実行できる状態になる場合は,レディキューにつ
///  なぐ.
///
pub fn make_non_wait(p_tcb: *TCB) void {
    assert(isWaiting(p_tcb.tstat));

    if (!isSuspended(p_tcb.tstat)) {
        // 待ち状態から実行できる状態への遷移
        p_tcb.tstat = TS_RUNNABLE;
        traceLog("taskStateChange", .{ p_tcb });
        make_runnable(p_tcb);
    }
    else {
        // 二重待ち状態から強制待ち状態への遷移
        p_tcb.tstat = TS_SUSPENDED;
        traceLog("taskStateChange", .{ p_tcb });
    }
}

これはとくに見所はありませんね。 make_runnable関数はact_tskから呼ばれていたmake_active関数とは微妙に異なるのでしょうね。 定義はkernel/task.zigにあります。

///
///  実行できる状態への遷移
///
///  p_tcbで指定されるタスクをレディキューに挿入する.また,必要な場合
///  には,実行すべきタスクを更新する.
///
pub fn make_runnable(p_tcb: *TCB) void {
    const prio = p_tcb.prio;

    ready_queue[prio].insertPrev(&p_tcb.task_queue);
    ready_primap.set(prio);

    if (dspflg) {
        if (p_schedtsk == null or prio < p_schedtsk.?.prio) {
            p_schedtsk = p_tcb;
        }
    }
}

act_tskを見たときにはmake_active関数まで深掘りしていなかったと思うので、この機会に見ていくことにします。

///
///  休止状態から実行できる状態への遷移
///
///  p_tcbで指定されるタスクの状態を休止状態から実行できる状態とする.
///
pub fn make_active(p_tcb: *TCB) void {
    target_impl.activateContext(p_tcb);
    p_tcb.tstat = TS_RUNNABLE;
    traceLog("taskStateChange", .{ p_tcb });
    make_runnable(p_tcb);
}

どうやらmake_active関数は休止状態から実装可能状態へ遷移させる処理が入っているだけのようです。 あとはmake_runnable関数の仕事ですね。

make_runnable関数はレディキューに登録して、必要ならp_schedtskを設定しているだけですね。 だんだんZigにも慣れてきてスラスラ読めるようになってきました。

それでは今回はここまでにします。

明日からは少し更新頻度が落ちるかもしれませんが、なるべく1日に1回以上は更新できるように頑張りますので応援お願いします。

それでは!!

Zig版ASP3カーネルのslp_tsk

こんにちは、めのんです!

平日になりましたので、今回はいつものようにZig版TOPPERS/ASP3カーネルの話題です。 念のため前回のリンクも貼っておきますね。

menonfled.hateblo.jp

今回見ていくのはタスク付属同期機能の代表的なサービスコールのひとつslp_tskです。 slp_tskの定義はkernel/task_sync.zigにあります。

///
///  起床待ち[NGKI1252]
///
pub fn slp_tsk() ItronError!void {
    var winfo: WINFO = undefined;

    traceLog("slpTskEnter", .{});
    errdefer |err| traceLog("slpTskLeave", .{ err });
    try checkDispatch();                        //[NGKI1254]
    {
        target_impl.lockCpuDsp();
        defer target_impl.unlockCpuDsp();

        var p_selftsk = p_runtsk.?;
        if (p_selftsk.flags.raster) {           //[NGKI3455]
            return ItronError.TerminationRequestRaised;
        }
        else if (p_selftsk.flags.wupque > 0) {
            p_selftsk.flags.wupque -= 1;        //[NGKI1259]
        }
        else {
            make_wait(TS_WAITING_SLP, &winfo);  //[NGKI1260]
            traceLog("taskStateChange", .{ p_selftsk });
            target_impl.dispatch();
            if (winfo.werror) |werror| {
                return werror;
            }
        }
    }
    traceLog("slpTskLeave", .{ null });
}

実質二十数行しかない関数ですなんですが、本質的な処理はmake_wait関数にあるはずです。 確かC版のASP3カーネルでもそうなっていましたから。

Zigの文法的な見所はとくにないのですが、C版ではlock_cpu_dspやunlock_cpu_dspというマクロが使われていたので、どこで定義されているのか分かりにくかったのですが、Zig版ではtarget_impl.lockCpuDspのようになっているのでターゲット依存部で定義されていることがすぐにわかります。 こういうのはZigの利点かもしれませんね。

それでは、slp_tskの肝になるmake_wait関数の定義を見ていきましょう。 kernel/wait.zigに定義があるようです。

///
///  待ち状態への遷移
///
///  実行中のタスクを待ち状態に遷移させる.具体的には,実行中のタスク
///  のタスク状態をtstatにしてレディキューから削除し,TCBのp_winfoフィー
///  ルド,WINFOのp_tmevtbフィールドを設定する.
///
pub fn make_wait(tstat: u8, p_winfo: *WINFO) void {
    p_runtsk.?.tstat = tstat;
    make_non_runnable(p_runtsk.?);
    p_runtsk.?.p_winfo = p_winfo;
    p_winfo.* = WINFO{ .p_tmevtb = null };
}

拍子抜けするほど短い関数でした。

内容を見ていくと、p_runtskが参照しているTCBに情報を設定しているほかはmake_non_runnable関数の呼び出しのようです。 make_non_runnable関数も見ていきましょう。 定義はkernel/task.zigにあります。

///
///  実行できる状態から他の状態への遷移
///
///  p_tcbで指定されるタスクをレディキューに挿入する.また,必要な場合
///  には,実行すべきタスクを更新する.
///
pub fn make_non_runnable(p_tcb: *TCB) void {
    const prio = p_tcb.prio;
    const p_queue = &ready_queue[prio];

    p_tcb.task_queue.delete();
    if (p_queue.isEmpty()) {
        ready_primap.clear(prio);
        if (p_schedtsk == p_tcb) {
            assert(dspflg);
            p_schedtsk = if (ready_primap.isEmpty()) null
                else searchSchedtsk();
        }
    }
    else {
        if (p_schedtsk == p_tcb) {
            assert(dspflg);
            p_schedtsk = getTCBFromQueue(p_queue.p_next);
        }
    }
}

見た感じだと、レディキューから指定したタスクを取り除いて、次に実行するタスクをp_schedtskに設定しているだけのようです。

slp_tskはセマフォやイベントフラグのようなタスク間同期・通信オブジェクトの状態変化を待ているわけではないですし、タイマーイベントを待っているわけでもありません。 wup_tskで起こされるまで待つだけなので、別のキューにつなぐわけでもなく単純なんだろうなと思いました。

次回はwup_tskかtslp_tskを見ていきたいのですが、タイマーイベントを先に見るならdly_tskが先の方がいいかもしれませんね。 今夜までに考えておくことにします。

それでは!!

OpenSiv3Dでボタンを離したことを検知する

こんにちは、めのんです!

前回はOpenSiv3DのSimpleGUI::Buttonについて調べてみました。 名前の通り本当にシンプルでいいんですけど、ボタンを押したときしかイベントを取れないことがわかりました。

私がやりたいのは、ボタンを押したときにLED器具を点灯させて、ボタンを離したときに消灯させるという動きです。 どうにかしてボタンを離したことを検知しないといけません。

まず、ボタン(正確にいうとマウスの左ボタン)を離したこと自体は簡単に検知することができました。

if (MouseL.up())
{
  // ボタンを離したときの処理
}

これでいいようです。 簡単ですね。

でも、これだけだとどのボタンを離したのかわかりません。 じゃあ、ボタンを囲む矩形の範囲でボタンを離したときだけ有効にすればいいのかというとそんなこともないんですよ。

どういうことかというと、ボタンを押して、そのまま離さずにマウスカーソルを移動させてしまった場合を考えないといけないんですよね。 ちょっと手が動いてしまったとか、故意に動かした場合も含めて、ボタンの領域からマウスカーソルが出てしまうと離せなくなってしまうのは問題です。

ということは、ボタンが押されたときにどのボタンが押されたのかを覚えておいて、マウスの左ボタンが離されたときにはさっき覚えたボタンが離されたんだということを知る必要があります。

# include <Siv3D.hpp>
# include <Windows.h>

void Main()
{
  String pressedButton;

  while (System::Update())
  {
    if (SimpleGUI::Button(U"A", Vec2(100, 100)))
    {
      pressedButton = U"A";
      ::OutputDebugStringW(L"down A\n");
    }

    if (SimpleGUI::Button(U"B", Vec2(100, 150)))
    {
      pressedButton = U"B";
      ::OutputDebugStringW(L"down B\n");
    }

    if (MouseL.up())
    {
      if (pressedButton == U"A")
      {
        // Aボタンが離された
        ::OutputDebugStringW(L"up A\n");
      }
      else if (pressedButton == U"B")
      {
        // Bボタンが離された
        ::OutputDebugStringW(L"up B\n");
      }
      pressedButton = U"";
    }
  }
}

このサンプルではpressedButtonという変数にどのボタンが押されたのかを覚えるようにしています。 もっと簡単に使えるようにするにはクラスにした方がいいんでしょうね。

とにかくこれでやりたいことは実現できるようになりました。 次はフェーダーを作りたいのでスライダーを見ていきたいと思います。 今週は木曜日から連休なので、OpenSiv3Dを触る時間が増えるかなと期待しています。

それでは!!

OpenSiv3DのSimpleGUI::Button

こんにちは、めのんです!

いつも土日はOpenSiv3Dの話題だったんですけど、いろいろ事情があって土曜日は別の話題になってしまいました。 でも今日はOpenSiv3Dの話題ですよ。

先週、OpenSiv3DとBoost.Asioを使ってsACNでLED器具(に見立てたエミュレーション画面)を制御してみました。

menonfled.hateblo.jp

前回は受信側だけOpenSiv3Dを使ったので、次の目標としては送信側にOpenSiv3Dで操作部を作ろうと考えています。 必要になるのはボタンとフェーダー(スライダーで実現できると思っています)なんですが、まずは簡単そうなボタンから調べていくことにします。

OpenSiv3Dのボタンについては、9. GUI - Siv3Dを参考にしました。 SimpleGUIというだけあって本当にシンプルです。

設定できることがほとんどなくて、ボタンの外観やフォント、色なんかも設定できそうな雰囲気がありません。 それより一番困ったのが、ボタンを押下したことしかわからないことです。

LED器具の制御の場合、「ピアノ」というらしいんですけど、ボタンを押したときに点灯させて離したときに消灯させるという操作が普通にあるようです。 押したことはわかっても離したことがわからないと実現できませんね。

しかたがないのでOpenSiv3Dのソースコードを読んでみました。 Siv3D/src/Siv3D/SimpleGUI/SimpleGUI.cppにSimpleGUI::Button関数の定義は見つかりました。

bool Button(const String& label, const Vec2& pos, const Optional<double>& _width, const bool enabled)
{
    const Vec2 center = ButtonRegion(label, pos, _width).center();

    return ButtonAt(label, center, _width, enabled);
}

bool ButtonAt(const String& label, const Vec2& center, const Optional<double>& _width, const bool enabled)
{
    const Font font = detail::GetSimpleGUIFont();

    const int32 labelWidth = font(label).region().w;
    const double width = _width.value_or(labelWidth + 40);

    const RectF rect(Arg::center = center, width, 36);
    const Vec2 labelPos(static_cast<int32>(rect.x + (width - labelWidth) / 2), center.y - font.height() / 2);

    const bool mouseOver = enabled && rect.mouseOver();
    const bool pushed = mouseOver && Cursor::OnClientRect() && MouseL.down();

    if (enabled)
    {
        rect.rounded(4.8)
            .draw(mouseOver ? ColorF(0.92, 0.96, 1.0) : ColorF(1.0))
            .drawFrame(1, 0, ColorF(0.67, pushed ? 0.0 : 1.0));

        font(label).draw(labelPos, ColorF(0.2));
    }
    else
    {
        rect.rounded(4.8)
            .draw(ColorF(0.92))
            .drawFrame(1, 0, ColorF(0.67));

        font(label).draw(labelPos, ColorF(0.67));
    }

    if (mouseOver)
    {
        Cursor::RequestStyle(CursorStyle::Hand);
    }

    return pushed;
}

ButtonAt関数はボタンの中心座標でボタンの位置を指定するようで、Button関数は左上の頂点座標で指定するようです。

それはそうと、ButtonAt関数の次の部分がヒントになりそうです。

 const bool mouseOver = enabled && rect.mouseOver();
    const bool pushed = mouseOver && Cursor::OnClientRect() && MouseL.down();

これを見様見真似すれば、ボタンを離したときだけじゃなくて、右ボタンのクリックやマウスカーソルを動かしたときのイベントも取れそうな気がしてきました。

繰り返しになりますが、SimpleGUIは本当にシンプルな機能しかありません。 実際の製品で使うには、商品らしい高級感あふれるデザインにしないといけませんし、機能的にももう少し充実させる必要があります。 でも、SimpleGUIのソースコードを読めば、それらを実現するためのヒントが詰まっているように思いました。

将来的にはSimpleGUIならぬRichGUIみたいなのを自作してみたいですね。

それでは!!