|
|
1.1 root 1: //
2: // nono
3: // Copyright (C) 2020 nono project
4: // Licensed under nono-license.txt
5: //
6:
7: //
8: // プレーン VRAM モニターウィンドウ
9: //
10:
11: #include "wxplanemonitor.h"
12: #include "wxcolor.h"
13: #include "wxuimessage.h"
1.1.1.2 ! root 14: #include "bt45x.h"
1.1 root 15: #include "planevram.h"
16: #include "videoctlr.h"
17:
18: enum {
19: ID_PLANE0 = IDGROUP_PLANEVRAM,
20: ID_PLANE1,
21: ID_PLANE2,
22: ID_PLANE3,
23: ID_PLANE4,
24: ID_PLANE5,
25: ID_PLANE6,
26: ID_PLANE7,
27: ID_PALETTE,
28: ID_SCALE,
29:
30: ID_local_end, // 最後に置く (チェック用)
31: };
32: static_assert(ID_local_end - 1 <= (int)IDGROUP_PLANEVRAM_END, "ID exceeded");
33:
34: #define ID_PLANE(id) (ID_PLANE0 + (id))
35:
36: //
37: // プレーン VRAM ビットマップ
38: //
39:
1.1.1.2 ! root 40: // イベントテーブル
! 41: wxBEGIN_EVENT_TABLE(WXPlaneVRAMBitmapPanel, inherited)
! 42: EVT_MOUSE_EVENTS(WXPlaneVRAMBitmapPanel::OnMouse)
! 43: wxEND_EVENT_TABLE()
! 44:
1.1 root 45: // コンストラクタ
46: WXPlaneVRAMBitmapPanel::WXPlaneVRAMBitmapPanel(wxWindow *parent)
47: : inherited(parent)
48: {
1.1.1.2 ! root 49: // デバイスを取得
! 50: planevram = GetPlaneVRAMDevice();
! 51: // どちらか一方は NULL のはず
! 52: bt45x = gMainApp.FindObject<BT45xDevice>(OBJ_BT45x);
! 53: videoctlr = gMainApp.FindObject<VideoCtlrDevice>(OBJ_VIDEOCTLR);
! 54:
! 55: plane_width = planevram->GetComposite().GetWidth();
! 56: plane_height = planevram->GetComposite().GetHeight();
1.1 root 57:
58: // まず等倍でセット
59: scale = 1;
60: SetScale(1);
61:
1.1.1.2 ! root 62: cursor = wxDefaultPosition;
! 63:
1.1 root 64: // VM からの通知を受け取る
65: WXUIMessage::Connect(UIMessage::PALETTE, this,
66: wxCommandEventHandler(WXPlaneVRAMBitmapPanel::OnPaletteChanged));
67: }
68:
69: // デストラクタ
70: WXPlaneVRAMBitmapPanel::~WXPlaneVRAMBitmapPanel()
71: {
1.1.1.2 ! root 72: WXUIMessage::Disconnect(UIMessage::PALETTE, this,
1.1 root 73: wxCommandEventHandler(WXPlaneVRAMBitmapPanel::OnPaletteChanged));
74: }
75:
76: // XXX 実際にはこの辺かどこかで更新チェックをしないといけない。
77: // 今は表示領域を毎回描画しているので無駄。
78:
79: // ビットマッププレーンの内容を描画する
80: void
81: WXPlaneVRAMBitmapPanel::Draw()
82: {
83: assert(view.GetRight() < virtual_width);
84: assert(view.GetBottom() < virtual_height);
85:
1.1.1.2 ! root 86: const BitmapI8& src = planevram->GetComposite();
1.1 root 87:
88: if (scale > 1) {
89: Rect dr(0, 0, bitmap.GetWidth(), bitmap.GetHeight());
90: bitmap.DrawScale(dr, src, &pal[0], view.x, scale, 1, view.y, scale, 1);
91: } else {
92: bitmap.DrawBitmap(0, 0, src, &pal[0], view);
93: }
94: }
95:
96: // プレーン選択変更
97: void
98: WXPlaneVRAMBitmapPanel::EnablePlane(int plane, bool value)
99: {
100: if (value) {
101: planemask |= 1U << plane;
102: } else {
103: planemask &= ~(1U << plane);
104: }
105:
106: // パレット生成
107: GenPalette();
108: }
109:
110: // パレット適用変更
111: void
112: WXPlaneVRAMBitmapPanel::EnablePalette(bool value)
113: {
114: apply_palette = value;
115:
116: // パレット生成
117: GenPalette();
118: }
119:
120: // VM からのパレット変更通知
121: void
122: WXPlaneVRAMBitmapPanel::OnPaletteChanged(wxCommandEvent& event)
123: {
124: // パレット生成
125: GenPalette();
126: }
127:
128: // パレット生成
129: void
130: WXPlaneVRAMBitmapPanel::GenPalette()
131: {
132: if (apply_palette) {
133: // パレットを適用する場合
134: const ColorXBGR *vmpal;
135: // 雑に X680x0/LUNA 対応
1.1.1.2 ! root 136: if (videoctlr) {
! 137: vmpal = &(videoctlr->GetHostPalette())[0];
1.1 root 138: } else {
1.1.1.2 ! root 139: vmpal = &(bt45x->GetHostPalette())[0];
1.1 root 140: }
141: const Color bg = vmpal[0].ToColor();
142: for (int i = 0; i < pal.size(); i++) {
143: if ((i & planemask)) {
144: int n = i % 16;
145: pal[i] = vmpal[n].ToColor();
146: } else {
147: pal[i] = bg;
148: }
149: }
150: } else {
151: // パレットを適用しない場合
152: for (int i = 0; i < pal.size(); i++) {
153: if ((i & planemask)) {
154: pal[i] = Color(255, 255, 255);
155: } else {
156: pal[i] = Color(0, 0, 0);
157: }
158: }
159: }
160: }
161:
162: // 倍率を設定
163: void
164: WXPlaneVRAMBitmapPanel::SetScale(int scale_)
165: {
166: int before = scale;
167:
168: scale = scale_;
169:
170: // 仮想画面の大きさを計算
171: virtual_width = plane_width * scale;
172: virtual_height = plane_height * scale;
173:
174: // ビューのスケール変換は前後のスケールがいるのでここでやる
175: view.x = view.x * scale / before;
176: view.y = view.y * scale / before;
177: view.w = view.w * scale / before;
178: view.h = view.h * scale / before;
179:
180: SetMaxClientSize(wxSize(virtual_width, virtual_height));
181: }
182:
183: // スクロール位置変更通知
184: void
185: WXPlaneVRAMBitmapPanel::OnScroll(wxScrollEvent& event)
186: {
187: int orient = event.GetOrientation();
188: int pos = event.GetPosition();
189:
190: if (orient == wxHORIZONTAL) {
191: view.x = pos;
192: } else {
193: view.y = pos;
194: }
195: }
196:
1.1.1.2 ! root 197: // マウスイベント
! 198: void
! 199: WXPlaneVRAMBitmapPanel::OnMouse(wxMouseEvent& event)
! 200: {
! 201: auto window = dynamic_cast<WXPlaneVRAMWindow*>(GetGrandParent());
! 202: assert(window);
! 203:
! 204: // pt はこのパネル左上からの位置
! 205: const wxPoint& pt = event.GetPosition();
! 206:
! 207: // pt がパネル内の座標なのに Leaving() になることがある。
! 208: // pt がパネル外の座標なのに Leaving() にならないことがある。
! 209: if (event.Leaving() == false && GetClientRect().Contains(pt)) {
! 210: // cursor は仮想画面上の座標
! 211: cursor.x = (view.x + pt.x) / scale;
! 212: cursor.y = (view.y + pt.y) / scale;
! 213: assertmsg(cursor.x < plane_width, "cursor.x=%d", cursor.x);
! 214: assertmsg(cursor.y < plane_height, "cursor.y=%d", cursor.y);
! 215: } else {
! 216: cursor = wxDefaultPosition;
! 217: }
! 218: }
! 219:
1.1 root 220:
221: //
222: // プレーン VRAM パネル
223: //
224:
1.1.1.2 ! root 225: // +------------------------+------------------+
! 226: // | WXPlaneVRAMBitmapPanel | WXScrollBar(V) |
! 227: // | VRAM 表示パネル | 縦スクロールバー |
! 228: // +------------------------+------------------+
! 229: // | WXScrollBar(H) | WXBitmapPanel |
! 230: // | 横スクロールバー | 空き地 |
! 231: // +------------------------+------------------+
! 232:
1.1 root 233: // イベントテーブル
234: wxBEGIN_EVENT_TABLE(WXPlaneVRAMPanel, inherited)
235: EVT_SIZE(WXPlaneVRAMPanel::OnSize)
236: wxEND_EVENT_TABLE()
237:
238: // コンストラクタ
239: WXPlaneVRAMPanel::WXPlaneVRAMPanel(wxWindow *parent)
240: : inherited(parent)
241: {
242: // 表示パネル
243: viewctl = new WXPlaneVRAMBitmapPanel(this);
244:
245: // スクロールバーを自分で持つ (配置は Layout() で行う)
246: vscroll = new WXScrollBar(this, wxID_ANY, wxSB_VERTICAL);
247: hscroll = new WXScrollBar(this, wxID_ANY, wxSB_HORIZONTAL);
248:
249: // 右下の空き地
250: corner = new WXBitmapPanel(this);
251:
252: // Sizer 使わず自前でレイアウトする。
253: SetAutoLayout(true);
254:
255: // 最小値は適当
256: int vscr_width = vscroll->GetClientSize().x;
257: int hscr_height = hscroll->GetClientSize().y;
258: SetMinClientSize(wxSize(16 + vscr_width, 16 + hscr_height));
259: SetMaxClientSize(wxSize(viewctl->virtual_width + vscr_width,
260: viewctl->virtual_height + hscr_height));
261:
262: // スクロールバーからの通知イベントは表示パネルへ回す
263: vscroll->Connect(NONO_EVT_SCROLL,
264: wxScrollEventHandler(WXPlaneVRAMBitmapPanel::OnScroll), NULL, viewctl);
265: hscroll->Connect(NONO_EVT_SCROLL,
266: wxScrollEventHandler(WXPlaneVRAMBitmapPanel::OnScroll), NULL, viewctl);
267: }
268:
269: // デストラクタ
270: WXPlaneVRAMPanel::~WXPlaneVRAMPanel()
271: {
272: }
273:
274: bool
275: WXPlaneVRAMPanel::Layout()
276: {
277: if (inherited::Layout() == false) {
278: return false;
279: }
280:
281: // コントロールを配置
282: const wxSize size = GetClientSize();
283: int vscr_width = vscroll->GetClientSize().x;
284: int hscr_height = hscroll->GetClientSize().y;
285: int view_width = size.x - vscr_width;
286: int view_height = size.y - hscr_height;
287: viewctl->SetSize(0, 0, view_width, view_height);
288: vscroll->SetSize(view_width, 0, vscr_width, view_height);
289: hscroll->SetSize(0, view_height, view_width, hscr_height);
290: corner->SetSize(view_width, view_height, vscr_width, hscr_height);
291:
292: // XXX 内部のコントロールのサイズが確定してからでないと DoSize() できない
293: DoSize(size);
294:
295: return true;
296: }
297:
298: // サイズ変更イベント
299: void
300: WXPlaneVRAMPanel::OnSize(wxSizeEvent& event)
301: {
302: // Mac ではウィンドウ作成時に 0 以下の値が来ることがある。
303: // GTK では 1 で来ることがある。
304: wxSize size = event.GetSize();
305: if (size.x <= 1 || size.y <= 1) {
306: return;
307: }
308:
309: // Layout() を呼ぶために必要
310: event.Skip();
311:
312: // XXX レイアウトが終わった後でないと DoSize() できない
313: // DoSize(size);
314: }
315:
316: // サイズ変更処理。パネルサイズと倍率が変わったら呼ぶこと。
317: // 倍率が変わった場合もスクロールバーの再設定とかがあるため。
318: void
319: WXPlaneVRAMPanel::DoSize(wxSize size)
320: {
321: wxSize viewsize = viewctl->GetClientSize();
322: int virtual_width = viewctl->virtual_width;
323: int virtual_height = viewctl->virtual_height;
324: Rect& view = viewctl->view;
325:
326: // スクロールバーが右端まで行っている場合は右を固定したように見せる。
327: // ただし全体が見えている場合は除く。
328:
329: if (view.x > 0 &&
330: (view.x + view.w == virtual_width ||
331: view.x + viewsize.x >= virtual_width))
332: {
333: view.x = virtual_width - viewsize.x;
334: if (view.x < 0) {
335: view.x = 0;
336: }
337: }
338: view.w = viewsize.x;
339:
340: // 下端も同様
341: if (view.y > 0 &&
342: (view.y + view.h >= virtual_height ||
343: view.y + size.y >= virtual_height))
344: {
345: view.y = virtual_height - viewsize.y;
346: if (view.y < 0) {
347: view.y = 0;
348: }
349: }
350: view.h = viewsize.y;
351:
352: // スクロールバー(横)を再設定
353: assert(hscroll);
354: int hpos = view.x;
355: int hthumbsize = view.w;
356: int hrange = virtual_width;
357: int hpagesize = view.w;
358: hscroll->SetScrollParam(hpos, hthumbsize, hrange, hpagesize);
359:
360: // スクロールバー(縦)を再設定
361: assert(vscroll);
362: int vpos = view.y;
363: int vthumbsize = view.h;
364: int vrange = virtual_height;
365: int vpagesize = view.h;
366: vscroll->SetScrollParam(vpos, vthumbsize, vrange, vpagesize);
367: }
368:
369: // プレーン選択変更
370: void
371: WXPlaneVRAMPanel::EnablePlane(int plane, bool value)
372: {
373: viewctl->EnablePlane(plane, value);
374: }
375:
376: // パレット適用変更
377: void
378: WXPlaneVRAMPanel::EnablePalette(bool value)
379: {
380: viewctl->EnablePalette(value);
381: }
382:
383: // 倍率を設定
384: void
385: WXPlaneVRAMPanel::SetScale(int scale_)
386: {
387: viewctl->SetScale(scale_);
388:
389: int vscr_width = vscroll->GetClientSize().x;
390: int hscr_height = hscroll->GetClientSize().y;
391: SetMaxClientSize(wxSize(viewctl->virtual_width + vscr_width,
392: viewctl->virtual_height + hscr_height));
393:
394: DoSize(GetSize());
395: }
396:
397:
398: //
399: // プレーン VRAM ウィンドウ
400: //
401:
1.1.1.2 ! root 402: // ↓+-----------------------------+
! 403: // | wx コントロール用パネル |
! 404: // +-----------------------------+
! 405: // | WXTextScreen *statuspanel | ← 情報欄
! 406: // +-----------------------------+
! 407: // | WXPlaneVRAMPanel *viewpanel | ← ビットマップ (スクロールバー込み)
! 408: // +-----------------------------+
! 409:
1.1 root 410: // イベントテーブル
411: wxBEGIN_EVENT_TABLE(WXPlaneVRAMWindow, inherited)
412: EVT_COMMAND_RANGE(ID_PLANE0, ID_PLANE7, wxEVT_CHECKBOX,
413: WXPlaneVRAMWindow::OnPlane)
414: EVT_CHECKBOX(ID_PALETTE, WXPlaneVRAMWindow::OnApplyPalette)
415: EVT_CHOICE(ID_SCALE, WXPlaneVRAMWindow::OnScale)
1.1.1.2 ! root 416: EVT_TIMER(wxID_ANY, WXPlaneVRAMWindow::OnTimer)
1.1 root 417: wxEND_EVENT_TABLE()
418:
419: // コンストラクタ
420: WXPlaneVRAMWindow::WXPlaneVRAMWindow(wxWindow *parent, const wxString& name)
421: : inherited(parent, wxID_ANY, name,
422: inherited::DEFAULT_STYLE | wxRESIZE_BORDER)
423: {
1.1.1.2 ! root 424: // デバイスを取得
! 425: planevram = GetPlaneVRAMDevice();
! 426:
! 427: int nplane = planevram->GetPlaneCount();
! 428:
1.1 root 429: auto *topsizer = new wxBoxSizer(wxVERTICAL);
430:
431: // 上にコントロールパネル(のまず下敷き)
1.1.1.2 ! root 432: ctrlpanel = new wxPanel(this);
1.1 root 433: topsizer->Add(ctrlpanel, 0, wxEXPAND);
434:
435: // コントロールパネル用の横 Sizer
436: auto *ctrlbox = new wxBoxSizer(wxHORIZONTAL);
437:
1.1.1.2 ! root 438: // プレーン選択チェックボックス
! 439: auto *csbox = new wxStaticBoxSizer(wxHORIZONTAL, ctrlpanel,
! 440: _("Planes to display"));
! 441: // GTK3 標準のレンダリングだとコントロールの周りの空きがなさすぎて
! 442: // 特にスタティックボックスは読みづらいので自力で少し空ける。どうして…。
! 443: ctrlbox->Add(csbox, 0, wxALIGN_CENTER | wxALL, 3);
! 444: // X68030 なら 4プレーン。
! 445: // LUNA で 1bpp なら 4プレーン (上位3つは Disabled)、
! 446: // LUNA で 4bpp なら 4プレーン、
! 447: // LUNA で 8bpp なら 8プレーン。
! 448: int nplane4 = (nplane == 1) ? 4 : nplane;
! 449: for (int i = 0; i < nplane4; i++) {
! 450: auto *ctrl = new wxCheckBox(ctrlpanel, ID_PLANE(i),
! 451: string_format("%d", i));
! 452: planesw.push_back(ctrl);
! 453: }
! 454: // 降順に並べる
! 455: for (int i = nplane4 - 1; i >= 0; i--) {
! 456: csbox->Add(planesw[i]);
1.1 root 457: }
458:
459: // パレット合成チェックボックス
460: applysw = new wxCheckBox(ctrlpanel, ID_PALETTE, _("Apply palette"));
1.1.1.2 ! root 461: ctrlbox->Add(applysw, 0, wxALIGN_CENTER | wxALL, 3);
1.1 root 462:
463: // セパレータを出したいのだが…
1.1.1.2 ! root 464: ctrlbox->Add(new wxStaticText(ctrlpanel, wxID_ANY, "|"), 0,
! 465: wxALIGN_CENTER | wxALL, 3);
1.1 root 466:
467: // 倍率
468: wxString scale_choice[] = {
469: "x1",
470: "x2",
471: "x4",
472: };
473: scalesw = new wxChoice(ctrlpanel, ID_SCALE,
474: wxDefaultPosition, wxDefaultSize,
475: countof(scale_choice), scale_choice);
1.1.1.2 ! root 476: ctrlbox->Add(scalesw, 0, wxALIGN_CENTER | wxALL, 3);
1.1 root 477:
478: // sizer と下敷きパネルを紐付ける
479: ctrlpanel->SetSizer(ctrlbox);
480: ctrlbox->SetSizeHints(ctrlpanel);
481:
1.1.1.2 ! root 482: statuspanel = new WXTextScreen(this, nnSize(50, nplane));
! 483: topsizer->Add(statuspanel, 0, wxEXPAND);
! 484:
1.1 root 485: viewpanel = new WXPlaneVRAMPanel(this);
486: topsizer->Add(viewpanel, 1, wxEXPAND);
487:
488: SetSizer(topsizer);
489:
490: // 最小サイズは上コントロール部の横幅が最小基準
491: wxSize minsize = ctrlpanel->GetMinClientSize();
492: minsize.y += viewpanel->GetMinClientSize().y;
493: SetMinClientSize(minsize);
494: // 最大サイズは DoScale() 内で設定している
495:
1.1.1.2 ! root 496: // 1bpp ならプレーン選択はすべて無効、それ以外はすべて有効。
! 497: if (nplane == 1) {
1.1 root 498: for (int i = 0; i < planesw.size(); i++) {
499: planesw[i]->Enable(false);
500: }
501: }
502:
503: // 初期値をコントロールにセットする
504: if (sticky_used) {
505: // 前回の値があればそれを使う。
506: for (int i = 0; i < planesw.size(); i++) {
507: planesw[i]->SetValue(sticky_plane[i]);
508: }
509: applysw->SetValue(sticky_apply);
510: scalesw->SetSelection(sticky_scale);
511: } else {
1.1.1.2 ! root 512: if (nplane == 1) {
1.1 root 513: // 初回 1bpp なら #0 プレーンのみオン。
514: for (int i = 0; i < planesw.size(); i++) {
515: planesw[i]->SetValue((i == 0));
516: }
517: applysw->SetValue(true);
518: } else {
519: // 初回 4bpp なら全プレーンオン。
520: for (int i = 0; i < planesw.size(); i++) {
521: planesw[i]->SetValue(true);
522: }
523: applysw->SetValue(true);
524: }
525: scalesw->SetSelection(0);
526: }
527:
528: // SetValue() ではイベントが飛ばないので、
529: // 直接呼び出してイベントが来たことにする。うーん…。
530: for (int i = 0; i < planesw.size(); i++) {
531: DoPlane(i, planesw[i]->IsChecked());
532: }
533: DoApplyPalette(applysw->IsChecked());
534: DoScale(scalesw->GetSelection());
1.1.1.2 ! root 535:
! 536: // 更新タイマーはモニターパネル同様の 30Hz にしておく。
! 537: timer.SetOwner(this);
! 538: timer.Start(33);
1.1 root 539: }
540:
541: // デストラクタ
542: WXPlaneVRAMWindow::~WXPlaneVRAMWindow()
543: {
544: }
545:
546: void
547: WXPlaneVRAMWindow::FontChanged()
548: {
549: // 親 (wxSubWindow) の FontChanged() を実行しないようにするための
550: // 空のオーバーライド。親の FontChanged() が呼ぶ DoSize() が
551: // ウィンドウサイズを変更しようとするが、ここはテキストフォントを
552: // 持っていないのでその操作は不要。うーん。
553: }
554:
555: // プレーン選択のチェックボックスイベント
556: void
557: WXPlaneVRAMWindow::OnPlane(wxCommandEvent& event)
558: {
559: int plane = event.GetId() - ID_PLANE0;
560: bool value = event.IsChecked();
561:
562: DoPlane(plane, value);
563: }
564:
565: // プレーン選択 ON/OFF 処理
566: void
567: WXPlaneVRAMWindow::DoPlane(int plane, bool value)
568: {
569: // コントロールに指示
570: viewpanel->EnablePlane(plane, value);
571:
572: // 値を保存
573: sticky_plane[plane] = value;
574: sticky_used = true;
575: }
576:
577: // パレット合成のチェックボックスイベント
578: void
579: WXPlaneVRAMWindow::OnApplyPalette(wxCommandEvent& event)
580: {
581: bool value = event.IsChecked();
582:
583: DoApplyPalette(value);
584: }
585:
586: // パレット合成 ON/OFF 処理
587: void
588: WXPlaneVRAMWindow::DoApplyPalette(bool value)
589: {
590: // コントロールに指示
591: viewpanel->EnablePalette(value);
592:
593: // 値を保存
594: sticky_apply = value;
595: sticky_used = true;
596: }
597:
598: // 倍率変更イベント
599: void
600: WXPlaneVRAMWindow::OnScale(wxCommandEvent& event)
601: {
602: int scale_index = event.GetSelection();
603:
604: DoScale(scale_index);
605: }
606:
607: // 倍率変更処理
608: void
609: WXPlaneVRAMWindow::DoScale(int scale_index)
610: {
611: viewpanel->SetScale(1 << scale_index);
612:
613: // 最大サイズは表示部の最大サイズから
614: wxSize maxsize = viewpanel->GetMaxClientSize();
615: maxsize.y += ctrlpanel->GetMinClientSize().y;
1.1.1.2 ! root 616: maxsize.y += statuspanel->GetMinClientSize().y;
1.1 root 617: SetMaxClientSize(maxsize);
618:
619: // 保存しておくのはインデックスのほう
620: sticky_scale = scale_index;
621: }
622:
1.1.1.2 ! root 623: // タイマーイベント
! 624: void
! 625: WXPlaneVRAMWindow::OnTimer(wxTimerEvent& event)
! 626: {
! 627: viewpanel->Refresh();
! 628:
! 629: // この位置の情報で screen を更新
! 630: TextScreen& screen = statuspanel->GetScreen();
! 631: const wxPoint& cursor = viewpanel->GetCursor();
! 632:
! 633: // UpdateInfo() は cursor.x が範囲外の時の描画も行っている。
! 634: planevram->UpdateInfo(screen, cursor.x, cursor.y);
! 635: statuspanel->Refresh();
! 636: }
! 637:
1.1 root 638: // static 変数
1.1.1.2 ! root 639: std::array<bool, WXPlaneVRAMWindow::MAX_PLANES> WXPlaneVRAMWindow::sticky_plane;
1.1 root 640: bool WXPlaneVRAMWindow::sticky_apply;
641: int WXPlaneVRAMWindow::sticky_scale;
642: bool WXPlaneVRAMWindow::sticky_used;
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.