mpv-engine 0.2.0

Toolkit-agnostic libmpv embedding core: handle lifecycle, commands, typed events, and render seams.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
//! Raw Windows plumbing for the exported-frame backend: a hidden WGL
//! context on an invisible window, GL framebuffers whose color storage
//! *is* a shared D3D11 texture (via `WGL_NV_DX_interop2`), and the
//! opengl32 symbol loader mpv renders through. opengl32, d3d11, dxgi,
//! user32, gdi32 and kernel32 are linked directly — no Rust dependency
//! enters the tree for this.
//!
//! Why D3D11 is the export currency here: Windows has no IOSurface and
//! no dmabuf, but every consumer API on the platform (D3D11, D3D12,
//! Vulkan via `VK_KHR_external_memory_win32`, and therefore wgpu) can
//! open a *shared NT handle* to a DXGI resource. `WGL_NV_DX_interop2`
//! is the one seam that lets a GL context render straight into such a
//! resource — it is what mpv's own `--gpu-context=dxinterop` and OBS
//! ride on — so the exportable framebuffer is a BGRA8
//! `D3D11_BIND_RENDER_TARGET` texture registered as a GL texture and
//! attached to an FBO.
//!
//! Two textures per buffer, not one. `wglDXRegisterObjectNV` refuses a
//! texture created with `D3D11_RESOURCE_MISC_SHARED_NTHANDLE` —
//! registration fails outright, verified on both the NVIDIA and Intel
//! ICDs — while `ID3D12Device::OpenSharedHandle`, and therefore wgpu's
//! DX12 import, accepts *only* NT handles. Those two requirements
//! cannot meet on one resource, so each pool buffer carries an interop
//! texture that GL renders into (legacy `D3D11_RESOURCE_MISC_SHARED`,
//! see [`D3D11_MISC_SHARED`]) plus an export texture that the shared NT
//! handle names, with a `CopyResource` between them in the publish
//! barrier. The copy is GPU-local on the render device — no readback —
//! but it does put the Windows tier one copy off the genuinely
//! zero-copy macOS and Linux paths.
//!
//! Thread discipline: everything that touches GL or the interop device —
//! context creation and currency, [`SurfaceBuffer::new`],
//! [`GlContext::begin_render`], [`GlContext::publish_barrier`],
//! [`SurfaceBuffer::delete_gl`] — runs only on the engine's export
//! render thread (the module's only GL caller), so GL's thread rules are
//! upheld by construction rather than by types. The D3D11 device is
//! free-threaded; its immediate context is not, so it lives behind a
//! mutex ([`D3d11::context`]) taken by [`SurfaceBuffer::copy_pixels`],
//! which any thread may call, by the publish barrier's `CopyResource`,
//! and by every `WGL_NV_DX_interop2` call — those drive the immediate
//! context inside the ICD, so being on the render thread does not
//! excuse them from the mutex. The shared NT handle is a kernel object, valid
//! from any thread or process — which is exactly why it is the export
//! currency (the IOSurface / dmabuf-fd analog).
//!
//! Buffer lifetime: each pool buffer owns two D3D11 textures, the export
//! texture's shared NT handle, and an interop registration plus GL
//! names. The export texture and handle travel across threads inside
//! [`ExportedFrame`](crate::ExportedFrame) and keep the memory alive on
//! their own — the buffer holds an `Arc` on the D3D11 device, so a frame
//! outliving the render thread stays readable; the GL and interop
//! handles are render-thread-only and die with the context. The same
//! split as macOS's retained IOSurface vs. GL names.

use std::ffi::{CString, c_char, c_void};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use parking_lot::Mutex;

use super::{ExportBackend, ExportBuffer};
use crate::error::{Error, Result};

// ---- Win32 base types ----

type Handle = *mut c_void;
type Hwnd = *mut c_void;
type Hdc = *mut c_void;
type Hglrc = *mut c_void;
type Hmodule = *mut c_void;
type Bool32 = i32;
type Hresult = i32;

const NULL_HANDLE: Handle = std::ptr::null_mut();

#[repr(C)]
#[derive(Clone, Copy)]
struct Guid {
    data1: u32,
    data2: u16,
    data3: u16,
    data4: [u8; 8],
}

/// `IID_IDXGIFactory1`.
const IID_IDXGI_FACTORY1: Guid = Guid {
    data1: 0x770a_ae78,
    data2: 0xf26f,
    data3: 0x4dba,
    data4: [0xa8, 0x29, 0x25, 0x3c, 0x83, 0xd1, 0xb3, 0x87],
};
#[cfg(feature = "wgpu")]
/// `IID_IUnknown` — the one interface [`ReleaseKeeper`] answers to.
const IID_IUNKNOWN: Guid = Guid {
    data1: 0x0000_0000,
    data2: 0x0000,
    data3: 0x0000,
    data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46],
};
#[cfg(feature = "wgpu")]
/// Private-data slot this module parks its [`ReleaseKeeper`] in. A
/// GUID of our own: private data is keyed by GUID, so a unique one
/// cannot collide with wgpu's or the driver's own slots.
const GUID_EXPORT_RELEASE_KEEPER: Guid = Guid {
    data1: 0x7b1c_4f2a,
    data2: 0x9d63,
    data3: 0x4c58,
    data4: [0xa1, 0x0e, 0x6f, 0x2d, 0x83, 0xb4, 0x71, 0x95],
};
/// `IID_ID3D11Multithread`.
const IID_ID3D11_MULTITHREAD: Guid = Guid {
    data1: 0x9b7e_4e00,
    data2: 0x342c,
    data3: 0x4106,
    data4: [0xa1, 0x9f, 0x4f, 0x27, 0x04, 0xf6, 0x89, 0xf0],
};
/// `IID_IDXGIResource1`.
const IID_IDXGI_RESOURCE1: Guid = Guid {
    data1: 0x3096_1379,
    data2: 0x4609,
    data3: 0x4a41,
    data4: [0x99, 0x8e, 0x54, 0xfe, 0x56, 0x7e, 0xe0, 0xc1],
};

/// `DXGI_ERROR_NOT_FOUND` — `EnumAdapters1` past the last adapter.
const DXGI_ERROR_NOT_FOUND: Hresult = 0x887A_0002u32 as Hresult;

#[link(name = "kernel32")]
unsafe extern "system" {
    fn GetModuleHandleW(name: *const u16) -> Hmodule;
    fn GetModuleHandleA(name: *const c_char) -> Hmodule;
    fn GetProcAddress(module: Hmodule, name: *const c_char) -> *mut c_void;
    fn CloseHandle(object: Handle) -> Bool32;
}

// ---- The hidden window (user32 + gdi32) ----

const CS_OWNDC: u32 = 0x0020;
const WS_OVERLAPPED: u32 = 0;
const PFD_DRAW_TO_WINDOW: u32 = 0x0000_0004;
const PFD_SUPPORT_OPENGL: u32 = 0x0000_0020;
const PFD_DOUBLEBUFFER: u32 = 0x0000_0001;
const PFD_TYPE_RGBA: u8 = 0;
const PFD_MAIN_PLANE: u8 = 0;

#[repr(C)]
struct WndClassW {
    style: u32,
    wnd_proc: Option<unsafe extern "system" fn(Hwnd, u32, usize, isize) -> isize>,
    cls_extra: i32,
    wnd_extra: i32,
    instance: Hmodule,
    icon: Handle,
    cursor: Handle,
    background: Handle,
    menu_name: *const u16,
    class_name: *const u16,
}

