2007-05-26 09:57:39 -07:00
|
|
|
/**
|
|
|
|
|
* =========================================================================
|
|
|
|
|
* File : whrt.cpp
|
|
|
|
|
* Project : 0 A.D.
|
|
|
|
|
* Description : Windows High Resolution Timer
|
|
|
|
|
* =========================================================================
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// license: GPL; see lib/license.txt
|
|
|
|
|
|
|
|
|
|
#include "precompiled.h"
|
|
|
|
|
#include "whrt.h"
|
|
|
|
|
|
|
|
|
|
#include <process.h> // _beginthreadex
|
|
|
|
|
|
2007-09-23 08:36:29 -07:00
|
|
|
#include "lib/sysdep/cpu.h"
|
2008-06-30 10:34:18 -07:00
|
|
|
#include "lib/sysdep/os/win/win.h"
|
|
|
|
|
#include "lib/sysdep/os/win/winit.h"
|
2007-05-29 09:28:34 -07:00
|
|
|
#include "lib/sysdep/acpi.h"
|
2007-05-26 09:57:39 -07:00
|
|
|
#include "lib/adts.h"
|
|
|
|
|
#include "lib/bits.h"
|
|
|
|
|
|
2007-05-30 17:11:38 -07:00
|
|
|
#include "counter.h"
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-09-08 01:09:32 -07:00
|
|
|
WINIT_REGISTER_EARLY_INIT2(whrt_Init); // wutil -> whrt -> wtime
|
2007-06-04 15:59:14 -07:00
|
|
|
WINIT_REGISTER_LATE_SHUTDOWN(whrt_Shutdown);
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
namespace ERR
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
const LibError WHRT_COUNTER_UNSAFE = 140000;
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
2007-05-30 17:11:38 -07:00
|
|
|
// choose best available safe counter
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
// (moved into a separate function to simplify error handling)
|
|
|
|
|
static inline LibError ActivateCounter(ICounter* counter)
|
|
|
|
|
{
|
|
|
|
|
RETURN_ERR(counter->Activate());
|
|
|
|
|
|
|
|
|
|
if(!counter->IsSafe())
|
|
|
|
|
return ERR::WHRT_COUNTER_UNSAFE; // NOWARN (happens often)
|
|
|
|
|
|
|
|
|
|
return INFO::OK;
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
/**
|
|
|
|
|
* @return the newly created and unique instance of the next best counter
|
|
|
|
|
* that is deemed safe, or 0 if all have already been created.
|
|
|
|
|
**/
|
|
|
|
|
static ICounter* GetNextBestSafeCounter()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
|
|
|
|
for(;;)
|
|
|
|
|
{
|
had to remove uint and ulong from lib/types.h due to conflict with other library.
this snowballed into a massive search+destroy of the hodgepodge of
mostly equivalent types we had in use (int, uint, unsigned, unsigned
int, i32, u32, ulong, uintN).
it is more efficient to use 64-bit types in 64-bit mode, so the
preferred default is size_t (for anything remotely resembling a size or
index). tile coordinates are ssize_t to allow more efficient conversion
to/from floating point. flags are int because we almost never need more
than 15 distinct bits, bit test/set is not slower and int is fastest to
type. finally, some data that is pretty much directly passed to OpenGL
is now typed accordingly.
after several hours, the code now requires fewer casts and less
guesswork.
other changes:
- unit and player IDs now have an "invalid id" constant in the
respective class to avoid casting and -1
- fix some endian/64-bit bugs in the map (un)packing. added a
convenience function to write/read a size_t.
- ia32: change CPUID interface to allow passing in ecx (required for
cache topology detection, which I need at work). remove some unneeded
functions from asm, replace with intrinsics where possible.
This was SVN commit r5942.
2008-05-11 11:48:32 -07:00
|
|
|
static size_t nextCounterId = 0;
|
2007-05-28 02:25:38 -07:00
|
|
|
ICounter* counter = CreateCounter(nextCounterId++);
|
|
|
|
|
if(!counter)
|
|
|
|
|
return 0; // tried all, none were safe
|
|
|
|
|
|
|
|
|
|
LibError err = ActivateCounter(counter);
|
|
|
|
|
if(err == INFO::OK)
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2008-02-25 13:19:52 -08:00
|
|
|
debug_printf("HRT| using name=%s freq=%f\n", counter->Name(), counter->NominalFrequency());
|
2007-05-28 02:25:38 -07:00
|
|
|
return counter; // found a safe counter
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
char buf[100];
|
2008-02-25 13:19:52 -08:00
|
|
|
debug_printf("HRT| activating %s failed: %s\n", counter->Name(), error_description_r(err, buf, ARRAY_SIZE(buf)));
|
2007-05-28 02:25:38 -07:00
|
|
|
DestroyCounter(counter);
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
2007-05-28 02:25:38 -07:00
|
|
|
// counter that drives the timer
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static ICounter* counter;
|
2007-05-30 17:11:38 -07:00
|
|
|
// (these counter properties are cached for efficiency and convenience:)
|
2007-05-28 02:25:38 -07:00
|
|
|
static double nominalFrequency;
|
|
|
|
|
static double resolution;
|
had to remove uint and ulong from lib/types.h due to conflict with other library.
this snowballed into a massive search+destroy of the hodgepodge of
mostly equivalent types we had in use (int, uint, unsigned, unsigned
int, i32, u32, ulong, uintN).
it is more efficient to use 64-bit types in 64-bit mode, so the
preferred default is size_t (for anything remotely resembling a size or
index). tile coordinates are ssize_t to allow more efficient conversion
to/from floating point. flags are int because we almost never need more
than 15 distinct bits, bit test/set is not slower and int is fastest to
type. finally, some data that is pretty much directly passed to OpenGL
is now typed accordingly.
after several hours, the code now requires fewer casts and less
guesswork.
other changes:
- unit and player IDs now have an "invalid id" constant in the
respective class to avoid casting and -1
- fix some endian/64-bit bugs in the map (un)packing. added a
convenience function to write/read a size_t.
- ia32: change CPUID interface to allow passing in ecx (required for
cache topology detection, which I need at work). remove some unneeded
functions from asm, replace with intrinsics where possible.
This was SVN commit r5942.
2008-05-11 11:48:32 -07:00
|
|
|
static size_t counterBits;
|
2007-05-28 02:25:38 -07:00
|
|
|
static u64 counterMask;
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static void InitCounter()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
// we used to support switching counters at runtime, but that's
|
2007-05-26 09:57:39 -07:00
|
|
|
// unnecessarily complex. it need and should only be done once.
|
2007-05-28 02:25:38 -07:00
|
|
|
debug_assert(counter == 0);
|
|
|
|
|
counter = GetNextBestSafeCounter();
|
|
|
|
|
debug_assert(counter != 0);
|
|
|
|
|
|
|
|
|
|
nominalFrequency = counter->NominalFrequency();
|
|
|
|
|
resolution = counter->Resolution();
|
|
|
|
|
counterBits = counter->CounterBits();
|
2007-05-26 09:57:39 -07:00
|
|
|
|
had to remove uint and ulong from lib/types.h due to conflict with other library.
this snowballed into a massive search+destroy of the hodgepodge of
mostly equivalent types we had in use (int, uint, unsigned, unsigned
int, i32, u32, ulong, uintN).
it is more efficient to use 64-bit types in 64-bit mode, so the
preferred default is size_t (for anything remotely resembling a size or
index). tile coordinates are ssize_t to allow more efficient conversion
to/from floating point. flags are int because we almost never need more
than 15 distinct bits, bit test/set is not slower and int is fastest to
type. finally, some data that is pretty much directly passed to OpenGL
is now typed accordingly.
after several hours, the code now requires fewer casts and less
guesswork.
other changes:
- unit and player IDs now have an "invalid id" constant in the
respective class to avoid casting and -1
- fix some endian/64-bit bugs in the map (un)packing. added a
convenience function to write/read a size_t.
- ia32: change CPUID interface to allow passing in ecx (required for
cache topology detection, which I need at work). remove some unneeded
functions from asm, replace with intrinsics where possible.
This was SVN commit r5942.
2008-05-11 11:48:32 -07:00
|
|
|
counterMask = bit_mask<u64>(counterBits);
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
// sanity checks
|
2007-08-25 09:38:36 -07:00
|
|
|
debug_assert(nominalFrequency >= 500.0-DBL_EPSILON);
|
|
|
|
|
debug_assert(resolution <= 2e-3+DBL_EPSILON);
|
2007-05-28 02:25:38 -07:00
|
|
|
debug_assert(8 <= counterBits && counterBits <= 64);
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static void ShutdownCounter()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
DestroyCounter(counter);
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static inline u64 Counter()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
return counter->Counter();
|
|
|
|
|
}
|
|
|
|
|
|
2007-05-30 17:11:38 -07:00
|
|
|
/**
|
|
|
|
|
* @return difference [ticks], taking rollover into account.
|
|
|
|
|
* (time-critical, so it's not called through ICounter.)
|
|
|
|
|
**/
|
2007-05-28 02:25:38 -07:00
|
|
|
static inline u64 CounterDelta(u64 oldCounter, u64 newCounter)
|
|
|
|
|
{
|
|
|
|
|
return (newCounter - oldCounter) & counterMask;
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double whrt_Resolution()
|
|
|
|
|
{
|
|
|
|
|
return resolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
// timer state
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-30 17:11:38 -07:00
|
|
|
// we're not going to bother calibrating the counter (i.e. measuring its
|
|
|
|
|
// current frequency by means of a second timer). rationale:
|
|
|
|
|
// - all counters except the TSC are stable and run at fixed frequencies;
|
|
|
|
|
// - it's not clear that any other HRT or the tick count would be useful
|
|
|
|
|
// as a stable time reference (if it were, we should be using it instead);
|
|
|
|
|
// - calibration would complicate the code (we'd have to make sure the
|
|
|
|
|
// secondary counter is safe and can co-exist with the primary).
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
/**
|
|
|
|
|
* stores all timer state shared between readers and the update thread.
|
|
|
|
|
* (must be POD because it's used before static ctors run.)
|
|
|
|
|
**/
|
|
|
|
|
struct TimerState
|
|
|
|
|
{
|
2007-05-30 17:11:38 -07:00
|
|
|
// value of the counter at last update.
|
2007-05-28 02:25:38 -07:00
|
|
|
u64 counter;
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
// total elapsed time [seconds] since first update.
|
|
|
|
|
// converted from tick deltas with the *then current* frequency
|
2007-05-30 17:11:38 -07:00
|
|
|
// (this enables calibration, which is currently not implemented,
|
|
|
|
|
// but leaving open the possibility costs nothing)
|
2007-05-28 02:25:38 -07:00
|
|
|
double time;
|
|
|
|
|
};
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
// how do we detect when the old TimerState is no longer in use and can be
|
|
|
|
|
// freed? we use two static instances (avoids dynamic allocation headaches)
|
|
|
|
|
// and swap between them ('double-buffering'). it is assumed that all
|
|
|
|
|
// entered critical sections (the latching of TimerState fields) will have
|
|
|
|
|
// been exited before the next update comes around; if not, TimerState.time
|
|
|
|
|
// changes, the critical section notices and re-reads the new values.
|
|
|
|
|
static TimerState timerStates[2];
|
|
|
|
|
// note: exchanging pointers is easier than XORing an index.
|
|
|
|
|
static TimerState* volatile ts = &timerStates[0];
|
|
|
|
|
static TimerState* volatile ts2 = &timerStates[1];
|
|
|
|
|
|
|
|
|
|
static void UpdateTimerState()
|
|
|
|
|
{
|
|
|
|
|
// how can we synchronize readers and the update thread? locks are
|
|
|
|
|
// preferably avoided since they're dangerous and can be slow. what we
|
|
|
|
|
// need to ensure is that TimerState doesn't change while another thread is
|
|
|
|
|
// accessing it. the first step is to linearize the update, i.e. have it
|
|
|
|
|
// appear to happen in an instant (done by building a new TimerState and
|
|
|
|
|
// having it go live by switching pointers). all that remains is to make
|
|
|
|
|
// reads of the state variables consistent, done by latching them all and
|
|
|
|
|
// retrying if an update came in the middle of this.
|
|
|
|
|
|
|
|
|
|
const u64 counter = Counter();
|
|
|
|
|
const u64 deltaTicks = CounterDelta(ts->counter, counter);
|
|
|
|
|
ts2->counter = counter;
|
2007-05-30 17:11:38 -07:00
|
|
|
ts2->time = ts->time + deltaTicks/nominalFrequency;
|
2008-04-19 11:10:00 -07:00
|
|
|
ts = (TimerState*)InterlockedExchangePointer((volatile PVOID*)&ts2, ts);
|
2007-05-28 02:25:38 -07:00
|
|
|
}
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
double whrt_Time()
|
|
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
retry:
|
|
|
|
|
// latch timer state (counter and time must be from the same update)
|
|
|
|
|
const double time = ts->time;
|
|
|
|
|
const u64 counter = ts->counter;
|
|
|
|
|
// ts changed after reading time. note: don't compare counter because
|
|
|
|
|
// it _might_ have the same value after two updates.
|
|
|
|
|
if(time != ts->time)
|
|
|
|
|
goto retry;
|
|
|
|
|
|
|
|
|
|
const u64 deltaTicks = CounterDelta(counter, Counter());
|
2007-05-30 17:11:38 -07:00
|
|
|
return (time + deltaTicks/nominalFrequency);
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
2007-05-28 02:25:38 -07:00
|
|
|
// update thread
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
// note: we used to discipline the HRT timestamp to the system time, so it
|
2007-05-30 17:11:38 -07:00
|
|
|
// was advantageous to trigger updates via WinMM event (thus reducing
|
|
|
|
|
// instances where we're called in the middle of a scheduler tick).
|
2007-05-26 09:57:39 -07:00
|
|
|
// since that's no longer relevant, we prefer using a thread, because that
|
|
|
|
|
// avoids the dependency on WinMM and its lengthy startup time.
|
|
|
|
|
|
|
|
|
|
// rationale: (+ and - are reasons for longer and shorter lengths)
|
|
|
|
|
// + minimize CPU usage
|
2007-05-28 02:25:38 -07:00
|
|
|
// + ensure all threads currently using TimerState return from those
|
|
|
|
|
// functions before the next interval
|
2007-05-30 17:11:38 -07:00
|
|
|
// - avoid more than 1 counter rollover per interval (InitUpdateThread makes
|
|
|
|
|
// sure our interval is shorter than the current counter's rollover rate)
|
2007-05-28 02:25:38 -07:00
|
|
|
static const DWORD UPDATE_INTERVAL_MS = 1000;
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
static HANDLE hExitEvent;
|
2007-05-28 02:25:38 -07:00
|
|
|
static HANDLE hUpdateThread;
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static unsigned __stdcall UpdateThread(void* UNUSED(data))
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
debug_set_thread_name("whrt_UpdateThread");
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
for(;;)
|
|
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
const DWORD ret = WaitForSingleObject(hExitEvent, UPDATE_INTERVAL_MS);
|
2007-05-26 09:57:39 -07:00
|
|
|
// owner terminated or wait failed or exit event signaled - exit thread
|
|
|
|
|
if(ret != WAIT_TIMEOUT)
|
|
|
|
|
break;
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
UpdateTimerState();
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static inline LibError InitUpdateThread()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
// make sure our interval isn't too long
|
2007-05-30 17:11:38 -07:00
|
|
|
// (counterBits can be 64 => BIT64 would overflow => calculate period/2)
|
2007-05-28 02:25:38 -07:00
|
|
|
const double period_2 = BIT64(counterBits-1) / nominalFrequency;
|
had to remove uint and ulong from lib/types.h due to conflict with other library.
this snowballed into a massive search+destroy of the hodgepodge of
mostly equivalent types we had in use (int, uint, unsigned, unsigned
int, i32, u32, ulong, uintN).
it is more efficient to use 64-bit types in 64-bit mode, so the
preferred default is size_t (for anything remotely resembling a size or
index). tile coordinates are ssize_t to allow more efficient conversion
to/from floating point. flags are int because we almost never need more
than 15 distinct bits, bit test/set is not slower and int is fastest to
type. finally, some data that is pretty much directly passed to OpenGL
is now typed accordingly.
after several hours, the code now requires fewer casts and less
guesswork.
other changes:
- unit and player IDs now have an "invalid id" constant in the
respective class to avoid casting and -1
- fix some endian/64-bit bugs in the map (un)packing. added a
convenience function to write/read a size_t.
- ia32: change CPUID interface to allow passing in ecx (required for
cache topology detection, which I need at work). remove some unneeded
functions from asm, replace with intrinsics where possible.
This was SVN commit r5942.
2008-05-11 11:48:32 -07:00
|
|
|
const size_t rolloversPerInterval = UPDATE_INTERVAL_MS / cpu_i64FromDouble(period_2*2.0*1000.0);
|
2007-05-28 02:25:38 -07:00
|
|
|
debug_assert(rolloversPerInterval <= 1);
|
|
|
|
|
|
2007-05-26 09:57:39 -07:00
|
|
|
hExitEvent = CreateEvent(0, TRUE, FALSE, 0); // manual reset, initially false
|
|
|
|
|
if(hExitEvent == INVALID_HANDLE_VALUE)
|
|
|
|
|
WARN_RETURN(ERR::LIMIT);
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
hUpdateThread = (HANDLE)_beginthreadex(0, 0, UpdateThread, 0, 0, 0);
|
|
|
|
|
if(!hUpdateThread)
|
2007-05-26 09:57:39 -07:00
|
|
|
WARN_RETURN(ERR::LIMIT);
|
|
|
|
|
|
|
|
|
|
return INFO::OK;
|
|
|
|
|
}
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
static inline void ShutdownUpdateThread()
|
2007-05-26 09:57:39 -07:00
|
|
|
{
|
|
|
|
|
// signal thread
|
|
|
|
|
BOOL ok = SetEvent(hExitEvent);
|
|
|
|
|
WARN_IF_FALSE(ok);
|
|
|
|
|
// the nice way is to wait for it to exit
|
2007-05-28 02:25:38 -07:00
|
|
|
if(WaitForSingleObject(hUpdateThread, 100) != WAIT_OBJECT_0)
|
|
|
|
|
TerminateThread(hUpdateThread, 0); // forcibly exit (dangerous)
|
2007-05-26 09:57:39 -07:00
|
|
|
CloseHandle(hExitEvent);
|
2007-05-28 02:25:38 -07:00
|
|
|
CloseHandle(hUpdateThread);
|
2007-05-26 09:57:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static LibError whrt_Init()
|
|
|
|
|
{
|
2007-05-29 09:28:34 -07:00
|
|
|
// note: several counter implementations use acpi.cpp. if a counter is
|
|
|
|
|
// deemed unsafe, it is shut down, which releases the (possibly only)
|
|
|
|
|
// reference to the ACPI module. unloading and reloading it after trying
|
|
|
|
|
// each counter would be a waste of time, so we grab a reference here.
|
|
|
|
|
(void)acpi_Init();
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
InitCounter();
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-06-12 16:29:27 -07:00
|
|
|
// latch initial counter value so that timer starts at 0
|
|
|
|
|
ts->counter = Counter(); // must come before UpdateTimerState
|
|
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
UpdateTimerState(); // must come before InitUpdateThread to avoid race
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-28 02:25:38 -07:00
|
|
|
RETURN_ERR(InitUpdateThread());
|
2007-05-26 09:57:39 -07:00
|
|
|
|
|
|
|
|
return INFO::OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static LibError whrt_Shutdown()
|
|
|
|
|
{
|
2007-05-28 02:25:38 -07:00
|
|
|
ShutdownUpdateThread();
|
|
|
|
|
|
|
|
|
|
ShutdownCounter();
|
2007-05-26 09:57:39 -07:00
|
|
|
|
2007-05-29 09:28:34 -07:00
|
|
|
acpi_Shutdown();
|
|
|
|
|
|
2007-05-26 09:57:39 -07:00
|
|
|
return INFO::OK;
|
|
|
|
|
}
|