The Pedigree Project 0.1
VirtioGpu.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2#include "VirtioGpu.h"
3#include "pedigree/kernel/LockGuard.h"
4#include "pedigree/kernel/Log.h"
5#include "pedigree/kernel/Service.h"
6#include "pedigree/kernel/ServiceFeatures.h"
7#include "pedigree/kernel/ServiceManager.h"
8#include "pedigree/kernel/machine/Pci.h"
9#include "pedigree/kernel/panic.h"
10#include "pedigree/kernel/process/Scheduler.h"
11#include "pedigree/kernel/process/Thread.h"
12#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
13#include "pedigree/kernel/processor/VirtualAddressSpace.h"
14#include "pedigree/kernel/time/Time.h"
15#include "pedigree/kernel/utilities/String.h"
16#include "pedigree/kernel/utilities/utility.h"
17
18namespace {
19constexpr uint32_t Create2d = 0x0101;
20constexpr uint32_t SetScanout = 0x0103;
21constexpr uint32_t Flush = 0x0104;
22constexpr uint32_t Transfer2d = 0x0105;
23constexpr uint32_t AttachBacking = 0x0106;
24constexpr uint32_t GetDisplayInfo = 0x0100;
25constexpr uint32_t OkNoData = 0x1100;
26constexpr uint32_t OkDisplayInfo = 0x1101;
27constexpr uint32_t Fence = 1;
28constexpr uint32_t BgrxFormat = 2;
29constexpr uint32_t ResourceId = 1;
30constexpr size_t MaxWidth = 1920;
31constexpr size_t MaxHeight = 1200;
32constexpr size_t RefreshMilliseconds = 50;
33constexpr size_t ResponseOffset = 512;
34
35struct Header {
36 uint32_t type;
37 uint32_t flags;
38 uint64_t fenceId;
39 uint32_t contextId;
40 uint8_t ringIndex;
41 uint8_t padding[3];
42} __attribute__((packed));
43
44struct Rect {
45 uint32_t x;
46 uint32_t y;
47 uint32_t width;
48 uint32_t height;
49} __attribute__((packed));
50
51struct DisplayInfo {
52 Header header;
53 struct {
54 Rect rect;
55 uint32_t enabled;
56 uint32_t flags;
57 } scanouts[16];
58} __attribute__((packed));
59
60struct CreateResource {
61 Header header;
62 uint32_t resourceId;
63 uint32_t format;
64 uint32_t width;
65 uint32_t height;
66} __attribute__((packed));
67
68struct ResourceRectangle {
69 Header header;
70 Rect rect;
71 uint32_t resourceId;
72 uint32_t extra;
73} __attribute__((packed));
74
75struct TransferResource {
76 Header header;
77 Rect rect;
78 uint64_t offset;
79 uint32_t resourceId;
80 uint32_t padding;
81} __attribute__((packed));
82
83struct BackingEntry {
84 uint64_t address;
85 uint32_t length;
86 uint32_t padding;
87} __attribute__((packed));
88
89struct AttachResource {
90 Header header;
91 uint32_t resourceId;
92 uint32_t entries;
93} __attribute__((packed));
94
95static_assert(sizeof(Header) == 24, "virtio GPU control header has the wrong layout");
96static_assert(sizeof(DisplayInfo) == 408, "virtio GPU display response has the wrong layout");
97
98size_t pagesFor(size_t bytes) {
99 const size_t pageSize = PhysicalMemoryManager::getPageSize();
100 return (bytes + pageSize - 1) / pageSize;
101}
102} // namespace
103
104VirtioGpu::GpuFramebuffer::GpuFramebuffer(VirtioGpu* gpu)
105 : Framebuffer(), m_Callbacks(), m_Gpu(gpu) {}
106
107physical_uintptr_t VirtioGpu::GpuFramebuffer::getPhysicalPage(size_t offset) const {
108 if (!m_Gpu || offset >= getHeight() * getBytesPerLine()) {
109 return ~physical_uintptr_t(0);
110 }
111 const size_t pageSize = PhysicalMemoryManager::getPageSize();
112 auto* address =
113 static_cast<uint8_t*>(m_Gpu->m_Pixels.virtualAddress()) + (offset & ~(pageSize - 1));
114 physical_uintptr_t physical = 0;
115 size_t flags = 0;
116 if (!VirtualAddressSpace::getKernelAddressSpace().getMapping(address, physical, flags)) {
117 return ~physical_uintptr_t(0);
118 }
119 return physical;
120}
121
122void VirtioGpu::GpuFramebuffer::closeCallbacks() {
123 m_Callbacks.close();
124}
125
126void VirtioGpu::GpuFramebuffer::detach() {
127 m_Callbacks.wait();
128 m_Gpu = nullptr;
129 setActive(false);
130 setFramebuffer(0);
131}
132
133void VirtioGpu::GpuFramebuffer::hwRedraw(size_t x, size_t y, size_t width, size_t height) {
135 if (!m_Callbacks.tryAcquire(callback) || !m_Gpu) {
136 return;
137 }
138 m_Gpu->redraw(x, y, width, height);
139}
140
141VirtioGpu::VirtioGpu(Device* pci)
142 : Display(),
143 m_Pci(pci),
144 m_Transport(pci),
145 m_ControlQueue(),
146 m_Commands("virtio-gpu commands"),
147 m_Backing("virtio-gpu backing list"),
148 m_Pixels("virtio-gpu framebuffer"),
149 m_CommandLock(),
150 m_Framebuffer(nullptr),
151 m_Provider(nullptr),
152 m_RefreshThread(nullptr),
153 m_Mode(),
154 m_NextFence(0),
155 m_Scanout(0),
156 m_Ready(false),
157 m_Failed(false),
158 m_Registered(false),
159 m_Shutdown(false),
160 m_Stopping(false) {
161 setSpecificType(String("virtio-gpu-display"));
162}
163
164VirtioGpu::~VirtioGpu() {
165 shutdown();
166}
167
168bool VirtioGpu::transact(const void* request, size_t requestLength, uint32_t expectedResponse,
169 void* response, size_t responseLength) {
170 LockGuard<Mutex> guard(m_CommandLock);
171 if (!m_Ready || __atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE) ||
172 __atomic_load_n(&m_Stopping, __ATOMIC_ACQUIRE) || !request ||
173 requestLength < sizeof(Header) || responseLength < sizeof(Header) ||
174 responseLength > m_Commands.size() - ResponseOffset) {
175 return false;
176 }
177
178 uint64_t requestPhysical = 0;
179 uint8_t* requestBytes = nullptr;
180 if (requestLength <= ResponseOffset) {
181 requestBytes = static_cast<uint8_t*>(m_Commands.virtualAddress());
182 MemoryCopy(requestBytes, request, requestLength);
183 requestPhysical = m_Commands.physicalAddress();
184 } else if (request == m_Backing.virtualAddress() && requestLength <= m_Backing.size()) {
185 requestBytes = static_cast<uint8_t*>(m_Backing.virtualAddress());
186 requestPhysical = m_Backing.physicalAddress();
187 } else {
188 return false;
189 }
190
191 auto* header = reinterpret_cast<Header*>(requestBytes);
192 header->flags = Fence;
193 header->fenceId = ++m_NextFence;
194 auto* responseBytes = static_cast<uint8_t*>(m_Commands.virtualAddress()) + ResponseOffset;
195 ByteSet(responseBytes, 0, responseLength);
196
197 Virtio::Buffer buffers[] = {
198 {requestPhysical, static_cast<uint32_t>(requestLength), false},
199 {m_Commands.physicalAddress() + ResponseOffset, static_cast<uint32_t>(responseLength), true},
200 };
201 if (!m_ControlQueue.submit(buffers, 2, this)) {
202 __atomic_store_n(&m_Failed, true, __ATOMIC_RELEASE);
203 ERROR("virtio-gpu: control queue submission failed");
204 return false;
205 }
206 m_Transport.notify(0);
207
208 const uint64_t deadline = Time::getTicks() + 500 * Time::Multiplier::Millisecond;
209 Virtio::Completion completion{};
210 while (Time::getTicks() < deadline) {
211 if (m_ControlQueue.pop(completion)) {
212 break;
213 }
214 Time::delay(Time::Multiplier::Millisecond);
215 }
216 if (completion.cookie != this || completion.length < responseLength ||
217 completion.length > responseLength) {
218 __atomic_store_n(&m_Failed, true, __ATOMIC_RELEASE);
219 ERROR("virtio-gpu: control command timed out or returned an invalid length");
220 return false;
221 }
222
223 FENCE();
224 auto* result = reinterpret_cast<const Header*>(responseBytes);
225 if (result->type != expectedResponse || !(result->flags & Fence) ||
226 result->fenceId != header->fenceId) {
227 __atomic_store_n(&m_Failed, true, __ATOMIC_RELEASE);
228 ERROR("virtio-gpu: control command returned error " << Hex << result->type);
229 return false;
230 }
231 if (response) {
232 MemoryCopy(response, responseBytes, responseLength);
233 }
234 return true;
235}
236
237bool VirtioGpu::initialise() {
238 if (!m_Pci || m_Pci->getPciVendorId() != 0x1af4 || m_Pci->getPciDeviceId() != 0x1050 ||
239 !m_Transport.initialise() || !m_Transport.negotiate(0) ||
240 !m_Transport.setupQueue(0, m_ControlQueue)) {
241 return false;
242 }
243
244 auto& memory = PhysicalMemoryManager::instance();
246 if (!memory.allocateRegion(m_Commands, 1, PhysicalMemoryManager::continuous, mapFlags) ||
247 !m_Transport.ready() || !PciBus::instance().updateCommand(m_Pci, 0, 0x400U)) {
248 return false;
249 }
250 m_Ready = true;
251 (void)m_Transport.readIsr();
252
253 uint32_t scanoutCount = 0;
254 if (!m_Transport.readDeviceConfig32(8, scanoutCount) || !scanoutCount || scanoutCount > 16) {
255 return false;
256 }
257 const Header query = {GetDisplayInfo, 0, 0, 0, 0, {0, 0, 0}};
258 DisplayInfo info{};
259 if (!transact(&query, sizeof(query), OkDisplayInfo, &info, sizeof(info))) {
260 return false;
261 }
262
263 size_t width = 1024;
264 size_t height = 768;
265 for (uint32_t i = 0; i < scanoutCount; ++i) {
266 const auto& candidate = info.scanouts[i];
267 if (candidate.enabled && candidate.rect.width && candidate.rect.height &&
268 candidate.rect.width <= MaxWidth && candidate.rect.height <= MaxHeight) {
269 m_Scanout = i;
270 width = candidate.rect.width;
271 height = candidate.rect.height;
272 break;
273 }
274 }
275
276 const size_t bytes = width * height * 4;
277 const size_t pixelPages = pagesFor(bytes);
278 const size_t backingBytes = sizeof(AttachResource) + pixelPages * sizeof(BackingEntry);
279 if (!memory.allocateRegion(m_Pixels, pixelPages, 0, mapFlags) ||
280 !memory.allocateRegion(m_Backing, pagesFor(backingBytes), PhysicalMemoryManager::continuous,
281 mapFlags)) {
282 ERROR("virtio-gpu: framebuffer allocation failed");
283 return false;
284 }
285 ByteSet(m_Pixels.virtualAddress(), 0, bytes);
286
287 CreateResource create{};
288 create.header.type = Create2d;
289 create.resourceId = ResourceId;
290 create.format = BgrxFormat;
291 create.width = width;
292 create.height = height;
293 if (!transact(&create, sizeof(create), OkNoData, nullptr, sizeof(Header))) {
294 return false;
295 }
296
297 auto* attach = static_cast<AttachResource*>(m_Backing.virtualAddress());
298 ByteSet(attach, 0, backingBytes);
299 attach->header.type = AttachBacking;
300 attach->resourceId = ResourceId;
301 attach->entries = pixelPages;
302 auto* entries = reinterpret_cast<BackingEntry*>(
303 static_cast<uint8_t*>(m_Backing.virtualAddress()) + sizeof(AttachResource));
304 for (size_t i = 0; i < pixelPages; ++i) {
305 auto* page =
306 static_cast<uint8_t*>(m_Pixels.virtualAddress()) + i * PhysicalMemoryManager::getPageSize();
307 physical_uintptr_t physical = 0;
308 size_t flags = 0;
309 if (!VirtualAddressSpace::getKernelAddressSpace().getMapping(page, physical, flags)) {
310 return false;
311 }
312 entries[i].address = physical;
313 entries[i].length = bytes - i * PhysicalMemoryManager::getPageSize();
314 if (entries[i].length > PhysicalMemoryManager::getPageSize()) {
315 entries[i].length = PhysicalMemoryManager::getPageSize();
316 }
317 }
318 if (!transact(attach, backingBytes, OkNoData, nullptr, sizeof(Header))) {
319 return false;
320 }
321
322 m_Mode.width = width;
323 m_Mode.height = height;
324 ResourceRectangle scanout{};
325 scanout.header.type = SetScanout;
326 scanout.rect.width = width;
327 scanout.rect.height = height;
328 scanout.resourceId = m_Scanout;
329 scanout.extra = ResourceId;
330 if (!transact(&scanout, sizeof(scanout), OkNoData, nullptr, sizeof(Header)) ||
331 !update(0, 0, width, height)) {
332 return false;
333 }
334
335 m_Framebuffer = new GpuFramebuffer(this);
336 m_Provider = new GraphicsService::GraphicsProvider;
337 if (!m_Framebuffer || !m_Provider) {
338 ERROR("virtio-gpu: could not allocate graphics provider");
339 return false;
340 }
341 m_Framebuffer->setWidth(width);
342 m_Framebuffer->setHeight(height);
343 m_Framebuffer->setBytesPerPixel(4);
344 m_Framebuffer->setBytesPerLine(width * 4);
345 m_Framebuffer->setFormat(Graphics::Bits32_Rgb);
346 m_Framebuffer->setFramebuffer(reinterpret_cast<uintptr_t>(m_Pixels.virtualAddress()));
347
348 m_Mode.id = 1;
349 m_Mode.framebuffer = 0;
350 m_Mode.pf.mRed = 8;
351 m_Mode.pf.pRed = 16;
352 m_Mode.pf.mGreen = 8;
353 m_Mode.pf.pGreen = 8;
354 m_Mode.pf.mBlue = 8;
355 m_Mode.pf.pBlue = 0;
356 m_Mode.pf.mAlpha = 0;
357 m_Mode.pf.pAlpha = 24;
358 m_Mode.pf.nBpp = 32;
359 m_Mode.pf.nPitch = width * 4;
360 m_Mode.pf2 = Graphics::Bits32_Rgb;
361 m_Mode.bytesPerLine = width * 4;
362 m_Mode.bytesPerPixel = 4;
363 m_Mode.textMode = false;
364
365 m_Provider->pDisplay = this;
366 m_Provider->pFramebuffer = m_Framebuffer;
367 m_Provider->maxWidth = width;
368 m_Provider->maxHeight = height;
369 m_Provider->maxDepth = 32;
370 m_Provider->maxTextWidth = 0;
371 m_Provider->maxTextHeight = 0;
372 m_Provider->bHardwareAccel = false;
373 m_Provider->bTextModes = false;
374 m_Provider->bFirmwareFallback = false;
375 if (!registerProvider()) {
376 return false;
377 }
378
379 m_RefreshThread = new Thread(Scheduler::instance().getKernelProcess(), refreshThread, this,
380 nullptr, false, false, true);
381 if (!m_RefreshThread) {
382 ERROR("virtio-gpu: could not allocate refresh worker");
383 return false;
384 }
385 m_RefreshThread->setName(String("virtio-gpu refresh"));
386 if (!m_RefreshThread->start()) {
387 ERROR("virtio-gpu: could not start refresh worker");
388 __atomic_store_n(&m_Stopping, true, __ATOMIC_RELEASE);
389 if (!m_RefreshThread->joinForCompletion()) {
390 panic("virtio-gpu: failed refresh worker could not be joined");
391 }
392 m_RefreshThread = nullptr;
393 return false;
394 }
395 NOTICE("virtio-gpu: scanout " << Dec << m_Scanout << " at " << width << "x" << height << "x32"
396 << Hex);
397 return true;
398}
399
400bool VirtioGpu::update(size_t x, size_t y, size_t width, size_t height) {
401 if (!width || !height) {
402 return true;
403 }
404 TransferResource transfer{};
405 transfer.header.type = Transfer2d;
406 transfer.rect = {static_cast<uint32_t>(x), static_cast<uint32_t>(y), static_cast<uint32_t>(width),
407 static_cast<uint32_t>(height)};
408 transfer.offset = (y * m_Mode.width + x) * 4;
409 transfer.resourceId = ResourceId;
410 if (!transact(&transfer, sizeof(transfer), OkNoData, nullptr, sizeof(Header))) {
411 return false;
412 }
413 ResourceRectangle flush{};
414 flush.header.type = Flush;
415 flush.rect = transfer.rect;
416 flush.resourceId = ResourceId;
417 return transact(&flush, sizeof(flush), OkNoData, nullptr, sizeof(Header));
418}
419
420void VirtioGpu::redraw(size_t x, size_t y, size_t width, size_t height) {
421 if (!m_Ready || __atomic_load_n(&m_Stopping, __ATOMIC_ACQUIRE) ||
422 __atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE) || x >= m_Mode.width || y >= m_Mode.height) {
423 return;
424 }
425 if (width > m_Mode.width - x) {
426 width = m_Mode.width - x;
427 }
428 if (height > m_Mode.height - y) {
429 height = m_Mode.height - y;
430 }
431 (void)update(x, y, width, height);
432}
433
434int VirtioGpu::refreshThread(void* context) {
435 auto* gpu = static_cast<VirtioGpu*>(context);
436 while (!__atomic_load_n(&gpu->m_Stopping, __ATOMIC_ACQUIRE) &&
437 !__atomic_load_n(&gpu->m_Failed, __ATOMIC_ACQUIRE)) {
438 Time::delay(RefreshMilliseconds * Time::Multiplier::Millisecond);
439 if (!__atomic_load_n(&gpu->m_Stopping, __ATOMIC_ACQUIRE) &&
440 !__atomic_load_n(&gpu->m_Failed, __ATOMIC_ACQUIRE)) {
441 gpu->redraw(0, 0, gpu->m_Mode.width, gpu->m_Mode.height);
442 }
443 }
444 return 0;
445}
446
447bool VirtioGpu::registerProvider() {
448 ServiceFeatures* features = ServiceManager::instance().enumerateOperations(String("graphics"));
449 Service* service = ServiceManager::instance().getService(String("graphics"));
450 if (!features || !service || !features->provides(ServiceFeatures::touch)) {
451 ERROR("virtio-gpu: graphics service unavailable");
452 return false;
453 }
454 if (!service->serve(ServiceFeatures::touch, m_Provider, sizeof(*m_Provider))) {
455 ERROR("virtio-gpu: graphics service rejected provider");
456 return false;
457 }
458 m_Registered = true;
459 return true;
460}
461
462void VirtioGpu::unregisterProvider() {
463 if (!m_Registered) {
464 return;
465 }
466 Service* service = ServiceManager::instance().getService(String("graphics"));
467 if (!service || !service->serve(ServiceFeatures::withdraw, m_Provider, sizeof(*m_Provider))) {
468 panic("virtio-gpu: graphics provider could not be withdrawn");
469 }
470 m_Registered = false;
471}
472
473void VirtioGpu::shutdown() {
474 if (m_Shutdown) {
475 return;
476 }
477 __atomic_store_n(&m_Stopping, true, __ATOMIC_RELEASE);
478 if (m_Framebuffer) {
479 m_Framebuffer->closeCallbacks();
480 }
481 unregisterProvider();
482 if (m_RefreshThread) {
483 if (!m_RefreshThread->joinForCompletion()) {
484 panic("virtio-gpu: refresh worker could not be joined");
485 }
486 m_RefreshThread = nullptr;
487 }
488 if (m_Framebuffer) {
489 m_Framebuffer->detach();
490 }
491 if (!m_Transport.reset()) {
492 panic("virtio-gpu: device did not stop DMA");
493 }
494 m_ControlQueue.stop();
495 m_Ready = false;
496 delete m_Provider;
497 delete m_Framebuffer;
498 m_Provider = nullptr;
499 m_Framebuffer = nullptr;
500 m_Shutdown = true;
501}
502
504 name.assign("virtio-gpu", 10);
505}
506
507void VirtioGpu::dump(String& description) {
508 description.assign("virtio 2D graphics card", 23);
509}
510
512 return m_Pixels.virtualAddress();
513}
514
516 format = m_Mode.pf;
517 return m_Ready && !__atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE);
518}
519
521 mode = m_Mode;
522 return m_Ready && !__atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE);
523}
524
526 if (!m_Ready || __atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE)) {
527 return false;
528 }
529 auto* mode = new ScreenMode(m_Mode);
530 if (!mode) {
531 return false;
532 }
533 modes.pushBack(mode);
534 return true;
535}
536
538 return m_Ready && !__atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE) && mode.id == m_Mode.id &&
539 mode.width == m_Mode.width && mode.height == m_Mode.height && mode.pf.nBpp == 32;
540}
541
542bool VirtioGpu::setScreenMode(size_t modeId) {
543 return m_Ready && !__atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE) &&
544 (modeId == 0 || modeId == m_Mode.id);
545}
546
547bool VirtioGpu::setScreenMode(size_t width, size_t height, size_t bpp) {
548 return m_Ready && !__atomic_load_n(&m_Failed, __ATOMIC_ACQUIRE) && width == m_Mode.width &&
549 height == m_Mode.height && bpp == 32;
550}
uint16_t getPciDeviceId()
Definition Device.h:223
uint16_t getPciVendorId()
Definition Device.h:219
virtual void setSpecificType(String str)
Definition Device.h:182
Definition List.h:61
void * virtualAddress() const
physical_uintptr_t physicalAddress() const
size_t size() const
static PhysicalMemoryManager & instance()
static Scheduler & instance()
Definition Scheduler.h:96
virtual bool provides(Type service)
Service * getService(const String &serviceName)
ServiceFeatures * enumerateOperations(const String &serviceName)
virtual bool serve(ServiceFeatures::Type type, void *pData, size_t dataLen)=0
bool joinForCompletion()
Definition Thread.cc:2702
bool start()
Definition Thread.cc:725
physical_uintptr_t getPhysicalPage(size_t offset) const override
Definition VirtioGpu.cc:107
void hwRedraw(size_t x, size_t y, size_t width, size_t height) override
Inherited by drivers that provide a hardware redraw function.
Definition VirtioGpu.cc:133
void dump(String &description) override
Definition VirtioGpu.cc:507
void * getFramebuffer() override
Definition VirtioGpu.cc:511
void getName(String &name) override
Definition VirtioGpu.cc:503
bool setScreenMode(ScreenMode mode) override
Definition VirtioGpu.cc:537
bool getCurrentScreenMode(ScreenMode &mode) override
Definition VirtioGpu.cc:520
bool getScreenModes(List< ScreenMode * > &modes) override
Definition VirtioGpu.cc:525
bool getPixelFormat(PixelFormat &format) override
Definition VirtioGpu.cc:515
virtual bool getMapping(void *virtualAddress, physical_uintptr_t &physicalAddress, size_t &flags)=0
static EXPORTED_PUBLIC VirtualAddressSpace & getKernelAddressSpace()
void EXPORTED_PUBLIC panic(const char *msg) NORETURN
Definition panic.cc:117
@ Dec
Definition Log.h:126
@ Hex
Definition Log.h:124
void pushBack(const T &value)
Definition List.h:216
uint8_t pAlpha
Position of the alpha field.
Definition Display.h:61
uint8_t pBlue
Position of blue field.
Definition Display.h:59
uint8_t mAlpha
Alpha mask.
Definition Display.h:60
uint8_t mRed
Red mask.
Definition Display.h:54
uint8_t mGreen
Green mask.
Definition Display.h:56
uint8_t nBpp
Bits per pixel (total).
Definition Display.h:62
uint8_t pRed
Position of red field.
Definition Display.h:55
uint32_t nPitch
Bytes per scanline.
Definition Display.h:63
uint8_t pGreen
Position of green field.
Definition Display.h:57
uint8_t mBlue
Blue mask.
Definition Display.h:58
bool bFirmwareFallback
Firmware scanout is a fallback once a native driver owns the display.