#[repr(C)]
#[derive(Default)]
struct PixelFormatDescriptor {
    size: u16,
    version: u16,
    flags: u32,
    pixel_type: u8,
    color_bits: u8,
    red_bits: u8,
    red_shift: u8,
    green_bits: u8,
    green_shift: u8,
    blue_bits: u8,
    blue_shift: u8,
    alpha_bits: u8,
    alpha_shift: u8,
    accum_bits: u8,
    accum_red_bits: u8,
    accum_green_bits: u8,
    accum_blue_bits: u8,
    accum_alpha_bits: u8,
    depth_bits: u8,
    stencil_bits: u8,
    aux_buffers: u8,
    layer_type: u8,
    reserved: u8,
    layer_mask: u32,
    visible_mask: u32,
    damage_mask: u32,
}

#[link(name = "user32")]
unsafe extern "system" {
    fn RegisterClassW(class: *const WndClassW) -> u16;
    fn DefWindowProcW(hwnd: Hwnd, msg: u32, wparam: usize, lparam: isize) -> isize;
    fn CreateWindowExW(
        ex_style: u32,
        class_name: *const u16,
        window_name: *const u16,
        style: u32,
        x: i32,
        y: i32,
        width: i32,
        height: i32,
        parent: Hwnd,
        menu: Handle,
        instance: Hmodule,
        param: *mut c_void,
    ) -> Hwnd;
    fn DestroyWindow(hwnd: Hwnd) -> Bool32;
    fn GetDC(hwnd: Hwnd) -> Hdc;
    fn ReleaseDC(hwnd: Hwnd, hdc: Hdc) -> i32;
}

#[link(name = "gdi32")]
unsafe extern "system" {
    fn ChoosePixelFormat(hdc: Hdc, pfd: *const PixelFormatDescriptor) -> i32;
    fn SetPixelFormat(hdc: Hdc, format: i32, pfd: *const PixelFormatDescriptor) -> Bool32;
}

// ---- WGL plus the GL 1.1 core opengl32 exports ----

// Texture/FBO GL enums shared with the other backends.
use super::gl_consts::{
    GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER, GL_FRAMEBUFFER_COMPLETE, GL_NEAREST, GL_RGBA8,
    GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER,
};

/// `DXGI_FORMAT_B8G8R8A8_UNORM`: little-endian B,G,R,A bytes — the
/// layout Vulkan calls `B8G8R8A8_UNORM` and wgpu calls `Bgra8Unorm`,
/// and byte-identical to the macOS and Linux backends' output.
pub(crate) const DXGI_FORMAT_BGRA8_UNORM: u32 = 87;

#[link(name = "opengl32")]
unsafe extern "system" {
    fn wglCreateContext(hdc: Hdc) -> Hglrc;
    fn wglDeleteContext(ctx: Hglrc) -> Bool32;
    fn wglMakeCurrent(hdc: Hdc, ctx: Hglrc) -> Bool32;
    fn wglGetCurrentContext() -> Hglrc;
    fn wglGetProcAddress(name: *const c_char) -> *mut c_void;

    fn glGenTextures(n: i32, textures: *mut u32);
    fn glDeleteTextures(n: i32, textures: *const u32);
    fn glBindTexture(target: u32, texture: u32);
    fn glTexParameteri(target: u32, pname: u32, param: i32);
    fn glFinish();
}

// ---- WGL_NV_DX_interop2 and ARB_framebuffer_object (loaded, not
// linked: both are extensions, resolved against the live context) ----

/// `WGL_ACCESS_READ_WRITE_NV` — mpv both clears and blends into the
/// target, so the GL side must be able to read it back.
const WGL_ACCESS_READ_WRITE_NV: u32 = 0x0000_0001;

type PfnDxOpenDevice = unsafe extern "system" fn(*mut c_void) -> Handle;
type PfnDxCloseDevice = unsafe extern "system" fn(Handle) -> Bool32;
type PfnDxRegisterObject = unsafe extern "system" fn(Handle, *mut c_void, u32, u32, u32) -> Handle;
type PfnDxUnregisterObject = unsafe extern "system" fn(Handle, Handle) -> Bool32;
type PfnDxLockObjects = unsafe extern "system" fn(Handle, i32, *mut Handle) -> Bool32;

#[allow(non_snake_case)]
struct GlFns {
    GenFramebuffers: unsafe extern "system" fn(i32, *mut u32),
    DeleteFramebuffers: unsafe extern "system" fn(i32, *const u32),
    BindFramebuffer: unsafe extern "system" fn(u32, u32),
    FramebufferTexture2D: unsafe extern "system" fn(u32, u32, u32, u32, i32),
    CheckFramebufferStatus: unsafe extern "system" fn(u32) -> u32,
    DXOpenDevice: PfnDxOpenDevice,
    DXCloseDevice: PfnDxCloseDevice,
    DXRegisterObject: PfnDxRegisterObject,
    DXUnregisterObject: PfnDxUnregisterObject,
    DXLockObjects: PfnDxLockObjects,
    DXUnlockObjects: PfnDxLockObjects,
}

/// Load one entry point through [`gl_proc_address`] as the given fn
/// type, erroring on NULL.
macro_rules! load_fn {
    ($name:literal as $ty:ty) => {{
        let ptr = gl_proc_address($name);
        if ptr.is_null() {
            return Err(Error::ExportSetup(
                concat!("required GL/WGL entry point missing: ", $name).into(),
            ));
        }
        // SAFETY: resolved from the live GL implementation under the
        // name whose documented signature `$ty` spells.
        unsafe { std::mem::transmute::<*mut c_void, $ty>(ptr) }
    }};
}

// ---- D3D11 / DXGI, hand-rolled COM ----

const D3D_DRIVER_TYPE_UNKNOWN: u32 = 0;
const D3D_DRIVER_TYPE_HARDWARE: u32 = 1;
const D3D11_SDK_VERSION: u32 = 7;
/// `D3D11_CREATE_DEVICE_BGRA_SUPPORT` — required for BGRA render
/// targets on some feature levels, and free everywhere else.
const D3D11_CREATE_DEVICE_BGRA_SUPPORT: u32 = 0x20;

const D3D11_USAGE_DEFAULT: u32 = 0;
const D3D11_USAGE_STAGING: u32 = 3;
const D3D11_BIND_SHADER_RESOURCE: u32 = 0x8;
const D3D11_BIND_RENDER_TARGET: u32 = 0x20;
const D3D11_CPU_ACCESS_READ: u32 = 0x0002_0000;
/// `D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE`.
/// Deliberately *not* `_KEYEDMUTEX`: a keyed mutex would have to be
/// acquired by the consumer, and neither wgpu nor a plain
/// `OpenSharedHandle` importer knows to do that. Cross-API ordering is
/// carried by the publish barrier instead — the same discipline as the
/// Linux backend's `glFinish` (see [`GlContext::publish_barrier`]).
const D3D11_MISC_SHARED_NTHANDLE: u32 = 0x0002 | 0x0800;
/// `D3D11_RESOURCE_MISC_SHARED` — the *legacy* shared flag, and the only
/// one `wglDXRegisterObjectNV` will register. Adding
/// `_NTHANDLE` to it makes registration fail on every ICD tested, which
/// is why the interop texture and the export texture are separate
/// resources (see the module docs).
const D3D11_MISC_SHARED: u32 = 0x0002;
const D3D11_MAP_READ: u32 = 1;
/// `GENERIC_ALL` — the access mask `CreateSharedHandle` wants for a
/// resource the consumer may both read and (in principle) write.
const GENERIC_ALL: u32 = 0x1000_0000;

#[repr(C)]
struct Texture2dDesc {
    width: u32,
    height: u32,
    mip_levels: u32,
    array_size: u32,
    format: u32,
    sample_count: u32,
    sample_quality: u32,
    usage: u32,
    bind_flags: u32,
    cpu_access_flags: u32,
    misc_flags: u32,
}

#[repr(C)]
struct MappedSubresource {
    data: *mut c_void,
    row_pitch: u32,
    depth_pitch: u32,
}

/// Vtable prefixes: only the methods this module calls are typed, and
/// everything before them is an opaque slot — so the layout below spells
/// the real COM indices, and nothing past the last named method is ever
/// dereferenced.
#[repr(C)]
struct IUnknownVtbl {
    query_interface:
        unsafe extern "system" fn(*mut c_void, *const Guid, *mut *mut c_void) -> Hresult,
    add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
    release: unsafe extern "system" fn(*mut c_void) -> u32,
}

#[repr(C)]
struct ID3D11DeviceVtbl {
    _unknown: IUnknownVtbl,
    _create_buffer: *const c_void,
    _create_texture_1d: *const c_void,
    /// 5.
    create_texture_2d: unsafe extern "system" fn(
        *mut c_void,
        *const Texture2dDesc,
        *const c_void,
        *mut *mut c_void,
    ) -> Hresult,
}

#[repr(C)]
struct ID3D11DeviceContextVtbl {
    _unknown: IUnknownVtbl,
    /// `ID3D11DeviceChild` (3..=6).
    _device_child: [*const c_void; 4],
    /// 7..=13.
    _before_map: [*const c_void; 7],
    /// 14.
    map: unsafe extern "system" fn(
        *mut c_void,
        *mut c_void,
        u32,
        u32,
        u32,
        *mut MappedSubresource,
    ) -> Hresult,
    /// 15.
    unmap: unsafe extern "system" fn(*mut c_void, *mut c_void, u32),
    /// 16..=46.
    _before_copy_resource: [*const c_void; 31],
    /// 47.
    copy_resource: unsafe extern "system" fn(*mut c_void, *mut c_void, *mut c_void),
    /// 48..=110.
    _before_flush: [*const c_void; 63],
    /// 111.
    flush: unsafe extern "system" fn(*mut c_void),
}

#[cfg(feature = "wgpu")]
/// `ID3D12Object`'s prefix: `IUnknown`, then `GetPrivateData` (3),
/// `SetPrivateData` (4), `SetPrivateDataInterface` (5). Every
/// `ID3D12Resource` is an `ID3D12Object`, so slot 5 is reachable
/// through any resource pointer.
#[repr(C)]
struct ID3D12ObjectVtbl {
    _unknown: IUnknownVtbl,
    _get_private_data: *const c_void,
    _set_private_data: *const c_void,
    /// 5.
    set_private_data_interface:
        unsafe extern "system" fn(*mut c_void, *const Guid, *mut c_void) -> Hresult,
}

#[repr(C)]
struct ID3D11MultithreadVtbl {
    _unknown: IUnknownVtbl,
    /// 3, 4.
    _enter_leave: [*const c_void; 2],
    /// 5.
    set_multithread_protected: unsafe extern "system" fn(*mut c_void, Bool32) -> Bool32,
}

#[repr(C)]
struct IDXGIResource1Vtbl {
    _unknown: IUnknownVtbl,
    /// `IDXGIObject` (3..=6).
    _object: [*const c_void; 4],
    /// `IDXGIDeviceSubObject` (7).
    _device_subobject: *const c_void,
    /// `IDXGIResource` (8..=11).
    _resource: [*const c_void; 4],
    /// 12.
    _create_subresource_surface: *const c_void,
    /// 13.
    create_shared_handle: unsafe extern "system" fn(
        *mut c_void,
        *const c_void,
        u32,
        *const u16,
        *mut Handle,
    ) -> Hresult,
}

#[repr(C)]
struct IDXGIFactory1Vtbl {
    _unknown: IUnknownVtbl,
    /// `IDXGIObject` (3..=6).
    _object: [*const c_void; 4],
    /// `IDXGIFactory` (7..=11).
    _factory: [*const c_void; 5],
    /// 12.
    enum_adapters1: unsafe extern "system" fn(*mut c_void, u32, *mut *mut c_void) -> Hresult,
}

/// Borrow a COM object's vtable. An interface pointer's first word *is*
/// the vtable pointer, by the ABI's definition.
unsafe fn vtbl<'a, V>(obj: *mut c_void) -> &'a V {
    unsafe { &**obj.cast::<*const V>() }
}

unsafe fn com_release(obj: *mut c_void) {
    if !obj.is_null() {
        unsafe { (vtbl::<IUnknownVtbl>(obj).release)(obj) };
    }
}

#[link(name = "d3d11")]
unsafe extern "system" {
    fn D3D11CreateDevice(
        adapter: *mut c_void,
        driver_type: u32,
        software: Hmodule,
        flags: u32,
        feature_levels: *const u32,
        num_feature_levels: u32,
        sdk_version: u32,
        device: *mut *mut c_void,
        feature_level: *mut u32,
        immediate_context: *mut *mut c_void,
    ) -> Hresult;
}

#[link(name = "dxgi")]
unsafe extern "system" {
    fn CreateDXGIFactory1(riid: *const Guid, factory: *mut *mut c_void) -> Hresult;
}

fn hresult_error(what: &str, hr: Hresult) -> Error {
    Error::ExportSetup(format!("{what}: HRESULT {:#010x}", hr as u32))
}

// ---- Completion seam for cross-API imports ----

#[cfg(feature = "wgpu")]
/// A minimal `IUnknown` whose only job is to run a callback when its
/// last reference goes away.
///
/// Parked in an `ID3D12Resource`'s private data, it becomes the drop
/// callback D3D12 does not otherwise offer: the resource releases its
/// private-data interfaces when it is destroyed, and the consumer
/// (wgpu) destroys the resource only once the GPU has finished every
/// submission that used it. That is precisely the moment the pool may
/// recycle the buffer again — the same guarantee wgpu-hal's Metal
/// backend hands us through `DropCallback`, which the DX12 backend has
/// no equivalent of.
///
/// Layout is load-bearing: `vtable` must be the first field, because a
/// COM interface pointer *is* a pointer to its vtable pointer.
#[repr(C)]
struct ReleaseKeeper {
    vtable: *const IUnknownVtbl,
    refcount: std::sync::atomic::AtomicU32,
    /// Taken and run by the final `Release`. `None` once fired, or when
    /// the attach failed and the caller reclaimed responsibility.
    on_release: Option<Box<dyn FnOnce() + Send>>,
}

#[cfg(feature = "wgpu")]
/// SAFETY: `this` is a live `ReleaseKeeper` — the only pointer ever
/// handed to D3D12 under [`GUID_EXPORT_RELEASE_KEEPER`], kept alive by
/// its own refcount.
unsafe fn keeper<'a>(this: *mut c_void) -> &'a ReleaseKeeper {
    unsafe { &*this.cast::<ReleaseKeeper>() }
}

