Reworked kernel timer lock
This commit is contained in:
parent
4f86a5a26f
commit
5b8ae8393b
|
@ -449,6 +449,8 @@ XBSYSAPI EXPORTNUM(96) xboxkrnl::BOOLEAN NTAPI xboxkrnl::KeCancelTimer
|
||||||
|
|
||||||
assert(Timer);
|
assert(Timer);
|
||||||
|
|
||||||
|
KiTimerLock();
|
||||||
|
|
||||||
/* Lock the Database and Raise IRQL */
|
/* Lock the Database and Raise IRQL */
|
||||||
KiLockDispatcherDatabase(&OldIrql);
|
KiLockDispatcherDatabase(&OldIrql);
|
||||||
|
|
||||||
|
@ -461,6 +463,8 @@ XBSYSAPI EXPORTNUM(96) xboxkrnl::BOOLEAN NTAPI xboxkrnl::KeCancelTimer
|
||||||
/* Release Dispatcher Lock */
|
/* Release Dispatcher Lock */
|
||||||
KiUnlockDispatcherDatabase(OldIrql);
|
KiUnlockDispatcherDatabase(OldIrql);
|
||||||
|
|
||||||
|
KiTimerUnlock();
|
||||||
|
|
||||||
/* Return the old state */
|
/* Return the old state */
|
||||||
RETURN(Inserted);
|
RETURN(Inserted);
|
||||||
}
|
}
|
||||||
|
@ -1686,6 +1690,8 @@ XBSYSAPI EXPORTNUM(150) xboxkrnl::BOOLEAN NTAPI xboxkrnl::KeSetTimerEx
|
||||||
assert(Timer);
|
assert(Timer);
|
||||||
assert(Timer->Header.Type == TimerNotificationObject || Timer->Header.Type == TimerSynchronizationObject);
|
assert(Timer->Header.Type == TimerNotificationObject || Timer->Header.Type == TimerSynchronizationObject);
|
||||||
|
|
||||||
|
KiTimerLock();
|
||||||
|
|
||||||
KiLockDispatcherDatabase(&OldIrql);
|
KiLockDispatcherDatabase(&OldIrql);
|
||||||
|
|
||||||
// Same as KeCancelTimer(Timer) :
|
// Same as KeCancelTimer(Timer) :
|
||||||
|
@ -1718,6 +1724,8 @@ XBSYSAPI EXPORTNUM(150) xboxkrnl::BOOLEAN NTAPI xboxkrnl::KeSetTimerEx
|
||||||
/* Exit the dispatcher */
|
/* Exit the dispatcher */
|
||||||
KiUnlockDispatcherDatabase(OldIrql);
|
KiUnlockDispatcherDatabase(OldIrql);
|
||||||
|
|
||||||
|
KiTimerUnlock();
|
||||||
|
|
||||||
RETURN(Inserted);
|
RETURN(Inserted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1946,6 +1954,7 @@ XBSYSAPI EXPORTNUM(158) xboxkrnl::NTSTATUS NTAPI xboxkrnl::KeWaitForMultipleObje
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup a timer for the thread
|
// Setup a timer for the thread
|
||||||
|
KiTimerLock();
|
||||||
PKTIMER Timer = &Thread->Timer;
|
PKTIMER Timer = &Thread->Timer;
|
||||||
PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock;
|
PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock;
|
||||||
WaitBlock->NextWaitBlock = WaitTimer;
|
WaitBlock->NextWaitBlock = WaitTimer;
|
||||||
|
@ -1954,10 +1963,12 @@ XBSYSAPI EXPORTNUM(158) xboxkrnl::NTSTATUS NTAPI xboxkrnl::KeWaitForMultipleObje
|
||||||
WaitTimer->NextWaitBlock = WaitBlock;
|
WaitTimer->NextWaitBlock = WaitBlock;
|
||||||
if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) {
|
if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) {
|
||||||
WaitStatus = (NTSTATUS)STATUS_TIMEOUT;
|
WaitStatus = (NTSTATUS)STATUS_TIMEOUT;
|
||||||
|
KiTimerUnlock();
|
||||||
goto NoWait;
|
goto NoWait;
|
||||||
}
|
}
|
||||||
|
|
||||||
DueTime.QuadPart = Timer->DueTime.QuadPart;
|
DueTime.QuadPart = Timer->DueTime.QuadPart;
|
||||||
|
KiTimerUnlock();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WaitBlock->NextWaitBlock = WaitBlock;
|
WaitBlock->NextWaitBlock = WaitBlock;
|
||||||
|
@ -2128,6 +2139,7 @@ XBSYSAPI EXPORTNUM(159) xboxkrnl::NTSTATUS NTAPI xboxkrnl::KeWaitForSingleObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup a timer for the thread
|
// Setup a timer for the thread
|
||||||
|
KiTimerLock();
|
||||||
PKTIMER Timer = &Thread->Timer;
|
PKTIMER Timer = &Thread->Timer;
|
||||||
PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock;
|
PKWAIT_BLOCK WaitTimer = &Thread->TimerWaitBlock;
|
||||||
WaitBlock->NextWaitBlock = WaitTimer;
|
WaitBlock->NextWaitBlock = WaitTimer;
|
||||||
|
@ -2136,10 +2148,12 @@ XBSYSAPI EXPORTNUM(159) xboxkrnl::NTSTATUS NTAPI xboxkrnl::KeWaitForSingleObject
|
||||||
WaitTimer->NextWaitBlock = WaitBlock;
|
WaitTimer->NextWaitBlock = WaitBlock;
|
||||||
if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) {
|
if (KiInsertTreeTimer(Timer, *Timeout) == FALSE) {
|
||||||
WaitStatus = (NTSTATUS)STATUS_TIMEOUT;
|
WaitStatus = (NTSTATUS)STATUS_TIMEOUT;
|
||||||
|
KiTimerUnlock();
|
||||||
goto NoWait;
|
goto NoWait;
|
||||||
}
|
}
|
||||||
|
|
||||||
DueTime.QuadPart = Timer->DueTime.QuadPart;
|
DueTime.QuadPart = Timer->DueTime.QuadPart;
|
||||||
|
KiTimerUnlock();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WaitBlock->NextWaitBlock = WaitBlock;
|
WaitBlock->NextWaitBlock = WaitBlock;
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
// *
|
// *
|
||||||
// ******************************************************************
|
// ******************************************************************
|
||||||
|
|
||||||
// Acknowledgment: ReactOS (GPLv2)
|
// Acknowledgment (timer functions): ReactOS (GPLv2)
|
||||||
// https://github.com/reactos/reactos
|
// https://github.com/reactos/reactos
|
||||||
|
|
||||||
// Changed from ReactOS: slight changes to the Hand parameter usage
|
// Changed from ReactOS: slight changes to the Hand parameter usage
|
||||||
|
@ -58,6 +58,8 @@ namespace xboxkrnl
|
||||||
// ReactOS uses a size of 512, but disassembling the kernel reveals it to be 32 instead
|
// ReactOS uses a size of 512, but disassembling the kernel reveals it to be 32 instead
|
||||||
#define TIMER_TABLE_SIZE 32
|
#define TIMER_TABLE_SIZE 32
|
||||||
|
|
||||||
|
#define ASSERT_TIMER_LOCKED assert(xboxkrnl::KiTimerMtx.Acquired == true)
|
||||||
|
|
||||||
xboxkrnl::KTIMER_TABLE_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
|
xboxkrnl::KTIMER_TABLE_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
|
||||||
xboxkrnl::KDPC KiTimerExpireDpc;
|
xboxkrnl::KDPC KiTimerExpireDpc;
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ VOID xboxkrnl::KiInitSystem()
|
||||||
|
|
||||||
InitializeListHead(&KiWaitInListHead);
|
InitializeListHead(&KiWaitInListHead);
|
||||||
|
|
||||||
|
KiTimerMtx.Acquired = false;
|
||||||
KeInitializeDpc(&KiTimerExpireDpc, KiTimerExpiration, NULL);
|
KeInitializeDpc(&KiTimerExpireDpc, KiTimerExpiration, NULL);
|
||||||
for (i = 0; i < TIMER_TABLE_SIZE; i++) {
|
for (i = 0; i < TIMER_TABLE_SIZE; i++) {
|
||||||
InitializeListHead(&KiTimerTableListHead[i].Entry);
|
InitializeListHead(&KiTimerTableListHead[i].Entry);
|
||||||
|
@ -76,6 +79,18 @@ VOID xboxkrnl::KiInitSystem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VOID xboxkrnl::KiTimerLock()
|
||||||
|
{
|
||||||
|
KiTimerMtx.Mtx.lock();
|
||||||
|
KiTimerMtx.Acquired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID xboxkrnl::KiTimerUnlock()
|
||||||
|
{
|
||||||
|
KiTimerMtx.Mtx.unlock();
|
||||||
|
KiTimerMtx.Acquired = false;
|
||||||
|
}
|
||||||
|
|
||||||
VOID xboxkrnl::KiClockIsr(unsigned int ScalingFactor)
|
VOID xboxkrnl::KiClockIsr(unsigned int ScalingFactor)
|
||||||
{
|
{
|
||||||
KIRQL OldIrql;
|
KIRQL OldIrql;
|
||||||
|
@ -107,14 +122,16 @@ VOID xboxkrnl::KiClockIsr(unsigned int ScalingFactor)
|
||||||
|
|
||||||
// Because this function must be fast to continuously update the kernel clocks, if somebody else is currently
|
// Because this function must be fast to continuously update the kernel clocks, if somebody else is currently
|
||||||
// holding the lock, we won't wait and instead skip the check of the timers for this cycle
|
// holding the lock, we won't wait and instead skip the check of the timers for this cycle
|
||||||
if (TimerMtx.try_lock()) {
|
if (KiTimerMtx.Mtx.try_lock()) {
|
||||||
|
KiTimerMtx.Acquired = true;
|
||||||
// Check if a timer has expired
|
// Check if a timer has expired
|
||||||
Hand = KeTickCount & (TIMER_TABLE_SIZE - 1);
|
Hand = KeTickCount & (TIMER_TABLE_SIZE - 1);
|
||||||
if (KiTimerTableListHead[Hand].Entry.Flink != &KiTimerTableListHead[Hand].Entry &&
|
if (KiTimerTableListHead[Hand].Entry.Flink != &KiTimerTableListHead[Hand].Entry &&
|
||||||
InterruptTime.QuadPart >= KiTimerTableListHead[Hand].Time.QuadPart) {
|
InterruptTime.QuadPart >= KiTimerTableListHead[Hand].Time.QuadPart) {
|
||||||
KeInsertQueueDpc(&KiTimerExpireDpc, (PVOID)&KeTickCount, 0);
|
KeInsertQueueDpc(&KiTimerExpireDpc, (PVOID)&KeTickCount, 0);
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
KiTimerMtx.Mtx.unlock();
|
||||||
|
KiTimerMtx.Acquired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
KfLowerIrql(OldIrql);
|
KfLowerIrql(OldIrql);
|
||||||
|
@ -125,7 +142,7 @@ VOID xboxkrnl::KxInsertTimer(
|
||||||
IN xboxkrnl::ULONG Hand
|
IN xboxkrnl::ULONG Hand
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Try to insert the timer */
|
/* Try to insert the timer */
|
||||||
if (KiInsertTimerTable(Timer, Hand))
|
if (KiInsertTimerTable(Timer, Hand))
|
||||||
|
@ -133,7 +150,6 @@ VOID xboxkrnl::KxInsertTimer(
|
||||||
/* Complete it */
|
/* Complete it */
|
||||||
KiCompleteTimer(Timer, Hand);
|
KiCompleteTimer(Timer, Hand);
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VOID FASTCALL xboxkrnl::KiCompleteTimer(
|
VOID FASTCALL xboxkrnl::KiCompleteTimer(
|
||||||
|
@ -144,7 +160,7 @@ VOID FASTCALL xboxkrnl::KiCompleteTimer(
|
||||||
LIST_ENTRY ListHead;
|
LIST_ENTRY ListHead;
|
||||||
BOOLEAN RequestInterrupt = FALSE;
|
BOOLEAN RequestInterrupt = FALSE;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Remove it from the timer list */
|
/* Remove it from the timer list */
|
||||||
KiRemoveEntryTimer(Timer, Hand);
|
KiRemoveEntryTimer(Timer, Hand);
|
||||||
|
@ -164,7 +180,6 @@ VOID FASTCALL xboxkrnl::KiCompleteTimer(
|
||||||
if (RequestInterrupt) {
|
if (RequestInterrupt) {
|
||||||
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
|
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VOID xboxkrnl::KiRemoveEntryTimer(
|
VOID xboxkrnl::KiRemoveEntryTimer(
|
||||||
|
@ -175,7 +190,7 @@ VOID xboxkrnl::KiRemoveEntryTimer(
|
||||||
ULONG Hand;
|
ULONG Hand;
|
||||||
PKTIMER_TABLE_ENTRY TableEntry;
|
PKTIMER_TABLE_ENTRY TableEntry;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Remove the timer from the timer list and check if it's empty */
|
/* Remove the timer from the timer list and check if it's empty */
|
||||||
if (RemoveEntryList(&Timer->TimerListEntry))
|
if (RemoveEntryList(&Timer->TimerListEntry))
|
||||||
|
@ -192,7 +207,6 @@ VOID xboxkrnl::KiRemoveEntryTimer(
|
||||||
/* Clear the list entries so we can tell the timer is gone */
|
/* Clear the list entries so we can tell the timer is gone */
|
||||||
Timer->TimerListEntry.Flink = NULL;
|
Timer->TimerListEntry.Flink = NULL;
|
||||||
Timer->TimerListEntry.Blink = NULL;
|
Timer->TimerListEntry.Blink = NULL;
|
||||||
TimerMtx.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VOID xboxkrnl::KxRemoveTreeTimer(
|
VOID xboxkrnl::KxRemoveTreeTimer(
|
||||||
|
@ -202,7 +216,7 @@ VOID xboxkrnl::KxRemoveTreeTimer(
|
||||||
ULONG Hand = KiComputeTimerTableIndex(Timer->DueTime.QuadPart);
|
ULONG Hand = KiComputeTimerTableIndex(Timer->DueTime.QuadPart);
|
||||||
PKTIMER_TABLE_ENTRY TimerEntry;
|
PKTIMER_TABLE_ENTRY TimerEntry;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Set the timer as non-inserted */
|
/* Set the timer as non-inserted */
|
||||||
Timer->Header.Inserted = FALSE;
|
Timer->Header.Inserted = FALSE;
|
||||||
|
@ -218,7 +232,6 @@ VOID xboxkrnl::KxRemoveTreeTimer(
|
||||||
TimerEntry->Time.u.HighPart = 0xFFFFFFFF;
|
TimerEntry->Time.u.HighPart = 0xFFFFFFFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTimerTable(
|
xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTimerTable(
|
||||||
|
@ -234,7 +247,7 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTimerTable(
|
||||||
|
|
||||||
DBG_PRINTF("%s: inserting Timer %p, Hand: %lu\n", __func__, Timer, Hand);
|
DBG_PRINTF("%s: inserting Timer %p, Hand: %lu\n", __func__, Timer, Hand);
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Check if the period is zero */
|
/* Check if the period is zero */
|
||||||
if (!Timer->Period) {
|
if (!Timer->Period) {
|
||||||
|
@ -272,7 +285,6 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTimerTable(
|
||||||
Expired = TRUE;
|
Expired = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
|
||||||
|
|
||||||
/* Return expired state */
|
/* Return expired state */
|
||||||
return Expired;
|
return Expired;
|
||||||
|
@ -286,7 +298,7 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTreeTimer(
|
||||||
BOOLEAN Inserted = FALSE;
|
BOOLEAN Inserted = FALSE;
|
||||||
ULONG Hand = 0;
|
ULONG Hand = 0;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Setup the timer's due time */
|
/* Setup the timer's due time */
|
||||||
if (KiComputeDueTime(Timer, Interval, &Hand))
|
if (KiComputeDueTime(Timer, Interval, &Hand))
|
||||||
|
@ -304,7 +316,6 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiInsertTreeTimer(
|
||||||
Inserted = TRUE;
|
Inserted = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TimerMtx.unlock();
|
|
||||||
|
|
||||||
return Inserted;
|
return Inserted;
|
||||||
}
|
}
|
||||||
|
@ -323,7 +334,7 @@ xboxkrnl::BOOLEAN xboxkrnl::KiComputeDueTime(
|
||||||
{
|
{
|
||||||
LARGE_INTEGER InterruptTime, SystemTime, DifferenceTime;
|
LARGE_INTEGER InterruptTime, SystemTime, DifferenceTime;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Convert to relative time if needed */
|
/* Convert to relative time if needed */
|
||||||
Timer->Header.Absolute = FALSE;
|
Timer->Header.Absolute = FALSE;
|
||||||
|
@ -344,8 +355,6 @@ xboxkrnl::BOOLEAN xboxkrnl::KiComputeDueTime(
|
||||||
Timer->Header.SignalState = TRUE;
|
Timer->Header.SignalState = TRUE;
|
||||||
Timer->DueTime.QuadPart = 0;
|
Timer->DueTime.QuadPart = 0;
|
||||||
*Hand = 0;
|
*Hand = 0;
|
||||||
TimerMtx.unlock();
|
|
||||||
|
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,7 +371,6 @@ xboxkrnl::BOOLEAN xboxkrnl::KiComputeDueTime(
|
||||||
/* Get the handle */
|
/* Get the handle */
|
||||||
*Hand = KiComputeTimerTableIndex(Timer->DueTime.QuadPart);
|
*Hand = KiComputeTimerTableIndex(Timer->DueTime.QuadPart);
|
||||||
Timer->Header.Inserted = TRUE;
|
Timer->Header.Inserted = TRUE;
|
||||||
TimerMtx.unlock();
|
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
@ -376,7 +384,7 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiSignalTimer(
|
||||||
ULONG Period = Timer->Period;
|
ULONG Period = Timer->Period;
|
||||||
LARGE_INTEGER Interval, SystemTime;
|
LARGE_INTEGER Interval, SystemTime;
|
||||||
|
|
||||||
TimerMtx.lock();
|
ASSERT_TIMER_LOCKED;
|
||||||
|
|
||||||
/* Set default values */
|
/* Set default values */
|
||||||
Timer->Header.Inserted = FALSE;
|
Timer->Header.Inserted = FALSE;
|
||||||
|
@ -417,8 +425,6 @@ xboxkrnl::BOOLEAN FASTCALL xboxkrnl::KiSignalTimer(
|
||||||
RequestInterrupt = TRUE;
|
RequestInterrupt = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimerMtx.unlock();
|
|
||||||
|
|
||||||
/* Return whether we need to request a DPC interrupt or not */
|
/* Return whether we need to request a DPC interrupt or not */
|
||||||
return RequestInterrupt;
|
return RequestInterrupt;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,14 +43,25 @@ namespace xboxkrnl
|
||||||
ULARGE_INTEGER Time;
|
ULARGE_INTEGER Time;
|
||||||
} KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;
|
} KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;
|
||||||
|
|
||||||
const ULONG CLOCK_TIME_INCREMENT = 0x2710;
|
|
||||||
LIST_ENTRY KiWaitInListHead;
|
|
||||||
// Actually, this lock isn't required, but because raising the irql to dpc level doesn't really prevent thread switching at the
|
// Actually, this lock isn't required, but because raising the irql to dpc level doesn't really prevent thread switching at the
|
||||||
// moment, we need it for now to prevent concurrent access to the timer table
|
// moment, we need it for now to prevent concurrent access to the timer table
|
||||||
std::recursive_mutex TimerMtx;
|
typedef struct _KI_TIMER_LOCK
|
||||||
|
{
|
||||||
|
std::recursive_mutex Mtx;
|
||||||
|
bool Acquired;
|
||||||
|
} KI_TIMER_LOCK;
|
||||||
|
|
||||||
|
const ULONG CLOCK_TIME_INCREMENT = 0x2710;
|
||||||
|
LIST_ENTRY KiWaitInListHead;
|
||||||
|
KI_TIMER_LOCK KiTimerMtx;
|
||||||
|
|
||||||
|
|
||||||
VOID KiInitSystem();
|
VOID KiInitSystem();
|
||||||
|
|
||||||
|
VOID KiTimerLock();
|
||||||
|
|
||||||
|
VOID KiTimerUnlock();
|
||||||
|
|
||||||
VOID KiClockIsr(
|
VOID KiClockIsr(
|
||||||
IN unsigned int ScalingFactor
|
IN unsigned int ScalingFactor
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue