|
|
nono 1.4.0
//
// nono
// Copyright (C) 2020 nono project
// Licensed under nono-license.txt
//
//
// プレーン VRAM モニターウィンドウ
//
#include "wxplanemonitor.h"
#include "wxcolor.h"
#include "wxuimessage.h"
#include "bt45x.h"
#include "mainapp.h"
#include "planevram.h"
#include "videoctlr.h"
//#define WINDOW_DEBUG 1
#if defined(WINDOW_DEBUG)
#define DPRINTF(fmt...) printf(fmt)
#else
#define DPRINTF(fmt...) /**/
#endif
enum {
ID_PLANE0 = IDGROUP_PLANEVRAM,
ID_PLANE1,
ID_PLANE2,
ID_PLANE3,
ID_PLANE4,
ID_PLANE5,
ID_PLANE6,
ID_PLANE7,
ID_PALETTE,
ID_SCALE,
ID_local_end, // 最後に置く (チェック用)
};
static_assert(ID_local_end - 1 <= (int)IDGROUP_PLANEVRAM_END, "ID exceeded");
#define ID_PLANE(id) (ID_PLANE0 + (id))
//
// プレーン VRAM ビットマップ
//
// イベントテーブル
wxBEGIN_EVENT_TABLE(WXPlaneVRAMBitmapPanel, inherited)
EVT_MOUSE_EVENTS(WXPlaneVRAMBitmapPanel::OnMouse)
wxEND_EVENT_TABLE()
// コンストラクタ
WXPlaneVRAMBitmapPanel::WXPlaneVRAMBitmapPanel(wxWindow *parent)
: inherited(parent)
{
// デバイスを取得
planevram = GetPlaneVRAMDevice();
// どちらか一方は NULL のはず
bt45x = gMainApp.FindObject<BT45xDevice>(OBJ_BT45x);
videoctlr = gMainApp.FindObject<VideoCtlrDevice>(OBJ_VIDEOCTLR);
plane_width = planevram->GetComposite().GetWidth();
plane_height = planevram->GetComposite().GetHeight();
// まず等倍でセット
scale = 1;
SetScale(1);
cursor = wxDefaultPosition;
// VM からの通知を受け取る
WXUIMessage::Connect(UIMessage::PALETTE, this,
wxCommandEventHandler(WXPlaneVRAMBitmapPanel::OnPaletteChanged));
}
// デストラクタ
WXPlaneVRAMBitmapPanel::~WXPlaneVRAMBitmapPanel()
{
WXUIMessage::Disconnect(UIMessage::PALETTE, this,
wxCommandEventHandler(WXPlaneVRAMBitmapPanel::OnPaletteChanged));
}
// XXX 実際にはこの辺かどこかで更新チェックをしないといけない。
// 今は表示領域を毎回描画しているので無駄。
// ビットマッププレーンの内容を描画する
void
WXPlaneVRAMBitmapPanel::Draw()
{
assert(view.GetRight() < virtual_width);
assert(view.GetBottom() < virtual_height);
const BitmapI8& src = planevram->GetComposite();
if (scale > 1) {
Rect dr(0, 0, bitmap.GetWidth(), bitmap.GetHeight());
bitmap.DrawBitmapI8Scale(dr, src, &pal[0],
view.x, scale, 1,
view.y, scale, 1);
} else {
bitmap.DrawBitmapI8(0, 0, src, &pal[0], view);
}
}
// プレーン選択変更
void
WXPlaneVRAMBitmapPanel::EnablePlane(int plane, bool value)
{
if (value) {
planemask |= 1U << plane;
} else {
planemask &= ~(1U << plane);
}
// パレット生成
GenPalette();
}
// パレット適用変更
void
WXPlaneVRAMBitmapPanel::EnablePalette(bool value)
{
apply_palette = value;
// パレット生成
GenPalette();
}
// VM からのパレット変更通知
void
WXPlaneVRAMBitmapPanel::OnPaletteChanged(wxCommandEvent& event)
{
// パレット生成
GenPalette();
}
// パレット生成
void
WXPlaneVRAMBitmapPanel::GenPalette()
{
if (apply_palette) {
// パレットを適用する場合
// (雑に X680x0/LUNA 対応)
const std::vector<Color> *vmpal;
if (videoctlr) {
vmpal = &videoctlr->GetHostPalette();
} else {
vmpal = &bt45x->GetHostPalette();
}
int vmpal_size = vmpal->size();
const Color bg = (*vmpal)[0];
int d = 0;
int s;
for (; d < pal.size(); d++) {
s = d % vmpal_size;
if ((s & planemask)) {
pal[d] = (*vmpal)[s];
} else {
pal[d] = bg;
}
}
} else {
// パレットを適用しない場合
for (int i = 0; i < pal.size(); i++) {
if ((i & planemask)) {
pal[i] = Color(255, 255, 255);
} else {
pal[i] = Color(0, 0, 0);
}
}
}
}
// 倍率を設定
void
WXPlaneVRAMBitmapPanel::SetScale(int scale_)
{
int before = scale;
scale = scale_;
// 仮想画面の大きさを計算
virtual_width = plane_width * scale;
virtual_height = plane_height * scale;
// ビューのスケール変換は前後のスケールがいるのでここでやる
view.x = view.x * scale / before;
view.y = view.y * scale / before;
view.w = view.w * scale / before;
view.h = view.h * scale / before;
SetMaxSize(wxSize(virtual_width, virtual_height));
}
// スクロール位置変更通知
void
WXPlaneVRAMBitmapPanel::OnScroll(wxScrollEvent& event)
{
int orient = event.GetOrientation();
int pos = event.GetPosition();
if (orient == wxHORIZONTAL) {
view.x = pos;
} else {
view.y = pos;
}
}
// マウスイベント
void
WXPlaneVRAMBitmapPanel::OnMouse(wxMouseEvent& event)
{
auto window = dynamic_cast<WXPlaneVRAMWindow*>(GetGrandParent());
assert(window);
// pt はこのパネル左上からの位置
const wxPoint& pt = event.GetPosition();
// pt がパネル内の座標なのに Leaving() になることがある。
// pt がパネル外の座標なのに Leaving() にならないことがある。
if (event.Leaving() == false && GetClientRect().Contains(pt)) {
// cursor は仮想画面上の座標
cursor.x = (view.x + pt.x) / scale;
cursor.y = (view.y + pt.y) / scale;
assertmsg(cursor.x < plane_width, "cursor.x=%d", cursor.x);
assertmsg(cursor.y < plane_height, "cursor.y=%d", cursor.y);
} else {
cursor = wxDefaultPosition;
}
}
//
// プレーン VRAM パネル
//
// +------------------------+------------------+
// | WXPlaneVRAMBitmapPanel | WXScrollBar(V) |
// | VRAM 表示パネル | 縦スクロールバー |
// +------------------------+------------------+
// | WXScrollBar(H) | WXBitmapPanel |
// | 横スクロールバー | 空き地 |
// +------------------------+------------------+
// コンストラクタ
WXPlaneVRAMPanel::WXPlaneVRAMPanel(wxWindow *parent)
: inherited(parent)
{
// 表示パネル
viewctl = new WXPlaneVRAMBitmapPanel(this);
// スクロールバーを自分で持つ (配置は Layout() で行う)
vscroll = new WXScrollBar(this, wxID_ANY, wxSB_VERTICAL);
hscroll = new WXScrollBar(this, wxID_ANY, wxSB_HORIZONTAL);
// 右下の空き地
corner = new WXBitmapPanel(this);
// Sizer 使わず自前でレイアウトする。
SetAutoLayout(true);
FontChanged();
// スクロールバーからの通知イベントは表示パネルへ回す
vscroll->Connect(NONO_EVT_SCROLL,
wxScrollEventHandler(WXPlaneVRAMBitmapPanel::OnScroll), NULL, viewctl);
hscroll->Connect(NONO_EVT_SCROLL,
wxScrollEventHandler(WXPlaneVRAMBitmapPanel::OnScroll), NULL, viewctl);
DPRINTF("Panel.ctor end\n");
}
// デストラクタ
WXPlaneVRAMPanel::~WXPlaneVRAMPanel()
{
}
// フォントサイズ変更
void
WXPlaneVRAMPanel::FontChanged()
{
// このパネル自体はフォントを出力しないが
// フォントサイズ変更に反応する必要があるため、用意している。
// 自身がフォントを使わないので呼ばなくてもいいが、お作法として。
inherited::FontChanged();
DPRINTF("Panel.FontChanged %d\n", font_height);
// SubWindow はパネルの FontChanged() までは呼んでくれるけど、
// パネルが子を持ってるケースは自力で伝搬させるとかする必要がある。
hscroll->FontChanged();
vscroll->FontChanged();
Fit();
DPRINTF("Panel.FontChanged end\n");
}
void
WXPlaneVRAMPanel::Fit()
{
// 最小/最大サイズを再計算。
wxSize vmin = vscroll->GetMinSize();
wxSize hmin = hscroll->GetMinSize();
wxSize minsize(hmin.x + vmin.x, vmin.y + hmin.y);
wxSize maxsize(viewctl->virtual_width + vmin.x,
viewctl->virtual_height + hmin.y);
SetSizeHints(minsize, maxsize);
// 新しいサイズはビューサイズから決定。
wxSize viewsize = viewctl->GetSize();
wxSize newsize(viewsize.x + vmin.x, viewsize.y + hmin.y);
DPRINTF("Panel.Fit (%d,%d) min=(%d,%d) max=(%d,%d)\n",
newsize.x, newsize.y, minsize.x, minsize.y, maxsize.x, maxsize.y);
SetSize(newsize);
GetParent()->Fit();
}
bool
WXPlaneVRAMPanel::Layout()
{
const wxSize size = GetSize();
DPRINTF("Panel.Layout size=(%d,%d)\n", size.x, size.y);
wxSize vscr = vscroll->GetSize();
wxSize hscr = hscroll->GetSize();
// コントロールを配置。
int view_width = size.x - vscr.x;
int view_height = size.y - hscr.y;
DPRINTF("Panel.Layout view=(%d,%d)\n", view_width, view_height);
viewctl->SetSize(0, 0, view_width, view_height);
vscroll->SetSize(view_width, 0, vscr.x, view_height);
hscroll->SetSize(0, view_height, view_width, hscr.y);
corner->SetSize(view_width, view_height, vscr.x, hscr.y);
SetScroll();
DPRINTF("Panel.Layout done\n");
return true;
}
// サイズ変更処理。パネルサイズと倍率が変わったら呼ぶこと。
// 倍率が変わった場合もスクロールバーの再設定とかがあるため。
void
WXPlaneVRAMPanel::SetScroll()
{
wxSize viewsize = viewctl->GetSize();
DPRINTF("Panel.%s: view=(%d,%d)\n", __func__, viewsize.x, viewsize.y);
int virtual_width = viewctl->virtual_width;
int virtual_height = viewctl->virtual_height;
Rect& view = viewctl->view;
// スクロールバーが右端まで行っている場合は右を固定したように見せる。
// ただし全体が見えている場合は除く。
if (view.x > 0 &&
(view.x + view.w == virtual_width ||
view.x + viewsize.x >= virtual_width))
{
view.x = virtual_width - viewsize.x;
if (view.x < 0) {
view.x = 0;
}
}
view.w = viewsize.x;
// 下端も同様
if (view.y > 0 &&
(view.y + view.h >= virtual_height ||
view.y + viewsize.y >= virtual_height))
{
view.y = virtual_height - viewsize.y;
if (view.y < 0) {
view.y = 0;
}
}
view.h = viewsize.y;
// スクロールバー(横)を再設定
assert(hscroll);
int hpos = view.x;
int hthumbsize = view.w;
int hrange = virtual_width;
int hpagesize = view.w;
hscroll->SetScrollParam(hpos, hthumbsize, hrange, hpagesize);
// スクロールバー(縦)を再設定
assert(vscroll);
int vpos = view.y;
int vthumbsize = view.h;
int vrange = virtual_height;
int vpagesize = view.h;
vscroll->SetScrollParam(vpos, vthumbsize, vrange, vpagesize);
}
// プレーン選択変更
void
WXPlaneVRAMPanel::EnablePlane(int plane, bool value)
{
viewctl->EnablePlane(plane, value);
}
// パレット適用変更
void
WXPlaneVRAMPanel::EnablePalette(bool value)
{
viewctl->EnablePalette(value);
}
// 倍率を設定
void
WXPlaneVRAMPanel::SetScale(int scale_)
{
viewctl->SetScale(scale_);
int vscr_width = vscroll->GetSize().x;
int hscr_height = hscroll->GetSize().y;
SetMaxSize(wxSize(viewctl->virtual_width + vscr_width,
viewctl->virtual_height + hscr_height));
SetScroll();
}
//
// プレーン VRAM ウィンドウ
//
// ↓+-----------------------------+
// | wx コントロール用パネル |
// +-----------------------------+
// | WXTextScreen *statuspanel | ← 情報欄
// +-----------------------------+
// | WXPlaneVRAMPanel *viewpanel | ← ビットマップ (スクロールバー込み)
// +-----------------------------+
// イベントテーブル
wxBEGIN_EVENT_TABLE(WXPlaneVRAMWindow, inherited)
EVT_COMMAND_RANGE(ID_PLANE0, ID_PLANE7, wxEVT_CHECKBOX,
WXPlaneVRAMWindow::OnPlane)
EVT_CHECKBOX(ID_PALETTE, WXPlaneVRAMWindow::OnApplyPalette)
EVT_CHOICE(ID_SCALE, WXPlaneVRAMWindow::OnScale)
EVT_TIMER(wxID_ANY, WXPlaneVRAMWindow::OnTimer)
wxEND_EVENT_TABLE()
// コンストラクタ
WXPlaneVRAMWindow::WXPlaneVRAMWindow(wxWindow *parent, const wxString& name)
: inherited(parent, wxID_ANY, name,
inherited::DEFAULT_STYLE | wxRESIZE_BORDER)
{
// デバイスを取得
planevram = GetPlaneVRAMDevice();
int nplane = planevram->GetPlaneCount();
// 上段、コントロールパネル(のまず下敷き)
ctrlpanel = new wxPanel(this);
ctrlpanel->SetName("WXPlaneVRAMWindow.CtrlPanel");
// コントロールパネル用の横 Sizer
auto *ctrlbox = new wxBoxSizer(wxHORIZONTAL);
// プレーン選択チェックボックス
auto *csbox = new wxStaticBoxSizer(wxHORIZONTAL, ctrlpanel,
_("Planes to display"));
// GTK3 標準のレンダリングだとコントロールの周りの空きがなさすぎて
// 特にスタティックボックスは読みづらいので自力で少し空ける。どうして…。
ctrlbox->Add(csbox, 0, wxALIGN_CENTER | wxALL, 3);
// X68030 なら 4プレーン。
// LUNA で 1bpp なら 4プレーン (上位3つは Disabled)、
// LUNA で 4bpp なら 4プレーン、
// LUNA で 8bpp なら 8プレーン。
int nplane4 = (nplane == 1) ? 4 : nplane;
for (int i = 0; i < nplane4; i++) {
auto *ctrl = new wxCheckBox(ctrlpanel, ID_PLANE(i),
string_format("%d", i));
planesw.push_back(ctrl);
}
// 降順に並べる
for (int i = nplane4 - 1; i >= 0; i--) {
csbox->Add(planesw[i]);
}
// パレット合成チェックボックス
applysw = new wxCheckBox(ctrlpanel, ID_PALETTE, _("Apply palette"));
ctrlbox->Add(applysw, 0, wxALIGN_CENTER | wxALL, 3);
// セパレータを出したいのだが…
ctrlbox->Add(new wxStaticText(ctrlpanel, wxID_ANY, "|"), 0,
wxALIGN_CENTER | wxALL, 3);
// 倍率
wxString scale_choice[] = {
"x1",
"x2",
"x4",
};
scalesw = new wxChoice(ctrlpanel, ID_SCALE,
wxDefaultPosition, wxDefaultSize,
countof(scale_choice), scale_choice);
ctrlbox->Add(scalesw, 0, wxALIGN_CENTER | wxALL, 3);
// sizer と下敷きパネルを紐付ける
ctrlpanel->SetSizer(ctrlbox);
ctrlbox->SetSizeHints(ctrlpanel);
// 中段、情報パネルは大きさを固定
constexpr int status_col = 60;
statuspanel = new WXTextScreen(this, nnSize(status_col, nplane));
statuspanel->SetMinSize(statuspanel->GetBestSize());
// 中段の右側の空き地用。
spacer = new WXBitmapPanel(this);
// 下段、表示パネル。
// 大きさは適当だがフォントサイズを大きくした後で再び小さくすると
// このパネルはどうしたらいいのか難しいのと、最初から広いほうが見やすい
// というのもあるしね…ということにして、とりあえずステータスパネルを
// 最初からフォントサイズ 24 の時の幅にしておいて誤魔化す。高さは適当。
viewpanel = new WXPlaneVRAMPanel(this);
viewpanel->SetSize(wxSize(status_col * 12, status_col * 12 / 2));
SetAutoLayout(true);
FontChanged();
// 1bpp ならプレーン選択はすべて無効、それ以外はすべて有効。
if (nplane == 1) {
for (int i = 0; i < planesw.size(); i++) {
planesw[i]->Enable(false);
}
}
// 初期値をコントロールにセットする
if (sticky_used) {
// 前回の値があればそれを使う。
for (int i = 0; i < planesw.size(); i++) {
planesw[i]->SetValue(sticky_plane[i]);
}
applysw->SetValue(sticky_apply);
scalesw->SetSelection(sticky_scale);
} else {
if (nplane == 1) {
// 初回 1bpp なら #0 プレーンのみオン。
for (int i = 0; i < planesw.size(); i++) {
planesw[i]->SetValue((i == 0));
}
applysw->SetValue(true);
} else {
// 初回 4bpp なら全プレーンオン。
for (int i = 0; i < planesw.size(); i++) {
planesw[i]->SetValue(true);
}
applysw->SetValue(true);
}
scalesw->SetSelection(0);
}
// SetValue() ではイベントが飛ばないので、
// 直接呼び出してイベントが来たことにする。うーん…。
for (int i = 0; i < planesw.size(); i++) {
DoPlane(i, planesw[i]->IsChecked());
}
DoApplyPalette(applysw->IsChecked());
DoScale(scalesw->GetSelection());
// 更新タイマーはモニターパネル同様の 30Hz にしておく。
timer.SetOwner(this);
timer.Start(33);
DPRINTF("Window.ctor end\n");
}
// デストラクタ
WXPlaneVRAMWindow::~WXPlaneVRAMWindow()
{
}
void
WXPlaneVRAMWindow::Fit()
{
// 全部準備出来る前に呼ばれることがある。spacer は見ないので不要。
if (ctrlpanel == NULL || statuspanel == NULL || viewpanel == NULL) {
return;
}
// 最大最小サイズを設定。
DoSizeHints();
// コントロールからウィンドウサイズを計算。
const wxSize& ctrlsize = ctrlpanel->GetSize();
const wxSize& statsize = statuspanel->GetSize();
const wxSize& viewsize = viewpanel->GetSize();
int size_x = ctrlsize.x;
if (size_x < statsize.x) {
size_x = statsize.x;
}
if (size_x < viewsize.x) {
size_x = viewsize.x;
}
wxSize size(size_x, ctrlsize.y + statsize.y + viewsize.y);
DPRINTF("Window.Fit (%d,%d)\n", size.x, size.y);
SetClientSize(size);
}
void
WXPlaneVRAMWindow::DoSizeHints()
{
// 最小サイズを再計算。
const wxSize& ctrlmin = ctrlpanel->GetMinSize();
const wxSize& statmin = statuspanel->GetMinSize();
const wxSize& viewmin = viewpanel->GetMinSize();
DPRINTF("Window.%s ctrlmin=(%d,%d) statmin=(%d,%d) viewmin=(%d,%d)\n",
__func__,
ctrlmin.x, ctrlmin.y, statmin.x, statmin.y, viewmin.x, viewmin.y);
int minsize_x = ctrlmin.x;
if (minsize_x < statmin.x) {
minsize_x = statmin.x;
}
if (minsize_x < viewmin.x) {
minsize_x = viewmin.x;
}
min_height = ctrlmin.y + statmin.y + viewmin.y;
wxSize minsize(minsize_x, min_height);
// 最大サイズは viewsize に依存しそうだから手抜き。
const wxSize& viewmax = viewpanel->GetMaxSize();
wxSize maxsize(viewmax.x, ctrlmin.y + statmin.y + viewmax.y);
// 最大最小サイズを再設定。
DPRINTF("Window.%s min=(%d,%d), max=(%d,%d)\n", __func__,
minsize.x, minsize.y, maxsize.x, maxsize.y);
SetSizeHints(wxDefaultCoord, wxDefaultCoord);
SetSizeHints(ClientToWindowSize(minsize), ClientToWindowSize(maxsize));
}
bool
WXPlaneVRAMWindow::Layout()
{
// sizer があるので呼ばないといけない。
bool r = inherited::Layout();
// 全部準備出来る前に呼ばれることがある。
if (ctrlpanel == NULL || statuspanel == NULL || viewpanel == NULL ||
spacer == NULL)
{
return r;
}
wxSize client = GetClientSize();
DPRINTF("Window.Layout (%d,%d)\n", client.x, client.y);
const wxSize& ctrlsize = ctrlpanel->GetSize();
const wxSize& statsize = statuspanel->GetSize();
// wxmonitor.cpp の Layout() 内のコメント参照。
wxSize min = GetMinClientSize();
if (min.y >= min_height) {
// 正常なリサイズなのでこのサイズを覚えておく。
correct_clientsize = client;
} else {
// 変なリサイズが行われたので、やり直す。
client = correct_clientsize;
DoSizeHints();
SetClientSize(client);
min = GetMinClientSize();
DPRINTF("Window.%s patch client=(%d,%d) min=(%d,%d)\n", __func__,
client.x, client.y, min.x, min.y);
// SetClientSize() によってこの場で Layout() が呼ばれて戻ってきた
// はずなので、これ以降はもう処理しなくていい。
return true;
}
ctrlpanel->SetSize(0, 0, client.x, ctrlsize.y);
statuspanel->SetSize(0, ctrlsize.y, statsize.x, statsize.y);
viewpanel->SetSize(0, ctrlsize.y + statsize.y,
client.x, client.y - ctrlsize.y - statsize.y);
// ステータスパネル右に空き地が出来るなら埋める。
if (statsize.x < client.x) {
spacer->SetSize(statsize.x, ctrlsize.y,
client.x - statsize.x, statsize.y);
spacer->Show();
} else {
spacer->Hide();
}
return true;
}
// プレーン選択のチェックボックスイベント
void
WXPlaneVRAMWindow::OnPlane(wxCommandEvent& event)
{
int plane = event.GetId() - ID_PLANE0;
bool value = event.IsChecked();
DoPlane(plane, value);
}
// プレーン選択 ON/OFF 処理
void
WXPlaneVRAMWindow::DoPlane(int plane, bool value)
{
// コントロールに指示
viewpanel->EnablePlane(plane, value);
// 値を保存
sticky_plane[plane] = value;
sticky_used = true;
}
// パレット合成のチェックボックスイベント
void
WXPlaneVRAMWindow::OnApplyPalette(wxCommandEvent& event)
{
bool value = event.IsChecked();
DoApplyPalette(value);
}
// パレット合成 ON/OFF 処理
void
WXPlaneVRAMWindow::DoApplyPalette(bool value)
{
// コントロールに指示
viewpanel->EnablePalette(value);
// 値を保存
sticky_apply = value;
sticky_used = true;
}
// 倍率変更イベント
void
WXPlaneVRAMWindow::OnScale(wxCommandEvent& event)
{
int scale_index = event.GetSelection();
DoScale(scale_index);
}
// 倍率変更処理
void
WXPlaneVRAMWindow::DoScale(int scale_index)
{
viewpanel->SetScale(1 << scale_index);
// 最大サイズは表示部の最大サイズから
wxSize maxsize = viewpanel->GetMaxSize();
maxsize.y += ctrlpanel->GetMinSize().y;
maxsize.y += statuspanel->GetMinSize().y;
SetMaxClientSize(maxsize);
// 保存しておくのはインデックスのほう
sticky_scale = scale_index;
}
// タイマーイベント
void
WXPlaneVRAMWindow::OnTimer(wxTimerEvent& event)
{
viewpanel->Refresh();
// この位置の情報で screen を更新
TextScreen& screen = statuspanel->GetScreen();
const wxPoint& cursor = viewpanel->GetCursor();
// UpdateInfo() は cursor.x が範囲外の時の描画も行っている。
planevram->UpdateInfo(screen, cursor.x, cursor.y);
statuspanel->Refresh();
}
// static 変数
std::array<bool, WXPlaneVRAMWindow::MAX_PLANES> WXPlaneVRAMWindow::sticky_plane;
bool WXPlaneVRAMWindow::sticky_apply;
int WXPlaneVRAMWindow::sticky_scale;
bool WXPlaneVRAMWindow::sticky_used;
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.