#[cfg(feature = "wgpu")]
unsafe extern "system" fn keeper_query_interface(
    this: *mut c_void,
    iid: *const Guid,
    out: *mut *mut c_void,
) -> Hresult {
    const E_POINTER: Hresult = -0x7FFF_BFFD; // 0x80004003
    const E_NOINTERFACE: Hresult = -0x7FFF_BFFE; // 0x80004002
    if out.is_null() {
        return E_POINTER;
    }
    // Only `IUnknown`: nothing is ever meant to *use* this object, only
    // to hold and eventually release it.
    let matches = !iid.is_null()
        && unsafe {
            let iid = &*iid;
            iid.data1 == IID_IUNKNOWN.data1
                && iid.data2 == IID_IUNKNOWN.data2
                && iid.data3 == IID_IUNKNOWN.data3
                && iid.data4 == IID_IUNKNOWN.data4
        };
    if matches {
        unsafe {
            keeper_add_ref(this);
            *out = this;
        }
        return 0;
    }
    unsafe { *out = std::ptr::null_mut() };
    E_NOINTERFACE
}

#[cfg(feature = "wgpu")]
unsafe extern "system" fn keeper_add_ref(this: *mut c_void) -> u32 {
    unsafe { keeper(this) }
        .refcount
        .fetch_add(1, Ordering::Relaxed)
        + 1
}

#[cfg(feature = "wgpu")]
unsafe extern "system" fn keeper_release(this: *mut c_void) -> u32 {
    let left = unsafe { keeper(this) }
        .refcount
        .fetch_sub(1, Ordering::AcqRel)
        - 1;
    if left == 0 {
        // Reassemble the box and fire. Any thread may land here — this
        // is whichever thread wgpu destroys the resource on — so the
        // callback must be `Send` and must not assume the render thread.
        let mut owned = unsafe { Box::from_raw(this.cast::<ReleaseKeeper>()) };
        if let Some(on_release) = owned.on_release.take() {
            on_release();
        }
    }
    left
}

#[cfg(feature = "wgpu")]
static KEEPER_VTABLE: IUnknownVtbl = IUnknownVtbl {
    query_interface: keeper_query_interface,
    add_ref: keeper_add_ref,
    release: keeper_release,
};

#[cfg(feature = "wgpu")]
/// Arrange for `on_release` to run when `resource` is destroyed.
///
/// `resource` must be an `ID3D12Resource` (or any `ID3D12Object`) whose
/// only reference is about to be handed to a consumer. Returns whether
/// the callback was attached; on `false` it has *not* run and never
/// will, so the caller keeps responsibility for whatever it owns.
pub(crate) fn on_resource_release(
    resource: *mut c_void,
    on_release: Box<dyn FnOnce() + Send>,
) -> bool {
    let raw = Box::into_raw(Box::new(ReleaseKeeper {
        vtable: &KEEPER_VTABLE,
        // One reference: ours, released below once D3D12 has taken its own.
        refcount: std::sync::atomic::AtomicU32::new(1),
        on_release: Some(on_release),
    }));
    let hr = unsafe {
        (vtbl::<ID3D12ObjectVtbl>(resource).set_private_data_interface)(
            resource,
            &GUID_EXPORT_RELEASE_KEEPER,
            raw.cast::<c_void>(),
        )
    };
    if hr < 0 {
        // Nothing took a reference, so our release below would fire the
        // callback immediately — exactly what the caller must not have,
        // since it is about to fall back to handling this itself.
        unsafe { (*raw).on_release = None };
    }
    unsafe { com_release(raw.cast::<c_void>()) };
    hr >= 0
}

// ---- The D3D11 device shared by the context and every buffer ----

/// The D3D11 device the exportable textures are allocated on, plus its
/// immediate context (for CPU readback only). Held through an `Arc` by
/// the [`GlContext`] *and* by every [`SurfaceBuffer`], so a frame the
/// shell is still holding stays readable after the render thread is
/// gone — the Windows counterpart to a retained IOSurface, or to a
/// dmabuf fd outliving its EGL display.
struct D3d11 {
    device: *mut c_void,
    /// D3D11 devices are free-threaded; immediate contexts are not.
    /// This mutex is the module's one serialization point for the
    /// immediate context, and it covers more than the calls that name it
    /// directly: `WGL_NV_DX_interop2`'s open/register/lock/unlock/
    /// unregister/close all drive the immediate context inside the ICD,
    /// so they take it too. Leaving the interop calls out is not a
    /// theoretical hazard — the render thread hangs inside
    /// `wglDXLockObjectsNV` when the shell calls
    /// [`SurfaceBuffer::copy_pixels`] at the same moment, which an
    /// acquire-and-inspect consumer does on every frame.
    context: Mutex<*mut c_void>,
}

// SAFETY: the device is free-threaded by D3D11's own contract, and the
// immediate context is only ever touched under `context`'s mutex.
unsafe impl Send for D3d11 {}
unsafe impl Sync for D3d11 {}

impl D3d11 {
    /// Create a device on `adapter` (null = whatever
    /// `D3D_DRIVER_TYPE_HARDWARE` picks).
    fn create(adapter: *mut c_void) -> Result<Self> {
        let driver_type = if adapter.is_null() {
            D3D_DRIVER_TYPE_HARDWARE
        } else {
            // An explicit adapter obligates UNKNOWN — D3D11 returns
            // E_INVALIDARG otherwise.
            D3D_DRIVER_TYPE_UNKNOWN
        };
        let mut device: *mut c_void = std::ptr::null_mut();
        let mut context: *mut c_void = std::ptr::null_mut();
        let hr = unsafe {
            D3D11CreateDevice(
                adapter,
                driver_type,
                std::ptr::null_mut(),
                D3D11_CREATE_DEVICE_BGRA_SUPPORT,
                std::ptr::null(),
                0,
                D3D11_SDK_VERSION,
                &mut device,
                std::ptr::null_mut(),
                &mut context,
            )
        };
        if hr < 0 || device.is_null() || context.is_null() {
            unsafe {
                com_release(context);
                com_release(device);
            }
            return Err(hresult_error("D3D11CreateDevice", hr));
        }
        // Belt and braces with `context`'s mutex below. That mutex covers
        // every immediate-context call *this module* makes, including the
        // interop entry points; multithread protection additionally covers
        // the ones the ICD makes behind our back — `wglDXLockObjectsNV`
        // and friends drive the immediate context internally, and so does
        // the D3D11-on-D3D12/DXGI plumbing under some drivers. Failing to
        // acquire the interface is not fatal: our own mutex is what the
        // correctness argument rests on.
        unsafe {
            let mut multithread: *mut c_void = std::ptr::null_mut();
            let hr = (vtbl::<IUnknownVtbl>(context).query_interface)(
                context,
                &IID_ID3D11_MULTITHREAD,
                &mut multithread,
            );
            if hr >= 0 && !multithread.is_null() {
                (vtbl::<ID3D11MultithreadVtbl>(multithread).set_multithread_protected)(
                    multithread,
                    1,
                );
                com_release(multithread);
            } else {
                tracing::debug!("export: ID3D11Multithread unavailable (HRESULT {hr:#010x})");
            }
        }
        Ok(Self {
            device,
            context: Mutex::new(context),
        })
    }
}

impl Drop for D3d11 {
    fn drop(&mut self) {
        unsafe {
            com_release(*self.context.get_mut());
            com_release(self.device);
        }
    }
}

