mirror of
https://github.com/ApfelTeeSaft/reactos.git
synced 2026-08-27 03:43:36 +00:00
539 lines
17 KiB
C
539 lines
17 KiB
C
/*
|
|
* PROJECT: ReactOS Generic Framebuffer Boot Video Driver
|
|
* LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
|
|
* or MIT (https://spdx.org/licenses/MIT)
|
|
* PURPOSE: Main file
|
|
* COPYRIGHT: Copyright 2023-2026 Hermès Bélusca-Maïto <[email protected]>
|
|
*/
|
|
|
|
#include "precomp.h"
|
|
|
|
#define NDEBUG
|
|
#include <debug.h>
|
|
|
|
/* Include the Boot-time (POST) display discovery helper functions */
|
|
#include <drivers/bootvid/framebuf.c>
|
|
|
|
/* Scaling of the bootvid 640x480 default virtual screen to the larger video framebuffer */
|
|
#define SCALING_SUPPORT
|
|
#define SCALING_PROPORTIONAL
|
|
|
|
/* Keep borders black or controlled with palette */
|
|
// #define COLORED_BORDERS
|
|
|
|
|
|
/* GLOBALS ********************************************************************/
|
|
|
|
#define BB_PIXEL(x, y) \
|
|
((PUCHAR)BackBuffer + (y) * SCREEN_WIDTH + (x))
|
|
|
|
#define FB_PIXEL(x, y) \
|
|
((PUCHAR)FrameBufferStart + (PanV + VidpYScale * (y)) * BytesPerScanLine \
|
|
+ (PanH + VidpXScale * (x)) * BytesPerPixel)
|
|
|
|
static ULONG_PTR FrameBufferStart = 0;
|
|
static ULONG FrameBufferSize;
|
|
static ULONG ScreenWidth, ScreenHeight, BytesPerScanLine;
|
|
static UCHAR BytesPerPixel;
|
|
static PUCHAR BackBuffer = NULL;
|
|
static SIZE_T BackBufferSize;
|
|
|
|
#ifdef SCALING_SUPPORT
|
|
static USHORT VidpXScale = 1;
|
|
static USHORT VidpYScale = 1;
|
|
#else
|
|
#define VidpXScale 1
|
|
#define VidpYScale 1
|
|
#endif
|
|
static ULONG PanH, PanV;
|
|
|
|
static RGBQUAD CachedPalette[BV_MAX_COLORS];
|
|
|
|
|
|
/* PRIVATE FUNCTIONS *********************************************************/
|
|
|
|
static VOID
|
|
ApplyPalette(VOID)
|
|
{
|
|
PULONG Frame = (PULONG)FrameBufferStart;
|
|
ULONG x, y;
|
|
|
|
#ifdef COLORED_BORDERS
|
|
/* Top border */
|
|
for (x = 0; x < PanV * ScreenWidth; ++x)
|
|
{
|
|
*Frame++ = CachedPalette[BV_COLOR_BLACK];
|
|
}
|
|
|
|
/* Left border */
|
|
for (y = 0; y < VidpYScale * SCREEN_HEIGHT; ++y)
|
|
{
|
|
// Frame = (PULONG)(FrameBufferStart + FB_OFFSET(-(LONG)PanH, y));
|
|
Frame = (PULONG)(FrameBufferStart + (PanV + y) * BytesPerScanLine);
|
|
for (x = 0; x < PanH; ++x)
|
|
{
|
|
*Frame++ = CachedPalette[BV_COLOR_BLACK];
|
|
}
|
|
}
|
|
#endif // COLORED_BORDERS
|
|
|
|
/* Screen redraw */
|
|
PUCHAR Back = BackBuffer;
|
|
for (y = 0; y < SCREEN_HEIGHT; ++y)
|
|
{
|
|
Frame = (PULONG)FB_PIXEL(0, y);
|
|
PULONG Pixel = Frame;
|
|
for (x = 0; x < SCREEN_WIDTH; ++x)
|
|
{
|
|
for (ULONG j = VidpXScale; j > 0; --j)
|
|
*Pixel++ = CachedPalette[*Back];
|
|
Back++;
|
|
}
|
|
Pixel = Frame;
|
|
for (ULONG i = VidpYScale-1; i > 0; --i)
|
|
{
|
|
Pixel = (PULONG)((ULONG_PTR)Pixel + BytesPerScanLine);
|
|
RtlCopyMemory(Pixel, Frame, VidpXScale * SCREEN_WIDTH * BytesPerPixel);
|
|
}
|
|
}
|
|
|
|
#ifdef COLORED_BORDERS
|
|
/* Right border */
|
|
for (y = 0; y < VidpYScale * SCREEN_HEIGHT; ++y)
|
|
{
|
|
// Frame = (PULONG)(FrameBufferStart + FB_OFFSET(SCREEN_WIDTH, y));
|
|
Frame = (PULONG)(FrameBufferStart + (PanV + y) * BytesPerScanLine + (PanH + VidpXScale * SCREEN_WIDTH) * BytesPerPixel);
|
|
for (x = 0; x < PanH; ++x)
|
|
{
|
|
*Frame++ = CachedPalette[BV_COLOR_BLACK];
|
|
}
|
|
}
|
|
|
|
/* Bottom border */
|
|
// Frame = (PULONG)(FrameBufferStart + FB_OFFSET(-(LONG)PanH, SCREEN_HEIGHT));
|
|
Frame = (PULONG)(FrameBufferStart + (PanV + VidpYScale * SCREEN_HEIGHT) * BytesPerScanLine);
|
|
for (x = 0; x < PanV * ScreenWidth; ++x)
|
|
{
|
|
*Frame++ = CachedPalette[BV_COLOR_BLACK];
|
|
}
|
|
#endif // COLORED_BORDERS
|
|
}
|
|
|
|
/* PUBLIC FUNCTIONS **********************************************************/
|
|
|
|
BOOLEAN
|
|
NTAPI
|
|
VidInitialize(
|
|
_In_ BOOLEAN SetMode)
|
|
{
|
|
PHYSICAL_ADDRESS FrameBuffer;
|
|
PHYSICAL_ADDRESS VramAddress;
|
|
ULONG VramSize;
|
|
CM_FRAMEBUF_DEVICE_DATA VideoConfigData; /* Configuration data from hardware tree */
|
|
INTERFACE_TYPE Interface;
|
|
ULONG BusNumber;
|
|
NTSTATUS Status;
|
|
|
|
/* Find boot-time framebuffer display information from the LoaderBlock */
|
|
Status = FindBootDisplay(&VramAddress,
|
|
&VramSize,
|
|
&VideoConfigData,
|
|
NULL, // MonitorConfigData
|
|
&Interface,
|
|
&BusNumber);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DPRINT1("Boot framebuffer does not exist!\n");
|
|
return FALSE;
|
|
}
|
|
|
|
/* The VRAM address must be page-aligned */
|
|
if (VramAddress.QuadPart % PAGE_SIZE != 0) // DPRINTed for diagnostics on some systems
|
|
DPRINT1("** VramAddress 0x%I64X isn't PAGE_SIZE aligned\n", VramAddress.QuadPart);
|
|
ASSERT(VramAddress.QuadPart % PAGE_SIZE == 0);
|
|
if (VramSize % PAGE_SIZE != 0)
|
|
DPRINT1("** VramSize %lu (0x%lx) isn't multiple of PAGE_SIZE\n", VramSize, VramSize);
|
|
// ASSERT(VramSize % PAGE_SIZE == 0); // This assert may fail, e.g. 800x600@32bpp UEFI GOP display
|
|
|
|
/* Retrieve the framebuffer address, its visible screen dimensions, and its attributes */
|
|
FrameBuffer.QuadPart = VramAddress.QuadPart + VideoConfigData.FrameBufferOffset;
|
|
ScreenWidth = VideoConfigData.ScreenWidth;
|
|
ScreenHeight = VideoConfigData.ScreenHeight;
|
|
if (ScreenWidth < SCREEN_WIDTH || ScreenHeight < SCREEN_HEIGHT)
|
|
{
|
|
DPRINT1("Unsupported screen resolution!\n");
|
|
return FALSE;
|
|
}
|
|
|
|
BytesPerPixel = (VideoConfigData.BitsPerPixel + 7) / 8; // Round up to nearest byte.
|
|
ASSERT(BytesPerPixel >= 1 && BytesPerPixel <= 4);
|
|
if (BytesPerPixel != 4)
|
|
{
|
|
UNIMPLEMENTED;
|
|
DPRINT1("Unsupported BytesPerPixel = %u\n", BytesPerPixel);
|
|
return FALSE;
|
|
}
|
|
|
|
ASSERT(ScreenWidth <= VideoConfigData.PixelsPerScanLine);
|
|
BytesPerScanLine = VideoConfigData.PixelsPerScanLine * BytesPerPixel;
|
|
if (BytesPerScanLine < 1)
|
|
{
|
|
DPRINT1("Invalid BytesPerScanLine = %lu\n", BytesPerScanLine);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Compute the visible framebuffer size */
|
|
FrameBufferSize = ScreenHeight * BytesPerScanLine;
|
|
|
|
/* Verify that the framebuffer actually fits inside the video RAM */
|
|
if (FrameBuffer.QuadPart + FrameBufferSize > VramAddress.QuadPart + VramSize)
|
|
{
|
|
DPRINT1("The framebuffer exceeds video memory bounds!\n");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Translate the framebuffer from bus-relative to physical address */
|
|
PHYSICAL_ADDRESS TranslatedAddress;
|
|
ULONG AddressSpace = 0; /* MMIO space */
|
|
if (!BootTranslateBusAddress(Interface,
|
|
BusNumber,
|
|
FrameBuffer,
|
|
&AddressSpace,
|
|
&TranslatedAddress))
|
|
{
|
|
DPRINT1("Could not translate framebuffer bus address 0x%I64X\n", FrameBuffer.QuadPart);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Map it into system space if necessary */
|
|
ULONG MappedSize = 0;
|
|
PVOID FrameBufferBase = NULL;
|
|
if (AddressSpace == 0)
|
|
{
|
|
/* Calculate page-aligned address and size for MmMapIoSpace() */
|
|
FrameBuffer.HighPart = TranslatedAddress.HighPart;
|
|
FrameBuffer.LowPart = ALIGN_DOWN_BY(TranslatedAddress.LowPart, PAGE_SIZE);
|
|
MappedSize = FrameBufferSize;
|
|
MappedSize += (ULONG)(TranslatedAddress.QuadPart - FrameBuffer.QuadPart); // BYTE_OFFSET()
|
|
MappedSize = ROUND_TO_PAGES(MappedSize);
|
|
/* Essentially MmMapVideoDisplay() */
|
|
FrameBufferBase = MmMapIoSpace(FrameBuffer, MappedSize, MmFrameBufferCached);
|
|
if (!FrameBufferBase)
|
|
FrameBufferBase = MmMapIoSpace(FrameBuffer, MappedSize, MmNonCached);
|
|
if (!FrameBufferBase)
|
|
{
|
|
DPRINT1("Could not map framebuffer 0x%I64X (%lu bytes)\n",
|
|
FrameBuffer.QuadPart, MappedSize);
|
|
goto Failure;
|
|
}
|
|
FrameBufferStart = (ULONG_PTR)FrameBufferBase;
|
|
FrameBufferStart += (TranslatedAddress.QuadPart - FrameBuffer.QuadPart); // BYTE_OFFSET()
|
|
}
|
|
else
|
|
{
|
|
/* The base is the translated address, no need to map */
|
|
FrameBufferStart = (ULONG_PTR)TranslatedAddress.QuadPart;
|
|
}
|
|
|
|
|
|
/*
|
|
* Reserve off-screen area for the backbuffer that contains
|
|
* 8-bit indexed color screen image, plus preserved row data.
|
|
*/
|
|
BackBufferSize = SCREEN_WIDTH * (SCREEN_HEIGHT + (BOOTCHAR_HEIGHT + 1));
|
|
|
|
/* If there is enough video memory in the physical framebuffer,
|
|
* place the backbuffer in the hidden part of the framebuffer,
|
|
* otherwise allocate a zone for the backbuffer. */
|
|
if (VideoConfigData.FrameBufferOffset + FrameBufferSize + BackBufferSize
|
|
<= ((AddressSpace == 0) ? MappedSize : VramSize))
|
|
{
|
|
/* Backbuffer placed following the framebuffer in the hidden part */
|
|
BackBuffer = (PUCHAR)(FrameBufferStart + FrameBufferSize);
|
|
// BackBuffer = (PUCHAR)(VramAddress + VramSize - BackBufferSize); // Or at the end of VRAM.
|
|
}
|
|
else
|
|
{
|
|
/* Allocate the backbuffer */
|
|
PHYSICAL_ADDRESS NullAddress = {{0, 0}};
|
|
PHYSICAL_ADDRESS HighestAddress = {{-1, -1}};
|
|
BackBuffer = MmAllocateContiguousMemorySpecifyCache(
|
|
BackBufferSize, NullAddress, HighestAddress,
|
|
NullAddress, MmNonCached);
|
|
if (!BackBuffer)
|
|
{
|
|
DPRINT1("Could not allocate backbuffer (size: %lu)\n", (ULONG)BackBufferSize);
|
|
goto Failure;
|
|
}
|
|
}
|
|
|
|
#ifdef SCALING_SUPPORT
|
|
/* Compute autoscaling; only integer (not fractional) scaling is supported */
|
|
VidpXScale = ScreenWidth / SCREEN_WIDTH;
|
|
VidpYScale = ScreenHeight / SCREEN_HEIGHT;
|
|
ASSERT(VidpXScale >= 1);
|
|
ASSERT(VidpYScale >= 1);
|
|
#ifdef SCALING_PROPORTIONAL
|
|
VidpXScale = min(VidpXScale, VidpYScale);
|
|
VidpYScale = VidpXScale;
|
|
#endif
|
|
DPRINT1("Scaling X = %hu, Y = %hu\n", VidpXScale, VidpYScale);
|
|
#endif // SCALING_SUPPORT
|
|
|
|
/* Calculate left/right and top/bottom border values
|
|
* to keep the displayed area centered on the screen */
|
|
PanH = (ScreenWidth - VidpXScale * SCREEN_WIDTH) / 2;
|
|
PanV = (ScreenHeight - VidpYScale * SCREEN_HEIGHT) / 2;
|
|
DPRINT1("Borders X = %lu, Y = %lu\n", PanH, PanV);
|
|
|
|
/* Reset the video mode if requested */
|
|
if (SetMode)
|
|
VidResetDisplay(TRUE);
|
|
|
|
return TRUE;
|
|
|
|
Failure:
|
|
/* We failed somewhere; unmap the framebuffer if we mapped it */
|
|
if (FrameBufferBase && (AddressSpace == 0))
|
|
MmUnmapIoSpace(FrameBufferBase, MappedSize);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
VOID
|
|
NTAPI
|
|
VidCleanUp(VOID)
|
|
{
|
|
/* Just fill the screen black */
|
|
VidSolidColorFill(0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1, BV_COLOR_BLACK);
|
|
}
|
|
|
|
VOID
|
|
ResetDisplay(
|
|
_In_ BOOLEAN SetMode)
|
|
{
|
|
RtlZeroMemory(BackBuffer, BackBufferSize);
|
|
RtlZeroMemory((PVOID)FrameBufferStart, FrameBufferSize);
|
|
|
|
/* Re-initialize the palette and fill the screen black */
|
|
InitializePalette();
|
|
VidSolidColorFill(0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1, BV_COLOR_BLACK);
|
|
}
|
|
|
|
VOID
|
|
InitPaletteWithTable(
|
|
_In_reads_(Count) const ULONG* Table,
|
|
_In_ ULONG Count)
|
|
{
|
|
const ULONG* Entry = Table;
|
|
ULONG i;
|
|
BOOLEAN HasChanged = FALSE;
|
|
|
|
for (i = 0; i < Count; i++, Entry++)
|
|
{
|
|
HasChanged |= !!((CachedPalette[i] ^ *Entry) & 0x00FFFFFF);
|
|
CachedPalette[i] = *Entry | 0xFF000000;
|
|
}
|
|
|
|
/* Re-apply the palette if it has changed */
|
|
if (HasChanged)
|
|
ApplyPalette();
|
|
}
|
|
|
|
VOID
|
|
SetPixel(
|
|
_In_ ULONG Left,
|
|
_In_ ULONG Top,
|
|
_In_ UCHAR Color)
|
|
{
|
|
PUCHAR Back = BB_PIXEL(Left, Top);
|
|
PULONG Frame = (PULONG)FB_PIXEL(Left, Top);
|
|
|
|
*Back = Color;
|
|
for (ULONG i = VidpYScale; i > 0; --i)
|
|
{
|
|
PULONG Pixel = Frame;
|
|
for (ULONG j = VidpXScale; j > 0; --j)
|
|
*Pixel++ = CachedPalette[Color];
|
|
Frame = (PULONG)((ULONG_PTR)Frame + BytesPerScanLine);
|
|
}
|
|
}
|
|
|
|
VOID
|
|
PreserveRow(
|
|
_In_ ULONG CurrentTop,
|
|
_In_ ULONG Height,
|
|
_In_ BOOLEAN Restore)
|
|
{
|
|
PUCHAR NewPosition, OldPosition;
|
|
|
|
/* Calculate the position in memory for the row */
|
|
if (Restore)
|
|
{
|
|
/* Restore the row by copying back the contents saved off-screen */
|
|
NewPosition = BB_PIXEL(0, CurrentTop);
|
|
OldPosition = BB_PIXEL(0, SCREEN_HEIGHT);
|
|
}
|
|
else
|
|
{
|
|
/* Preserve the row by saving its contents off-screen */
|
|
NewPosition = BB_PIXEL(0, SCREEN_HEIGHT);
|
|
OldPosition = BB_PIXEL(0, CurrentTop);
|
|
}
|
|
|
|
/* Set the count and copy the pixel data back to the other position in the backbuffer */
|
|
ULONG Count = Height * SCREEN_WIDTH;
|
|
RtlCopyMemory(NewPosition, OldPosition, Count);
|
|
|
|
/* On restore, mirror the backbuffer changes to the framebuffer */
|
|
if (Restore)
|
|
{
|
|
NewPosition = BB_PIXEL(0, CurrentTop);
|
|
for (ULONG y = 0; y < Height; ++y)
|
|
{
|
|
PULONG Frame = (PULONG)FB_PIXEL(0, CurrentTop + y);
|
|
PULONG Pixel = Frame;
|
|
for (Count = 0; Count < SCREEN_WIDTH; ++Count)
|
|
{
|
|
for (ULONG j = VidpXScale; j > 0; --j)
|
|
*Pixel++ = CachedPalette[*NewPosition];
|
|
NewPosition++;
|
|
}
|
|
Pixel = Frame;
|
|
for (ULONG i = VidpYScale-1; i > 0; --i)
|
|
{
|
|
Pixel = (PULONG)((ULONG_PTR)Pixel + BytesPerScanLine);
|
|
RtlCopyMemory(Pixel, Frame, VidpXScale * SCREEN_WIDTH * BytesPerPixel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
VOID
|
|
DoScroll(
|
|
_In_ ULONG Scroll)
|
|
{
|
|
ULONG RowSize = VidpScrollRegion.Right - VidpScrollRegion.Left + 1;
|
|
|
|
/* Calculate the position in memory for the row */
|
|
PUCHAR OldPosition = BB_PIXEL(VidpScrollRegion.Left, VidpScrollRegion.Top + Scroll);
|
|
PUCHAR NewPosition = BB_PIXEL(VidpScrollRegion.Left, VidpScrollRegion.Top);
|
|
|
|
/* Start loop */
|
|
for (ULONG Top = VidpScrollRegion.Top; Top <= VidpScrollRegion.Bottom; ++Top)
|
|
{
|
|
/* Scroll the row */
|
|
RtlCopyMemory(NewPosition, OldPosition, RowSize);
|
|
|
|
PULONG Frame = (PULONG)FB_PIXEL(VidpScrollRegion.Left, Top);
|
|
PULONG Pixel = Frame;
|
|
for (ULONG Count = 0; Count < RowSize; ++Count)
|
|
{
|
|
for (ULONG j = VidpXScale; j > 0; --j)
|
|
*Pixel++ = CachedPalette[NewPosition[Count]];
|
|
}
|
|
Pixel = Frame;
|
|
for (ULONG i = VidpYScale-1; i > 0; --i)
|
|
{
|
|
Pixel = (PULONG)((ULONG_PTR)Pixel + BytesPerScanLine);
|
|
RtlCopyMemory(Pixel, Frame, VidpXScale * RowSize * BytesPerPixel);
|
|
}
|
|
|
|
OldPosition += SCREEN_WIDTH;
|
|
NewPosition += SCREEN_WIDTH;
|
|
}
|
|
}
|
|
|
|
VOID
|
|
DisplayCharacter(
|
|
_In_ CHAR Character,
|
|
_In_ ULONG Left,
|
|
_In_ ULONG Top,
|
|
_In_ ULONG TextColor,
|
|
_In_ ULONG BackColor)
|
|
{
|
|
/* Get the font line for this character */
|
|
const UCHAR* FontChar = GetFontPtr(Character);
|
|
|
|
/* Loop each pixel height */
|
|
for (ULONG y = Top; y < Top + BOOTCHAR_HEIGHT; ++y, FontChar += FONT_PTR_DELTA)
|
|
{
|
|
/* Loop each pixel width */
|
|
ULONG x = Left;
|
|
for (UCHAR bit = 1 << (BOOTCHAR_WIDTH - 1); bit > 0; bit >>= 1, ++x)
|
|
{
|
|
/* If we should draw this pixel, use the text color. Otherwise
|
|
* this is a background pixel, draw it unless it's transparent. */
|
|
if (*FontChar & bit)
|
|
SetPixel(x, y, (UCHAR)TextColor);
|
|
else if (BackColor < BV_COLOR_NONE)
|
|
SetPixel(x, y, (UCHAR)BackColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
VOID
|
|
NTAPI
|
|
VidSolidColorFill(
|
|
_In_ ULONG Left,
|
|
_In_ ULONG Top,
|
|
_In_ ULONG Right,
|
|
_In_ ULONG Bottom,
|
|
_In_ UCHAR Color)
|
|
{
|
|
for (; Top <= Bottom; ++Top)
|
|
{
|
|
PUCHAR Back = BB_PIXEL(Left, Top);
|
|
// NOTE: Assumes 32bpp
|
|
PULONG Frame = (PULONG)FB_PIXEL(Left, Top);
|
|
PULONG Pixel = Frame;
|
|
for (ULONG L = Left; L <= Right; ++L)
|
|
{
|
|
*Back++ = Color;
|
|
for (ULONG j = VidpXScale; j > 0; --j)
|
|
*Pixel++ = CachedPalette[Color];
|
|
}
|
|
Pixel = Frame;
|
|
for (ULONG i = VidpYScale-1; i > 0; --i)
|
|
{
|
|
Pixel = (PULONG)((ULONG_PTR)Pixel + BytesPerScanLine);
|
|
RtlCopyMemory(Pixel, Frame, VidpXScale * (Right - Left + 1) * BytesPerPixel);
|
|
}
|
|
}
|
|
}
|
|
|
|
VOID
|
|
NTAPI
|
|
VidScreenToBufferBlt(
|
|
_Out_writes_bytes_all_(Height * Stride) PUCHAR Buffer,
|
|
_In_ ULONG Left,
|
|
_In_ ULONG Top,
|
|
_In_ ULONG Width,
|
|
_In_ ULONG Height,
|
|
_In_ ULONG Stride)
|
|
{
|
|
ULONG x, y;
|
|
|
|
/* Clear the destination buffer */
|
|
RtlZeroMemory(Buffer, Height * Stride);
|
|
|
|
/* Start the outer Y height loop */
|
|
for (y = 0; y < Height; ++y)
|
|
{
|
|
/* Set current scanline */
|
|
PUCHAR Back = BB_PIXEL(Left, Top + y);
|
|
PUCHAR Buf = Buffer + y * Stride;
|
|
|
|
/* Start the X inner loop */
|
|
for (x = 0; x < Width; x += sizeof(USHORT))
|
|
{
|
|
/* Read the current value */
|
|
*Buf = (*Back++ & 0xF) << 4;
|
|
*Buf |= *Back++ & 0xF;
|
|
Buf++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* EOF */
|