/// Every DXGI adapter, in the factory's own order. Empty when the
/// factory itself is unavailable — [`GlContext::new`] then falls back to
/// the one "let D3D11 pick" attempt. Callers must release each adapter.
fn enumerate_adapters() -> Vec<*mut c_void> {
    let mut factory: *mut c_void = std::ptr::null_mut();
    let hr = unsafe { CreateDXGIFactory1(&IID_IDXGI_FACTORY1, &mut factory) };
    if hr < 0 || factory.is_null() {
        return Vec::new();
    }
    let mut adapters = Vec::new();
    let mut index = 0u32;
    loop {
        let mut adapter: *mut c_void = std::ptr::null_mut();
        let hr = unsafe {
            (vtbl::<IDXGIFactory1Vtbl>(factory).enum_adapters1)(factory, index, &mut adapter)
        };
        if hr == DXGI_ERROR_NOT_FOUND || hr < 0 || adapter.is_null() {
            break;
        }
        adapters.push(adapter);
        index += 1;
    }
    unsafe { com_release(factory) };
    adapters
}

// ---- RAII pieces so `GlContext::new`'s error paths unwind cleanly ----

/// The invisible window whose DC carries the pixel format. Never shown
/// and never pumped: WGL wants a DC with a pixel format and nothing
/// more, because every rendering target here is an FBO.
struct HiddenWindow {
    hwnd: Hwnd,
    hdc: Hdc,
}

impl HiddenWindow {
    fn new() -> Result<Self> {
        // "mpv-engine-export", UTF-16 and NUL-terminated.
        const CLASS_NAME: &[u16] = &[
            0x6d, 0x70, 0x76, 0x2d, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2d, 0x65, 0x78, 0x70,
            0x6f, 0x72, 0x74, 0x00,
        ];
        let instance = unsafe { GetModuleHandleW(std::ptr::null()) };
        // The class is process-wide and outlives any one context, so
        // register it exactly once; a repeat registration would fail and
        // `CreateWindowExW` below is the real check anyway.
        static REGISTERED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
        REGISTERED.get_or_init(|| {
            let class = WndClassW {
                style: CS_OWNDC,
                wnd_proc: Some(DefWindowProcW),
                cls_extra: 0,
                wnd_extra: 0,
                instance,
                icon: NULL_HANDLE,
                cursor: NULL_HANDLE,
                background: NULL_HANDLE,
                menu_name: std::ptr::null(),
                class_name: CLASS_NAME.as_ptr(),
            };
            unsafe { RegisterClassW(&class) };
        });
        let hwnd = unsafe {
            CreateWindowExW(
                0,
                CLASS_NAME.as_ptr(),
                CLASS_NAME.as_ptr(),
                WS_OVERLAPPED,
                0,
                0,
                1,
                1,
                std::ptr::null_mut(),
                NULL_HANDLE,
                instance,
                std::ptr::null_mut(),
            )
        };
        if hwnd.is_null() {
            return Err(Error::ExportSetup(
                "CreateWindowExW failed for the hidden GL window (a session with no window \
                 station — a service, or SSH without an interactive desktop?)"
                    .into(),
            ));
        }
        let hdc = unsafe { GetDC(hwnd) };
        if hdc.is_null() {
            unsafe { DestroyWindow(hwnd) };
            return Err(Error::ExportSetup(
                "GetDC failed for the hidden window".into(),
            ));
        }
        Ok(Self { hwnd, hdc })
    }

    /// Pick and set the DC's pixel format. Any RGBA format does — the
    /// default framebuffer is never drawn to — so this is a plain
    /// `ChoosePixelFormat` on a minimal descriptor rather than the
    /// `WGL_ARB_pixel_format` two-context dance.
    fn set_pixel_format(&self) -> Result<()> {
        let pfd = PixelFormatDescriptor {
            size: std::mem::size_of::<PixelFormatDescriptor>() as u16,
            version: 1,
            flags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
            pixel_type: PFD_TYPE_RGBA,
            color_bits: 32,
            alpha_bits: 8,
            depth_bits: 24,
            stencil_bits: 8,
            layer_type: PFD_MAIN_PLANE,
            ..Default::default()
        };
        let format = unsafe { ChoosePixelFormat(self.hdc, &pfd) };
        if format == 0 {
            return Err(Error::ExportSetup(
                "ChoosePixelFormat found no OpenGL-capable format".into(),
            ));
        }
        if unsafe { SetPixelFormat(self.hdc, format, &pfd) } == 0 {
            return Err(Error::ExportSetup("SetPixelFormat failed".into()));
        }
        Ok(())
    }
}

impl Drop for HiddenWindow {
    fn drop(&mut self) {
        unsafe {
            ReleaseDC(self.hwnd, self.hdc);
            DestroyWindow(self.hwnd);
        }
    }
}

/// Half-built WGL context, so [`GlContext::new`]'s error paths unwind;
/// forgotten once it moves into the finished context.
struct WglContext {
    hglrc: Hglrc,
}

impl Drop for WglContext {
    fn drop(&mut self) {
        unsafe {
            if wglGetCurrentContext() == self.hglrc {
                wglMakeCurrent(std::ptr::null_mut(), std::ptr::null_mut());
            }
            wglDeleteContext(self.hglrc);
        }
    }
}

/// The hidden GL context the export render thread owns: a WGL context on
/// an invisible window's DC, tied to a D3D11 device through
/// `WGL_NV_DX_interop2`. Created, used, and dropped on that one thread.
pub(crate) struct GlContext {
    hglrc: Hglrc,
    /// The interop device tying the GL context to `d3d` — the handle
    /// every `wglDX*ObjectNV` call goes through.
    dx_device: Handle,
    fns: GlFns,
    // Teardown order = declaration order once `Drop`'s explicit part has
    // closed the interop device and the GL context: the D3D11 device
    // (released only when no frame still holds it), then the window
    // whose DC that context rendered on.
    d3d: Arc<D3d11>,
    window: HiddenWindow,
}

impl GlContext {
    /// Size of the throwaway buffer a candidate adapter is proved with.
    /// Small enough to cost nothing; the registration is what is under
    /// test, not the dimensions.
    const PROBE_SIZE: u32 = 64;

    /// One rung of the ladder: the whole backend on one adapter, proved
    /// end to end. The window and GL context are rebuilt per candidate
    /// rather than shared across the loop so that a rejected rung tears
    /// down through the ordinary `Drop` — which closes the interop
    /// device while its GL context is still current and its D3D11 device
    /// still alive, the only order the extension allows. Both are cheap,
    /// and this runs once per attach.
    fn try_adapter(adapter: *mut c_void) -> Result<Self> {
        let window = HiddenWindow::new()?;
        window.set_pixel_format()?;
        // A plain `wglCreateContext` yields the driver's best
        // compatibility profile (4.6 on every current ICD), which is
        // what mpv is happiest on — the `wglCreateContextAttribsARB`
        // ladder would buy nothing the CGL profile ladder buys on macOS.
        // Creating it first is not optional either way:
        // `wglGetProcAddress` resolves nothing without a current
        // context.
        let hglrc = unsafe { wglCreateContext(window.hdc) };
        if hglrc.is_null() {
            return Err(Error::ExportSetup(
                "wglCreateContext failed (no OpenGL ICD? the Microsoft software rasterizer is \
                 GL 1.1 and cannot drive mpv)"
                    .into(),
            ));
        }
        let context = WglContext { hglrc };
        if unsafe { wglMakeCurrent(window.hdc, context.hglrc) } == 0 {
            return Err(Error::ExportSetup("wglMakeCurrent failed".into()));
        }
        let fns = GlFns {
            GenFramebuffers: load_fn!(
                "glGenFramebuffers" as unsafe extern "system" fn(i32, *mut u32)
            ),
            DeleteFramebuffers: load_fn!(
                "glDeleteFramebuffers" as unsafe extern "system" fn(i32, *const u32)
            ),
            BindFramebuffer: load_fn!("glBindFramebuffer" as unsafe extern "system" fn(u32, u32)),
            FramebufferTexture2D: load_fn!(
                "glFramebufferTexture2D" as unsafe extern "system" fn(u32, u32, u32, u32, i32)
            ),
            CheckFramebufferStatus: load_fn!(
                "glCheckFramebufferStatus" as unsafe extern "system" fn(u32) -> u32
            ),
            DXOpenDevice: load_fn!("wglDXOpenDeviceNV" as PfnDxOpenDevice),
            DXCloseDevice: load_fn!("wglDXCloseDeviceNV" as PfnDxCloseDevice),
            DXRegisterObject: load_fn!("wglDXRegisterObjectNV" as PfnDxRegisterObject),
            DXUnregisterObject: load_fn!("wglDXUnregisterObjectNV" as PfnDxUnregisterObject),
            DXLockObjects: load_fn!("wglDXLockObjectsNV" as PfnDxLockObjects),
            DXUnlockObjects: load_fn!("wglDXUnlockObjectsNV" as PfnDxLockObjects),
        };

        let d3d = D3d11::create(adapter)?;
        let dx_device = unsafe { (fns.DXOpenDevice)(d3d.device) };
        if dx_device.is_null() {
            return Err(Error::ExportSetup(
                "wglDXOpenDeviceNV refused the D3D11 device (GL and D3D on different \
                 GPUs, or no WGL_NV_DX_interop2 support)"
                    .into(),
            ));
        }
        let hglrc = context.hglrc;
        std::mem::forget(context);
        let gl = Self {
            hglrc,
            dx_device,
            fns,
            d3d: Arc::new(d3d),
            window,
        };
        // The proof. Dropping `gl` on failure runs the full teardown, so
        // the next rung starts from a clean slate.
        let mut probe = SurfaceBuffer::new(&gl, Self::PROBE_SIZE, Self::PROBE_SIZE)?;
        probe.delete_gl(&gl);
        Ok(gl)
    }
}

impl ExportBackend for GlContext {
    type Buffer = SurfaceBuffer;

    /// The FBO color format reported to mpv (`OpenGlFbo::internal_format`).
    /// A hint only — the real storage layout is fixed by the D3D11 texture's
    /// `DXGI_FORMAT_B8G8R8A8_UNORM`, which GL learns through the interop
    /// registration.
    const FBO_INTERNAL_FORMAT: i32 = GL_RGBA8 as i32;

    /// Bring the backend up on the first adapter that actually *works*.
    /// A GL context and a D3D11 device that each come up fine can still
    /// refuse to interop when they landed on different GPUs — the
    /// ordinary case on a hybrid laptop, where the GL ICD picks the
    /// discrete chip while DXGI's adapter 0 is the integrated one — so
    /// every failure falls through to the next adapter (the same
    /// device-selection discipline as the Linux backend's render-node
    /// ladder) rather than failing the attach.
    ///
    /// "Works" has to mean a *registered buffer*, not an opened interop
    /// device. `wglDXOpenDeviceNV` returns a live handle for a
    /// mismatched GL/D3D pair on at least some ICDs, and the rejection
    /// only lands later, on the first `wglDXRegisterObjectNV`. Accepting
    /// an adapter on the open alone therefore produces the worst
    /// possible outcome: an attach that reports success, and a render
    /// loop that retries the failing allocation forever without ever
    /// publishing a frame. Each candidate is proved with a throwaway
    /// buffer here instead, so a bad adapter is a fall-through and total
    /// failure is an honest `ExportSetup` error the shell can see.
    fn new() -> Result<Self> {
        let adapters = enumerate_adapters();
        // The null entry is the "let D3D11 pick" fallback, so a box
        // whose DXGI factory refused to enumerate still gets one try.
        let candidates: Vec<*mut c_void> = if adapters.is_empty() {
            vec![std::ptr::null_mut()]
        } else {
            adapters.clone()
        };
        let mut last_error = None;
        let mut opened = None;
        for adapter in candidates {
            match Self::try_adapter(adapter) {
                Ok(context) => {
                    opened = Some(context);
                    break;
                }
                Err(e) => {
                    tracing::debug!("export: adapter unusable for GL interop: {e}");
                    last_error = Some(e);
                }
            }
        }
        for adapter in adapters {
            unsafe { com_release(adapter) };
        }
        opened.ok_or_else(|| {
            last_error.unwrap_or_else(|| {
                Error::ExportSetup("no D3D11 adapter could be opened for GL interop".into())
            })
        })
    }

    fn make_current(&self) -> Result<()> {
        if unsafe { wglMakeCurrent(self.window.hdc, self.hglrc) } == 0 {
            return Err(Error::ExportSetup("wglMakeCurrent failed".into()));
        }
        Ok(())
    }

    /// Hand the buffer's storage to GL before mpv renders into it. Under
    /// `WGL_NV_DX_interop2` a registered object's GL texture has usable
    /// storage only while *locked*, and D3D — hence anything that opened
    /// the shared handle — may only touch it while *unlocked*. Locking
    /// here and unlocking in
    /// [`publish_barrier`](Self::publish_barrier) is what makes that
    /// handover per-frame instead of a race.
    ///
    /// A no-op on macOS and Linux, whose exportable memory needs no
    /// handover.
    fn begin_render(&self, buffer: &SurfaceBuffer) {
        buffer.lock_for_gl(self);
    }

    /// Publish barrier before the shared texture is consumed by another
    /// API. Two things happen: `glFinish` retires mpv's GPU work — a
    /// shared NT handle carries no cross-API ordering guarantee, the
    /// consumer's D3D12/Vulkan device is a *different* device, and this
    /// backend deliberately allocates without a keyed mutex so wgpu can
    /// import the texture at all (same reasoning as the Linux dmabuf
    /// path) — and the interop unlock hands the texture back to D3D so
    /// the copy that follows is legal. [`SurfaceBuffer::copy_to_shared`]
    /// then fills the export texture and flushes, which is what makes
    /// the handle's openers see finished pixels.
    fn publish_barrier(&self, buffer: &SurfaceBuffer) {
        unsafe { glFinish() };
        buffer.unlock_from_gl(self);
        buffer.copy_to_shared();
    }

    fn proc_address(name: &str) -> *mut c_void {
        gl_proc_address(name)
    }
}

impl Drop for GlContext {
    fn drop(&mut self) {
        unsafe {
            // The interop device closes first: it is the only thing here
            // that needs both the GL context current and the D3D11
            // device alive. Under the immediate-context mutex, like
            // every other interop call: a frame the shell still holds
            // can be running `copy_pixels` on another thread right now.
            let ctx = self.d3d.context.lock();
            (self.fns.DXCloseDevice)(self.dx_device);
            drop(ctx);
            if wglGetCurrentContext() == self.hglrc {
                wglMakeCurrent(std::ptr::null_mut(), std::ptr::null_mut());
            }
            wglDeleteContext(self.hglrc);
        }
        // RAII fields finish the teardown in declaration order.
    }
}

/// One pool entry: a pair of BGRA8 `D3D11_BIND_RENDER_TARGET` textures —
/// the interop one registered through `WGL_NV_DX_interop2` as a GL
/// texture and attached to a framebuffer mpv renders into, and the
/// export one carrying the shared NT handle, filled from it by the
/// publish barrier (the module docs explain why one texture cannot do
/// both jobs). The texture
/// and handle travel across threads inside
/// [`ExportedFrame`](crate::ExportedFrame); the GL name and the interop
/// object are handles only the render thread ever dereferences.
pub(crate) struct SurfaceBuffer {
    /// Held by every buffer, not just by the context: an outstanding
    /// frame must stay readable after detach.
    d3d: Arc<D3d11>,
    /// The exported half: NT-handle-shareable, never registered with GL,
    /// written only by the publish barrier's `CopyResource`.
    texture: *mut c_void,
    /// The GL half: legacy-shared so `wglDXRegisterObjectNV` accepts it,
    /// never handed to a consumer.
    interop_texture: *mut c_void,
    handle: Handle,
    dx_object: Handle,
    gl_texture: u32,
    fbo: u32,
    /// Whether the interop object is currently locked for GL. Render
    /// thread only; atomic so the buffer stays `Sync` without a mutex.
    locked: AtomicBool,
    width: u32,
    height: u32,
}

// SAFETY: the D3D11 texture is free-threaded (read only through the
// mutex-guarded immediate context) and the shared handle is a kernel
// object. `dx_object` and the GL name are plain handles here; every call
// against them (creation, locking, deletion, mpv's renders) happens on
// the render thread — see the module docs' thread discipline. `Drop`
// touches only the texture's refcount and the handle.
unsafe impl Send for SurfaceBuffer {}
unsafe impl Sync for SurfaceBuffer {}

impl ExportBuffer for SurfaceBuffer {
    type Backend = GlContext;

    /// Create a `width`×`height` buffer. Render thread only, GL context
    /// current.
    fn new(gl: &GlContext, width: u32, height: u32) -> Result<Self> {
        let interop_texture =
            create_shared_texture(gl.d3d.device, width, height, D3D11_MISC_SHARED)?;
        let texture =
            match create_shared_texture(gl.d3d.device, width, height, D3D11_MISC_SHARED_NTHANDLE) {
                Ok(texture) => texture,
                Err(e) => {
                    unsafe { com_release(interop_texture) };
                    return Err(e);
                }
            };
        let handle = match create_shared_handle(texture) {
            Ok(handle) => handle,
            Err(e) => {
                unsafe {
                    com_release(texture);
                    com_release(interop_texture);
                }
                return Err(e);
            }
        };
        // From here on the value's own `Drop` covers the D3D side, and
        // every early return goes through `delete_gl` for the GL side.
        let mut buffer = Self {
            d3d: Arc::clone(&gl.d3d),
            texture,
            interop_texture,
            handle,
            dx_object: NULL_HANDLE,
            gl_texture: 0,
            fbo: 0,
            locked: AtomicBool::new(false),
            width,
            height,
        };
        let f = &gl.fns;
        unsafe {
            glGenTextures(1, &mut buffer.gl_texture);
            // Registration touches the immediate context too (see
            // `lock_for_gl`); the guard is scoped so the `lock_for_gl`
            // below — the mutex is not reentrant — takes it afresh.
            let _ctx = gl.d3d.context.lock();
            buffer.dx_object = (f.DXRegisterObject)(
                gl.dx_device,
                interop_texture,
                buffer.gl_texture,
                GL_TEXTURE_2D,
                WGL_ACCESS_READ_WRITE_NV,
            );
        }
        if buffer.dx_object.is_null() {
            buffer.delete_gl(gl);
            return Err(Error::ExportSetup(format!(
                "wglDXRegisterObjectNV failed for a {width}x{height} BGRA8 render target"
            )));
        }
        // The registered texture only has storage while locked, so the
        // framebuffer is built and validated inside a lock.
        buffer.lock_for_gl(gl);
        let status = unsafe {
            (f.GenFramebuffers)(1, &mut buffer.fbo);
            (f.BindFramebuffer)(GL_FRAMEBUFFER, buffer.fbo);
            (f.FramebufferTexture2D)(
                GL_FRAMEBUFFER,
                GL_COLOR_ATTACHMENT0,
                GL_TEXTURE_2D,
                buffer.gl_texture,
                0,
            );
            let status = (f.CheckFramebufferStatus)(GL_FRAMEBUFFER);
            (f.BindFramebuffer)(GL_FRAMEBUFFER, 0);
            glBindTexture(GL_TEXTURE_2D, buffer.gl_texture);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glBindTexture(GL_TEXTURE_2D, 0);
            status
        };
        buffer.unlock_from_gl(gl);
        if status != GL_FRAMEBUFFER_COMPLETE {
            buffer.delete_gl(gl);
            return Err(Error::ExportSetup(format!(
                "shared-texture framebuffer incomplete (status {status:#x})"
            )));
        }
        Ok(buffer)
    }

    fn fbo(&self) -> u32 {
        self.fbo
    }

    fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    /// Delete the GL texture/framebuffer names and the interop
    /// registration. Render thread only, GL context current; the D3D11
    /// texture and its shared handle are released by `Drop` (any
    /// thread). Buffers retired anywhere else simply skip this — their
    /// GL name and interop object die with the render thread's context.
    fn delete_gl(&mut self, gl: &GlContext) {
        // Unregistering a locked object is invalid, and a buffer can
        // reach here locked if mpv's render errored out mid-frame.
        self.unlock_from_gl(gl);
        let f = &gl.fns;
        unsafe {
            if self.fbo != 0 {
                (f.DeleteFramebuffers)(1, &self.fbo);
                self.fbo = 0;
            }
            if !self.dx_object.is_null() {
                let ctx = self.d3d.context.lock();
                (f.DXUnregisterObject)(gl.dx_device, self.dx_object);
                drop(ctx);
                self.dx_object = NULL_HANDLE;
            }
            if self.gl_texture != 0 {
                glDeleteTextures(1, &self.gl_texture);
                self.gl_texture = 0;
            }
        }
    }

    /// Copy the texture's pixels out as tightly packed BGRA rows
    /// (row 0 = top). Any thread: this goes through a staging texture on
    /// the D3D11 immediate context — no GL and no interop — so it works
    /// even after the render context is gone (the buffer's `Arc<D3d11>`
    /// keeps the device alive). Returns zeroed pixels if the staging
    /// copy or the map fails, mirroring the other platforms'
    /// lock-failure behavior.
    fn copy_pixels(&self) -> Vec<u8> {
        let (w, h) = (self.width as usize, self.height as usize);
        if w == 0 || h == 0 {
            return vec![0u8; w * h * 4];
        }
        let desc = Texture2dDesc {
            width: self.width,
            height: self.height,
            mip_levels: 1,
            array_size: 1,
            format: DXGI_FORMAT_BGRA8_UNORM,
            sample_count: 1,
            sample_quality: 0,
            usage: D3D11_USAGE_STAGING,
            bind_flags: 0,
            cpu_access_flags: D3D11_CPU_ACCESS_READ,
            misc_flags: 0,
        };
        let device = self.d3d.device;
        let mut staging: *mut c_void = std::ptr::null_mut();
        let hr = unsafe {
            (vtbl::<ID3D11DeviceVtbl>(device).create_texture_2d)(
                device,
                &desc,
                std::ptr::null(),
                &mut staging,
            )
        };
        if hr < 0 || staging.is_null() {
            return vec![0u8; w * h * 4];
        }
        let ctx = self.d3d.context.lock();
        unsafe {
            let v = vtbl::<ID3D11DeviceContextVtbl>(*ctx);
            (v.copy_resource)(*ctx, staging, self.texture);
            let mut mapped = MappedSubresource {
                data: std::ptr::null_mut(),
                row_pitch: 0,
                depth_pitch: 0,
            };
            let out = if (v.map)(*ctx, staging, 0, D3D11_MAP_READ, 0, &mut mapped) >= 0
                && !mapped.data.is_null()
            {
                // SAFETY: the staging copy is mapped; `data` covers
                // `row_pitch * h` readable bytes with `row_pitch >= w * 4`.
                let out = super::pack_bgra_rows(
                    mapped.data.cast::<u8>(),
                    mapped.row_pitch as usize,
                    w,
                    h,
                );
                (v.unmap)(*ctx, staging, 0);
                out
            } else {
                vec![0u8; w * h * 4]
            };
            com_release(staging);
            out
        }
    }
}

impl SurfaceBuffer {
    /// Take the interop object for GL, idempotently — re-locking a
    /// locked object is an error in the extension, and the render path
    /// pairs lock/unlock across a *fallible* mpv render.
    fn lock_for_gl(&self, gl: &GlContext) {
        if self.dx_object.is_null() || self.locked.swap(true, Ordering::SeqCst) {
            return;
        }
        let mut object = self.dx_object;
        // Under the immediate-context mutex: the ICD implements the lock
        // by driving the D3D11 immediate context, which `copy_pixels`
        // may be using from the shell's thread at this very moment (it
        // runs on every acquired frame). Racing the two hangs the render
        // thread inside the driver — see `D3d11::context`.
        let _ctx = self.d3d.context.lock();
        if unsafe { (gl.fns.DXLockObjects)(gl.dx_device, 1, &mut object) } == 0 {
            tracing::warn!("export: wglDXLockObjectsNV failed; this frame may be stale");
        }
    }

    /// Hand the interop object back to D3D, idempotently.
    fn unlock_from_gl(&self, gl: &GlContext) {
        if self.dx_object.is_null() || !self.locked.swap(false, Ordering::SeqCst) {
            return;
        }
        let mut object = self.dx_object;
        // Same immediate-context serialization as `lock_for_gl`.
        let _ctx = self.d3d.context.lock();
        if unsafe { (gl.fns.DXUnlockObjects)(gl.dx_device, 1, &mut object) } == 0 {
            tracing::warn!("export: wglDXUnlockObjectsNV failed; this frame may be torn");
        }
    }

    /// Fill the export texture from the one GL just rendered into.
    /// Called by the publish barrier *after* the interop unlock, because
    /// D3D may only touch a registered resource while it is unlocked.
    ///
    /// The `Flush` is not optional: the consumer opens the shared handle
    /// on a *different* device, and with no keyed mutex in play nothing
    /// else forces this copy out of the immediate context's queue before
    /// that device reads the texture.
    fn copy_to_shared(&self) {
        let ctx = self.d3d.context.lock();
        unsafe {
            let v = vtbl::<ID3D11DeviceContextVtbl>(*ctx);
            (v.copy_resource)(*ctx, self.texture, self.interop_texture);
            (v.flush)(*ctx);
        }
    }

    /// Windows's platform-native export accessor: the shared NT `HANDLE`.
    /// Deliberately outside the [`ExportBuffer`](super::ExportBuffer)
    /// contract — each platform's export currency differs — and used only
    /// by the Windows wgpu import and the frame's public `shared_handle`
    /// accessor.
    pub(crate) fn shared_handle(&self) -> Handle {
        self.handle
    }
}

impl Drop for SurfaceBuffer {
    fn drop(&mut self) {
        // Only the D3D side needs an explicit release here; GL names and
        // the interop registration are handled per `delete_gl`'s
        // contract. Closing our handle does not free the memory while a
        // consumer's opened copy still references it.
        unsafe {
            if !self.handle.is_null() {
                CloseHandle(self.handle);
            }
            com_release(self.texture);
            com_release(self.interop_texture);
        }
    }
}

/// Allocate one of a buffer's two textures: BGRA8 (the one format every
/// Windows consumer — D3D11, D3D12, Vulkan, wgpu `Bgra8Unorm` — takes
/// without conversion), renderable and shader-readable. `misc_flags`
/// picks which half it is: [`D3D11_MISC_SHARED`] for the texture GL
/// registers, [`D3D11_MISC_SHARED_NTHANDLE`] for the one the export
/// handle names. Neither takes a keyed mutex.
fn create_shared_texture(
    device: *mut c_void,
    width: u32,
    height: u32,
    misc_flags: u32,
) -> Result<*mut c_void> {
    let desc = Texture2dDesc {
        width,
        height,
        mip_levels: 1,
        array_size: 1,
        format: DXGI_FORMAT_BGRA8_UNORM,
        sample_count: 1,
        sample_quality: 0,
        usage: D3D11_USAGE_DEFAULT,
        bind_flags: D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE,
        cpu_access_flags: 0,
        misc_flags,
    };
    let mut texture: *mut c_void = std::ptr::null_mut();
    let hr = unsafe {
        (vtbl::<ID3D11DeviceVtbl>(device).create_texture_2d)(
            device,
            &desc,
            std::ptr::null(),
            &mut texture,
        )
    };
    if hr < 0 || texture.is_null() {
        return Err(hresult_error(
            &format!("CreateTexture2D for a shared {width}x{height} BGRA8 render target"),
            hr,
        ));
    }
    Ok(texture)
}

/// Export the texture's shared NT handle — the handle
/// [`ExportedFrame::shared_handle`](crate::ExportedFrame::shared_handle)
/// gives the shell.
fn create_shared_handle(texture: *mut c_void) -> Result<Handle> {
    let mut resource: *mut c_void = std::ptr::null_mut();
    let hr = unsafe {
        (vtbl::<IUnknownVtbl>(texture).query_interface)(
            texture,
            &IID_IDXGI_RESOURCE1,
            &mut resource,
        )
    };
    if hr < 0 || resource.is_null() {
        return Err(hresult_error("QueryInterface(IDXGIResource1)", hr));
    }
    let mut handle: Handle = NULL_HANDLE;
    let hr = unsafe {
        (vtbl::<IDXGIResource1Vtbl>(resource).create_shared_handle)(
            resource,
            std::ptr::null(),
            GENERIC_ALL,
            std::ptr::null(),
            &mut handle,
        )
    };
    unsafe { com_release(resource) };
    if hr < 0 || handle.is_null() {
        return Err(hresult_error("IDXGIResource1::CreateSharedHandle", hr));
    }
    Ok(handle)
}

/// mpv's GL loader for the hidden context, and this module's own.
/// Windows needs both halves of the well-known two-step: extensions
/// (everything framebuffer- and interop-related) resolve only through
/// `wglGetProcAddress` against the current context, while the GL 1.1
/// core that opengl32.dll exports resolves only through
/// `GetProcAddress` — and some drivers answer 1/2/3/-1 rather than NULL
/// from `wglGetProcAddress` for the latter, so those sentinels are
/// filtered too. A loader doing only one half silently fails mpv on half
/// its symbol requests (the Windows twin of the README's libepoxy note).
fn gl_proc_address(name: &str) -> *mut c_void {
    let Ok(cname) = CString::new(name) else {
        return std::ptr::null_mut();
    };
    let ptr = unsafe { wglGetProcAddress(cname.as_ptr()) };
    if !matches!(ptr as isize, -1..=3) {
        return ptr;
    }
    static OPENGL32: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
    // opengl32 is linked into the process, so it is already loaded.
    let module =
        *OPENGL32.get_or_init(|| unsafe { GetModuleHandleA(c"opengl32.dll".as_ptr()) as usize });
    if module == 0 {
        return std::ptr::null_mut();
    }
    unsafe { GetProcAddress(module as Hmodule, cname.as_ptr()) }
}